【C语言】函数栈帧空间的创建与销毁

         咕咕咕~


目录

 (一)前言

(二)将要解决的问题

注意

(三)基本知识

3.1寄存器

esp与ebp

 (四)演绎过程

4.1改变认识main函数

 4.2观察反汇编

4.3汇编代码

4.3.1栈的创建

4.3.2执行指令(main函数)

 4.3.3执行指令(main函数中调用Add函数)

 4.3.4Add函数调用结束

 4.4开始出栈


 (一)前言

        看了动态的读者估计此时此刻心里有一万个问号。 ????

        在本次更新,本应是讨论  《对指针的深入理解》,但是,我想到了更加重要的内容,一直拖延了很久,迟迟没有动笔写出来。

        今天,11月11日,是个好日子,状态不错,于是就有了本篇。毫无疑问这是重头内容,在动笔写本篇之前,我有预感——完成对《函数栈帧空间的创建的销毁》的讨论一定不是一件容易事。

        但是,完成这件事,无论对我,还是对我的读者,都是一次很大的提升。


(二)将要解决的问题

        在学习C到了一定程度后,我们或许会有很多疑问?

 【C语言】函数栈帧空间的创建与销毁_第1张图片

         本篇目的就是通过可视化的手段再现函数栈帧的创建与销毁的过程,试图回答以上的问题,依次解决读者的疑惑。

注意

         本次演示以VS2019为例,你也可以在阅读本文后自行演示。

        不同 编译器 的演示内容可能会有所不同,但是总体思路是一致的。

(三)基本知识

        为了能够顺利解释函数栈帧的创建,我们先引入概念:

3.1寄存器

         寄存器是计算机内部的一种存储器件(例如:eax,ebx,ecx,edx等),用来暂时存储和处理CPU需要的数据、指令、地址等信息。寄存器通常被安置在CPU内部,是计算机中最快速的存储器,由于其与CPU处于同一芯片上,因此访问速度非常快。CPU在执行指令时,需要从主存中取出数据并在寄存器中进行计算和处理,然后再将结果存回主存

esp与ebp

        ebp和esp是x86架构CPU中的两个寄存器,它们都是用来管理栈的。

        具体来说:

  • ebp寄存器:也称为“帧指针寄存器”,用于存储当前函数栈帧的基地址(栈底)。在函数调用时,ebp寄存器的值会被压入栈中,然后被新的函数覆盖,以保留上一层函数的堆栈信息,以便函数返回时恢复上一层函数的堆栈状态。
  • esp寄存器:也称为“栈指针寄存器”,用于指向栈顶的位置。在函数调用时,esp寄存器会被修改,以指向新的函数的栈帧,新的函数的参数、局部变量等都是通过esp寄存器进行操作。

         也许你对寄存器的概念并不感兴趣,但是这段话的内容将会在本篇被进行生动的演绎和验证!

         总之,ebp与esp寄存器中存放的是地址,是用来维护函数的栈帧的。

 (四)演绎过程

4.1改变认识main函数

        我们知道,函数在调用时,都会在内存的栈区开辟一块空间。

        并且,栈区的使用惯例是——先使用高地址,再使用低地址!

        【C语言】函数栈帧空间的创建与销毁_第2张图片

         在一般情况下,我们认为,C语言中的main函数是程序的入口,程序从main函数进入,然后按照逻辑从上向下一步一步执行指令代码。但是,在汇编层面上,主函数main实际上也是被调用的。

        在程序执行时,操作系统会首先加载可执行文件到内存中,然后跳转到main函数的入口地址开始执行代码。

        (VS2019被封装的比较严密,目前我没有在调用堆栈中找到调用main函数的函数,但是,它一定是存在的。在这里推荐大家使用版本较低的编译器,比如VS2013,在这样的编译器上,我们可以很容易发现汇编代码中调用main函数的函数):

