细谈函数栈帧的创建与销毁

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

函数栈帧是如何创建的又是如何销毁的涉及到内存空间的本质问题我们大多数人可能是一头雾水栈是什么有什么用接触细节原理印象才能更加深刻带你深入的学习栈是如何创建与销毁能帮助你理解更多深层的问题局部变量是如何创建的为什么一开始要从main函数开始呢函数的形参与实参有和关系返回值又怎么返回的等等问题。理解这些问题你对编程的理解将会大大提升的。

一. 什么是函数栈帧

我们在写C语言代码时经常会把一个独立的功能抽象为函数所以C程序是以函数为基本单位的。
那函数如何调用函数的返回值如何返回的函数参数是如何传递的这些问题都与函数栈帧有关系。

函数栈帧stack frame就是函数调用过程中在程序的调用栈所开辟的空间这些空间是用来存放

  • 函数参数和函数返回值
  • 局部变量
  • 保存上下文信息包括在函数调用前后需要保持不变的寄存器。

二. 理解函数栈帧能解决什么问题呢

理解函数栈帧有什么用呢

只要理解了函数栈帧的创建和销毁一下问题就能很好的理解了

  • 局部变量是如何创建的
  • 为什么局部变量不初始化内容是随机值
  • 函数调用时参数是如何传递的传参的顺序是怎样的
  • 函数的形参与实参分别是怎么实例化的
  • 函数的返回值是如何带回来的

等讲完函数栈帧的创建于销毁的过程后这些问题我会一一解释的。

三.剖析函数栈帧的创建和销毁

1.什么是栈

栈stack是现代计算机程序里最为重要的概念之一几乎每一个程序都使用了栈没有栈就没有函
数没有局部变量也就没有我们如今看到的所有的计算机语言。

在经典的计算机科学中栈被定义为一种特殊的容器用户可以将数据压入栈中入栈push也可
以将已经压入栈中的数据弹出出栈pop但是栈这个容器必须遵守一条规则先入栈的数据后出
栈First In Last Out FIFO。就像叠成一叠的术先叠上去的书在最下面因此要最后才能取出。

在计算机系统中栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中也可以将数据
从栈顶弹出。压栈操作使得栈增大而弹出操作使得栈减小。
在经典的操作系统中栈总是向下增长由高地址向低地址的。
在我们常见的i386或者x86-64下栈顶由成为 esp 的寄存器进行定位的。

2.认识相关寄存器和汇编指令

寄存器寄存器是独立于内存是集成到CPU上的
硬盘
内存
寄存器
三者都是存放内存的是相互独立的。

2.1相关寄存器

eax: 通用寄存器保留临时数据常用于返回值。
ebx通用寄存器保留临时数据
ebp栈底寄存器
esp栈顶寄存器
eip指令寄存器保存当前指令的下一条指令的地址。

2.2相关汇编命令

mov:数据转移指令
push数据入栈同时esp栈顶寄存器也要发生改变
pop数据弹出至指定位置同时esp栈顶寄存器也要发生改变
sub减法命令
add加法命令
call函数调用1.压入返回值2.转入目标函数
jump通过修改eip转入目标函数进行调用
ret恢复返回地址压入eip类似pop eip命令

3.解析函数栈帧的创建和销毁

3.1相关知识

首先我们需要知道一些基本的知识才能有效的帮助我们理解函数栈帧的创建和销毁。

  1. 函数每次调用的时候都要为本次函数开辟空间就是函数栈帧的空间
  2. 这块函数栈帧空间是由两个寄存器维护的esp ebpebp是维护记录栈底的地址esp是维护记录栈顶的地址。所谓栈顶栈底是根据栈的使用方式来确定的栈是先使用的后弹出就像书本从上往下叠而书本的下面是栈底书的上面是栈顶。
    例如
    在这里插入图片描述
  3. 函数栈帧的创建和销毁在不同的编译器的实现方法大同小异本次演示的是VS2019。
  4. 栈空间的使用方式是先使用高地址的空间再使用低地址的空间
  5. push压栈给栈顶放一个元素。pop出栈从栈顶删除一个元素。
    在这里插入图片描述

3.2函数的调用堆栈

当我们按F10调试进入函数的调用堆栈里发现main函数被调用了。可是main函数又被谁调用的呢

在这里插入图片描述

函数调用堆栈是反馈函数调用逻辑的那我们可以清晰的观察到main函数调用之前是由谁调用main函数的。
main函数是由一个_…main函数调用的。

在_…main函数之前还有函数调用我们先不管。

那我们可以确定_…main函数应该也会有自己的函数栈帧main函数和Add函数也会维护自己的栈帧每个函数栈帧都有自己的ebpesp来维护栈帧空间。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
所以
在这里插入图片描述
在main函数调用之前已经有函数在开辟空间了。
在这里插入图片描述
好的理解这个我们就从main函数的栈帧创建开始将

3.3转到反汇编

调试到main函数开始执行的第一行右击鼠标转到反汇编上。
在这里插入图片描述

