当前位置: 首页 > news >正文

《函数栈帧的创建和销毁》

目录

这篇博客的收获

前期知识储备

常用到的汇编指令:

形成共识:

开始讲解

示例描述:

代码示例:

汇编代码:

图文并茂讲解

总结

步骤总结

问题解答

彩蛋时刻!!!


💫只有认知的突破💫才能带来真正的成长💫编程技术的学习💫没有捷径💫一起加油💫

           🍁感谢各位的观看🍁欢迎大家留言🍁咱们一起加油🍁努力成为更好的自己🍁

这篇博客的收获

这篇博客的内容很重要,因为它能帮我们从更底层且深刻的理解函数是怎样创建的。同时,它也是在面试中被常问的问题。所以,我会详细的写这篇博客。这篇博客能解答如下问题:

Q1:局部变量是如何创建的?

Q2: 为什么局部变量不初始化内容是随机的?
Q3: 函数调用时参数时如何传递的?传参的顺序是怎样的?
Q4: 函数的形参和实参分别是怎样实例化的?
Q5: 函数的返回值是如何带会的?

我会把这些问题穿插在整个的讲解过程,我会边结合代码和画图讲解。最后,再对以上的问题进行总结讲解。我能理解大家在开始学习这个知识点的时候,会有点懵逼。因为我也是这样过来的,所以我希望大家能耐住性子继续往后看。我建议大家这篇文章可以多看几遍

前期知识储备

函数栈帧的创建和销毁是建立在汇编语言层面上。我相信大多数都没有学过汇编语言,包括博主我也是没学过汇编。但是,对于理解这个知识点,我们不需要深入了解汇编。我们只需要了解个别基本的汇编指令就OK了。下面所列的几个指令是我们讲解这个知识点要用到的,也就是说大家知道以下几个指令的意思就已经够用了。当然,如果大家对汇编感兴趣的话,也可以自学一下汇编。

常用到的汇编指令:

- - - - - - - - - - - - - - - - - - - - - - - - - - - 寄存器 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

ebp:是栈底寄存器,用来存储栈底的地址

esp:是栈顶寄存器,用来存储栈顶的地址

eax , ebx , ecx : 这几个寄存器不需要知道什么作用,只需要知道它们是用来存储数据就OK

- - - - - - - - - - - - - - - - - - - - - - - - - - - - 指令- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

push(压栈):申请一块内存,把寄存器里面的数据给存放到内存里面。压栈的同时,esp里面会存储新申请的这块内存地址

mov(移动):把一个寄存器里面的值给赋值到另一个寄存器里面

pop(出栈):弹出内存里面的数据,回收内存。出栈的同时,esp里面存放的地址会增大

add(相加):把两个寄存器里面的值相加,或两数值相加。

形成共识:

如果大家看了我以往的博客话,大家对于内存的大概划分是有印象的。内存分为:栈区,堆区和静态区。数组,结构体和函数的创建,这些都是在栈上开辟的空间。内存是以1个字节为最小为单位进行划分的。而且在栈上使用内存有个默认规则:优先使用高地址内存。我们在讲解的时候,所画的图是以4个字节为单位的。因为函数开辟的空间很大,如果我们以1个字节画图的话,所画的图太大了。因为我们的示例代码也是int类型,占有4个字节。所以以4个字节为单位画图,比较好一些。虽然画图的单位不一样,但是对于我们理解这个知识点没有丝毫影响的。

开始讲解

示例描述:

写一个加法函数,这个函数被main函数调用。这个代码的所有数据的数据类型为int。

代码示例:

#include <stdio.h>int Add(int x,int y)
{int z=0;z=x+y;return z;
}int main()
{int a=3;int b=5;int ret=0;ret=Add(a,b);printf("%d\n",ret);return 0;
}

汇编代码:

我教大家怎样转化,转化的步骤如下所示:

我们先把程序进入调试模式(ctrl+F5),鼠标放在界面任意空白处。然后鼠标右键,点击转为反汇编,就可以转成功了。如图所示:

以下就是转化后的汇编代码。提示:下面版本的汇编代码是vs2019版本的,因为2019之前的版本对于汇编的封装比较简单,便于我们观察。vs2022版本的封装就比较复杂一些,不利于我们观察。所以,我们就以vs2019这个版本为主。如下所示的汇编代码:

