函数栈帧的创建与销毁

目录

前言

一.函数栈帧定义

1.栈的定义

二.相关寄存器和汇编指令

三.函数栈帧是如何创建和销毁的呢 

1.初步知识

2.函数的调用堆栈

3.准备环境

 4.转到反汇编

 5.函数栈帧的创建

我们来一步步拆解代码

接下来的代码是在初始化main函数的栈帧空间

 一个小知识

 接下来我们就要分析main函数的核心代码了,小伙伴们准备好了吗

接下来就是调用add函数了 

接下来就进入调用函数阶段了 

 这时候我们就跳转到add函数了。开始观察add函数的反汇编代码了

讲到这,我们就可以解释清楚形参和实参直接的关系了 

 刚刚不小心取消了调试,导致后面的地址与前面不对,实在不好意思,但不影响大家理解,我们接着继续讲。

在这里我们回到main函数

总结 


 

前言

在我们前期的学习中可能会遇到一些无法解决的问题,如以下几个问题。

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

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

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

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

还有等等问题

这些问题其实是与函数栈帧的创建与销毁有很大关系,当你看完我这篇文章,保证能对你理解这些问题有很大帮助。

一.函数栈帧定义

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

注意:在不同的编译器,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。我这是用vs2022来讲明函数栈帧的。

1.栈的定义

在计算机系统中栈是一个具有以上属性的动态内存区域。先入栈的后出栈。程序可以将数据从栈顶弹出,;压栈操作使得栈增大,而弹出操作使得栈减小,在操作系统中,栈总是向下增长的。

二.相关寄存器和汇编指令

接下来我们要认识一下相关寄存器及汇编指令,没有以下知识,会使我们学习函数栈帧变得十分的困难,直接上图了,下面这图就解释了相关的内容。

这些词语就是作用与函数栈帧中的一些,认识一下就行,后面还会讲到,这边看一下,留个印象。

三.函数栈帧是如何创建和销毁的呢 

1.初步知识

1.每一次函数调用,都要为本次函数调用开辟空间,就是函数栈在帧的空间。

2.这块空间维护时使用了两个寄存器:esp和ebp,ebp记录的是栈底的地址,esp记录的是栈顶的地址。

看下面代码 

我们可以画个图来表示这main函数内容存放在栈中

 

 这开辟出的空间大概就是这样,接下来就来讲解如何把代码实现。

2.函数的调用堆栈

函数的调用堆栈是反馈函数的调用逻辑的看,我们可以观察到,;main函数调用之前,是由invoke_main函数调用的。看下图

 我们从这可以知道invoke_main函数应该会有自己的栈帧,main函数和add函数也会维护自己的栈帧,每个函数栈帧都有自己的ebp和esp来维护栈帧空间。

3.准备环境

为了使我们更加清晰的观看汇编代码,我们要排除一些编译器附加的代码,如何操作看下图。

把支持仅我的代码调试改成否就可以了。 

 4.转到反汇编

调试到main函数的第一行,转到反汇编,如下图所示操作

 注意:vs编译器每次调试都会为程序重新分配内存,课件中的反汇编代码是一次调试代码过程的数据,每次调试略有差异。 

反汇编后就是如下图所示了 

上面的这串反汇编代码是在x64上运行的,它是用rbp和rsp来表示的,其原理大同小异,这次我们讲解是以x86运行的及x32,遇到上面这种情况不用惊慌,把其改成x86就行哈。下面是x86的反汇编代码,如下图

 

 5.函数栈帧的创建

我们来一步步拆解代码

1.009418B0  push        ebp  

上面这串代码表示把ebp寄存器的值进行压栈,由上面所讲,ebp为栈底寄存器,此时ebp中存放的是invoke_main函数栈帧的ebp,esp-4;为什么是esp-4呢,因为是一个int型,所以需要栈顶寄存器esp-4. 如下图所示

2009418B1  mov         ebp,esp   move指令会把esp的值存放到ebp中,相当于产生了main函数的ebp,这个值就是invoke_main函数栈帧的esp,这么说你们可能不明白,就是把ebp放到esp的位置,如下图。

3.009418B3  sub         esp,0E4h     sub是减法命令,会让esp中的地址减去一个16进制数字0xe4h,产生新的esp,此时esp是main函数栈帧的esp,此时结合上一条指令的ebp和当前的esp,ebp和esp之间维护了一个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一块空间将储存main函数的局部变量,临时数据以及调试信息等。如下图所示 

4.009418B9  push        ebx  此时与1的操作形式相同,将寄存器ebx的值压栈,同时esp又要-4,如下图

 5.009418BA  push        esi同上,将寄存器esi的值压栈,esp-4

6.009418BB  push        edi  

同上,将寄存器edi的值压栈,esp-1

注意 :上面三条寄存器的值在栈区,这三个寄存器的在函数执行后可能会被修改,所以先保存寄存器原来的值,以便在退出函数时修复。图如下。

接下来的代码是在初始化main函数的栈帧空间

7,009418BC  lea         edi,[ebp-24h]   在这里我首先来解释一下lea的意思,lea及load  effective   address,这串代码及把ebp-24h的地址,放到edi中

8.009418BF  mov         ecx,9 把9放到ecx中

9.009418C4  mov         eax,0CCCCCCCCh        把 0CCCCCCCCh放在eax中

10 .009418C9  rep stos    dword ptr es:[edi]   将从edp-0x2h到ebp这一段的内存的每个字节都初始化0xccccccccc,最后如下图

 一个小知识

在以前的时候,有时候我们会输出烫烫烫这个结果,很多小伙伴不知道这是为什么,十分的疑惑,现在看到这里了,就可以解开这个疑惑了。看以下代码

