> 文章列表 > 以反汇编角度浅析【函数栈帧的建立与销毁】

以反汇编角度浅析【函数栈帧的建立与销毁】

以反汇编角度浅析【函数栈帧的建立与销毁】

前言:相信大家对C语言中的函数并不陌生,通过函数我们可以使代码更加简洁、可读性更高、复用性更高等。关于对C语言中函数的具体介绍感兴趣的朋友们可以看看支持一下博主的这篇文章【逐步剖C】第二章-函数,而本文将展示关于函数调用更深层次一些的东西,所以本文内容较干,看完并理解可能需要一定的耐心和精力,不过相信你在看完并理解后对C语言中的函数调用尤其是递归将有会一个新的认识,以后将会以一个全新的视角来看待函数的调用。
那么话不多说,让我们开始吧。

一、函数栈帧概念的介绍

1. 栈的基本概念

栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函
数,没有局部变量,也就没有我们如今看到的所有的计算机语言。
在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈(存储到栈)中(入栈,push),也可以将已经压入栈中的数据弹出(取出)(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出(First In Last Out,FIFO)
栈就像叠成一叠的书,先叠上去的书在最下面,因此要最后才能取出。在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。(因为在经典的操作系统中,栈总是向下增长(由高地址向低地址)的)。
在我们常见的i386或者x86-64系统下,栈顶由成为 esp 的寄存器进行定位的。

2. 函数栈帧的概念

函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间用来存放:
函数参数和函数返回值;
临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量);
保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

二、理解函数栈帧后能“重新理解”问题

  1. 局部变量是如何创建的
  2. 所谓不初始化所得到的随机值的本质是什么
  3. 函数在进行调用时参数的具体传递方式是什么
  4. 为什么说改变形参不会影响实参
  5. 函数的返回值是如何带回的

那么接下来,容我为大家正式地介绍函数栈帧的创建和销毁的过程。

三、函数栈帧的创建和销毁

1. 预备知识

前言:由于是从反汇编的角度分析,故需要理解一些基本的汇编指令以及一些寄存器的功能,不过这里无需完全理解每条指令的具体用法或某个寄存器的全部功能等,重点是要理解整个函数的调用和返回的过程

(1)相关寄存器:

  • ebp:栈顶寄存器,指向当前正在调用的函数的栈底的指针;
  • esp:栈顶寄存器,指向当前正在调用的函数的栈顶的指针,准确来说是指向栈顶元素的指针,即每当有一个元素入栈时,该栈顶指针就会指向那个元素;
  • eax:通用寄存器,主要用于数据的保存、传输及运算等,常用于返回值;
  • ebx:通用寄存器,主要功能通eax;
  • eip:指令寄存器,保存当前指令下一条指令的地址

其中最需要我们注意的就是前三个寄存器,他们是整个过程中的关键角色。相信这里大家只看对寄存器的介绍可能会感到非常生硬和陌生,在后面的具体过程的介绍中大家再来慢慢体会到每个寄存器的作用。

(2)相关反汇编指令:

  • mov:数据转移指令。即将数据从一块空间转移到另一块空间;
  • push:数据入栈指令。即将一个数据进行压栈操作,同时栈顶指针esp发生改变指向入栈的元素;
  • pop:数据弹出指令,同时栈顶指针esp发生改变指向出栈的元素的下一个元素;
  • sub:减法指令。即令相应的数据执行减法运算;
  • add:加法指令。即令相应的数据执行加法运算;
  • call:函数调用指令。此指令执行时会进行两个操作:将返回地址进行压栈;转入目标函数,准备为目标开辟函数栈帧并执行函数内容;
  • jump:通过修改eip,转入目标函数,进行调用
  • ret:恢复返回地址,压入eip,类似pop eip命令

同样地,但看这些指令大家可能会不明所以,但是没关系,在后面的具体过程的介绍中大家会慢慢理解每条指令的作用。

(3)相关规则

  • 每一次函数调用都要为本次函数调用开辟空间,这块空间就是函数栈帧的空间
  • 每一次开辟的栈帧空间都会由两个寄存器进行维护。它们分别是栈底指针ebp和栈顶指针esp

结合本部分所述内容与上一部分对栈与栈帧概念的介绍我们可以得到如下函数在调用时的栈帧空间示意图:
以反汇编角度浅析【函数栈帧的建立与销毁】

2. main函数栈帧的开辟

下面是整个说明过程所使用的代码,请看:

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;
}