int Add(int x, int y)
{
00BE1760 push ebp     //将main函数栈帧的ebp保存,esp-4
00BE1761 mov ebp,esp //将main函数的esp赋值给新的ebp,ebp现在是Add函数的ebp
00BE1763 sub esp,0CCh //给esp-0xCC,求出Add函数的esp
00BE1769 push ebx //将ebx的值压栈,esp-4
00BE176A push esi //将esi的值压栈,esp-4
00BE176B push edi //将edi的值压栈,esp-4
int z = 0;
00BE176C mov dword ptr [ebp-8],0 //将0放在ebp-8的地址处,其实就是创建z
z = x + y;    //接下来计算的是x+y,结果保存到z中
00BE1773 mov eax,dword ptr [ebp+8] //将ebp+8地址处的数字存储到eax中
00BE1776 add eax,dword ptr [ebp+0Ch] //将ebp+12地址处的数字加到eax寄存中
00BE1779 mov dword ptr [ebp-8],eax //将eax的结果保存到ebp-8的地址处,其实就是放到z中
return z;
00BE177C mov eax,dword ptr [ebp-8] //将ebp-8地址处的值放在eax中,其实就是把z的值存储到eax寄存器中,这里是想通过eax寄存器带回计算的结果,做函数的返回值。
}
00BE177F pop edi
00BE1780 pop esi
00BE1781 pop ebx
00BE1782 mov esp,ebp
00BE1784 pop ebp
00BE1785 retint main()
{
//函数栈帧的创建
00BE1820 push ebp
00BE1821 mov ebp,esp
00BE1823 sub esp,0E4h
00BE1829 push ebx
00BE182A push esi
00BE182B push edi
00BE182C lea edi,[ebp-24h]
00BE182F mov ecx,9
00BE1834 mov eax,0CCCCCCCCh
00BE1839 rep stos dword ptr es:[edi]
//main函数中的核心代码
int a = 3;
00BE183B mov dword ptr [ebp-8],3
int b = 5;
00BE1842 mov dword ptr [ebp-14h],5
int ret = 0;
00BE1849 mov dword ptr [ebp-20h],0
ret = Add(a, b);
00BE1850 mov eax,dword ptr [ebp-14h]
00BE1853 push eax
00BE1854 mov ecx,dword ptr [ebp-8]
00BE1857 push ecx
00BE1858 call 00BE10B4
00BE185D add esp,8
00BE1860 mov dword ptr [ebp-20h],eax
printf("%d\n", ret);
00BE1863 mov eax,dword ptr [ebp-20h]
00BE1866 push eax
00BE1867 push 0BE7B30h
00BE186C call 00BE10D2
00BE1871 add esp,8
return 0;
00BE1874 xor eax,eax
}

我们就按照上面汇编代码的顺序进行讲解。我们之前也有提到过,main函数也是被调用。在vs2022中,main函数是被__tmainCRTStartup函数调用的,__tmainCRTStartup函数是被tmainCRTStartup函数调用。如下所示的三者关系:

图文并茂讲解

第一步:

esp(栈顶指针)ebp(栈底指针),这俩指针(寄存器)是用来维护一段函数的空间的。main函数也是被别的函数调用的,也就是说在创建main函数之前,就已经先创建好了__tmain...和tmain...函数。如图所示:

 接下来我们就按照汇编代码来进行画图讲解,我们先从main函数的创建开始。如图所示:

main函数是被__tmainCRTStartup函数调用的,所以在创建main函数之前,就已经创建好了__tmainCRTStartup的函数空间,也就是esp和ebp所维护的栈帧空间。创建main函数空间前,先执行push ebp指令,会把ebp里面的值给压栈,同时esp会指向新开辟的空间地址。也就是如上图所示的结果。

第二步:

指令mov ebp,esp。把esp里面的值赋值到ebp,那么ebp指针就和esp指针指向了同一块空间。如图所示的结果:

第三步:

指令 sub esp,0E4h。意思就是:esp里面的地址减去0E4h,然后再赋值给esp。esp里面的地址减少后,就会往上移动到新的空间(移动到低地址),它和ebp之间相差了228个空间。如图所示:

 228个地址空间太多了,我就简单画了几个空间表示一下。

第四步:

没什么可说的,就是依次对图示的指令进行操作就OK了。如图所示:

第五步:

指令 lea edi,[ebp-24h] 。意思是:把ebp-24h后的地址给存储到edi寄存器里面。指令 mov ecx,9。意思是:把9给存储到ecx寄存器里面。指令 mov eax,0CCCCCCCCh。意思是:把0CCCCCCCCh值存储到eax寄存器。指令 rep stos dword ptr es:[dei] 意思是:rep就是repeat(重复)的意思,也就是说这条语句会被重复执行。至于重复多少次是由ecx里面的值决定的。也就是说,会从edi对应的地址开始,一直循环9次,在循环的过程中,会对每个空间内存进行0CCCCCCCCh值的初始化。每个空间的大小是4个字节,执行9次,刚好会循环36个空间。而24h==36。所以刚好循环到ebp就结束。其实,上面所说的循环过程,可以进行如下所示的代码:

edi=ebp-0x24h;
ecx=9;
eax=0CCCCCCCCh;for(;ecx>0;--ecx,edi+=4)
{*(int*)edi=eax;
}

如下图所示:

第六步:

 指令 mov操作,依次把数值放入对应的空间里面。如图所示:

从上面的图,我们可以得出一个我们之前讲到的一个习惯——我们创建变量的时候,最好是初始化,要不然就会随机值。上面的图就可以解释这一现象。