此时输出的是好多个烫,之所以输出烫这么一个奇怪的字,是因为main函数调用时,在栈区开辟的空间,每个字节都被初始化为0xcc,而arr数组为初始化,而0xcc的汉字编码就是烫,所以最后0xcc被当作文本就是烫。

 接下来我们就要分析main函数的核心代码了,小伙伴们准备好了吗

11.    int a = 10;
009418D5  mov         dword ptr [a],0Ah        此时就是将0Ah存储到a的地址处, 

此时及如下图 

那么为什么为ebp-8呢,通过内存可知,此时a的地址是 0x004FFC60,ebp的地址是0x004FFC68,a为ebp-8.

12.int b = 20;
009418DC  mov         dword ptr [b],14h  同上,将20存放到b的地址处,我们可以看看b的地址为0x004FFC54,大家不要看这是54,ebp就是是68就是减14,此时是16进制的数字,故他们相差20,此时b的位置如下图所示

13.int c = 0;
009418E3  mov         dword ptr [c],0     将0存储到c的地址处,同上,此时c的地址为0x004FFC48,与b相差12,故c的位置如下图 

 

接下来就是调用add函数了 

在调用add函数时的传参,其实就是把参数push传到 栈帧空间中 

14.009418EA  mov         eax,dword ptr [b]     传递b,将b处放的20传到b的寄存器中。

15.009418ED  push        eax      将b的值压栈,此时esp-4

16.009418EE  mov         ecx,dword ptr [a]   传递a,将10放在a寄存器中

17.009418F1  push        ecx     将a的值压栈,esp-4,此时如下图所示 

 

接下来就进入调用函数阶段了 

18.009418F2  call        _add (09413B6h)       此时call指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令下一条指令的地方,继续往后执行,此时按f11进入add函数内部,位置及如下图所示。

 这时候我们就跳转到add函数了。开始观察add函数的反汇编代码了

 

add函数初始化内容 

 1.00F34070 55                   push        ebp   将main函数栈帧的ebp保存,esp-4.

2.00F34071 8B EC                mov         ebp,esp   将main函数的esp赋值给新的ebp,ebp现在是add函数的ebp

 3.00F34073 81 EC CC 00 00 00    sub         esp,0CCh  给esp-0xcc,求出add函数的esp

4.00F34079 53                   push        ebx      将ebx的值压栈,esp-4

5.00F3407A 56                   push        esi    将 esi的值压栈,esp-4

6.00F3407B 57                   push        edi     将edi的值压栈,esp-4

 7.00F3407C 8D 7D F4             lea         edi,[ebp-0Ch]    把ebp-0Ch的地址放在edi中

8.00F3407F B9 03 00 00 00       mov         ecx,3   把3放在ecx中

9.00F34084 B8 CC CC CC CC       mov         eax,0CCCCCCCCh   把0 CCCCCCCCh 放在eax中。

10.00F34089 F3 AB                rep stos    dword ptr es:[edi]  把从edi开始到ebp中间所有内容初始化为ccccccccc,如下图所示

 

对add函数中的调用了 

11.int d = 0;
00F34095 C7 45 F8 00 00 00 00 mov         dword ptr [ebp-8],0      将0放在ebp-8的地址处,就是创建d。如下图所示

 

12. d = a + b;
00F3409C 8B 45 08             mov         eax,dword ptr [ebp+8]    将ebp+8的地址处的数字存储到eax中,由上图可知,及将a=10储存到eax中

13.00F3409F 03 45 0C             add         eax,dword ptr [ebp+0Ch]      将ebp+0ch,及12处的地址处的数字加到eax中,及加上b=20.

14.00F340A2 89 45 F8             mov         dword ptr [ebp-8],eax     将eax的结果保存到ebp-8的地址处,其实就是放到d中 

讲到这,我们就可以解释清楚形参和实参直接的关系了 

函数在进行传值调用时,形参其实是实参的一份临时拷贝,对形参的修改不会影响实参,它是将其值进行压栈,然后了解他们的位置后直接调用的,并不会影响实参

15.return d;
00F340A5 8B 45 F8             mov         eax,dword ptr [ebp-8]  把ebp-8地址处的值凡在eax中,其实就是把d的值储存到eax中,通过eax寄存器带回计算的结果,做函数的返回值。

接下来就是弹出了 

16.00F340A8 5F                   pop         edi  
00F340A9 5E                   pop         esi  
00F340AA 5B                   pop         ebx 弹出这三给寄存器,使esp指向add,如下图

 

17.​​​​​​​00F340B8 8B E5                mov         esp,ebp   把ebp的地址赋给esp,相当于回收了add函数的栈。

18.00F340BA 5D                   pop         ebp        ebp被弹出一格,因为它指向main函数,故它重新回到main函数中ebp的位置,如下图,而esp也就指向call下一条指令,如下图 

 

 刚刚不小心取消了调试,导致后面的地址与前面不对,实在不好意思,但不影响大家理解,我们接着继续讲。

在这里我们回到main函数

大家可以看到c=add(a,b)后面有这么多个指令,实际上call前面的是不执行的,add函数结束后它会直接跳到 call下一条指令,所以我们现在就直接从add开始。

1.00F318F7 83 C4 08             add         esp,8     esp直接加8,由上图可知,esp就是跳过了main函数中压栈的a和b。此时如下图

2.00F318FA 89 45 E0             mov         dword ptr [ebp-20h],eax      将eax中的值,存档到ebp-0x20的地址处,其实就是存储到c的变量中。

接着就是直接打印,输出了,这就不过多讲解了。

总结 

         本篇文章到这里就结束了,我也给大家完整的演示了函数栈帧的创建与销毁,如果这篇文章对你有帮助的话,就点个免费的赞和关注吧。

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值