
2.3.5 栈管理
栈通过维护自动的进程状态数据来支持程序的执行。例如,如果一个程序的主例程(main routine)调用了a()函数,a()又调用了b()函数,则b()函数最终会将控制权返回给a(),a()则会接着将控制权返回给main()函数,如图2.6所示。
图2.6 栈管理
要做到将程序控制返回到正确的位置,就需要将返回地址的序列存储起来。栈很适合做这项工作,因为动态的LIFO数据结构在内存限制允许的情况下可以支持任意层数的嵌套。当调用一个子例程时,调用例程中的下一条将要执行的指令地址被压入栈中。当被调用的子例程返回时,预先存储的返回地址从栈中弹出,程序的执行点就跳到该指定位置上,如图2.7所示。栈维护的这些信息反映了任何时刻进程的执行状态。
图2.7 调用一个子例程
除了返回地址以外,栈还被用来保存子例程的参数以及局部(或自动)变量。帧(frame)指由函数调用引发的压入栈的数据。当前帧的地址被存储到帧或者基址寄存器中。在x86-32架构上,扩展基址指针(extended base pointer,ebp)寄存器就是用作此目的。帧指针在栈中是一个定点的引用。当调用一个子例程时,调用端函数的帧指针同样被压入栈,这样当被调用子例程退出时,帧指针能被重新恢复。
Intel指令有两种符号,微软使用Intel符号。
mov eax, 4 # Intel Notation
GCC使用AT&T符号
mov $4, %eax # AT&T Notation
这两种指令都把直接数4移动到eax寄存器。例2.4展示了调用foo(MyInt.MyStrPtr)所得的使用Intel符号表示的x86-32反汇编形式。
例2.4 使用Intel符号表示的反汇编
01 void foo(int, char *); // 函数原形 02 03 int main(void) { 04 int MyInt=1; // 栈变量位于 ebp-8 05 char *MyStrPtr="MyString"; // 栈变量位于ebp-4 06 /* ... */ 07 foo(MyInt, MyStrPtr); // 调用 foo 函数 08 mov eax, [ebp-4] 09 push eax # 把第2 个参数压入栈 10 mov ecx, [ebp-8] 11 push ecx # 把第1 个参数压入栈 12 call foo # 把返回地址压入栈 13 # 并跳到那个地址 14 add esp, 8 15 /* ... */ 16 }
调用由三个步骤组成,如下所示。
1.第二个参数被移到eax寄存器中,接着被压入栈(第8行和第9行)。注意mov指令是如何利用ebp寄存器来引用参数以及栈中的局部变量的。
2.第一个参数被移到ecx寄存器中,接着被压入栈(第10行和第11行)。
3.call指令将一个返回地址(call指令下一条指令的地址)压入栈,然后将控制转移到foo()函数(第12行)。
指令指针(eip)指向将要执行的下一条指令。当执行连续指令时,它会按照每个指令的大小自动递增,从而使CPU按顺序执行序列中的下一条指令。通常情况下,不能直接修改eip,相反,它必须通过诸如跳转(jump),调用(call)和返回(return)指令间接修改。
当控制返回到返回地址时,栈指针(SP)被递增8个字节(第14行)。(在x86-32中,栈指针被命名为esp,e前缀代表“扩展”,用来区分32位栈指针与16位栈指针)。栈指针指向栈的顶端。栈增长的方向取决于具体架构上的pop和push指令的实现(换言之,取决于对栈指针是递增还是递减操作)。对于很多流行的架构,包括x86、SPARC以及MIPS处理器在内,栈向低地址方向增长。在具有这些架构的机器上,递增栈指针就意味着从栈中弹出数据。
foo()函数开头。函数开头中包含一个函数调用后所执行的指令。foo()函数的函数开头如下所示:
1 void foo(int i, char *name) { 2 char LocalChar[24]; 3 int LocalInt; 4 push ebp # 保存帧指针 5 mov ebp, esp # 子例程的帧指针被设置为 6 # 当前栈指针. 7 sub esp, 28 # 为局部变量分配空间. 8 /* ... */
push指令将保存有指向调用者栈帧指针的ebp寄存器压入栈。mov指令将函数的帧指针(ebp寄存器)指向当前栈指针。最后,函数在栈上为局部变量分配了总共28个字节的空间(Local Char占24字节,Local Int占4字节)。
函数开头部分执行之后,foo()的栈帧如表2.2所示。在x86上,栈向内存低地址增长。
foo()的函数结尾。函数结尾包含将一个函数返回给调用者所执行的指令。下面是从foo()函数返回的函数结尾:
1 /* ... */ 2 return; 3 mov esp, ebp # 恢复栈指针 4 pop ebp # 恢复帧指针 5 ret # 将返回地址从栈弹出 6 # 并把控制移交给那个位置 7 }
表2.2 函数开头部分执行之后,foo()的栈帧
这些返回序列可以看成是前面的函数开头的逆序执行形式。mov指令将栈指针(esp)从帧指针(ebp)中恢复。pop指令将调用者的帧指针从栈中恢复。ret指令从栈中弹出调用函数中的返回地址,并且将控制转移到该位置。