(函数栈帧的创建和销毁)
预备知识:
相关汇编命令
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.如果没有初始化,变量在栈的值是
CCCCCCCC
3.传参是在调用函数的前一个语句时,创建新的形参压在栈顶,传参的顺序是从右到左 4.函数的形参是赋值一个实参的值,将它压入栈顶,而实参就是主函数的栈帧中,将其值赋给某一地址指向的值 5.先把要返回的值放在寄存器中,因为寄存器不会随着栈帧而被销毁,然后在主函数中,会把寄存器中的值赋给接受变量的地址