写点什么

C 生万物 | 反汇编深挖【函数栈帧】的创建和销毁

作者:Fire_Shield
  • 2023-04-13
    浙江
  • 本文字数:9410 字

    阅读完需:约 31 分钟

C生万物 | 反汇编深挖【函数栈帧】的创建和销毁

一、 什么是函数栈帧

我们在写 C 语言代码的时候,经常会把一个独立的功能抽象为函数,所以 C 程序是以函数为基本单位的。那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和==函数栈帧==有关系。


函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:


  • 函数参数和函数返回值

  • 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)

  • 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)

二、 理解函数栈帧能解决什么问题呢?

只要理解了函数栈帧的创建和销毁,以下问题就能够很好的额理解了


📚<font color = "#0000ff">局部变量是如何创建的?</font>📚<font color = "#0000ff">为什么局部变量不初始化内容是随机的?</font>📚<font color = "#0000ff">函数调用时参数时如何传递的?传参的顺序是怎样的?</font>📚<font color = "#0000ff">函数的形参和实参分别是怎样实例化的?</font>📚<font color = "#0000ff">函数调用是怎么做的?函数的返回值是如何带会的?</font>


我们就带着这些疑问一起走近函数栈帧吧:walking:

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

1、什么是栈?

栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。



  • 在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出

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


==光看文字可能有点晦涩难懂,可以看看我的这篇文章==——> 数据结构 | 站与队列的交际舞


<font color = "#ff0000">不过大家不要将内存中的【栈】和数据结构中的【栈】混为一谈,二者还是有区别的</font>


在经典的操作系统中,栈总是向下增长(由高地址向低地址)的在我们常见的 i386 或者 x86-64 下,栈顶由成为 esp 的寄存器进行定位的

2、认识相关寄存器和汇编指令

接着我来了解一下函数栈帧的相关寄存器,以及常用的汇编指令

2.1 相关寄存器

  • [x] 【eax】:通用寄存器,保留临时数据,常用于返回值

  • [x] 【ebx】 :通用寄存器,保留临时数据

  • [x] 【ebp】:栈底寄存器

  • [x] 【esp】:栈顶寄存器

  • [x] 【eip】:指令寄存器,保存当前指令的下一条指令的地址

2.2 相关汇编命令

  • [x] 【mov】:数据转移指令

  • [x] 【push】:数据入栈,同时 esp 栈顶寄存器也要发生改变

  • [x] 【pop】:数据弹出至指定位置,同时 esp 栈顶寄存器也要发生改变

  • [x] 【add】:加法命令

  • [x] 【sub】:减法命令

  • [x] 【lea】 :load effective address,加载有效地址

  • [x] 【call】:函数调用,1. 压入返回地址 2. 转入目标函数

  • [x] 【jump】:通过修改 eip,转入目标函数,进行调用

  • [x] 【ret】:恢复返回地址,压入 eip,类似 pop eip 命令

3、解析函数栈帧的创建和销毁

3.1 预备知识

首先我们达成一些预备知识才能有效的帮助我们理解,函数栈帧的创建和销毁📕


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

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


==如图所示==


3.2 代码 && 环境搭建

  • 对于代码而言我就以最简单的两数相加为例,而且每一步都详细展开,方便观看每一个变量是如何被创建和销毁的