【C语言】函数栈帧空间的创建与销毁_第3张图片

 (VS2013的调用堆栈)

         于是,在main函数之下其实已经有一些函数了:

        【C语言】函数栈帧空间的创建与销毁_第4张图片

         于是,到这里我们知道,main函数与其他函数一样都是被调用的——我们的目的是观察函数栈帧的创建与销毁的过程,但是:

        从  (调用main函数的函数)—> (main函数)的过程是比较复杂的,不太容易观察,所以我们在main函数内自己创建一个函数——Add函数,通过观察main函数内部  Add函数的  栈帧创建与销毁  的过程,来了解  函数栈帧创建与销毁  的一般过程。

 【C语言】函数栈帧空间的创建与销毁_第5张图片

 4.2观察反汇编

         在调试窗口打开反汇编:【C语言】函数栈帧空间的创建与销毁_第6张图片

        我们可以看到这段C代码的汇编代码:

         可是,它们是什么意思,是怎么操作的?

4.3汇编代码

         我们先观察前几句汇编代码:

【C语言】函数栈帧空间的创建与销毁_第7张图片

 (图.1)

4.3.1栈的创建

         (1.)push      ebp   ——就是把ebp压入栈中:

(其实,实际上是起一个记录的作用,在调用完main函数之后,main函数的栈帧被销毁,于是可以根据压入栈中的 ebp 的值,找到 调用main函数的函数 的栈底地址,也就是方便返回到 调用main函数的函数 中去)

        (这个ebp的值实际上是  调用main函数的函数的栈底地址)

        (由于 esp  是维护函数栈顶的,所以esp会向上移动4个字节)

于是:

 【C语言】函数栈帧空间的创建与销毁_第8张图片

         我们可以观察esp存储地址的变化:

压栈前:

 

 压栈后:

 

 (2.)mov      esp,ebp  ——就是把 esp 的值赋给 ebp:

         于是ebp与esp指向同一个位置:

        【C语言】函数栈帧空间的创建与销毁_第9张图片

(3.)sub         esp,0E4h    ——  将esp的值减去  0E4h: (转化为16进制数字,就是228)

         于是,esp向上移动了一段距离,而新的esp与ebp之间的内存,就是编译器为main函数预开辟的栈帧空间:【C语言】函数栈帧空间的创建与销毁_第10张图片

 (4.)接下来,连续进行三次压栈操作:

        push        ebx  

        push        esi 

        push        edi  

        (每次压栈操作后,栈顶esp都会向上移动4字节)

【C语言】函数栈帧空间的创建与销毁_第11张图片

 (5.)lea         edi,[ebp-FFFFFFF1ch(也就是0E4h)]  ——  lea  其实是加载有效地址(load effecitive address)

        而  [ebp-FFFFFFF1ch(也就是0E4h)]就是mian函数的栈顶地址,并把它放在  edi  中去

         【C语言】函数栈帧空间的创建与销毁_第12张图片

  

(6.)mov        ecx,39h  ——将ecx赋值为39h

(7.)mov        eax,0CCCCCCCCh   ——将eax 赋值为0CCCCCCCCh

(8.)rep stos         dword ptr es:[edi]  —— 将从edi开始的ecx次(39h次)的dword(一个word为2字节,dword(双字)为4字节)都改成ecx(0CCCCCCCCh)的值;

         具体效果,可以在调试窗口内存中查看:

        也就是说,有很大一块空间被初始化为:cc  cc  cc  cc

【C语言】函数栈帧空间的创建与销毁_第13张图片

         图示:

 【C语言】函数栈帧空间的创建与销毁_第14张图片


4.3.2执行指令(main函数)

接下来才真正开始执行我们编写的指令

 图中

 的

 (9.)

         由(图.1)这句代码的作用是在  ebp-8  的地址处 创建变量a  

( 下图的地址为:0x008FFB9C 的位置是变量a的位置)

【C语言】函数栈帧空间的创建与销毁_第15张图片


 图中

的 

 (10.)

         由(图.1)这句代码的作用是在  ebp-14h  的地址处 创建变量b  


 图中

(11.) 

          由(图.1)这句代码的作用是在  ebp-20h  的地址处 创建变量c 

 至此,我们的栈帧实际图为:

