以下方程序为例。
#include <stdio.h>
int main(void)
{
int apple = 10;
int pear = 20;
int total = 0;
printf("apple = %d, pear = %d.\n", apple, pear);
total = apple + pear;
return 0;
}
第一步,函数参数入栈。printf
函数调用之前,参数从右向左入栈,所以入栈顺序为——变量 pear
的值,变量 apple
的值,字符串 "apple = %d, pear = %d.\n"
的地址。
pop <printf 函数的参数>
; ...
第二步,返回地址入栈,准备跳转。调用 call 指令,此时存储在指令寄存器 ip 中的值是 printf
函数下一条语句 total = apple + pear;
对应的机器指令的地址即返回地址,该地址入栈,同时指令寄存器 ip 的值修改为 printf
函数在代码段中的第一条指令的地址。
call printf
; 等价于以下汇编程序
; push ip
; mov ip, <printf 函数入口地址>
第三步,保存原栈帧的栈底地址,通过特定的两个寄存器之间赋值来设置新栈帧的栈底地址,通过修改特定一个寄存器值来设置新栈帧的栈顶地址,然后正式执行函数。开始执行 printf
函数时,会进行三步操作——在 printf
函数栈帧中保存原栈帧即 main
函数栈帧的栈底地址;将 main
函数栈帧的栈顶地址作为 printf
函数栈帧的栈底地址;为 printf
函数的局部变量开辟足够的空间。三步操作执行完之后便开始执行 printf
函数的主体机器指令段。
push ebp
mov ebp, esp
; 栈空间往低地址方向扩展,故用减法指令
sub esp, <printf 函数局部变量所占空间字节数>
第四步,销毁新栈帧的局部变量空间,恢复原栈帧的栈底地址,往特定寄存器装载返回地址,销毁函数参数空间,前四部都完成后原栈帧的栈底地址也能恢复。printf
函数的主体机器指令段执行完毕后,便开始收尾工作——将 esp 恢复为为 printf
函数局部变量开辟空间之前的值;将 ebp 恢复为 main
函数栈帧的栈底地址;将 eip 恢复为语句 total = apple + pear;
对应的机器指令地址;将 esp 值恢复为为 printf
函数的参数开辟空间之前的值,恢复后,esp 的值恰好是 total
的地址。
mov esp, ebp
mov ebp, [esp]
mov ip, [esp]
pop <printf 函数的参数>