(PS:其实只介绍到ret = Add(a, b);语句为止,因为此时函数大体的调用和返回的逻辑已经展现完成,这也是本文着重想介绍的)

(1)对main函数的补充认识

我们在调试下调用堆栈的窗口下可以看到,main函数其实也是由其他函数调用的:
以反汇编角度浅析【函数栈帧的建立与销毁】
这里先做个知识的补充即可,我们暂且就先只关注invoke_main函数(直接调用main函数的函数),来介绍main函数函数栈帧的开辟过程。
下面先列出本次函数调用过程所用的汇编代码,并在后文逐句进行解析,请看:

int main()
{
00672B30  push        ebp  
00672B31  mov         ebp,esp  
00672B33  sub         esp,0E4h  
00672B39  push        ebx  
00672B3A  push        esi  
00672B3B  push        edi  
00672B3C  lea         edi,[ebp-24h]  
00672B3F  mov         ecx,9  
00672B44  mov         eax,0CCCCCCCCh  
00672B49  rep stos    dword ptr es:[edi]  int a = 3;
00672B4B  mov         dword ptr [ebp-8],3  int b = 5;
00672B52  mov         dword ptr [ebp-14h],5  int ret = 0;
00672B59  mov         dword ptr [ebp-20h],0  ret = Add(a, b);
00672B60  mov         eax,dword ptr [ebp-14h]  
00672B63  push        eax  
00672B64  mov         ecx,dword ptr [ebp-8]  
00672B67  push        ecx  
00672B68  call        006710EB  
00672B6D  add         esp,8  
00672B70  mov         dword ptr [ebp-20h],eax  printf("%d\\n", ret);
00672B73  mov         eax,dword ptr [ebp-20h]  
00672B76  push        eax  
00672B77  push        679C24h  
00672B7C  call        00671109  
00672B81  add         esp,8  return 0;
00672B84  xor         eax,eax  
}

这里大家可以先关注一个点:每条汇编指令其实都对应着一个地址,通过地址可以根据需要执行指定的汇编指令

(2)栈帧空间的开辟与初始化

main函数栈帧的开辟为下面这几行汇编代码:

