图解函数栈帧-函数的创建与销毁(代码片段)

Aaronskr Aaronskr     2023-01-10     120

关键词:

🛸🛸文章开始之前,我想对各位提几个问题,看看你们能答出几个,看完本文之后,你们又能回答出几个?

  • 局部变量是怎么创建的?
  • 为什么局部变量的值是随机值?
  • 函数是怎么传参的?传参的顺序是怎样的?
  • 形参和实参是什么关系?
  • 函数调用是怎么做的?
  • 函数调用结束后是怎么返回的?

🎂前言

研究的函数: 一个加法函数。

原因:加法函数是比较简单的函数,实现逻辑比较单一,可以更为清楚的观察到函数栈帧的创建和销毁,而不是花费更多精力去研究复杂的函数。

#include <stdio.h>

int Add(int x, int y)

	int z = 0;
	z = x + y;
	return z;


int main()

	int a = 10;
	int b = 20;
	int c = 0;

	c = Add(a, b);
	printf("%d\\n", c);

	return 0;

使用的编译器: VS2013。

原因:版本过高过新的编译器在对栈帧分配上进行的封装处理较为完善,我们在学习时不易于看清楚里面的具体步骤,较低版本的编译器在学习时较为友好。

研究的方法: 图解。

原因:本文将以画图、截图配上文字解释加以说明,可以更加直观的理解函数栈帧的分配情况。

🌹栈帧的概念

栈帧是指为一个函数调用单独分配的那部分栈空间。 比如,当运行中的程序调用另一个函数时,就要进入一个新的栈帧,原来函数的栈帧称为调用者的帧,新的栈帧称为当前帧。 被调用的函数运行结束后当前帧全部收缩,回到调用者的帧。

💖准备工作

  1. 将代码编辑在编译器中。

  1. 开始调试并按下鼠标右键。(按下F10)

  1. 转到反汇编。

😀main函数栈帧的创建及初始化

😁main函数的被调用

首先我们需要明确,main()函数也就是我们平时说的主函数,他其实也是需要被其他函数调用的。

  1. 我们先在调试状态下打开调用堆栈窗口。

显示如下:

  1. 接下来我们一直按F10进行调试,直到主函数return 0被返回,即可出现以下界面。

往上翻即可找到调用main()函数的函数。


也就是说main()函数其实是被一个叫__tmainCRTStartup的函数所调用的。


😂main函数栈帧的开辟

我们知道,函数和局部变量的开辟是在栈上完成的,并且栈的使用习惯是先使用高地址,后使用低地址。

假设栈空间如下:

我们知道main函数也是被其他函数调用的,所以在栈上其实还有编译器为__tmainCRTStartup函数开辟的空间,这一点心中要明确。

接下来我们看反汇编里的汇编指令:

这一部分汇编指令其实就是对main函数的栈帧进行开辟。

这里介绍一下大家对指令里陌生的东西:

寄存器:ebp,esp,ebx,esi,edi,ecx,eax等等。

其中我们需要着重记住几个寄存器的功能。

维护函数栈帧的寄存器:

  1. esp - 存放指向栈顶的地址的寄存器。
  2. ebp - 存放指向栈底的地址的寄存器。

初始化函数值的寄存器:

  1. edi - 用于存放开始进行初始化的地址。
  2. ecx - 用于存放初始化元素的数量。
  3. eax - 用于存放将要初始化为什么东西的内容。

下面先给出在执行main函数之前栈的情况:

这里是编译器为__tmainCRTStartup函数开辟的函数栈帧,可以看见,ebp寄存器指向栈底,esp寄存器指向栈顶,以此来维护__tmainCRTStartup函数的函数栈帧。

下面我们一一分析main函数的汇编指令操作:

push        ebp

指将ebp寄存器中的值进行压栈操作(push)。

即在编译器已为__tmainCRTStartup函数开辟的栈帧上面进行压栈。

因为esp是指向栈顶的寄存器,所以每次压栈之后esp寄存器所指的位置会上升,反之如果执行pop弹出的操作,esp寄存器所指的位置则会下降。

此时__tmainCRTStartup函数的函数栈帧也随之增加。

下一个操作。

mov            ebp,esp

这条操作的指令是将esp的值赋给ebp。

也就是说让esp里存放所指向的地址赋给ebp,那么ebp所指向的位置将会发生更改。