第七步:

这几个指令的操作就是为了创建形参,然后把实参的值传给形参。也就是我们之前所说的,形参是实参的一份拷贝。所以,在调用函数之前,先创建形参,再传给形参数值。

第八步:

call指令就是调用函数的意思。在调用函数之前,会先把下一条指令的地址给压栈。目的:在调用函数之后能找到下一条指令。如下图所示:

第九步:

接下来的操作就是创建Add函数,它和创建main函数一样的过程。如下图所示:

第十步:

mov ...[ebp-8],0 把0赋值给z对应的空间。mov eax...[ebp+8] 把x形参对应的数值3给存储到eax寄存器。 add eax...[ebp+0ch] 把ebp+0ch对应的空间里面的值——也就是y形参对应的数值5和eax里面的值进行相加,再存储到eax寄存器里面。此时eax寄存器里面存储的值就是x+y的最后结果8。最后再把eax里面的数值给存储到ebp-8对应的空间,也就是说z对应的空间,此时z=8。如图所示:

第十一步:

再把z=8存储到eax寄存器里面。如图所示:

return z;
00BE177C mov eax,dword ptr [ebp-8] //将ebp-8地址处的值放在eax中,其实就是把z的值存储到eax寄存器中,这里是想通过eax寄存器带回计算的结果,做函数的返回值。

第十二步(栈帧的销毁):

依次对edi,esi和ebx空间进行pop弹出(内存空间回收)。pop的同时,esp指针也要向下移动。如下图所示:

回收内存后,如图所示:

第十三步:

mov esp,ebp 把ebp里面的值复制给esp,esp就和ebp指向同一块空间了。0cch个空间会被直接销毁(回收)。然后 pop ebp。Add创建的栈帧空间就彻底被回收干净了。如图所示:

 第十四步(返回值):

00BE1785 ret

从第十一步到第十三步,可以看出来,再函数栈帧销毁之前,会先把最后的返回值给保存在寄存器里面。最后,函数栈帧销毁之后,才返回返回值。这也就是我们之前提到过的——在函数销毁之前,会把返回值先存储到寄存器里面,然后返回寄存器里面的数值。

总结

步骤总结

以上十四步就是核心步骤,至于Add后面的指令操作都是重复的。大家可以按照我给的方法和思路进行画图操作。

问题解答

经过前面的讲解,对于开头的几个问题,我们就可以进行解答了。

Q1:局部变量是如何创建的?

        局部变量是在函数栈帧里面创建,经过一些列的push(压栈)操作,并会进行初始化。

Q2:为什么局部变量不初始化内容是随机的?
         会执行 rep指令,会对其一部分空间进行0cccccccch值的初始化(具体是什么值依据编译器),所以不初始化的话,会被赋值0cccccccch值(随机值)。
Q3:函数调用时参数时如何传递的?传参的顺序是怎样的?
          先进行形参的空间压栈,然后再把实参的值,赋值给形参压栈的空间内存里面。
         从右向左依次进行传参。
Q4:函数的形参和实参分别是怎样实例化的?
         实参是在函数内部进行实例化的,形参是在函数栈顶开辟的空间,然后由实参进行传值实例化
Q5:函数的返回值是如何带会的?
        先把返回值存放在寄存器里面,然后再进行栈帧的销毁。

彩蛋时刻!!!

每章一句:山有顶峰,湖有彼岸,在人生的漫漫长途中,万物皆有回转,当我们觉得余味苦涩,请你相信,一切皆有回甘”。

相关文章:

  • 【Fifty Project - D32】
  • HTML5基础
  • github actions入门指南
  • C++030(内联函数)
  • Vision Pro发布!开发者如何快速上手空间UI设计?
  • 深入理解计算机科学中的“递归”:原理、应用与优化
  • 我的世界模组开发——方块的深入探索(1)
  • 【深度学习-pytorch篇】5. 卷积神经网络与LLaMA分类模型
  • qemu安装risc-V 64
  • WPF的基础设施:XAML基础语法
  • 利用仿真软件学习一下RC无源滤波和有源滤波电路
  • 第二节 LED模块
  • 电脑革命家测试版:硬件检测,6MB 轻量无广告 清理垃圾 + 禁用系统更新
  • Nacos注册中心原理
  • 算法-背包问题
  • 交换机环路故障分析以及解决方案
  • CAD背景怎么改成黑色?
  • web第七次课后作业--springbootWeb响应
  • 大型软件系统日志记录最佳实践
  • 153. 寻找旋转排序数组中的最小值
  • 网站在线客服插件代码/北京网络推广公司排行
  • 对于网站建设提出建议/推广手段有哪些
  • 网站建设 论文/大数据营销系统多少钱
  • 做体育网站/福州网站建设方案外包
  • 网站建设公司怎么做业务/现在的网络推广怎么做
  • 网站建设需求指引/搜索引擎优化的简写是