【C语言】函数栈帧空间的创建与销毁_第16张图片

         演示图为:【C语言】函数栈帧空间的创建与销毁_第17张图片


 4.3.3执行指令(main函数中调用Add函数)

        接下来,程序将要调用执行下面的Add函数 

【C语言】函数栈帧空间的创建与销毁_第18张图片

 (12.)

         这时,你就对汇编代码有了一些读写能力了,这两句就是:

         把  ebp-14h 的值放在eax里面    (其实 ebx-14h 位置存储的是局部变量b的值)

         把eax压入栈中

 (13.)

  

         我们再次发现 ebp-8 处的值就是 局部变量 a的值,于是,这两句就是把a的值赋给ecx,并将ecx压入栈中。


        其实,仔细想想我们会发现,12,13,两个操作就是函数的传参(实参向形参的传递),这也同时验证了——函数的形参其实就是实参的一份临时copy!(在后半篇会有更加生动的证明)


(14.)

         接下来执行call(调用)指令,我们按F11进入函数内部,在进入函数内部后,我们通过内存

窗口看到在栈中压入了一陌生的值:

         经过观察,我们发现这个值其实就是调用Add函数后的下一条指令的地址!!(从右相向左读)

如图:

 

         也就是说,在栈顶压了一条地址,这个地址是call指令的下一条地址。

        其实也很容易理解——当我们调用完Add函数后,程序需要再次回到主函数的下一条指令处!!

 进入Add函数后:

        我们发现前几条指令与主函数的栈帧的创建时相同的;

Add函数的汇编指令:

【C语言】函数栈帧空间的创建与销毁_第19张图片

 main函数的汇编指令:【C语言】函数栈帧空间的创建与销毁_第20张图片

         于是,这里直接给出了前面相同指令执行后的演示图:

【C语言】函数栈帧空间的创建与销毁_第21张图片

         接下来执行:

        在 ebp -8 的位置 创建 局部变量 z,并且将地址为 ebp+8 和地址为ebp+0ch 的值累加,最后将累加值赋值给 地址为ebp-8 的变量;

        在这里,我们发现,

        ebp+8 的位置不就是 a 的一份copy吗?(a的值传递给的形参)!!

         ebp+0ch 的位置不就是 b 的一份copy吗?(b的值传递给的形参)!!

【C语言】函数栈帧空间的创建与销毁_第22张图片

        看图后,也许你会更加理解: 【C语言】函数栈帧空间的创建与销毁_第23张图片

 4.3.4Add函数调用结束

         接下来,Add函数调用就要结束了,

         从这句汇编指令中,我们发现 将 ebp-8 的值赋给eax中,作为暂时保存。

(也就是将z的值赋给eax寄存器中)(这也就解释了为什么函数调用结束后为什么返回值没有被销毁,而是被完好无缺地返回)

 4.4开始出栈

         接下来的几条指令pop(弹出),就是出栈操作。

        ——依次从低地址到高地址依次弹出edi ,esi,ebx。

                 【C语言】函数栈帧空间的创建与销毁_第24张图片

 弹出:(一共三次)

【C语言】函数栈帧空间的创建与销毁_第25张图片

将esp  赋给   ebp:(ebp 由低地址指向 高地址 )(也就是返回,为Add函数栈的销毁做准备)

【C语言】函数栈帧空间的创建与销毁_第26张图片

         pop        ebp:将main函数的ebp弹出,也就意味着ebp回到了原本main函数的栈底。

【C语言】函数栈帧空间的创建与销毁_第27张图片

                 ret        其实就是弹出 call指令的下一条地址,并跳到call指令的下一条指令,于是Add函数的栈帧被完全销毁:【C语言】函数栈帧空间的创建与销毁_第28张图片

         怎么知道的呢?

        因为esp与ebp相互配合来维护一个函数栈帧,它们之间的维护的空间就是函数的栈。

        接下来就是跟着主函数继续执行了,后面的过程水墨就不再分享了。

到这里,你对开篇的问题是不是都有答案了呢?【C语言】函数栈帧空间的创建与销毁_第29张图片

欢迎在评论区分享你的理解和答案 



未经作者同意禁止转载

你可能感兴趣的:(C进阶,开发语言,数据结构,c语言)