int main()
{
//函数栈帧的创建
00C21410 	 push                   ebp
00C21411 	 mov                    ebp,esp 
中00C21413	 sub                    esp, 0E4h
00C21419 	 push                   ebx
00C2141A 	 push                   esi
00C2141B 	 push                   edi                   
00C2141C 	lea                     edi, [ebp+FFFFFF1Ch]
00C21422 	mov                     ecx, 39h
00C21427 	mov                     eax, OCCCCCCCCh
00C2142C 	rep stos                dword ptr es:[edi]

//main函数中的核心代码
inta=10;
00C2142E	 mov                    dword ptr [ebp-8], 0Ah
intb=20;
00C21435	 mov                    dword ptr [ebp-14h],14h
intC=0;
00C2143C	 mov                    dword ptr [ebp-20h],0
C = Add(a, b);00C21443 8B 45 EC      mov        eax, dword ptr [ebp-14h]
00C21446 50            	push		eax
00C21447 8B 4D F8		mov			ecx,dword ptr [ebp-8]
00C2144A 51				push		ecx
00C2144B E8 91 FC FF FF call		00C210E1
00C21450 83 C4 08  		add			esp,8
00C21453 89 45 E0		mov			dword ptr [ebp- 20h],eax



3.4剖解函数栈帧的创建

上面是main函数转化而来的汇编代码

接下来我们就一行一行的拆解汇编代码。

int main()
{
//函数栈帧的创建
00C21410 	 push                   ebp   把ebp寄存器中的值进行压栈此时的ebp中存放的是_tmainCRTStartup函数栈帧的ebpebp-4
00C21411 	 mov                    ebp,esp move指令会把esp的值存放到ebp中相当于产生了main函数的ebp这个值就是_tmainCRTStartup函数栈帧的esp00C21413	 sub                    esp, 0E4h//sub会让esp中的地址减去一个16进制数字0xe4h,产生新的
//esp此时的esp是main函数栈帧的esp此时结合上一条指令的ebp和当前的espebp和esp之间维护了一
//个块栈空间这块栈空间就是为main函数开辟的就是main函数的栈帧空间这一段空间中将存储main函数中的局部变量
00C21419 	 push                   ebx//将寄存器ebx的值压栈esp-4
00C2141A 	 push                   esi//将寄存器esi的值压栈esp-4
00C2141B 	 push                   edi//将寄存器edi的值压栈esp-4         
//上面3条指令保存了3个寄存器的值在栈区这3个寄存器的在函数随后执行中可能会被修改所以先保存寄
//存器原来的值以便在退出函数时恢复。      

//下面的代码是在初始化main函数的栈帧空间。
//1. 先把ebp-0e4h的地址放在edi中
//2. 把9放在ecx中
//3. 把0xCCCCCCCC放在eax中
//4. 将从edp-0e4h到ebp这一段的内存的每个字节都初始化为0xCC   
00C2141C 	lea                     edi, [ebp+FFFFFF1Ch]
00C21422 	mov                     ecx, 39h
00C21427 	mov                     eax, OCCCCCCCCh
00C2142C 	rep stos                dword ptr es:[edi]

//main函数中的核心代码
inta=10;
00C2142E	 mov                    dword ptr [ebp-8], 0Ah
intb=20;
00C21435	 mov                    dword ptr [ebp-14h],14h
intC=0;
00C2143C	 mov                    dword ptr [ebp-20h],0
C = Add(a, b);00C21443 8B 45 EC      mov        eax, dword ptr [ebp-14h]
00C21446 50            	push		eax
00C21447 8B 4D F8		mov			ecx,dword ptr [ebp-8]
00C2144A 51				push		ecx
00C2144B E8 91 FC FF FF call		00C210E1
00C21450 83 C4 08  		add			esp,8
00C21453 89 45 E0		mov			dword ptr [ebp- 20h],eax

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
所以你知道为什么局部变量不初始化会变成随机值了吗
因为在函数栈帧开辟的时候栈帧里面已经被初始化成cccccccc是不是有时你还会莫名其妙的打印出

烫烫烫烫烫烫烫烫烫~就是因为这个原因。
在这里插入图片描述
之所以上面的程序输出“烫”这么一个奇怪的字是因为main函数调用时在栈区开辟的空间的其中每一
个字节都被初始化为0xCC而arr数组是一个未初始化的数组恰好在这块空间上创建的0xCCCC两
个连续排列的0xCC的汉字编码就是“烫”所以0xCCCC被当作文本就是“烫”。

以上就是main函数栈帧的开辟过程没错这只是开辟main函数栈帧空间main函数的功能还没实现接下来再来看看main函数是如何实现功能的。

//main函数中的核心代码--main函数功能的实现
inta=10;
00C2142E	 mov                    dword ptr [ebp-8], 0Ah
//将Ah存储到ebp-8的地址处ebp-8的位置其实就a变量
intb=20;
00C21435	 mov                    dword ptr [ebp-14h],14h
//将14h存储到ebp-14h的地址处ebp-14h的位置就是b变量
intC=0;
00C2143C	 mov                    dword ptr [ebp-20h],0
//将0存储到ebp-20h的地址处ebp-20h的位置其实是ret变量

以上汇编代码就表示局部变量a,b,ret的创建和初始化

所以局部变量的创建是在栈帧空间中创建的
在这里插入图片描述
好了main函数的局部变量也创建好了要准备进入Add函数了
也就是要调用Add函数了,不过Add函数是怎么传参的呢
请往下面接着看

//调用Add函数
C = Add(a, b);
//调用Add函数时的传参
//其实传参就是把参数push到栈帧空间中00C21443 8B 45 EC      mov        eax, dword ptr [ebp-14h]
//传递b将ebp-14h处放的20放在eax寄存器
00C21446 50            	push		eax//将eax的值压栈esp-4
00C21447 8B 4D F8		mov			ecx,dword ptr [ebp-8]
//传递a将ebp-8处放的10放在ecx寄存器中
00C2144A 51				push		ecx//将ecx的值压栈esp-4
//跳转调用函数
00C2144B E8 91 FC FF FF call		00C2145083C408//看这是什么这是call指令下一条的地址。为什么这样呢
//因为函数调用后还要回来继续执行下面的代码所以将下面的指令的地址记住如何等函数结束时能找回来
00C21450 83 C4 08  		add			esp,8
00C21453 89 45 E0		mov			dword ptr [ebp- 20h],eax

在这里插入图片描述
call 指令是要执行函数调用逻辑的在执行call指令之前先会把call指令的下一条指令的地址进行压栈
操作这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方继续往后执行。

call调用函数后就会进入Add函数内部下面就是Add函数的反汇编了看看前面是不是跟main函数很像呢没错前面也是在为Add函数预开辟栈帧空间。我们再来好好顺一顺吧。

在这里插入图片描述
在这里插入图片描述

预栈帧空间
在这里插入图片描述
在这里插入图片描述
图片中的 ecx 和 eax 其实就是 Add 函数的形参 x , y 。这里的分析很好的说明了函数的传参过程以及

函数在进行值传递调用的时候形参其实是实参的一份拷贝。对形参的修改不会影响实参。
在这里插入图片描述
代码执行到Add函数的时候就要开始创建Add函数的栈帧空间了。

在Add函数中创建栈帧的方法和在main函数中是相似的在栈帧空间的大小上略有差异而已

  1. . 将main函数的 ebp 压栈
  2. 计算新的 ebp 和 esp
  3. 将 ebx esi edi 寄存器的值保存
  4. 计算求和在计算求和的时候我们是通过 ebp 中的地址进行偏移访问到了函数调用前压栈进去的参数这就是形参访问。
  5. 将求出的和放在 eax 寄存器尊准备带回

以上就是main函数和Add函数栈帧的创建。接下来我们来看函数栈帧是如何销毁的呢

3.5函数栈帧的销毁

当函数调用要结束返回的时候前面创建的函数栈帧也开始销毁。
那具体是怎么销毁的呢我们看一下反汇编代码

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
所以最后就还剩下main函数的栈帧空间
在这里插入图片描述

3.6.拓展了解

其实返回对象时内置类型时一般都是通过寄存器来带回返回值的返回对象如果时较大的对象时一
般会在主调函数的栈帧中开辟一块空间然后把这块空间的地址隐式传递给被调函数在被调函数中
通过地址找到主调函数中预留的空间将返回值直接保存到主调函数的。具体可以参考《程序员的自我修养》一书的第10章。

四.总结

我们来总结一下以上问题已经相关注意点。

1.局部变量是如何创建的

1.首先先为函数分配栈帧空间
2.初始化部分栈帧空间ccccccc
3.然后在函数栈帧里给局部变量分配一些空间

2.为什么局部变量不初始化内容是随机值

因为变量是我们自己放进去的如果初始化就会把随机值覆盖了不初始化就会保留原来的随机值

3.函数调用时参数是如何传递的传参的顺序是怎样的

1.在调用函数之前系统就先push push 把这两个参数从右向左开始压栈进去。
2.而真正到函数调用的时候我们是通过指针的偏移量来找到形参

4.函数的形参与实参分别是怎么实例化的

形参确实是在我们压栈时开辟的空间它和形参只是值上相同但空间是独立的是不同的空间
故形参是实参的临时拷贝改变形参不会影响实参

5.函数的返回值是如何带回来的

函数的返回值一般是通过存放在寄存器中然后带回来的

6.注意点

1.我们在调用之前就把call指令的下一条指令地址记住了存进去了压进去了

2.我们在调用的时候就把main函数指向栈底的地址记住下存进去压进去了然后最好pop弹出去把这个地址放进edp中edp就更新到mian函数栈底去了

3.epb和eps是存放维护当前栈空间函数的栈底地址跟函数地址没有关系。它当有函数调用时就维护这个函数栈帧有另一个函数调用时就维护另一个函数栈帧。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

“细谈函数栈帧的创建与销毁” 的相关文章