#include <stdio.h>
int Add(int x, int y){ int z = 0; z = x + y; return z;}
int main(void){ //从这里开始运行,按下F10 int a = 10; int b = 20; int c = 0;
c = Add(a, b);
printf("c = %d\n", c); return 0;}
复制代码


  • 然后我们来把环境做一个搭建,首先直接在键盘上按下 F10【笔记本按下 Fn + F10】。以往写代码的时候,我们都知道有这么一个 main 函数,如果程序要运行起来那就必须要在 main 函数里面对应的代码,无论是封装了多少的函数,最后都要在 main 函数里进行一个调用,==但是你有想过吗,这个 main 函数是被谁调用的呢?==

  • 首先我们来做一些环境的搭建工作



  • 接下去在按下 F10 后到监视窗口打开【调用堆栈】的窗口



  • 然后就出现了这样的界面。此时我们的 main 函数就从第 13 行开始运行了



  • 接下去一直按 F10,当调试箭头运行到第【22 行】的时候,就会自动进入到==exe_common.inl==,此时我们就可以观察到底是哪个函数调用了 main 函数

  • 通过下图可知是==invoke_main==这个函数调用的,我们了解到这里就可以了,再深挖下去的话可能就比较难以理解了



  • 好,了解了这个知识后我们就要正式开始进入函数的分析了,关掉这个【调用堆栈】的窗口后,我们准备调出【反汇编】【内存】【监视】这三个窗口


==【反汇编】==



==【内存】==



==【监视】==



好,到这么为止我们的环境已经全部搭建好了


3.3 函数栈帧的创建【保姆式教学】

  • 接下去,我们正式开始分析函数栈帧究竟是如何创建的




  • 从上图看到此时此刻我们已经进入到 main 函数了,那么通过刚才的【调用堆栈】可以知道,main 函数是由 invoke_main 这个函数来进行调用的,所以我们先画出它的函数栈帧



  • 首先看到左边的两个寄存器【esp】和【ebp】,分别用来维护栈顶和栈顶。可以看到左右有个双向箭头,因为对于栈来说是从【高地址】向【低地址】使用的,内存是向上漫延的。


==第一条指令==


  • 好,接下去的话就要执行第一条指令了。也就是向栈中 push 一个 ebp,即将 ebp 中的值进行一个压栈的操作,此时的 ebp 中存放的是 invoke_main 函数栈帧的 ebp


00461820 55   push   ebp
复制代码


  • 然后随着 push 入栈的操作,维护栈顶的 esp 就要往上

  • 此时我们还可以到 VS 中来观察一下寄存器所存放内存地址的变化



  • 好,然后我们执行一下 push 这句指令,你就会发现【esp】中所存放的地址变小了,然后它里面存放的就是原先【ebp】中的值,只是这个存放的形式是倒着存放的,这一块涉及我们后面的大小端存放问题,这里就先记住是倒着存放的





==第二条指令==


  • 接下来第二条,可以看到对应的汇编指令发生了变化,【mov】我们在上面有讲到过是一个数据转移指令。所以这条指令的含义就是把 esp 的值存放到 ebp 中去


00461821 8B EC   mov   ebp,esp
复制代码


  • 通过画图也可以这么来理解。此时相当于产生了 main 函数的【ebp】,这个值就是 invoke_main 函数栈帧的【esp】,从这里开始就要开始维护 main 函数的函数栈帧了



  • 一样,通过 VS 再来看一下。你就会看到【ebp】中就会存放【esp】的地址了





==第三条指令==


  • 接下来第三条,看到的汇编指令是【sub】,对于 sub 我们上面讲到过是一条减法命令,那意思就是让 esp 中的地址减去一个 16 进制数字【0xe4】,产生新的 esp,此时的 esp 是 main 函数栈帧的 esp


00461823 81 EC E4 00 00 00   sub   esp,0E4h
复制代码


  • 此时结合上一条指令的 ebp 和当前的 esp,ebp 和 esp 之间维护了一个块栈空间,这块栈空间就是为 main 函数开辟的,就是 main 函数的栈帧空间,这一段空间中将存储 main 函数中的局部变量,临时数据以及调试信息

  • 通过图,此时你也可以认为【esp】指向了低地址的一块空间

  • 来看一下寄存器中存放的内存变化





==第四、五、六条指令==


  • 好,接下去的三条指令我们一起说,因为都是一样的操作


