函数的调用过程(栈帧)

函数的调用过程(栈帧)
编译器: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弹回。