(函数栈帧的创建和销毁)
预备知识:
相关汇编命令
mov:数据转移指令 push:数据入栈,同时esp栈顶寄存器也要发生改变 pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变 sub:减法命令 add:加法命令 call:函数调用,1. 压入返回地址 2. 转入目标函数 jump:通过修改eip,转入目标函数,进行调用 ret:恢复返回地址,压入eip,类似pop eip命令
相关寄存器
eax:通用寄存器,保留临时数据,常用于返回值 ebx:通用寄存器,保留临时数据 <font color="#dd0000">ebp</font>:栈底寄存器 <font color="#dd0000">esp</font>:栈顶寄存器 eip:指令寄存器,保存当前指令的下一条指令的地址
什么是栈?
栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。
在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。 在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小
栈总是向下增长(由高地址向低地址)的。
相关知识
- 1.每一次函数调用,都要为本次函数调在栈区上用开辟空间,就是函数栈帧的空间。
- 2.这块空间的维护是使用了2个寄存器: esp和ebp,ebp记录的是栈底的地址,esp记录的是栈顶的地址。
- 3.正在调用哪个函数,ebp,esp就维护哪个函数的栈帧 
函数栈帧
什么是函数栈帧
我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。 那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。
函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放: 1.函数参数和函数返回值 2.临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量) 3.保存上下文信息(包括在函数调用前后需要保持不变的寄存器)
下面用VS2013去演示,每个编译器产生的过程会有差异 演示代码:
#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 c = 0;
	c = add(a, b);
	return 0;
}
函数的调用堆栈
- 首先按F10,进行编译,然后点击调试-->窗口-->调用堆栈 
- 进入堆栈后可以看到main被调用 
这时,我们会困惑main函数在被谁调用
- 继续按F10,让代码运行,当代码执行完时,会发现main函数被一个叫__tmainCRTStartup的函数调用 
这里要知道一点:
main函数也是被其他函数调用的
- 而__tmainCRTStartup函数也是被一个叫mainCRTStartup的函数调用 
- 此时,我们可以获知一个大概的轮廓: 
转到反汇编
- 
F10编译->单击右键->转到反汇编  
- 
然后就可以看见代码对应的汇编代码  
- 
在进入 main之前,main就被__tmainCRTStartup调用,所以此时,走到了main函数中,所以__tmainCRTStartup的函数栈帧已经创建 
- 
1.执行 00341410 push ebp语句,把ebp的值压栈,同时esp指针向上移动,指向栈顶 
这里也可以通过监视窗口看到,执行
push语句之后,esp的地址减小了,就说明esp向上移动了同时,在内存窗口中,也可以看到
ebp的值被存到了esp指向的空间里·
- 2.执行00341411 mov ebp,esp,这句的作用是:把esp的值赋给ebp 
此时
esp,ebp的值相等
- 3.执行00341413 sub esp,0E4h语句,给esp的值减去0E4h 
,这就意味着
esp指向上面的某一块区域,此时esp和ebp之间的空间,就是给main预开辟的空间
- 4.执行这段语句,在栈顶压入三个元素 
- 5.0034141C lea edi,[ebp-0E4h],edi
- 6.00341422 mov ecx,39h把39h这个值赋给ecx
- 7.00341427 mov eax,0CCCCCCCCh把0CCCCCCCCh赋给eax
- 8.0034142C rep stos dword ptr es:[edi]这句的意思是把从edi向下ecx(39h)大小的空间,改为eax的值,也就是0CCCCCCCCh 
此时
main的栈帧已经真正意义上开辟好了
- 9.0034142E mov dword ptr [ebp-8],0Ah,把0Ah的值放到[ebp-8]中去 
在程序中定义变量时进行了初始化,所以在相应位置把值存入栈中,如果没有初始化,栈中对应位置中的值就是
cccccccccc,这也就是有时未初始化变量而导致打印烫烫烫烫烫烫烫烫烫烫烫烫的原因
- 
10. 00341435 mov dword ptr [ebp-14h],14h,把14h的值放到[ebp-14h]中去 
- 
11. 0034143C mov dword ptr [ebp-20h],0把0的值放到[ebp-20h]中去 
- 
12. 00341443 mov eax,dword ptr [ebp-14h]把[ebp-14h]的值放到eax里,00341446 push eax再把eax压入栈中 
- 
13. 00341447 mov ecx,dword ptr [ebp-8]把[ebp-14h]的值放到eax里,0034144A push ecx再把ecx压入栈中 
12.13.步实际上是在传参,先压b,再压a,参数是从右往左传的
- 14.按F11执行0034144B call 00341096语句,会把它下一条语句的地址压到栈顶

