什么是堆栈帧

可以肯定地说,任何设计合理的程序都是围绕着数据进行设计的。哪些数据必须由程序来管理呢?在程序中这些数据最准确、高效的表示方法是什么?这些都是有经验的软件设计人员和软件开发人员必须知道的最基本的问题。
对****而言,数据也是同样重要的。要真正理解一个程序,逆向者必须理解这个程序的数据。只要理解了程序中关键数据结构的总体规划和设计目的,我们就可以相对比较轻松地破译我们感兴趣的特定的代码区域。
本附录讲解了各种各样与程序中底层数据管理相关的主题。我们将从堆栈开始,讨论在程序中是如何使用堆栈的,然后接着讨论程序中使用的几种最基本的数据构造(data constructs。译注:数据构造要比数据结构的含义更广泛。),比如说变量,等等。接下来一节我们讨论数据在内存中的布置,并且描述了(从底层代码的视角来看)数组、链表等常用的数据构造。最后,我将演示类在底层是怎样实现的以及怎样在****中识别类。
C.1 堆栈
可以说堆栈是一块连续的内存,在系统中运行的例程将它组织成“层状”结构。堆栈中的内存单元在函数的生命周期内可以使用,而当函数返回时,这些内存单元就会被释放(释放后其他函数就可以用了)。
接下来的几小节将展示堆栈是怎样组织的,并讲述各种决定堆栈的基本布局的调用约定。
C.1.1 堆栈帧
堆栈帧指的是在堆栈中为当前正在运行的函数分配的区域(或空间)。传入的参数、返回地址(当这个函数结束后必须跳转到该返回地址。译注:即主调函数的断点处)以及函数所用的内部存储单元(即函数存储在堆栈上的局部变量)都在堆栈帧中。
对函数来说,堆栈帧内部具体的布局是一个非常关键的问题,因为布局会影响到函数访问堆栈中存放的传入参数以及存放其内部数据(例如局部变量)的方式。大多数函数调用代码都是以一段为函数设置堆栈帧的序言(prologue)开始的。设置堆栈帧的目的是:通过将一个指针存放在堆栈中参数区域和局部变量区域之间的那个单元,使得函数可以简便而快捷地访问这些参数和局部变量。这个指针通常存放在一个辅助寄存器中(通常是EBP),而腾出来ESP(ESP是主堆栈指针)来记录当前堆栈位置(即堆栈顶)。当前堆栈位置非常重要,因为这个函数可能还需要调用另外一个函数——这种情况下在ESP指向的当前位置下面(译注:“下面”指的是更低内存地址,而不是图C.1中所示的那种“上下”关系)的区域就要被用来给被调函数创建一个新的堆栈帧了。
图C.1展示了堆栈的总体布局以及堆栈帧是怎样布置的。
什么是堆栈帧

图C.1 堆栈和堆栈帧的布局
C.1.2 ENTER指令和LEAVE指令
ENTER指令和LEAVE指令是由CPU提供的内置工具,用于实现某种类型的堆栈帧。它们使用起来非常简便,只需一步就可以完成在例程中建立堆栈帧的操作。
ENTER指令建立堆栈栈的过程是:将当前EBP寄存器压入堆栈,并使它指向局部变量区的顶部(见图C.1)。ENTER指令还支持嵌套堆栈帧的管理,通常嵌套堆栈是在同一个例程内(当然高级语言也得支持嵌套块才行)。为了实现嵌套,使用ENTER指令代码的必须指明当前嵌套的层数(这使得ENTER的使用与实现具体的例程调用之间关联性较小)。当给出嵌套的层数时,ENTER指令将指向当前每一个活动的堆栈帧的起始位置的指针存储到例程的堆栈帧中。然后,代码就可以使用这些指针来访问其它当前活动的堆栈帧了。