00672B30  push        ebp  
00672B31  mov         ebp,esp  
00672B33  sub         esp,0E4h  
00672B39  push        ebx  
00672B3A  push        esi  
00672B3B  push        edi  
00672B3C  lea         edi,[ebp-24h]  
00672B3F  mov         ecx,9  
00672B44  mov         eax,0CCCCCCCCh  
00672B49  rep stos    dword ptr es:[edi]  
  • 00672B30 push ebp
    此条语句的意思是:把此时寄存器ebp中的值进行压栈操作,同时栈顶指针esp-4PS:这里在文章开始时对栈的说明中有提到,减4是因为栈的地址是向下增长的,即在栈中从下往上地址是由高到低的)。
    语句执行前两个寄存器的值:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    具体内存信息:
    (PS:当前机器存储模式为小端字节序存储)
    以反汇编角度浅析【函数栈帧的建立与销毁】
    解释此时esp和ebp维护的是invoke_main函数的栈帧空间这里应该是esp在上,ebp在下的,虽然从地址可以看出,但还是为由于当时的疏忽没展现在图中而感到抱歉,后面的图中已改正
    ebp指向空间的地址为(或者说ebp的值为0x004ff938,而0x004ff938这块空间中的内容是调用invoke_main函数的函数的ebp,(PS:这里的理解逻辑很重要,后面还会提到,可以先注意一下,至于调用invoke_main函数的函数是什么我们可以先不关注)也就是图中的0x004ff994。也就是说,ebp是一个“指针”,通过“解引用”ebp就得到了调用invoke_main函数的函数的ebp。
    内存示意图:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    执行后
    以反汇编角度浅析【函数栈帧的建立与销毁】
    具体内存信息:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    可以看到,此时栈底元素就为ebp的值,也就是0x004ff938

内存示意图:
以反汇编角度浅析【函数栈帧的建立与销毁】

这里需要注意的是,此时只是将ebp的值进行压栈,栈底指针ebp实际所指向的地方并没有发生改变,故其值也就没有发生变化。

  • 00672B31 mov ebp,esp
    将esp的值给ebp,可以理解为让ebp和esp指向同一块空间,此时就相当于产生了main函数的ebp(那么可以看出,这里的逻辑其实和上面一样,main函数的ebp中存的是调用main函数的函数,也就是invoke_main函数的ebp)。
    在上面基础上执行语句后两个寄存器的值为:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    具体内存信息:
    以反汇编角度浅析【函数栈帧的建立与销毁】

内存示意图:
以反汇编角度浅析【函数栈帧的建立与销毁】

  • 00672B33 sub esp,0E4h
    让栈顶指针减去一个16进制数0E4h,即让esp向上移动了一大段空间,此时的esp就是main函数栈帧的esp,那么在上面的基础上,此时的ebp和esp就是维护main函数栈帧的栈顶指针和栈顶指针
    (PS:这里的十六进制数0E4h之所以表达为此种形式是编译器的原因,把鼠标光标停留到其上可以看到标准的十六进制数,如图:)
    以反汇编角度浅析【函数栈帧的建立与销毁】

在上面基础上执行语句后两个寄存器的值为:
以反汇编角度浅析【函数栈帧的建立与销毁】
具体内存信息:
以反汇编角度浅析【函数栈帧的建立与销毁】

内存示意图:
以反汇编角度浅析【函数栈帧的建立与销毁】

  • 00672B39 push ebx; 00672B3A push esi; 00672B3B push edi
    由于这三条语句大同小异,这里就放在一起说明。三条语句分别将ebx,esi与edi的值压栈,同时栈顶指针-12。上面三条指令的主要将三个寄存器的值保存在栈中,因为这三个寄存器的值在随后的函数执行中可能会被修改,所以先进行寄存器的值的保存,以便在函数退出时恢复(PS:前面将invoke_main函数的值进行压栈其实也是同样的道理)。
    在上面基础上执行语句后五个个寄存器的值为:
    以反汇编角度浅析【函数栈帧的建立与销毁】

内存具体信息:
以反汇编角度浅析【函数栈帧的建立与销毁】
内存示意图:
以反汇编角度浅析【函数栈帧的建立与销毁】

  • main函数栈帧的初始化
00672B3C  lea         edi,[ebp-24h]  
00672B3F  mov         ecx,9  
00672B44  mov         eax,0CCCCCCCCh  
00672B49  rep stos    dword ptr es:[edi]  

如上四条指令都都用于main函数的初始化,故此处也在一起进行说明。

  • 第一条语句00672B3C lea edi,[ebp-24h]
    其中的指令lea的全称为load effective address(加载有效地址),执行的结果是将ebp-24h的地址先存在edi中,可以理解为让edi指向对应的空间
    (PS:同上面提到的,这里的24h其实是:)
    以反汇编角度浅析【函数栈帧的建立与销毁】
    语句执行后寄存器的值与具体内存信息:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    内存示意图:
    以反汇编角度浅析【函数栈帧的建立与销毁】
  • 第二条语句00672B3F mov ecx,9
    执行的结果是将9放入寄存器ecx中
    语句执行前相应寄存器值的情况为:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    执行后
    以反汇编角度浅析【函数栈帧的建立与销毁】
  • 第三条语句00672B44 mov eax,0CCCCCCCCh :执行的结果是将0CCCCCCCCh放入寄存器eax中
    语句执行前相应寄存器值的情况为:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    执行后为:
    以反汇编角度浅析【函数栈帧的建立与销毁】
  • 第四条语句00672B49 rep stos dword ptr es:[edi]
    执行结果是将ebp到edi这一段的内存全都初始化为0xCCCCCCCC,而具体的初始化方式是:从edi所指向的内存开始,通过edi不断加4(向栈底移动),每移动一次就将所指向空间中的内容更改为0xCCCCCCCC,直到ebp为止(不包括ebp)
    语句执行后相应寄存器的值为:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    具体的内存信息:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    内存示意图:
    以反汇编角度浅析【函数栈帧的建立与销毁】

上面main函数栈帧初始化的过程其实可以等价为以下伪代码:

edi = ebp - 24h;
ecx = 9;
eax = 0xcccccccc;
for( ; ecx > 0; --ecx, edi+=4)
{*edi = eax;
}
//这里的ecx可以理解为次数

至此,main函数栈帧的开辟与初始化工作就完成了。

在开始介绍main函数中语句的执行之前补充两点:

  • 第一点是一个问题:ecx中的值每次都是9吗
    自己调试后发现,编译器应该会根据main函数中将要执行的操作开辟合适大小的空间,主打的就是一个智能。
  • 第二点是一个补充的**“趣味”知识点**:
    相信大家在学习C语言的时候多少会碰到过屏幕上输出烫烫烫烫的结果,这是因为所输出空间的内容尚未经我们自己初始化,而只经编译器初始化为了0xCCCCCCCC,即在栈区的空间中每一个字节的内容都为0xCC,而汉字“烫”对应的编码就为0xCCCC(两个连续排列的0xCCCC,一个字的大小为两个字节),故最后屏幕上会出现经典的 “烫烫烫”

(3)函数中语句的执行

接下来我们继续介绍main函数中的语句执行。
调用Add函数之前,main函数中语句对应的反汇编指令是这几句:

	int a = 3;
00672B4B  mov         dword ptr [ebp-8],3  int b = 5;
00672B52  mov         dword ptr [ebp-14h],5  int ret = 0;
00672B59  mov         dword ptr [ebp-20h],0  

如上三条语句执行的过程本质是一样的,把对应的值放对应的位置上,即:
将3存储到ebp-8的地址处;
将5存储到ebp-14h的地址处;
将0存储到ebp-20的地址处;
(PS:这里中间隔多少大小也取决于编译器)
三条语句执行前对应的内存信息为:
以反汇编角度浅析【函数栈帧的建立与销毁】

执行后为

以反汇编角度浅析【函数栈帧的建立与销毁】
内存示意图:
以反汇编角度浅析【函数栈帧的建立与销毁】

那么其实以上汇编代码是对局部变量a,b,ret的创建与初始化的过程,可以看出,局部变量的创建是在其所在函数的栈帧空间中创建的。

接下来我们进入Add函数的调用

3. Add函数的栈帧的开辟

前言:其实Add函数栈帧的开辟过程从大体上来说和main函数栈帧的开辟大同小异,其实所有函数栈帧的开辟都是如此:先将直接调用它的函数的ebp的值压栈,然后将指针esp的值赋给ebp,让ebp和esp指向同一块空间,然后再让esp减去一个十六进制数(具体减多少由编译器决定),减完后的esp和ebp之间的空间(或者说维护的空间)即为该被调用函数的栈帧空间。

那么下面同样以介绍main函数栈帧的开辟的方式介绍Add函数栈帧的开辟。

(1)函数参数的传递

在真正进入Add函数的调用逻辑之前会先进行函数参数的传递:

ret = Add(a, b);
00672B60  mov         eax,dword ptr [ebp-14h]  
00672B63  push        eax  
00672B64  mov         ecx,dword ptr [ebp-8]  
00672B67  push        ecx  
00672B68  call        006710EB  
00672B6D  add         esp,8  
00672B70  mov         dword ptr [ebp-20h],eax 

如上代码中的前四句就是是函数传参的过程:
将ebp-14h地址处的值(由前面所述其实就是b的值)放到寄存器eax中,再将eax压栈,esp-4;
将ebp-8地址处的值(由前面所述其实就是a的值)放到寄存器ecx中,再将ecx压栈,esp-4;
前四条语句执行前对应的内存信息为:
以反汇编角度浅析【函数栈帧的建立与销毁】
执行后为:
以反汇编角度浅析【函数栈帧的建立与销毁】
内存示意图:
以反汇编角度浅析【函数栈帧的建立与销毁】
从这里我们又可以得到一个知识点函数的传参是从右到左进行的。而且从这里我们其实就可以知道,改变形参的值并不会改变实参,因为从图中可以很明显看出它们在栈区上并不是使用同一块内存空间,即形参其实是实参的一份临时拷贝,改变形参不会影响实参。

那么如上只是进行函数参数的传递,Add的函数栈帧其实还没有开辟,那么从这我们又可以获得一个小知识函数参数的传递其实是先于函数栈帧的开辟的,也就是说,函数的形参实际上是不在该函数的栈帧空间中的

接下来的一条指令00672B68 call 006710EB 才会真正进入到Add函数中并开始Add函数栈帧的开辟。
这里做一个关于call指令的补充:call指令是要执行函数调用的逻辑的,在执行执行相应函数调用之前先会把call指令下一条指令的地址进行压栈(在上面一张的内存图中就是00672B6D),这样做的目的是方便函数调用结束后回到call指令下一条指令的地方而继续往后执行
call指令前面的地址本条指令的地址后面的地址对应的jmp指令的地址
下面是将call指令下一条指令压栈后的内存信息:
以反汇编角度浅析【函数栈帧的建立与销毁】
图中蓝框中jmp指令的地址就是原来call指令后面的地址,而jmp指令后面的地址也将会使程序跳转到相应的指令处(其实就是Add函数的内部)来为Add函数开辟函数栈帧。

内存示意图:
以反汇编角度浅析【函数栈帧的建立与销毁】

跳转到Add的函数调用逻辑后,其全部反汇编代码如下:

int Add(int x, int y)
{
00672890  push        ebp  
00672891  mov         ebp,esp  
00672893  sub         esp,0CCh  
00672899  push        ebx  
0067289A  push        esi  
0067289B  push        edi  int z = 0;
0067289C  mov         dword ptr [ebp-8],0  z = x + y;
006728A3  mov         eax,dword ptr [ebp+8]  
006728A6  add         eax,dword ptr [ebp+0Ch]  
006728A9  mov         dword ptr [ebp-8],eax  return z;
006728AC  mov         eax,dword ptr [ebp-8]  
}
006728AF  pop         edi  
006728B0  pop         esi  
006728B1  pop         ebx  
006728B2  mov         esp,ebp  
006728B4  pop         ebp  
006728B5  ret  

(2)函数栈帧的开辟:

Add函数反汇编代码中的前六句就是函数栈帧的开辟

00672890  push        ebp  
00672891  mov         ebp,esp  
00672893  sub         esp,0CCh  
00672899  push        ebx  
0067289A  push        esi  
0067289B  push        edi  
  • 第一条语句00672890 push ebp
    先将调用它的函数的ebp,也就是main函数的ebp的值压栈,esp-4
    执行前寄存器即内存具体信息为:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    执行后
    以反汇编角度浅析【函数栈帧的建立与销毁】
    内存示意图:
    以反汇编角度浅析【函数栈帧的建立与销毁】

  • 第二条语句00672891 mov ebp,esp
    将esp的值给ebp,让ebp和esp指向同一块空间
    在上面基础上语句执行后内存具体信息为:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    内存示意图:
    以反汇编角度浅析【函数栈帧的建立与销毁】

  • 第三条语句00672893 sub esp,0CCh
    让栈顶指针减去一个16进制数0CCh,即让esp向上移动了一段空间,此时的esp就是Add函数栈帧的esp,那么在上面的基础上,此时的ebp和esp就是维护main函数栈帧的栈顶指针和栈顶指针
    语句执行后具体内存信息为:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    内存示意图:
    以反汇编角度浅析【函数栈帧的建立与销毁】

  • 随后三条语句00672899 push ebx; 0067289A push esi; 0067289B push edi
    三条语句分别将ebx,esi与edi的值压栈,同时栈顶指针-12。
    语句执行前各相关寄存器的值情况为:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    三条语句执行完后具体内存信息为:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    内存示意图:
    以反汇编角度浅析【函数栈帧的建立与销毁】
    至此,Add函数栈帧的开辟完成,接下来开始Add函数中语句的执行

(3)函数语句的执行

  • 相加运算
    如下四条语句执行的是先创建变量z,然后计算x+y的结果,并将计算结果保存的z中:
0067289C  mov         dword ptr [ebp-8],0
//对应z = 0;  
006728A3  mov         eax,dword ptr [ebp+8]  
006728A6  add         eax,dword ptr [ebp+0Ch]  
006728A9  mov         dword ptr [ebp-8],eax
//对应z = x + y;

先将ebp-8地址处的内容更改为0;接着将ebp+8地址处的值存储到寄存器eax中;再将ebp+0Ch处的地址值与寄存器eax中的相加;最后将寄存器eax中的值存储到ebp-8的地址处。
语句执行前对应具体内存信息为:
以反汇编角度浅析【函数栈帧的建立与销毁】
(PS:可以看到这里ebp+8地址处的值与ebp+0Ch地址处的值刚好就是Add函数传参阶段压入栈的值)
执行后
以反汇编角度浅析【函数栈帧的建立与销毁】
内存示意图:
以反汇编角度浅析【函数栈帧的建立与销毁】

  • 保存返回结果
    计算完成后,准备进行结果的返回,也就是整个Add函数的返回。
    先通过语句006728AC mov eax,dword ptr [ebp-8] 将ebp-8地址处的值存放再寄存器eax中也就是将z的值存到eax中,通过eax寄存器带回函数计算的结果(因为存放z的值的空间要销毁(还给操作系统)了,而寄存器相当于是全局的)

值存放完毕后,将开始进行函数的返回(函数栈帧的销毁)

4. Add函数的返回(函数栈帧的销毁)

Add函数通过如下六条汇编指令进行返回:

006728AF  pop         edi  
006728B0  pop         esi  
006728B1  pop         ebx  
006728B2  mov         esp,ebp  
006728B4  pop         ebp  
006728B5  ret  

(1)出栈操作

可以发现,前三条汇编指令都是pop,也就是出栈。所以前三条语句执行的结果分别是:
在栈底弹出一个值,将该值赋给edi,esp+4;
在栈底弹出一个值,将该值赋给esi,esp+4;
在栈底弹出一个值,将该值赋给ebx,esp+4;
语句执行前具体内存信息为:
以反汇编角度浅析【函数栈帧的建立与销毁】
执行后
以反汇编角度浅析【函数栈帧的建立与销毁】
内存参考图:
以反汇编角度浅析【函数栈帧的建立与销毁】

这里似乎只有栈顶指针esp的值发生了改变,不过也确实如此,对比在进行Add函数调用之前main函数中ediesiebx的值来看,三个寄存器在Add函数中与main函数中的值其实是相等的

(2) Add栈帧空间的回收:

Add函数栈帧空间的回收只需要一条指令:006728B2 mov esp,ebp
即将此时ebp的值赋给esp,也就是让esp和ebp指向同一块空间。
语句执行后具体内存信息为:
以反汇编角度浅析【函数栈帧的建立与销毁】
内存参考图:
以反汇编角度浅析【函数栈帧的建立与销毁】

(3)最终返回

Add函数通过最后两句汇编指令进行最终返回:

006728B4  pop         ebp  
006728B5  ret  

首先通过指令006728B4 pop ebp 弹出栈顶的值放到ebp中,由前面一系列过程我们知道,栈顶的值恰好是main函数自己的ebp的值,此时将栈顶的值赋值给ebp也就是让此时的ebp指向了原来main函数栈帧中ebp的位置,即恢复了main函数栈帧的维护,esp+4后指向main函数栈帧的栈顶,ebp指向main函数栈帧的栈底。
语句执行后具体内存信息为:
以反汇编角度浅析【函数栈帧的建立与销毁】内存示意图:
以反汇编角度浅析【函数栈帧的建立与销毁】
至此,Add函数栈帧的销毁就完成了
最后还需要通过指令ret进行函数的最终返回以执行调用Add函数的函数这里采用这种说法,或者说是理解方式可以更好地理解函数递归的过程),也就是main函数中之后的内容
那么这里对ret指令做一个补充说明
ret指令的执行,会从栈顶弹出一个值,而此时栈顶要弹出的值(也就是esp所指向的空间的值)刚好就是调用Add函数的call指令的下一条指令的地址,那么此时就直接跳转到call指令下一条指令的地址处,继续往下执行
语句执行后具体内存信息为:
以反汇编角度浅析【函数栈帧的建立与销毁】
内存示意图:
以反汇编角度浅析【函数栈帧的建立与销毁】