00461829 53   push   ebx   //将寄存器ebx的值压栈,esp-40046182A 56   push   esi   //将寄存器esi的值压栈,esp-40046182B 57   push   edi   //将寄存器edi的值压栈,esp-4
复制代码


  • 上面 3 条指令保存了 3 个寄存器的值在栈区,这 3 个寄存器的在函数随后执行中可能会被修改,所以先保存寄存器原来的值,以便在退出函数时恢复

  • 那随着寄存器的入栈,维护栈顶的寄存器也将发生变化

  • 到 VS 里来看一下三次 push 后内存地址的变化







==第七、八、九、十条指令==


  • 接下去的四条指令是关键,要涉及栈帧的初始化操作,要重点掌握⭐


0046182C 8D 7D DC             lea         edi,[ebp-24h]  0046182F B9 09 00 00 00       mov         ecx,9  00461834 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  00461839 F3 AB                rep stos    dword ptr es:[edi]
复制代码


  • 首先要来看的就是【lea】就是我们在上面讲到过的【load effective address】加载有效地址的意思,那也就是从【ebp】这个维护栈顶的寄存器减去 24h 的位置,加载到寄存器【edi】里面去



  • 然后再将 9 放到【ecx】中去;以及将【0CCCCCCCCh】这块地址存到【eax】中去;最后一句指令的意思比较难理解,也就是从【edi】所存放的这块地址的开始,每次初始化 4 个字节的数据,dword 值得就是 4 个字节的大小

  • 上面这四条指令也可以写成下面四句伪代码


edi = ebp-0x24;ecx = 9;eax = 0xCCCCCCCC;while(--ecx){  *(int*)edi = eax;  edi+=4;}
复制代码


  • 总结一下这四句代码,就是从 edi 开始,每次初始化 4 个字节的数据,总共初始化 ecx 次,初始化的内容为【0xCCCCCCCC】,总共初始化到 ebp 的地址结束



  • 我们到 VS 里再来看看



  • 可以看到,main 函数的空间被初始化完成了



  • 可以看到,到这里为止,main 函数才刚刚被初始化完成

  • 这里再补充一个小知识:book:我们来执行一下下面这两行代码,你觉得会输出什么内容呢,


char arr[20];printf("%s",arr);
复制代码



  • 之所以上面的程序输出“烫”这么一个奇怪的字,是因为 main 函数调用时,在栈区开辟的空间的其中每一个字节都被初始化为 0xCC,而 arr 数组是一个未初始化的数组,恰好在这块空间上创建的,0xCCCC(两个连续排列的 0xCC)的汉字编码就是“烫”,所以 0xCCCC 被当作文本就是“烫”。




==第十一、十二、十三条指令==


  • 我们开始初始化三个变量,每条指令对应上一条代码


  int a = 10;0046183B C7 45 F8 0A 00 00 00   mov   dword ptr [ebp-8],0Ah    int b = 20;00461842 C7 45 EC 14 00 00 00   mov   dword ptr [ebp-14h],14h    int c = 0;00461849 C7 45 E0 00 00 00 00   mov   dword ptr [ebp-20h],0
复制代码


  • 解读一下上面三句指令的含义,【mov】是数据转移指令,第一句是将 10 这个值【ebp - 8】这块地址上,<font color = "#0000ff">为什么说 0Ah 就是 10 呢?因为 0Ah 是 10 的十六进制表示形式,在十六进制中 A 值得就是 10</font>,所以对于 14h 的话就是 16 * 1 + 4 = 20,那就是将 20 这个值放到【ebp - 14】这块地址上去;最后一句就是将 0 这个值放到【ebp - 20】这块地址上去

  • 一定有同学会比较疑惑为什么-8,-14,-20 呢,这个的话其实我也不知道,,ԾㅂԾ,,,这是取决于编译器本身的,我是用的是 VS2019,可能你到其他编译器上就不一样了,这就可以得出一个结论<font color = "#0000ff">我们所定义的变量在栈内存中并不是呈现一个连续存放的,可能是分散的</font>,

  • 接下去继续到 VS 中来看看





  • 然后将它们放入 main 函数的栈帧中