ENTER指令是一条非常复杂的指令,它实际上完成了相当多条指令的工作。在内部,ENTER指令是用相当冗长的一段微码(microcode)实现的,这样会导致一些执行效率的问题。为此,大多数编译器好像都在避免使用ENTER指令,尽管它们都支持C和C++等
语言中使用嵌套代码。这些编译器在为例程安排局部堆栈布局时就简单地忽视代码块的存在,而将所有的局部变量存放在一个独立的内存区域中。
LEAVE是与ENTER指令配合使用的指令。LEAVE指令只是恢复ESP和EBP寄存器之前存储的值。因为LEAVE指令相对比较简单,许多编译器好像常在函数的尾声中使用该指令(尽管在函数的序言中不一定使用ENTER指令)。
C.1.3 调用约定
调用约定(Calling Conventions)定义了程序中调用函数的方式。调用约定之所以与我们这里讨论的堆栈相关,是因为调用约定决定了在函数调用的时候数据(比如说参数)在堆栈中的组织方式。理解调用约定对你来说非常重要,因为在逆向过程中你会不时地遇到函数调用,准确地辨识所用的调用约定是哪一种将有助于你理解你正在解读的程序。
在讨论各个调用约定之前,我们先来讨论一下函数调用要用到的两条基本的指令:CALL指令和RET指令。CALL指令将当前的指令指针(这个指针指向紧接在CALL指令后面的那条指令)压入堆栈,然后执行一条无条件转移指令转移到新的代码地址。
RET是与CALL指令配合使用的指令,在绝大多数函数中它是最后一条指令。RET指令弹出返回地址(就是早些时候CALL指令压入堆栈的地址)并将其加载到EIP寄存器中,然后从这个地址开始继续执行。
接下来的几个小节中我们将讨论几种最常见的调用约定,及其它们在汇编语言中是怎样实现的。
cdecl调用约定
cdecl调用约定是C和C++语言中的标准调用约定。其特点是允许函数接收可变数量的参数。cdecl调用约定可以做到参数数量可变,是因为由主调函数负责在函数调用之后恢复堆栈指针。另外,与其他调用约定相比,cdecl函数是按照相反的顺序接收参数的。第一个参数被首先压入堆栈,最后一个参数最后压入堆栈。识别cdecl调用约定的方法非常简单:如果函数接收了一个或多个参数,并且以一个简单的不带任何操作数的RET指令收尾的话,这个函数很可能是采用cdecl调用约定。
fastcall调用约定
顾名思义,fastcall是一种相对比较高效的调用约定:它使用寄存器来给被调函数传递前两个参数,其余的参数通过堆栈传递给被调函数。最初,fastcall是Microsoft公司专用的调用约定,但是现在大多数主流编译器都支持支持这种调用了,所以你可以在更多的现代程序中碰见它。fastcall调用约定通常使用ECX寄存器和EDX寄存器来分别存放第一个参数和第二个参数。
stdcall调用约定
stdcall调用约定在windows系统中是非常常见的,因为windows系统函数和API都使用这种调用约定。stdcall调用约定在参数传递的方式和顺序上与cdecl调用约定相反。使用stdcall调用约定的函数接收参数的顺序与使用cdecl调用约定的函数的相反,即stdcall中最后一个参数最先压入堆栈。stdcall与cdecl另一个重要的区别在于:在stdcall中被调函数负责清栈,而在cdecl中是由主调函数负责清栈的。stdcall中函数使用RET指令清栈,这是RET指令带有一个操作数,该操作数指明在EIP跳回主要函数之前需要释放的堆栈空间的字节数。这就是说,stdcall调用约定中RET指令带的操作数往往就意味着函数一共传入几个参数。(操作数除以4=参数个数)这是在****中识别stdcall调用约定的一个重要的特征,并可以据此判断出函数所接收的参数的个数。
C++类成员调用约定(thiscall)
当C++程序中的类方法所接受的参数的个数是固定的时候,Microsoft和Intel编译器会使用这种调用约定。一种快速识别这种调用约定的技巧是:使用这种调用约定的函数指令流将在ecx寄存器中写入一个有效指针,并往堆栈中压入参数,但不使用edx寄存器。原因是每个C++的类方法都必须接收一个类指针(就是this指针),并可能较频繁的使用该指针。编译器则使用这种高效的技巧来传递和存储这个特殊的参数。
对于参数个数不确定的类方法,编译器就将使用cdecl调用约定,并把this指针作为第一个参数首先压入堆栈。

总结:
堆栈帧是一个为函数保留的区域,用来存储关于参数、局部变量和返回地址的信息。
堆栈帧通常是在新的函数调用的时候创建,并在函数返回的时候销毁。
说白了,堆栈由堆栈帧组成. 当调用函数时堆栈帧被压入栈中, 当函数返回时堆栈帧被从栈中弹出. 堆栈帧包括函数的参数, 函数地局部变量, 以及恢复前一个堆栈帧所需要的数据。

学习转载自http://blog.sina.com.cn/s/blog_4c7a35260100etgr.html