函数栈帧的创建和销毁
目录
1. 引言
2. 什么是函数栈帧?
3.什么是寄存器?
4. 什么是栈?
5. 什么是反汇编?
6. 函数栈帧能解决的问题?
7. 函数栈帧的创建和销毁
7.1 函数调用
7.2 函数堆栈
7.3 反汇编
编辑
7.4 main函数和Add函数的函数栈帧的创建与销毁
8. 结语
1. 引言
在学习C语言的过程中,我们肯定有很多问题:
- 局部变量是怎么创建的?
- 为什么局部变量的值是是随机值?
- 函数是怎么传参的?传参的顺序是怎样的?
- 形参和实参是什么关系?
- 函数调用是怎么做到的?
- 函数调用结束后是怎么返回的?
当你看完这篇博客后,相信你对这些问题会有充分的理解,有助于你后期继续学习C语言。
下面讲的内容是在vs2013环境下进行的,因为越高级的编译器,越不容易观察和学习。
当然在不同的编译器下面,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。
2. 什么是函数栈帧?
函数栈帧是计算机程序执行过程中用于管理函数调用和返回的一种数据类型,存在于内存的栈区,每个函数在调用的时候都会向内存的栈区申请一块空间,这块空间就是函数栈帧,
用于存储:函数执行所需的局部变量,形参,返回地址以及保存的寄存器状态等信息。
每一个函数对应一个函数栈帧。
3.什么是寄存器?
在计算机体系中,寄存器是CPU内部的高速存储单元,用于临时存放指令,数据和地址,它们的访问速度快于内存(RAM),数量少。
常见的寄存器类型:
而函数栈帧这里,主要用到esp和ebp这两个寄存器。
4. 什么是栈?
内存中存在栈区,堆区,静态区等,栈区是管理函数调用的嵌套关系(如保存返回地址,参数,局部变量),并且可以动态分配和释放函数栈帧。
栈的生命周期是:
函数调用->函数执行->函数返回
5. 什么是反汇编?
反汇编是将计算机执行的二进制码转换为汇编语言的过程,二进制码是给机器识别的,汇编语言是方便人理解,计算机在执行指令时的底层运行思维。
常见的汇编指令:
- mov :移动数据。 例如:mov eax,ebx ->意思是将ebx里面的值存放到eax里面
- sub : 减法。
- call : 调用函数并保存返回地址。
- ret :从调用的函数返回并恢复返回地址。
- push : 压栈。例如:push ebp ->意思是将ebp压入栈中。
- pop : 弹栈。例如:push ebp ->意思是将弹出当前栈顶元素并赋值给ebp寄存器。
6. 函数栈帧能解决的问题?
- 局部变量是怎么创建的?
- 为什么局部变量的值是是随机值?
- 函数是怎么传参的?传参的顺序是怎样的?
- 形参和实参是什么关系?
- 函数调用是怎么做到的?
- 函数调用结束后是怎么返回的?
7. 函数栈帧的创建和销毁
7.1 函数调用
esp和ebp这两个寄存器是用来存放地址的,esp存放的是函数栈顶的地址,ebp存放的是函数栈底的元素,而每次函数调用都要在栈区创建一块空间,这块空间也被称为函数栈帧,而esp和ebp是用来维护函数栈帧的。
左图是在调用main函数时,向栈区申请的空间(红色区域),也被称为main函数的函数栈帧,而esp和ebp分别是维护main函数的函数栈帧的,而在vs编译器中栈区下面是高地址,上面是低地址。
7.2 函数堆栈
函数堆栈是计算机程序执行时用于管理函数调用和返回的一种栈结构数据,主要用于记录函数的调用顺序,保存函数的执行上下文,确保程序能正确从调用函数返回到被调用函数的位置继续执行。
我这里用vs2013编写下面代码来讲解。
#include <stdio.h>int Add(int x, int y)
{int z = 0;z = x + y;return z;
}int main()
{int a = 0;int b = 0;int c = 0;c = Add(a, b);printf("%d\n", c);return 0;
}
对上面代码进行调试,然后按照下图找到反汇编。
随后将代码调试完,会出现下图的内容:
通过往上翻,我们可以找到哪一个函数调用的main函数。
由此可见mainCRTStartup函数调用了__tmainCRTStartup函数,__tmainCRTStartup函数又调用了main函数,而我又写了一个自定义函数Add函数,main函数调用Add函数。
上面四个函数在栈区里面的空间大概就是这样。先调用 mainCRTStartup函数,再调用__tmainCRTStartup函数,再调用main函数,最后调用Add函数。
7.3 反汇编
将上述代码调试起来后,打开反汇编。
下图是调用main函数的__tmainCRTStartup函数在栈区的函数栈帧:
接下来就按照下面反汇编里面的代码来执行:
7.4 main函数和Add函数的函数栈帧的创建与销毁
创建main函数的函数栈帧
第1条汇编指令 push ebp,push的意思是压栈,将ebp的值压入到栈中,并且esp上移到新栈顶,如下图:
此时,esp上移后指向的地址存放的ebp里面的值,如下图:
第2条汇编指令 mov ebp,esp,意思是将esp的值给ebp:如下图:
此时相当于esp的值给了ebp,ebp存放的地址变的跟esp的值一样。
第3条汇编指令 sub esp,0E4h, 意思是esp减去0E4h。
esp的值减小,如下图:
第4条汇编指令 push ebx
第5条汇编指令 push esi
第6条汇编指令 push edi 意思是将ebx,esi,edi,三个寄存器压到栈里面:
此时esp指向的地址里面存放着edi里面的值:
第7条汇编指令 lea edi,[ebp-0E4h],lea是load effective address(加载有效地址)的意思,将ebp-0E4h有效地址放到edi里面。
此时edi里面的值等于ebp-0E4h:
第8条汇编指令 mov ecx,39h ecx寄存器
第9条汇编指令 mov eax,0CCCCCCCCh eax寄存器
第10条汇编指令 rep stos dword ptr es:[edi] dword是四个字节
这三条指令意思是从edi里面存放的地址开始向下39h次,每次4个字节的地址替换成0CCCCCCCCh。也就是ebp-0E4h到ebp之间的地址都变成cccccccc:
此时 edi存的是ebp指向的地址:
下面为了直观的看到变量的地址,我们需要把显示符号名选项不勾选:
int a = 0;
第11条汇编指令 mov dword ptr [ebp-8] , 0,将a=0的值传给ebp-8这个地址。
此时ebp-8这个地址存的值为0,如下图:
注意:这里你会发现,如果你不给a赋初值,在编译器中打印这个值,会显示烫烫烫烫烫烫烫烫烫烫烫烫的报错,就是因为没有赋初值,系统提供的随机值cccccccc,编译器才会显示这样的结果。
int b = 0;
第12条汇编指令 mov dword ptr [ebp-14h] , 0 将b=0的值传给ebp-14h这个地址。
int c = 0;
第13条汇编指令 mov dword ptr [ebp-20h] , 0 将c=0的值传给ebp-20h这个地址。
不同的编译器栈中存放两个变量相隔的地址不同,vs编译器两个变量间相隔两个地址。
此时ebp-14h 存的 b=0,ebp-20h 存的 c=0。
c = Add( a, b);
第14条汇编指令 mov eax,dword ptr [ebp-14h] ,把eax-14h的值放到eax寄存器中。
第15条汇编指令 push eax ,把eax的值压到栈中。
第16条汇编指令 mov ecx,dword ptr [ebp-8] , 把eax-8的值放到ecx寄存器中。
第17条汇编指令 push ecx ,把ecx的值压到栈中。
此时esp上移后指向的是存放ecx的值0:
第18条汇编指令 call 00E710E1,call指令是调用自定义函数Add,并且保存下一条指令的地址,压到栈里面:
为什么要将call指令的下一条指令的地址,压到栈里面呢?
因为在执行call指令要去调用自定义函数Add,调用完Add函数后还要返回主函数继续执行下面的代码,直到程序结束,那如何在执行完Add函数返回到主函数中,这就用到压到栈中的call指令的下一条指令的地址,在运行完Add函数后,释放Add函数的栈区,跳到call指令下一条地址的位置时,接着执行后面的代码,直到主函数运行完成。
此时esp指向的位置存的地址是call指令的下一条指令的地址00e71450:
下面开始进入Add函数里面,开始执行:
创建Add函数的函数栈帧
第19条汇编指令 push ebp,将ebp的值压入栈中:
此时ebp指向的地址是main函数栈底地址,将ebp指向的地址传给esp,此时esp上移,指向的地址存的main函数栈底地址:
第20条汇编指令 mov ebp,esp,将esp指向的地址传给ebp:
第21条汇编指令 sub esp,0CCh,用esp减去0CCh:
相当于esp先将地址传给ebp,esp减去0CCh后指向的位置是esp新指向的位置,此时ebp-0CCh指向的位置等于esp指向的位置:
第22条汇编指令 push ebx ,将ebx压入栈中
第23条汇编指令 push esi ,将esi压入栈中
第24条汇编指令 push edi ,将edi压入栈中
第25条汇编指令 lea edi,[ebp-0CCh] 将ebp-0CCh的值给edi
第26条汇编指令 mov ecx,33h
第27条汇编指令 mov eax,0CCCCCCCCh
第28条汇编指令 rep stos dword ptr es:[edi] dword是四个字节
26,27,28三条汇编指令把从ebp-0CCh所在的地址开始,共33h次,每次四个字节的地址都改为
0CCCCCCCCh,也就是CCCCCCCC:
此时从ebp-0CCh的地址到ebp的地址都为CCCCCCCC:
int z = 0;
第29条汇编指令 mov dword ptr [ebp-8] , 0,将定义的z=0的值给ebp-8地址处:
此时ebp-8指向的地址存放的是z的值0:
z = x + y;
第30条汇编指令 mov eax,dword ptr [ebp+8] ebp+8存的参数a的值0给寄存器eax
第31条汇编指令 add eax,dword ptr [ebp+0Ch] ebp+0Ch存的参数b的值0给寄存器加 上a的值,此时ebp存的a+b的值。
第32条汇编指令 mov dword ptr [ebp-8],eax 将eax的值传给ebp-8指向的地址(z存值的 地方)
这里我们发现在进入Add函数前,main函数已经将a和b的值拷贝一份压到栈中,当Add函数调用参数时,只用访问栈中的地址得到a和b的值,进行计算就行,而在Add函数中并未创建新的地址存储a和b的值,说明传参的过程中,形参是实参的一份临时拷贝,改变形参的值并不会改变实参的值。
return z;
第33条汇编指令 mov eax,dword ptr [ebp-8] ,将ebp-8里面存的a+b的值存到eax寄存器里面。
相当于调用Add函数算出的结果(返回值)先放到eax这个寄存器中。
释放Add函数的函数栈帧
第34条汇编指令 pop edi ,
第35条汇编指令 pop esi ,
第36条汇编指令 pop ebx ,
这三条指令是依次弹出栈顶的三个元素,依次放到edi ,esi ,ebx ,这三个寄存器中。
此时 esp 指向的地址等于 ebp-0CCh 指向的地址 。
第37条汇编指令 mov esp,ebp,把ebp指向的地址传给esp
第38条汇编指令 pop ebp,把ebp指向的地址里面的main函数的函数栈帧栈底地址,放到 ebp里面。
此时esp指向的地址存放着call指令的下一条指令的地址。
第39条汇编指令 ret ,弹出栈顶存储的call指令的下一条指令的地址,回到main函数里面接着call 指令的下一条指令继续执行。
c = Add( a, b);
第39条汇编指令 add esp,8 ,将esp指向的地址加 8。这时候存储在栈中的两个形参的值被 销毁。
第40条汇编指令 mov dword ptr [ebp-20h] , eax,将eax存放的Add函数计算出来的返回 值,传给ebp-20h这个地址里存的c,此时 c 接收了Add函数的返回值。
到这里main函数调用Add函数,并返回值的过程就结束了,后面就是打印返回值到屏幕上,最后再销毁main函数的函数栈帧(跟Add函数栈帧销毁一样的道理),我就不再细说。
就是下图的汇编代码:
8. 结语
上面我们讲到函数如何在栈里面开辟函数栈帧,变量在内存中如何创建,什么啥时候创建参数,如何传递参数,参数如何销毁,返回值如何返回主函数中,函数栈帧如何销毁,相信大家也有所了解函数栈帧的创建与销毁。希望这篇文章对大家有所帮助。