==第十四、十五、十六、十七条指令==


  • 我们再来看四条指令,此时 main 函数中的变量创建好了,那就要调用 Add 函数了,该如何调用呢?我们来看看


00461850 8B 45 EC      mov         eax,dword ptr [ebp-14h]  00461853 50            push        eax  00461854 8B 4D F8      mov         ecx,dword ptr [ebp-8]  00461857 51            push        ecx
复制代码


  • 首先来看第一条,【mov】是数据转移指令,也就是将【ebp-14h】这块地址的内容放到寄存器【eax】中去,那这个时候你就会想到这个【ebp-14】不是我们刚才放数值 20 的地方吗;然后由可以看到下一条就是将【ebp-8】中的内容放到寄存器【ecx】中去,它【ebp-8】的地方存放的就是我们刚才放 10 的地方。然后分别再将这两个寄存器 push 入栈,一目了然



  • 这样其实就可以看出,这两个变量相当于实参的一份临时拷贝,那在函数中实参的【临时拷贝】是什么呢?没错,就是==形参==。这个我们后面还会再用到,因此先入栈

  • 再来到 VS 中看看




==第十八条指令==


  • 这条指令单独做讲解,因为其为 main 函数进入到 Add 函数的一个转折点:mountain:


00461858 E8 57 F8 FF FF   call   004610B4
复制代码


  • 对于这条【call】指令而言,比较特殊,它有两个作用①压入返回地址 ②转入目标函数,那有同学就问了,压地址?压哪条地址呀?这里的话我告诉你,要压的是 <font color = "#0000ff">call 指令的下一条地址</font>


0046185D   //这条就是要压入的地址
复制代码


  • 我们一起先到 VS 里来看看。当运行到这一句时,我们不能再按 F10 了,要按下 F11,这和调试是一个道理



  • 继续按下 F10,我们就可以进入到 Add 函数中



  • 把这块地址压入栈中




==第十九、二十、二一条指令==


  • 到 19 条指令开始,就进入 Add 函数了,此时我们可以先浏览一下 Add 函数中的前几条指令。可以看到是不是非常熟悉呢。因为在 main 函数中的前面也是这几条指令

  • 那其实聪明的你一定可以猜到这是在为 Add 函数开辟函数栈帧


00461760 55                   push        ebp  00461761 8B EC                mov         ebp,esp  00461763 81 EC CC 00 00 00    sub         esp,0CCh  00461769 53                   push        ebx  0046176A 56                   push        esi  0046176B 57                   push        edi  
复制代码


  • 首先来看第一条指令。也就是将之前的【ebp】栈底寄存器的值压入到栈顶中


00461760 55     push     ebp
复制代码


  • 这个【ebp】我们似乎好久都没有关注了,到 VS 中来看看吧

  • 可以看到,对于此处的【ebp】,自从它在维护 main 函数的栈底后就没有再动过来,所以这里 push 上来的就是 main 函数的【ebp】



00461761 8B EC     mov    ebp,esp
复制代码


  • 接着再来看第二条,也就是将 main 函数的【esp】重新赋给【ebp】,这里要注意了,不要搞混,此时的【ebp】应该算是在维护 Add 函数的栈底了



  • 于是,栈就变成了这样,此时就等待【esp】做一个变化



00461763 81 EC CC 00 00 00    sub     esp,0CCh
复制代码


  • 接着第三条,【sub】命令使得【esp】存放的地址块减去一个 CC 的大小,继续结合上面那条指令,此时 Add 函数的栈顶和栈底都被找到了



  • 此时就相当于是在做一个迭代的操作






==第二二、二三、二四条指令==


  • 接下去还是一样的三条压栈操作