此时ebp和esp指向了同一位置。

但是注意:

  1. 此时ebp和esp没有在维护__tmainCRTStartup函数了,但不代表他的函数栈帧被销毁了,因为栈空间的使用只能先销毁低地址,再销毁高地址。
  2. 将来main函数返回之后,esp寄存器和ebp寄存器还是要回来维护__tmainCRTStartup函数的,这里第一条指令push的ebpc操作就是伏笔,在main函数返回值后会执行pop这个ebp的操作,直接把ebp寄存器弹向之前存放的位置,也就是__tmainCRTStartup函数的栈底。

再下一个操作:

sub          esp,0E4h

这里解释一下,sub就是减法操作,这里0E4h表示十六进制的数字,h为标识符,所以0E4h其实就是十进制的228。

合起来就是将esp里存放的地址减去0E4h的大小。

因为上面是低地址下面是高地址,所以减去0E4h应该是向上走。

现在ebp和esp所维护的这段空间就是为main函数开辟的函数栈帧。

至此,main函数的函数栈帧就开辟完成了。


🤣main函数栈帧的初始化

我们在写代码的时候一定出现过一个问题,就是使用未初始化的变量或内容进行打印,结果控制台输出的东西完全是我们意想不到的结果。例如:


为什么这里会出现随机值呢?

下面就可以给出答案。

下面三条指令全部是push。

push                    ebx
push                    esi
push                    edi

三次push压栈之后,esp的位置自动发生变化,main函数的函数栈帧也随即增大。

接下来:
lea: load effective address(加载有效地址)

lea                                 edi,[ebp - 0E4h]

前面介绍过几个重要的寄存器:


所以这里的edi是用来存放开始进行初始化的地址的,也就是把【ebp - 0E4h】这个地址放进edi保存起来。

下面两个操作都是针对初始化用的寄存器的:

mov                          ecx,39h
mov                          eax,0CCCCCCCCh

ecx是存放初始化内容的次数的,所以是把39h这个次数存放在寄存器ecx中。

而eax是存放要初始化为的内容的,所以将0CCCCCCCCh存放进eax寄存器中。

接下来的指令就是初始化的关键:

rep stos                dword  ptr es:[edi]

dword的意思是double word - 一个word是两个字节,所以一个dword是4个字节。

整个指令的意思是从edi存放的位置开始往下每四个字节算一次,重复ecx里存放的值这么多次,把这些内容全部改为eax里存放的值。

也就是从ebp - 0E4h位置开始往下39h个整型的位置全部初始化为0CCCCCCCCh

至此main函数栈帧里的内容已被全部初始化。

此时栈里的情况:

为了防止有的码友不相信,这里我们计算一波。

十六进制39h转换成十进制是57。


57次,一次4个字节,也就是57乘以4等于228个字节。

而十六进制0E4h转化为十进制正号等于228。


所以至此,main函数栈帧里的所有内容全部被初始化为0CCCCCCCCh。


👩临时变量的创建。

这里的指令看的不够清晰,因为编译器默认显示了变量名,这不适合我们学习具体情况,所以我们应该把显示变量名给勾选掉。


把勾选去掉即可,效果如下:

这里就可以把具体位置看的比较清晰。

move                         dword ptr [ebp-8],0Ah

十六进制的0Ah转换成十进制也就是10,这条指令的意思就是将0Ah这个数放进[ebp-8]的位置。

也就是把ebp-8这个位置分配给变量a,将里面的值赋为10。

move                                  dword ptr [ebp-14h],14h

同上,这里的十六进制数字14h转换为十进制是20,将20放进[ebp-14h]的位置。

move                         dword ptr [ebp-20],0

同上,将0赋给[ebp-20h]的位置,也就是为c变量开辟空间并赋值。


看起来似乎到了函数调用了,但其实不然,调用函数之前,先在主调函数内创建实参的临时拷贝,再进行调用函数,接下来一一分析。

move               eax,dwor ptr [ebp-14h]

这句指令的意思是将[ebp-14h]位置存放的值赋给eax寄存器。

而我们可以看到:ebp - 14h的位置不就是我们刚刚创建的b变量吗?

这个操作把实参b的值存放到了寄存器eax中。

下一指令:

push               eax

将eax压栈,这里我们记住eax中存放的值就是实参b的值。


