在深入过程之前,先认识两个关键寄存器(以x86-32位为例):
esp (Extended Stack Pointer):栈顶指针,始终指向系统栈的顶部。
ebp (Extended Base Pointer):栈底指针,又称帧指针,指向当前函数栈帧的底部。函数内部通过ebp的固定偏移来访问参数和局部变量。
可以把ebp和esp想象成一个画框的上下边框,它们框出了当前函数栈帧的范围
假设我们有如下代码,main函数调用add函数。
int add(int a, int b) {
int result = a + b;
return result;
}
int main() {
int a = 10;
int b = 20;
int ret = add(a, b);
return 0;
}当main函数执行到int ret = add(a, b);这一行时,栈帧的创建过程如下:
步骤 1:参数压栈 (从右向左)
b的值(20)压入栈。
a的值(10)压入栈。
esp栈顶指针会向上(低地址方向)移动,指向最后压入的参数a。
注意:这一步有时也通过push指令完成,它会隐式地减少esp并存入数据。
步骤 2:调用函数,压入返回地址
call add 指令。这条指令做了两件事:
call add后面那条指令的地址,这里是int ret = ...的地址)压入栈中。这个地址就是返回地址,函数执行完后要回到这里。
add函数的代码开始处执行。
步骤 3:进入新函数,保存旧栈帧
add函数体的代码。
add函数首先要建立自己的“地盘”(栈帧)。
push ebp: 将调用者(main函数)的ebp 值压栈保存。这是为了在add函数返回时,能恢复main函数的栈帧。
mov ebp, esp: 让ebp指向新的栈顶。此时,ebp和esp指向同一个位置,这就是add函数栈帧的底部。
步骤 4:为新栈帧分配空间
sub esp, XXh: 将栈顶指针esp向上(低地址)移动一段距离(XX是16进制数)。这段新开辟的空间就是用于存放add函数的局部变量等数据。比如这里的int result。
至此,add函数的栈帧已经完全创建好了。此时栈的布局如下图所示:
高地址 ... ---------------------------- | 参数 b (20) | <-- main函数的栈帧 ---------------------------- | 参数 a (10) | ---------------------------- | main的返回地址 | ---------------------------- | 保存的main的ebp | <-- [ebp]指向这里 (add栈帧的底部) ---------------------------- | | | add函数的局部变量 | | (例如: result) | <-- [esp]指向这里 (add栈帧的顶部) | | ---------------------------- ... 低地址
在创建好的栈帧内,函数可以自由访问它的数据:
[ebp + 8] 访问第一个参数a,通过 [ebp + 12] 访问第二个参数b。
[ebp - 4] 等方式访问局部变量result。
函数体内部的运算(a + b)就在CPU的寄存器中进行,然后将结果存入局部变量result的位置。
当add函数执行到return result;时,开始销毁自己的栈帧。
步骤 1:返回值处理
eax 寄存器中。所以会执行 mov eax, [ebp - 4],将result的值放入eax。
步骤 2:恢复栈指针和基址指针
mov esp, ebp: 将esp移回ebp的位置。这一步直接回收了为局部变量分配的所有栈空间。esp现在指向保存的旧ebp。
pop ebp: 将栈顶(esp指向的值)弹出到ebp寄存器中。这个值正是之前保存的main函数的ebp。现在,ebp就恢复指向了main函数的栈帧底部。同时,esp会自动下移(pop指令的效果),指向返回地址。
步骤 3:返回调用者
ret 指令。这条指令会:
eip中。
eip指向的地址(即main函数中call add的下一条指令)开始执行。
步骤 4:清理栈上的参数
esp指向参数a。main函数需要清理为add函数调用压入的参数。
add esp, 8 指令实现(因为两个int参数共8字节)。这条指令让esp向下移动,完全回到了调用add之前的位置。
至此,add函数的栈帧被完全销毁,仿佛从未存在过。栈的状态和main函数调用add之前一模一样。main函数可以继续执行后续代码,并从eax寄存器中取得add函数的返回值,赋给局部变量ret。
阶段关键操作作用创建参数压栈(从右向左)传递参数
call指令压入返回地址确保函数能正确返回push ebp/mov ebp, esp保存旧栈帧,建立新栈帧基线sub esp, XX为局部变量分配空间销毁mov eax, [ebp-4]将返回值存入eaxmov esp, ebp回收局部变量空间pop ebp恢复调用者的栈帧基线ret跳回调用处,继续执行add esp, XX(在调用者中)清理参数空间
ebp,保证了函数能层层调用并正确返回。
ebp都指向上一个栈帧的底部)来生成调用堆栈(Call Stack)信息的。