• 函数栈帧的创建和销毁(加深递归函数开辟栈帧的理解)


    目录

    1.1  寄存器的理解

    1.2寄存器的概念

    1.3几种常见的寄存器

    2.1理解函数栈帧的创建和销毁所使用的代码

    2.2main函数的调用步骤(main函数栈帧的创建)

    1.先来看这样一段汇编代码,这里可以看到main函数调用的时候,首先进行push(压栈)操作:

    2.进行move(寄存器的移动操作)

    3.esp的地址进行减法操作(sub-0E4h) 

    2.3main函数空间中的值得初始化

     2.4Add函数的调用


    【前言】首先,我们要清楚的认识函数栈帧的创建与销毁,为什么函数栈帧如此重要,以及它能给我们带来什么?认识到这个问题后,再来阅读此文,相信会给你们带来不一样的收获。那么本篇博文的重点在哪呢?主要是解决六个问题。

    1.局部变量是怎么创建的?

    2.为什么局部变量的值是随机值?

    3.函数是怎么传参的?传参的顺序是怎样的?

    4.形参和实参是什么关系?

    5.函数调用是怎么做的?

    6.函数调用结束后怎么返回的?

    接下来步入正文:

    1.1  寄存器的理解

    从刚学习计算机的时候就已经听说过寄存器,而寄存器究竟是什么神秘人物,让我们揭开神秘人的面纱,一起来看一看他的正面目。

    1.2寄存器的概念

    寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。其实寄存器就是一种常用的时序逻辑电路,但是这种时序逻辑电路只包含存储电路。寄存器的存储电路是由锁存器或触发器构成的,因为一个锁存器或触发器能存储一位二进制数,所以由N个锁存器或触发器可以构成N位寄存器。寄存器是中央处理器内的组成部分。寄存器是有限存储容量的高速存储部件,它们可用来暂存指令、数据和位址。(详细内容来自百度百科)

    寄存器_百度百科

    1.3几种常见的寄存器

    eax        ebx        ecx        edx

    了解函数栈帧需要了解的两个主要寄存器用于维护函数栈帧(ebp栈底指针     esp栈顶指针)

    注意:每一次函数调用,都要在栈区开辟空间 。

    2.1理解函数栈帧的创建和销毁所使用的代码

    1. //函数栈帧的创建和销毁
    2. int Add(int x, int y)
    3. {
    4. int z = 0;
    5. z = x + y;
    6. return z;
    7. }
    8. int main()
    9. {
    10. int a = 10;
    11. int b = 20;
    12. int c = 0;
    13. c = Add(a, b);
    14. printf("%d\n", c);
    15. }

    函数栈帧的创建和销毁在不同编译器下略有差异本篇文章的运行及调试均来自VS2013),所以我们应该尽可能的去分析实验结果,而不是纠结于为什么我的结果和别人的结果不一样?是我的编译器有问题吗?等等可能出现的问题。

    2.2main函数的调用步骤(main函数栈帧的创建)

    压栈(push):给栈顶放一个元素。

    出栈(pop):给栈顶删除一个元素。

    在研究函数调用的实例时,先来观察调试状态下的调用堆栈,以及main函数是谁调用的?谁调用的?

    观察调用堆栈,不难发现main函数也是被其他函数调用的,main函数是由__tmainCRTStartup调用的,而__tmainCRTStartup又是由mainCRTStartup调用完成的。栈区的使用习惯是先试用高地址,在使用低地址。函数是如何被调用的?先来看看main函数和Add函数是如何完成调用的:

    在main函数调用之前,先调用__tmainCRTStartup,此时ebp和esp用来维护函数__tmainCRTStartup的栈帧,如图:

     接着我们把代码转换为汇编代码,看一下main函数是怎么一步一步被调用的:

    1.先来看这样一段汇编代码,这里可以看到main函数调用的时候,首先进行push(压栈)操作:

     

     压栈操作是进栈的一种操作,在这里我们需要注意的是在压栈过程中,由于esp和ebp是用来维护函数栈帧的,而且esp是用来栈顶的寄存器,所以在压栈之后寄存器esp也会随之移动。

    2.进行move(寄存器的移动操作)

    首先将esp的地址赋予ebp,进行复制操作后:

    3.esp的地址进行减法操作(sub-0E4h) 

    0E4h是一个数字,以十进制显示的话,0E4h是228,看着这个数字很小,其实这个数字的在内存中的空间还是很大的;

    函数是在栈区创建的,而栈区的使用习惯是先试用高地址,在使用低地址,所以进行减法操作的时候其实就是在位main函数开辟空间,也就是说减去的这样一个十进制数字其实就是为main函数开辟的空间:

    在main函数的栈帧创建之后,又进行了压栈操作,分别压入了三个值,分别是ebx、esi、edi 。

    2.3main函数空间中的值得初始化

     main函数初始化时所对应的汇编代码:

     第一句汇编代码:lea      edi,[ebp-0E4h]

    意思是在edi和ebp-0E4h的空间中加载一段数据。

    下面三句汇编代码:意思是将刚才加载的空间中全部初始化为CCCCCC.......

    注意:由于在函数栈帧创建的过程中图片较长,所以在提到哪一部分的时候会截取所对应的片段,而不会全部截取。

    初始化函数后,开始创建局部变量:

     

     0Ah转化为十进制就是10,也就是a的值。把a的值放在了[ebp-8]这个地址所对应的空间,b(20)放在[ebp-14h]所对应的地址处;

    c放在[ebp-20h]的地址处,借助于画图板能够帮助我们更好的理解:

    借助于调试中的内存来观察一下:

     从内存监视我们可以看出创建局部变量a之后,间隔两个整型的内存创建局部变量b,创建局部变量b后又间隔两个整型创建局部变量c,借助于画图板在刚才的图形中可以画出,更容易理解:

     2.4Add函数的调用

    Add函数调用时的汇编代码:

     把ebp-14h的值放在寄存器eax中,从上图中可以得出ebp-14h中存放的值正是b的值20,放入后进行压栈操作。

    压栈完成后,再次进行move操作,把ebp-8(a的值10)放在寄存器ecx中,再次进行压栈操作。

     那么为什么进行上面两个压栈操作呢?是不是在进行传参呢?

    答案是肯定的。

    call指令:调用函数

     这里我们来看一个细节:

     当进入call指令后内存空间的变化

     这个地址是call指令后面的地址,这个内存空间是栈顶的变化,也就相当于压栈把这个地址压了进去。

    进入Add函数:

     Add函数栈帧的创建:

    Add函数栈帧的创建和main函数的栈帧创建是相似的,以及对应的汇编指令也是相似的。这里直接画出图解:

    执行Add函数: 

    执行到现在,还没有看到我们的x,y在哪 ,那么接着往下执行:

    把ebp+8的值加到eax里面,ebp+8如图:

     以及ebp+12也加到eax里面,而ebp+8和ebp+12 其实就是刚刚压栈进来的a和b,这里便于区分用a'和b'代替。

    这就解释了形参是实参的一份临时拷贝的问题,在传参过程中,通过压栈将实参的值压到内存当中,进入函数需要用到形参时,通过地址找回压栈压进来的实参,改变实参不会影响实参

    结果算出来之后是怎么返回的?

    函数结束之后函数内部的值是会销毁的,所以函数的值需要寄存器来保存,最后通过寄存器返回:

     结果返回后空间的释放:

     从上往下进行出栈pop,回收空间:

    pop  ebp实际上弹出栈顶的ebp,找回main函数的ebp

     pop后通过call指令的下一个地址找到ma返回函数:

     寄存器中保存的30在返回后move到c中去,这就是函数栈帧的创建和销毁的大概内容。

    【结语】函数栈帧的创建和销毁是真正的内功,码字不易,喜欢的话就给个三连吧!

  • 相关阅读:
    数据库公共字段自动填充
    【Java-LangChain:使用 ChatGPT API 搭建系统-5】处理输入-思维链推理
    Autox.js和Auto.js4.1.1手机编辑器不好用我自己写了一个编辑器
    ElasticSearch的安装部署-----图文介绍
    【已解决】Vue全局引入scss 个别页面不生效 / 不自动引入全局样式
    罗丹明苯乙二醛,CAS号:2309313-01-5
    五个使用Delphi语言进行开发的案例
    lambda处理异常四种方式
    gcc编译器和gdb调试工具
    【简单介绍下爬山算法】
  • 原文地址:https://blog.csdn.net/qq_63179783/article/details/123510028