move            ecx,dword ptr [ebp-8]
push             ecx

同上,将[ebp-8]位置的值放在ecx里,之后将ecx压栈。

而ebp-8位置放的就是a变量。

执行到这里,其实就不难看出上面的操作其实是在给Add函数传参,开辟两个空间存放实参的临时拷贝。

注意: 这里传参的顺序是先传b后传a,并且是在main函数的栈帧内部进行的。

👨Add函数栈帧的创建

在创建Add函数的函数栈帧之前,编译器还做了一件事情:

call                             00B910E1

乍一看这个指令非常奇怪,但我们将调试进行下去,直到call指令的时候按F11进行逐语句调试。

会跳转到这个步骤。

这就是call指令的下一条指令,编译器把调用函数之后的下一条指令存放在栈上,将来被调用函数返回之后,便可根据这个地址直接执行调用函数后需要执行的指令。

在这一点上就可以体现编译器对函数栈帧的调用的严谨。

既要考虑到如何调用函数分配空间,也要考虑到函数调用结束怎么回到本该执行的下一条指令。

之后便开始对Add函数栈帧的创建。


🧑Add函数栈帧的创建


注意第一个操作:

push                 ebp

这里把ebp中存放的值进行压栈,也就是说这个位置存放的是原来ebp所指向的位置。


这里给出标记: ebp:main

表示的是这里的ebp存放的是main函数栈底的位置,将来在pop这个值得时候将会把ebp直接弹回main函数栈底的位置,继而继续维护main函数。

接下来的操作和开辟main函数栈帧十分相似:

mov                   ebp,esp
sub                    esp,0CCh

先将esp指向的位置赋给ebp,这样ebp就会和esp指向同一个位置,作为即将开辟栈帧的栈底。

再给esp减去0CCh的值,也就是往上偏移0CCh个字节长度,十六进制0CCh转化为十进制为204。这也就是编译器为Add函数分配的函数栈帧的大小。

注: 栈帧空间分配是编译器自行决定的,无法人为估测。

此时ebp到esp之间的部分就是编译器为Add函数分配的函数栈帧。


👧Add函数栈帧的初始化

和main函数一样,在函数栈帧创建完毕之后,会通过三个寄存器对函数栈帧

初始化,这里再次把寄存器作用给大家展示:


首先进行三个push指令

push              ebx
push              esi
push              edi


前面介绍过:

lea:load effective address(加载有效地址)

lea                          edi,[ebp+FFFFFF34h]
move                      ecx,33h
move                      eax,0CCCCCCCCh

指令的意思是:

  1. 将[ebp+FFFFFF34]地址加载到寄存器edi中。
  2. 将33h作为次数放进寄存器ecx中。
  3. 将0CCCCCCCCh作为要初始化为的内容存放在eax中。

而FFFFFF34的二进制序列是:11111111111111111111111100110100

显然,这是一个负数,所有ebp+FFFFFF34其实他的地址是在减小,所以此时存放的位置其实是可以计算得到的。

十六进制数33h的十进制形式为51,也就是要重复进行51次值覆盖。

覆盖值为0CCCCCCCCh。

rep stos           dword ptr es:[edi]

最后的指令就是从edi寄存器放的位置开始往下33h次进行值覆盖,覆盖内容为0CCCCCCCCh。

🎈Add函数实现加法运算

mov         dword ptr [edp-8],0

这是开辟临时变量的步骤。

将0赋给edp-8的位置,也就是给变量z开辟了一块空间。

mov               eax,dword ptr [ebp+8]

将ebp+8位置的值放进寄存器eax中保存。

可以从图上看到,ebp+8的位置时从main函数传过来的实参临时拷贝中的10。

此时

eax: 10

add                  eax,dword ptr [ebp+0Ch]

这里我们可以计算,0Ch转换为十进制也就是12,所有ebp+12应该是从当前ebp位置往下数3个格子(因为一个格子是4个字节)。

找到ebp+0Ch的位置:

将这里面的值加到寄存器eax中去:

此时

eax: 30

mov                      dword ptr [ebp-8],eax

把寄存器eax里的值放进[ebp-8]的位置里。

此时就已经完成了计算功能。

🧨Add函数返回值实现

函数功能实现之后,就要返回函数值了。

mov                         eax,dword ptr [ebp-8]