把
call的下一条语句压到栈顶的目的是记住当前位置,等调用完函数时,方便回来
15.再按F11,就进入到了add函数里

add函数内
- 
1.执行 003413C0 push ebp,把ebp压入栈顶,此时ebp正在维护主函数,所以压入栈顶的ebp是主函数的ebp 
- 
2. 003413C1 mov ebp,esp与主函数中的类似,这里不多介绍 
- 
3. 003413C3 sub esp,0CCh给esp地址减小0CCh大小,是在为add函数开辟函数栈帧 
- 
4.接下来连续 push三个寄存器,与main函数中的相同,这里不多介绍
- 
5.紧接着四句话与 main函数中的相同,从edi开始到ebp,ecx次赋值为eax也就是0CCCCCCCCh 
- 
6. 003413DE mov dword ptr [ebp-8],0把0放到ebp-8位置上 
- 
7.执行下三句:  
把
ebp+8的值赋给eax,再把ebp+0Ch加到eax上,最后把eax值赋给ebp-8,这就是执行z = x+y语句这里值得思考的是,参数是怎么传的? 形参根本不是再
add函数中创建的,而是回来找在调用add前压入栈顶的参数而说形参就是实参的临时拷贝是十分正确的
- 8.执行return语句,003413EE mov eax,dword ptr [ebp-8]把ebp-8的值赋给eax,即把30存到eax里
add函数栈帧的销毁
- 1.执行三句pop语句 
- 2.003413F4 mov esp,ebp把ebp的值赋给esp 
- 3.003413F6 pop ebp在栈顶弹出一个元素,并将其值赋给ebp,因为此时栈顶元素是main的ebp,所以弹出后,ebp就回到原先在main中的位置 
- 4.003413F7 ret,这句的本质是:弹出栈顶元素,也就是call的下一条语句的地址,并且返回main函数中,call语句的下一条语句 
- 5.00341450 add esp,8把esp地址值减8,即向下移动两个地址,把两个形参弹出栈 
- 6.00341453 mov dword ptr [ebp-20h],eax,将eax的值赋给ebp-20h中,ebp-20h就是c的位置,eax是在add中,将结果寄存到其中的那个寄存器
main函数栈帧的销毁

思考
现在,来思考几个问题: 1.局部变量是如何创建的? 2.为什么局部变量不初始化内容是随机的? 3.函数调用时参数时如何传递的?传参的顺序是怎样的? 4.函数的形参和实参分别是怎样实例化的? 5.函数的返回值是如何带会的?
【文章原创作者:阿里云代理商 http://www.558idc.com/aliyun.html 网络转载请说明出处】1.局部变量就是在其作用域的栈内某一块空间被赋予这个变量的值 2.如果没有初始化,变量在栈的值是
CCCCCCCC3.传参是在调用函数的前一个语句时,创建新的形参压在栈顶,传参的顺序是从右到左4.函数的形参是赋值一个实参的值,将它压入栈顶,而实参就是主函数的栈帧中,将其值赋给某一地址指向的值 5.先把要返回的值放在寄存器中,因为寄存器不会随着栈帧而被销毁,然后在主函数中,会把寄存器中的值赋给接受变量的地址








