函数的调用过程(栈帧)
函数的调用过程(栈帧)
编译器:VC6.0
用以下代码作为示例:
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
printf("ret = %d\n", ret);
return 0;
}
写好代码后F10进行调试,在打开运行堆栈,内存,寄存器。如果所示:
首先我们看栈,栈的特点是先进后出,我们可以看出main是先进来的,main也是个函数,它也是被调用的,可以看到,main是mainCRTStartup()函数调用的,而mainCRTStartup()又是被其它函数调用的,不做深入研究。只需要知道main()运行时会被其它函数调用,并不是程序运行的第一个函数,但它是你的逻辑入口。
然后我们点击查看 => 调试 =>Disassembly查看汇编代码
首先,main()下面有一大堆push,mov,我们先不管,我们直接看a = -10; 下面的 mov dword ptr [ebp-4],0Ah 是它的汇编代码,我们这里这么理解mov,把后面的东西,放到前面,push是栈的操作把数据压入栈顶,还有个指令call,call有两个作用:1.默认将当前正在执行指令的下一条指令的地址压入栈中。2.跳转至目标函数的地址开始执行新函数调用。
再来认识几个寄存器:
EBP: 基质寄存器(栈底寄存器)
ESP:栈顶寄存器
EIP:指令寄存器(程序计数器)
下面开始函数的调用过程:
下图表示,函数在开始调用时,栈结构已经形成,EBP指向栈底,ESP指向栈顶,EIP指向main函数,因为正在执行main函数,而系统中有个cpu,cpu中我们研究这三个寄存器(EBP,ESP,EIP)
继续调试,当运行到这一部的时候我们发现&a已经变成了0A 00 00 00了,mov把这个数据放在EBP-4的位置,下面把-20放在EBP-8的位置
此时开辟的栈中为这个样子
然后继续调试,就到了 int ret = Add(a,b);我们知道,当我们要调一个函数是,给这个函数传参时,要进行形参实例化,而进行传参时,我们要形成临时变量。
然后我们看到第一句,它将EBP - 8放进EAX,再push EAX; 就是把数据放到寄存器中,再压栈,放数据,然后ESP指针下移。
然后同样,再把EBP-4放进ECX,再压栈
继续调试,当到下面这一部时,可以看出ESP指向的是a
此时栈的结构为
现在栈中有两份a和b,一份为本来的变量,一份为临时变量(临时变量在栈顶),而且我们可以看出,先实例化的是后面的参数,然后是前一个,这就证明了C语言的参数在赋值时时从右往左赋值的。
继续编译,到call指令,我们之前说过call指令有两个作用:1.默认将当前正在执行指令的下一条指令的地址压入栈中。可以看到它下一条指令的地址为 00401093 然后跳转到Add函数,这是EIP会指向Add
栈结构为:
进入Add函数
进来后先push EBP 刚才EBP指向的是栈底,注意不是a,a是EBP-4,所以EBP指向main函数的栈底
然后mov EBP, ESP 把ESP的值赋给EBP
所以ESP指向哪,EBP就指向哪
然后sub esp, 44h 表示ESP减上某个值,具体减多少我们不管,但它肯定会下移
此时又形成了一个栈结构,这个栈结构给Add使用,这个结构叫做栈帧结构。此时EIP的地址变为Add的地址。剩下的是对栈帧加入一些必要信息,或对内存进行整体清零,我们不研究。
然后我们看这句代码
EBP + 8 放到 EAX中 +8指向a (但要注意,这两句代码知识把数值放到那个位置,并没有改变EBP的指向,EBP现在还是指向新开辟的栈的栈底)
EBP + 12 放到 EAX 中 同理+12指向b,都放入EAX,求和
完成后再把EAX 放到 EBP - 4
最后到return z;
我们先来看第三条指令,将EAX及x+y的和放入EBP-4,下图的return z又将EBP-4放入EAX,所以两个数相加的结果在EAX中(表明我们用寄存器返回)
把EBP放到ESP,所以ESP和EBP指向的一样
pop EBP是把栈顶的内容出栈,在放到EBP里,所以要把main函数的EBP放到EBP里,所以EBP指向栈底,而且因为是pop所以栈顶上移。
到了这一步,z变量已经不存了,这个返回的过程叫做栈帧结构的释放过程。
最后还有 ret 指令:弹出栈顶地址,将数据放置EIP中。
栈结构:
然后运行到刚才调用函数的下一个地方add esp,8 ESP + 8,平衡栈帧结构
现在看最后一条指令,将EAX,放到EBP - 12
这就是返回值的位置:
过程调用结束,随机变量被释放掉。
总结:调用函数时,形参实例化要形成临时变量,临时变量在栈顶,形参实例化的顺序是从右往左,要将返回值地址的下一条指令保存起来,形成新的栈帧结构,返回值通过ret弹回。