指令:将[ebp-8]地址的值放进eax寄存器,也就是把刚才计算结果30存放进寄存器中。

🎆Add函数栈帧的销毁

pop是出栈指令,把栈中的值弹出到指定的地方。

pop               edi
pop               esi
pop               ebx

连续三个pop,将之前初始化Add函数是压栈的三个元素弹出。


三个元素出栈后,Add函数的栈帧随之减少,esp所指向的位置也随机发生更改。

mov                               esp,ebp

和创建函数栈帧时的操作类似,但又不同,将ebp的值赋给esp。
即esp将会直接指向ebp指向的位置。


一旦执行完上面的操作指令,也就意味着esp,ebp两个寄存器不再维护Add函数栈帧了,开辟的空间将全部返还给操作系统。

pop                           ebp

将栈顶的元素弹出到ebp位置。

注意看这里的栈顶所放元素:

前文中已经提到过,这里压栈ebp的用途,就是为了在销毁Add函数之后ebp可以找到main函数栈底位置,继而继续维护main函数的栈帧。

所以这条指令将使ebp指向原main函数栈底。

此时Add函数栈帧已全部销毁。


🎇返回到main函数指令

接下来esp就指向了00B910E1。

前文提到过,这是call调用指令的下一条指令,所以直接返回到main函数的下一条指令。


现在在反汇编调试按F10将直接从Add函数跳转到main函数call指令的下一条指令。

add                          esp,8

把8加给esp寄存器,让其向下移动两个单元格(一个格子4个字节)


此时栈顶两个元素就不再被维护,main函数的函数栈帧也随之减少。

mov                           dword ptr [ebp-20h],eax

将eax寄存器里的值赋给ebp-20h位置,eax是我们在Add函数里计算结束后存放的返回值(30),ebp-20的位置时变量c的地址。

至此,Add函数的功能,栈帧开辟到结束就全部解释完毕了。

🍕🍕总结

函数栈帧用到汇编语言的知识,用最底层的角度看待函数调用的关系。

文章开头的几个问题其实在阅读到这的时候应该都能够解决了。

请注意:搞清楚函数栈帧并不能让你写代码更厉害,刷算法更牛逼,函数栈帧仅仅是类似于修炼内功一样的存在,理清楚底层的逻辑有助于我们思考一些比较复杂的问题,

例如递归算法,用函数栈帧的思想就很容易掌握。

最后,别忘了👍点赞👍+✔收藏✔+👀关注👀走一波~

函数栈帧的创建与销毁(代码片段)

目录写在前面函数栈帧的创建与销毁了解两个寄存器ebp和esp函数栈帧创建与销毁的具体过程main函数的函数栈帧变量的创建Add函数栈帧的创建与销毁回到main函数总结写在前面在我们前期的学习编程的过程中,我们会遇到许多... 查看详情

函数栈帧的创建与销毁(代码片段)

文章目录1.函数栈帧的概念2.函数栈帧的创建2.1main函数函数栈帧的创建过程2.2main函数中创建变量2.3Add函数函数栈帧的创建2.4Add函数栈帧的销毁1.函数栈帧的概念函数栈帧:使用每一个函数都要在栈区开辟一块空间.栈帧也叫过程活... 查看详情

函数栈帧的创建与销毁(代码片段)

...什么是栈帧C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈... 查看详情

图解c/c++语言底层:函数调用过程之函数栈帧的创建和销毁(上)(代码片段)

**文章目录函数栈帧的创建和销毁什么是寄存器?寄存器分类寄存器用途什么是"栈"?函数栈帧的概念函数压栈的过程示例代码和主函数汇编指令(部分)汇编指令:构建函数栈帧准备(一)汇编指令:构建函数栈... 查看详情

函数栈帧的创建与销毁(代码片段)

文章目录1.函数栈帧的概念2.函数栈帧的创建2.1main函数函数栈帧的创建过程2.2main函数中创建变量2.3Add函数函数栈帧的创建2.4Add函数栈帧的销毁1.函数栈帧的概念函数栈帧:使用每一个函数都要在栈区开辟一块空间.栈帧也叫过程活... 查看详情

c语言深入逐汇编详解函数栈帧的创建和销毁过程(代码片段)