00461769 53     push     ebx  0046176A 56     push     esi  0046176B 57     push     edi  
复制代码


  • 首先到 VS 中观看【esp】的变化



  • 接着将这三个寄存器压入栈



==第二五、二六、二七、二八条指令==


  • 对于这四条指令和上面 main 函数的创建过程类似,便不做不过分析


0046177B 57    lea           edi, [ebp-0ch]0046177C 58    mov           ecx, 30046177D 59    mov           eax,0CCCCCCCCCCh0046177E 60    rep stos      dword ptr es:[edi]
复制代码



==第二十九条指令==


  • 接下去我们进入第二十九条指令,也就是对 Add 函数中存放计算总和的变量 z 进行初始化操作。【mov】做数据转移,将 0 放到【ebp-8】这块地址上去


  int z = 0;0046176C C7 45 F8 00 00 00 00     mov      dword ptr [ebp-8],0
复制代码



  • 然后我们在 Add 的栈帧中初始化这个变量 z


==第三十、三十一、三十二条指令==


  • 接下去的三条指令就是对两个形参的值进行一个相加


00461773 8B 45 08       mov     eax,dword ptr [ebp+8]  00461776 03 45 0C       add     eax,dword ptr [ebp+0Ch]  00461779 89 45 F8       mov     dword ptr [ebp-8],eax
复制代码


  • 但是有同学一定很困惑,上面不是只初始化了一个变量 z 吗,变量 x 和变量 y 在哪里呢?那你可能忘了我们之前有做过了一步操作。也就是将这两个实参的拷贝进行了一个压栈操作,那时就说了对于这个就是形参


00461850 8B 45 EC      mov         eax,dword ptr [ebp-14h]  00461853 50            push        eax  00461854 8B 4D F8      mov         ecx,dword ptr [ebp-8]  00461857 51            push        ecx
复制代码


  • 此时我们就要通过这三句指令去找回这两个形参的值,关键的就是【ebp+8】和【ebp+0Ch】。因为我们在入栈的时候【ebp】寄存器存放的地址都是逐渐变小的,因为 <font color = "#0000ff">栈是从高地址往低地址生长的</font>,所以我们要去找回之前压入的内容,就要把地址加回去

  • 如下图所示



  • 找到这两个值之后,首先将【10】放到【eax】寄存器中去,然后再将【20】在加到寄存器【eax】原有的值上去,此时【eax】中存放的便是【30】

  • 我们到 VS 中来看看



  • 注意看寄存器【eax】的变化



  • 教你一个方法,还可以直接到指令这里来看。直接将鼠标放到【z】上面就可以看到了



  • 然后再将计算出来存放在【eax】中的值再放回【ebp-8】这块地址上去


00461779 89 45 F8       mov     dword ptr [ebp-8],eax
复制代码


  • 首先到 VS 中来看看变化



  • 然后修改一下之前 Add 函数栈帧中存放 z 的内容



==第三十三条指令==


  • z 计算出来了,此时就要执行【return z】这句代码,将 z 返回给 main 函数,但是函数栈帧中可不是这么做的


  return z;0046177C 8B 45 F8        mov      eax,dword ptr [ebp-8]
复制代码


  • 看上面的指令可以看到,是将【ebp-8】中的内容转存到寄存器【eax】中去,这里有同学肯定有疑惑,【eax】上面不是刚用到过吗,是存放计算出来的值,现在怎么又放回去了呢?

  • 这一块就涉及汇编指令和寄存器的一些知识了,从【eax】~【ebx】这些寄存器都可以用来存放临时数据,并不是说上一次用过了就不能再用了,这其实和我们在定义一个变量后进行反复使用是一个道理。

  • 然后在 Add 函数调用结束后,它所对应的函数栈帧就会被销毁,此时被创建出来的临时变量【z】就不复存在了,因为【z】也是存放在 Add 的函数栈帧中的,<font color = "#0000ff">所以这一步的操作其实就是将我们在 Add 函数中计算出来的值给保存起来,因为寄存器而言程序没有结束的话它是不会被销毁的,我们后面还可以到这个寄存器中去取数据</font>



  • 但是因为上一次我们刚好使用到了【eax】,所以在 VS 中看不出变化,还是【1e 00 00 00】


