今天我们将通过栈帧详解C程序中函数间的调用过程。
栈帧是什么?
栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。
首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧结构,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。但是系统并不是给每个栈帧创造一套独有的ebp和esp, 而是大家共用一套。那么他们是怎么公用的呢?
我们以如下一个简单的函数调用为例:
#include <stdio.h>
#include <windows.h>
int fun(int x, int y)
{
int c = 0xcccccccc;
return c;
}
int main()
{
int a = 0xaaaaaaaa;
int b = 0xbbbbbbbb;
int ret = fun(a, b);
printf("you should running here!\n");
return 0;
}
我们需要知道main函数执行的整个过程。
注意:main函数也是函数,它是被start函数调用的。
注意;函数的栈帧被释放,并不是将其内容清空,而是做一个标记,说明此空间里面的数据无效了。
当又有新函数被调用形成新的栈帧时,只要直接覆盖这片空间就好了。
调用main函数:(如图1)
(1)将当前正在执行的指令的下一条指令的地址(PC)压入start函数栈中。
(1)start函数调用main函数,把ebp先压栈,用来保存start函数ebp栈底指针的位置。
(2)将start函数的esp的值赋给ebp,即就是让ebp指向esp的位置,也就是让ebp作为将要开辟的main函数栈帧的栈底。
(3)让esp的值减去某个值,让它指向另一个位置,这是在为main函数栈帧开辟空间。
(4)将main函数中定义的变量按照定义的先后顺序存入main栈帧中。
调用fun函数:(如图2)
(1)从右向左进行参数初始化,因为fun函数是main函数调用的,所以按初始化顺序将参数压入main函数的栈中。
(2)将当前正在执行的指令的下一条指令的地址(PC)压入main函数栈中。
(3)然后将main函数的ebp压栈,用来保存main函数ebp指针的位置。
(4)将main的esp的值赋给ebp,即就是让ebp指向esp的位置,也就是让ebp作为将要开辟的fun函数栈帧的栈底。
(5)让esp的值减去某个值,让它指向另一个位置,这是在为fun函数栈帧开辟空间。
(6)将fun函数中定义的变量按照定义的先后顺序存入fun栈帧中.
fun函数调用完毕:(如图3)
(1)将esp指向ebp的地址,使fun函数栈帧的esp栈顶指针重新恢复为main函数的栈顶指针。
(2)然后从main函数的栈顶弹出main函数的ebp (因为在调用fun函数之前保存了main函数的ebp),使得ebp重新指向main函数的栈底。
这就完成了fun函数栈帧的释放,局部变量也跟着栈帧的释放被释放掉了。
同时从main函数栈顶弹出esp当前指向的内容(下一条指令的地址),带着fun函数返回值,去找下一条指令继续执行。
返回main函数:(如图4)
(1)fun函数调用完毕,初始化参数也随之失效,通过移动esp站定指针将其移除。
(2)将fun函数返回值赋给ret.
main函数调用完毕:(如图5)
(1)将esp减去加上某个值,这就完成了main函数栈帧的释放,局部变量也跟着栈帧的释放被释放掉了。
(2)再将esp指向ebp的地址,使main函数栈帧的esp栈顶指针重新恢复为start函数的栈顶指针。
(3)然后弹出start函数的ebp (因为在调用main函数之前保存了start函数的ebp),使得ebp重新指向start函数的栈底。
最后我们再来总结一下,通过上述介绍我们不难看出,每个函数都有一个自己的栈帧,函数的调用伴随着栈帧的创建,函数的局部变量也同时在栈帧中定义,不断地调用函数就不断地创建新的栈帧,当函数调用完毕时,释放其栈帧,局部变量也同时被销毁。