...C语言深入】逐汇编详解函数栈帧的创建和销毁过程一、图解大概过程二、函数栈帧的创建过程1、简介一些需要用到的汇编指令和寄存器2、调用main函数的函数3、局部变量的初始化4、形成临时拷贝5、函数调用6、形成栈帧7、提取... 查看详情

函数栈帧的创建与销毁,带你了解代码底层原理(代码片段)

...么创建的?(2)为什么局部变量的值是随机的?(3)函数是怎么传参的?传参的顺序如何?(4)形参和实参是什么关系?(5)函数调用是怎么做的?(6)函数调用结束后是怎么返回的?这些疑问其实都和函数栈帧... 查看详情

图解c/c++底层:函数栈帧的创建和销毁(下篇)

函数栈帧的创建和销毁(下篇)上篇原文链接根据上篇的函数栈帧过程的学习,我们了解到:什么是寄存器?计算机的速度最快的存储单元,因为寄存器是集成在CPU之上的,与内存是不同的独立的存储空间。什么... 查看详情

c语言进阶顶级神功!函数栈帧的创建和销毁(代码片段)

...垫正文开始1.大致轮廓了解(源代码及反汇编)2.函数栈帧的创建和销毁总流程3.函数栈帧的创建3.1main函数的创建(分解)3.2Add函数的创建(分解)4.函数栈帧的销毁4.1main函数的销毁(分解)5.1Add函... 查看详情

函数栈帧的创建和销毁(代码片段)

目录各种寄存器的作用main()函数的调用通过汇编观察函数调用过程main()函数栈帧开辟过程Add()函数栈帧开辟过程Add()函数栈帧销毁过程各种寄存器的作用eax是“累加器”(accumulator),它是很多加法乘法指令的缺省寄存器ebx是“... 查看详情

函数栈帧的创建和销毁(待写)(代码片段)

函数栈帧的创建和销毁main函数被调用的过程:具体过程main函数被调用的过程:mainCRTStartup()调用_tmainCRTStartup()再调用main()寄存器:ebp(栈底指针),esp(栈顶指针)(sp是esp的低16位,esp是rsp的低32位,ss是16位堆栈... 查看详情

学好c语言,还需要掌握这个内功——函数栈帧的创建与销毁(代码片段)

...是怎么创建的?为什么局部变量的值是随机值?函数是怎么传参的?传参的顺序是什么?形参和实参是什么关系?函数调用结束后怎么返回?看完这篇文章,一切将豁然开朗……预备知识在进入正题之... 查看详情

图解c/c++底层:函数栈帧的创建和销毁(下篇)

函数栈帧的创建和销毁(下篇)上篇原文链接根据上篇的函数栈帧过程的学习,我们了解到:什么是寄存器?计算机的速度最快的存储单元,因为寄存器是集成在CPU之上的,与内存是不同的独立的存储空间。什么... 查看详情

函数栈帧的创建和销毁——“c”(代码片段)

...你们好呀,今天小雅兰来为大家介绍一个知识点——函数栈帧的创建和销毁。其实这个知识点,我们很早之前就要讲,但是因为我的一系列原因,才一直拖到了现在,那么,话不多说,让我们一起进入... 查看详情

c语言学习--函数栈帧的创建和销毁(代码片段)

...说局部变量未初始化时,其中存储的时随机值?函数到底时如何传参的?实参传递的顺序又是怎样的?形参和实参之间有着什么关系?函数调用结束后,结果是如何返回的?这些问题大 查看详情

内功修炼《函数栈帧的创建和销毁》建议收藏(代码片段)

...️⃣为什么未初始化的局部变量的值是随机值?3️⃣函数是如何传参的?以及传参的顺序是怎样的?4️⃣形参和实参是什么关系?5️⃣ 查看详情

c语言的函数栈帧究竟是什么?你知道吗?(代码片段)

内容导读1.寄存器2.函数栈帧2.1函数栈帧的概述2.2函数栈帧创建过程2.2.1被调用的main函数2.2.2函数栈帧创建与销毁的过程前面的话:作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!博主的... 查看详情

c语言函数栈帧的创建和销毁,以简单函数的调用来进行详细刨析(代码片段)

1、为什么需要了解函数的栈帧大家对于程序很少有人会对底层的东西刨根问底,很多人写程序大多就是写出来能够成功运行起来就大无所措了,但是很少有人对程序是怎么跑起来较为关心,比如:局部变量是怎么... 查看详情