好,到这里为止,函数栈帧的创建和执行操作就全部结束了,你学废︿( ̄︶ ̄)︿了吗🐂

3.4 函数栈帧的销毁【保姆式教学】

接下去要进行的就是函数栈帧的销毁操作


==第三十四、三十五、三十六条指令==


  • 接下来就是三条 pop 的指令,也就是在栈顶弹出对应的值,然后放到对应的寄存器中去


0046177F 5F         pop         edi     //在栈顶弹出一个值,存放到edi中,esp+400461780 5E         pop         esi     //在栈顶弹出一个值,存放到esi中,esp+400461781 5B         pop         ebx     //在栈顶弹出一个值,存放到ebx中,esp+4
复制代码


  • 我们先到 VS 中来看看



  • 然后来看一下维护栈顶寄存器【esp】的变化


==第三十七条指令==


  • 上面我们有讲到过,当给 Add 函数预开辟函数栈帧的时候,最后一步是吧【esp】中存放的内容给到【ebp】,那相当于就是让【ebp】指向和【esp】的同一块空间

  • 下面这句指令就是将【ebp】中存放的内容给到【esp】,那其实就是让【esp】指向和【ebp】的同一块空间


00461782 8B E5        mov        esp,ebp
复制代码


  • 通过图示来看一下



  • 到 VS 中来看一下



==第三十八条指令==


  • 这句指令很重要,因为此时 Add 函数的函数栈帧已经被销毁了,此时我们要回到 main 函数的函数栈帧,那么两个维护栈顶和栈底的寄存器就要发生变化


00461784 5D      pop      ebp
复制代码


  • 然后仔细看,此时我们要 pop 的【ebp】是之前压栈进来的 main 函数的 ebp,通过下图你一定能回忆起来。再来仔细看 pop 的作用:<font color = "#00ff00">数据弹出至指定位置,同时 esp 栈顶寄存器也要发生改变</font>



  • 所以此时应该发生这样的变化,pop 了之后【esp】也要发生一个变化



  • 到 VS 中再来看一下变化。此时不要混淆了,栈是从高地址往低地址增长的,所以栈底的地址来的大一些



==第三十九条指令==


  • 第三十九条指令,可以看到只有一个【ret】,这个指令的执行会从栈顶弹出一个值,那这个时候从上图其实可以看到此时的【esp】栈顶寄存器指向的这块地址,这是什么地址呢?没错,<font color = "#0000ff">就是 call 指令的下一条指令地址,即是我们在进入 Add 函数前提前压入的地址</font>


00461785 C3        ret
复制代码


  • 此时就会直接跳转到 call 指令下一条指令的地址处,继续往下执行。我们到 VS 中来瞧瞧



  • 再来看看【esp】的变化



==第四十条指令!!!==


  • <font color = "#ff0000">本条指令非常重要,可以解决大多数人的困惑</font>

  • 但是有同学看了之后就觉得,这不是就是一个【esp】的变化嘛。【add】是加法命令,也就是将【esp】的位置加上一个 8,一块内存空间是 4,加 8 的话那此时【esp】是不是就来到了【edi】的位置

  • <font color = "#0000ff">这其实就是在【销毁 Add 函数的函数形参 x,y】,这下你应该明白函数形参是在什么时候销毁的了吧,没错,就是从 Add 函数回到 main 函数之后</font>


0046185D 83 C4 08      add      esp,8
复制代码


  • 我们来看看示意图



  • 一样,VS 也来看看【esp】的变化