5. Add函数返回后main函数中语句的继续执行

返回后的剩余指令为:

00672B6D  add         esp,8  
00672B70  mov         dword ptr [ebp-20h],eax  

(PS:由于到这一部分本文重点想介绍的内容已经介绍的差不多了,故这里就只介绍到将返回值赋给变量ret,之后关于ret值的打印与main函数栈帧的销毁等本文就不再进行说明啦,感兴趣的朋友可以自行研究一下)

(1)形参的销毁

通过指令00672B6D add esp,8 让esp直接加8,相当于直接跳过了之前作为形参压栈的3和5
语句执行后具体内存信息为:
以反汇编角度浅析【函数栈帧的建立与销毁】
内存示意图:
以反汇编角度浅析【函数栈帧的建立与销毁】

(2)Add返回值的处理:

通过指令00672B70 mov dword ptr [ebp-20h],eax 将寄存器eax中的值赋给(或者说“替换”)ebp-20h地址处(其实也就是变量ret)的值;由之前的内容我们知道,eax中的值其实就是Add函数中计算3+5的和;也就是说Add函数的返回值是通过 “全局” 作用的寄存器eax带回来的,而程序也通过该寄存器获取函数的返回值。
语句执行后具体内存信息为:
以反汇编角度浅析【函数栈帧的建立与销毁】
那么,本文想重点介绍的内容至此就接近尾声啦,希望本文能为朋友们提供更多一些关于函数调用的知识与看待函数调用的新视角。

本章完。

看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或有误地方的地方还恳请过路的朋友们留个评论,多多指点,谢谢朋友们!🌹🌹🌹