==第四十一条指令==


  • 接下去这条指令就是将我们现在保存在【eax】中的 30,放到【ebp-20h】的地方。那我们回忆一下,这块地方不就是 main 函数变量 c 的空间嘛,是吧:smile:

  • 那其实这个逻辑就全部打通了,先前在 Add 函数中计算出来的 30,首先放到【eax】寄存器中保存起来,现在过来好几条指令后,它还保存在里面,我们只需要使用【mov】将数据做一个转移即可


00461860 89 45 E0       mov     dword ptr [ebp-20h],eax
复制代码


  • 到 VS 里来看看变化



  • 再来更新一下【ebp-20h】这块栈空间



  • 最后的话只需要将结果打印出来即可


好,讲到这里,函数栈帧的销毁也结束了,你学废︿( ̄︶ ̄)︿了吗 c


  • 以下是这个栈的全局浏览图

四、开局疑难解答

在看了本文的内容后,我们来解答一下在开头提出来的这五个问题,这也是面试中可能会考到的,如果你在了解了函数栈帧的创建和销毁后去回答这些问题,那面试官一定会被你折服的(☆▽☆)


==① 局部变量是如何创建的?==


  • [x] 首先为函数分配好栈帧空间,将这块栈帧空间初始化好后,然后给局部在栈帧里分配空间


==② 为什么局部变量不初始化内容是随机的?==


  • [x] 因为函数栈帧中的空间是预先初始化好的【0xCCCCCCCCh】,若是不为变量初始化内容,那使用的就是初始化好后的内容,以字符的形式打印出来便是<font color = "#ff0000">烫烫烫烫烫烫</font>


==③ 函数调用时参数时如何传递的?传参的顺序是怎样的?==


  • [x] 当还没有进入函数的时候,就已经将函数实参做了一份临时拷贝,并从右向左压入栈中【FILO】,当真正进入到函数栈帧中时,通过指针的偏移量,就可以顺着找回来,找到这份临时拷贝的形参


==④ 函数的形参和实参分别是怎样实例化的?==


  • [x] 形参确实是我在压栈的时候开辟的一块空间,它和实参只是值相同,但是空间是独立的,所以形参是实参的一份临时拷贝,改变形参的值不会影响到实参


==⑤ 函数调用是怎么做的?返回值是如何带会的?==


  • [x] 当执行到【call】指令的时候,把 call 指令的下一条指令地址压入栈中,相当于记住了这个地址。接着进入到函数中,当函数执行结束的时候,回到主函数中,再执行【ret】指令就可以回到 call 指令的下一条指令地址

  • [x] 返回值是通过寄存器带回来的、将函数中计算出来的返回值存放到寄存器中,因为寄存器不会随着函数的调用结束而被销毁,最后再将寄存器中存放的数据转存回对应的内存块中即可

五、总结与提炼

好,我们来回顾一下本文所学习的内容,在本文中,主要带大家了解了函数栈帧的创建和销毁


  • 首先我们知道什么是函数栈帧,学习函数栈帧可以解决哪些我们所不了解的知识点

  • 接着我们初步了解了一下对于汇编代码所需要使用到的常用【寄存器】和【指令】,为后文的学习打下了坚实的基础

  • 然后我便用了很大的篇幅通过 VS 中的【反汇编】【内存块】【调试】这些窗口一步步展现了函数栈帧是如何建立和销毁的,真正地理解了函数传参和调用的过程

  • 理解了这些之后,对底层的一个脉络有了清晰的认识,便可以轻松地回答出开头提出的那些问题,使面试官被你所折服


最后,非常感谢您对本文的阅读,期待您的一键三连哦:heart::heart::heart:



发布于: 9 小时前阅读数: 3
用户头像

Fire_Shield

关注

语言观决定世界观 2022-09-02 加入

高校学生,热爱编程,喜欢写作

评论

发布
暂无评论
C生万物 | 反汇编深挖【函数栈帧】的创建和销毁_C语言_Fire_Shield_InfoQ写作社区