c语言(函数栈帧的创建和销毁)

前沿:

        可能很多人也是第一次听说函数栈帧这个词,想问什么是函数栈帧,理解函数栈帧有什么作用,函数栈帧的创建销毁是什么呢?这章节我们就来了解一下c语言中函数栈帧的创建和销毁。

思维导图:

目录

 

一、什么是函数栈帧

 1.1   函数栈帧:

1.2  栈:

 1.3寄存器:

   1.3.1  寄存器的概念是:

1.3.2寄存器的功能:

1.3.3相关的寄存器为:

1.3.4 相关汇编命令:

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

2.1首先了解运行时栈堆的使用:

2.2 代码演示栈帧创建销毁:

2.2.1首先对这个代码进行调试:​编辑

2.2.2 代码反汇编:

2.2.3函数栈帧的创建:

2.2.4函数栈帧的销毁:

三、总结:


一、什么是函数栈帧

 1.1   函数栈帧:

        1.1.1  函数被调用时,系统会在栈区为该函数开辟一块栈空间,这个栈空间就是该函数的函数栈帧。以main函数的调用为例,main函数是被 invoke_main函数调用的,nvoke_main函数被那个函数调用我们先不去了解。当main函数被调用时就会在栈区为其开辟一块空间来用于main函数的执行,而当main函数结束时这块空间将会进行销毁,这个过程就是函数栈帧的创建和销毁的初步了解和认识。

1.2  栈:

    1.2.1  学习栈帧前我们先了解一下栈 栈的概念是:栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
     1.2.2  栈 通俗的理解可以理解为我们所住的宾馆,当我们需要的时候就入住,而入住时间结束则需要退房,所以在计算机里面栈就是指数据暂时存储的地方,所以才有进栈、出栈的说法。
    1.2.3 在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据 从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。 在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。 在我们常见的i386 或者 x86-64 下,栈顶由成为 esp 的寄存器进行定位的。

 1.3寄存器:

~同样的我们也需要了解一下什么是寄存器。

   1.3.1  寄存器的概念是

       1、寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。其实寄存器就是一种常用的时序逻辑电路,但这种时序逻辑电路只包含存储电路。寄存器的存储电路是由锁存器或触发器构成的,因为一个锁存器或触发器能存储1位二进制数,所以由N个锁存器或触发器可以构成N位寄存器。寄存器是中央处理器内的组成部分。寄存器是有限存储容量的高速存储部件,它们可用来暂存指令、数据和位址。

1.3.2寄存器的功能:

①清除数码:将寄存器里的原有数码清除。

②接收数码:在接收脉冲作用下,将外输入数码存入寄存器中。

③存储数码:在没有新的写入脉冲来之前,寄存器能保存原有数码不变。

④输出数码:在输出脉冲作用下,才通过电路输出数码。

1.3.3相关的寄存器为

① eax:通用寄存器,保留临时数据,常用于返回值。
② ebx:通用寄存器,保留临时数据。
③ ebp:栈底寄存器。
④ esp:栈顶寄存器。

⑤ eip:指令寄存器,保存当前指令的下一条指令的地址。

1.3.4 相关汇编命令:

1、mov :数据转移指令
2、push :数据入栈,同时 esp 栈顶寄存器也要发生改变
3、pop :数据弹出至指定位置,同时 esp 栈顶寄存器也要发生改变
4、sub:减法命令
5、add:加法命令
6、call :函数调用, 1 . 压入返回地址 2. 转入目标函数
7、jump :通过修改 eip ,转入目标函数,进行调用
8、ret :恢复返回地址,压入 eip ,类似 pop eip 命令

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

2.1首先了解运行时栈堆的使用:

函数被调用时,系统会在栈区为该函数开辟一块栈空间,这个栈空间就是该函数的函数栈帧,而以用到的寄存器esp 和 ebp , ebp 记录的是栈底的地址, esp 记录的是栈顶 的地址。

2.2 代码演示栈帧创建销毁:

#include <stdio.h> 
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; 
}

 代码是很简单的主要是为了我们来进行深入的了解栈帧的创建和销毁。

2.2.1首先对这个代码进行调试:

      我们刚开始就已经说啦,main函数是被invoke_main函数调用的,invoke_main函数被那个函数调用我们先不去了解。但函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异,可能不同的编译器main调用函数不一样但是方法都是相同的。这样我们就可以知道invoke_main函数肯定存在自己的栈帧,相同的main函数和Add函数也会存在自己的栈帧,每个函数栈帧都有自己的 ebp esp 来维护栈帧空间。

2.2.2 代码反汇编:

2.2.3函数栈帧的创建:

  上面代码的反汇编们来帮助我们进行理解 

00 BE1820    push           ebp         // ebp 寄存器中的值进行压栈,此时的 ebp 中存放的是
invoke_main 函数栈帧的 ebp esp-4
00 BE1821    mov            ebp , esp          //move 指令会把 esp 的值存放到 ebp 中,相当于产生了 main 函数的 ebp,这个值就是 invoke_main 函数栈帧的 esp
00 BE1823    sub             esp , 0E4 h         //sub 会让 esp 中的地址减去一个 16 进制数字 0xe4, 产生新的esp,此时的 esp main 函数栈帧的 esp ,此时结合上一条指令的 ebp 和当前的 esp ebp esp 之间维护了一 个块栈空间,这块栈空间就是为main 函数开辟的,就是 main 函数的栈帧空间,这一段空间中将存储 main 函数 中的局部变量,临时数据已经调试信息等。
00 BE1829    push           ebx // 将寄存器 ebx 的值压栈, esp-4
00 BE182A    push           esi // 将寄存器 esi 的值压栈, esp-4
00 BE182B push edi // 将寄存器 edi 的值压栈, esp-4
// 上面 3 条指令保存了 3 个寄存器的值在栈区,这 3 个寄存器的在函数随后执行中可能会被修改,所以先保存寄
存器原来的值,以便在退出函数时恢复。
// 下面的代码是在初始化 main 函数的栈帧空间。
//1. 先把 ebp-24h 的地址,放在 edi
//2. 9 放在 ecx
//3. 0xCCCCCCCC 放在 eax
//4. 将从 edp-0x2h ebp 这一段的内存的每个字节都初始化为 0xCC
00 BE182C    lea                edi ,[ ebp - 24 h ]
00 BE182F    mov              ecx , 9
00 BE1834    mov              eax , 0 CCCCCCCCh
 

 这里我们知道一个小的知识点:

     为什么局部变量的时候一定要初始化,因为若无初始化系统在内存中初始化都为0xCC,产生的随机值我们是不知道的,所以一定要初始化你定的数。

                 int a = 3;

00 BE183B            mov              dword ptr [ ebp - 8 ], 3 // 3 存储到 ebp-8 的地址处, ebp-8 的位置其就是 变量
                int b = 5 ;
00 BE1842            mov              dword ptr [ ebp - 14 h ], 5 // 5 存储到 ebp-14h 的地址处, ebp-14h 的位置 其实是b 变量
                int ret = 0 ;
00 BE1849            mov              dword ptr [ ebp - 20 h ], 0 // 0 存储到 ebp-20h 的地址处, ebp-20h 的位 置其实是ret 变量
// 以上汇编代码表示的变量 a,b,ret 的创建和初始化,这就是局部的变量的创建和初始化
// 其实是局部变量的创建时在局部变量所在函数的栈帧空间中创建的
// 调用 Add 函数
                ret = Add ( a , b );
// 调用 Add 函数时的传参
// 其实传参就是把参数 push 到栈帧空间中
00 BE1850            mov                eax , dword ptr [ ebp - 14 h ]          // 传递 b ,将 ebp-14h 处放的 5 放在 eax 寄存器中
00 BE1853           push                eax          // eax 的值压栈, esp-4
00 BE1854           mov                 ecx , dword ptr [ ebp - 8 ]          // 传递 a ,将 ebp-8 处放的 3 放在 ecx 寄存器中
00 BE1857           push                ecx          // ecx 的值压栈, esp-4
//跳转调用函数
00 BE1858           call                  00 BE10B4
00 BE185D          add                 esp , 8
00 BE1860          mov                 dword ptr [ ebp - 20 h ], eax

Add的传参 : 

        //调用 Add 函数
        ret = Add ( a , b );
        //调用 Add 函数时的传参
        //其实传参就是把参数 push 到栈帧空间中,这里就是函数传参
00 BE1850         mov         eax , dword ptr [ ebp - 14 h ]         // 传递 b ,将 ebp-14h 处放的 5 放在 eax 寄存器中
00 BE1853         push         eax          // eax 的值压栈, esp-4
00 BE1854         mov         ecx , dword ptr [ ebp - 8 ]          // 传递 a ,将 ebp-8 处放的 3 放在 ecx 寄存器中
00 BE1857         push         ecx          // ecx 的值压栈, esp-4
// 跳转调用函数
00 BE1858         call         00 BE10B4
00 BE185D         add         esp , 8
00 BE1860         mov         dword ptr [ ebp - 20 h ], eax

 

 函数调用过程:

00 BE1858         call          00 BE10B4
00 BE185D         add         esp , 8
00 BE1860         mov         dword ptr [ ebp - 20 h ], eax
        call 指令是要执行函数调用逻辑的,在执行 call 指令之前先会把 call 指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call 指令的下一条指令的地方,继续往后执行。
Add函数的反汇编:

 int Add(int x, int y)

{
00 BE1760         push         ebp          // main 函数栈帧的 ebp 保存 ,esp-4
00 BE1761         mov         ebp , esp          // main 函数的 esp 赋值给新的 ebp ebp 现在是 Add 函数的 ebp
00 BE1763         sub         esp , 0 CCh          // esp-0xCC ,求出 Add 函数的 esp
00 BE1769         push         ebx          // ebx 的值压栈 ,esp-4
00 BE176A         push         esi          // esi 的值压栈 ,esp-4
00 BE176B         push         edi         // edi 的值压栈 ,esp-4
int z = 0 ;
00 BE176C         mov         dword ptr [ ebp - 8 ],           // 0 放在 ebp-8 的地址处,其实就是创建 z
z = x + y ;
// 接下来计算的是 x+y ,结果保存到 z
00 BE1773         mov         eax , dword ptr [ ebp + 8 ] // ebp+8 地址处的数字存储到 eax
00 BE1776         add         eax , dword ptr [ ebp + 0 Ch ] // ebp+12 地址处的数字加到 eax 寄存中
00 BE1779         mov         dword ptr [ ebp - 8 ], eax          // eax 的结果保存到 ebp-8 的地址处,其实 就是放到z
return z ;
00 BE177C         mov         eax , dword ptr [ ebp - 8 ]          // ebp-8 地址处的值放在 eax 中,其实就是把z 的值存储到 eax 寄存器中,这里是想通过 eax 寄存器带回计算的结果,做函数的返回值。
}
00 BE177F         pop         edi
00 BE1780         pop         esi
00 BE1781         pop         ebx
00 BE1782         mov         esp , ebp
00 BE1784         pop         ebp
00 BE1785         ret
    代码执行到Add函数的时候,就要开始创建Add函数的栈帧空间了。
 在Add函数中创建栈帧的方法和在main函数中是相似的,在栈帧空间的大小上略有差异而已。就不再进行演示啦。

2.2.4函数栈帧的销毁:

1、要知道我们创建的栈就好比我们住的宾馆,当不用的时候要退房间,栈也一样,当调用完成后要进行销毁。而如何销毁呢我们可以看一下。

00BE177F         pop         edi         //在栈顶弹出一个值,存放到edi中,esp+4

00 BE1780         pop         esi          // 在栈顶弹出一个值,存放到 esi 中, esp+4
00 BE1781         pop         ebx          // 在栈顶弹出一个值,存放到 ebx 中, esp+4
00 BE1782         mov         esp , ebp          //再将 Add 函数的 ebp 的值赋值给 esp ,相当于回收了 Add 函数的栈帧空间
00 BE1784         pop         ebp         // 弹出栈顶的值存放到 ebp ,栈顶此时的值恰好就是 main 函数的 ebp, esp+4,此时恢复了 main 函数的栈帧维护, esp 指向 main 函数栈帧的栈顶, ebp 指向了 main 函数栈帧的栈底。
00 BE1785         ret          //ret 指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是 call 指 令下一条指令的地址,此时esp+4 ,然后直接跳转到 call 指令下一条指令的地址处,继续往下执行。

 
但调用完 Add 函数,回到 main 函数的时候,继续往下执行,可以看到:
 
00 BE185D         add         esp , 8 //esp 直接 +8 ,相当于跳过了 main 函数中压栈的 a'和 b'
00 BE1860         mov         dword ptr [ ebp - 20 h ], eax // eax 中值,存档到 ebp-0x20 的地址处,其实就是存储到main 函数中 ret 变量中,而此时 eax 中就是 Add 函数中计算的 x y 的和,可以看出来,本次函数的返回值是由eax 寄存器带回来的。程序是在函数调用返回之后,在 eax 中去读取返回值的。

 到这里函数栈帧的创建和销毁就结束了。

三、总结:

学完想必我们就好理解一下几个问题啦:

1、局部变量是如何创建的?在申请栈帧空间的时候,会存在一小部分空间来存放我们的局部变量
2、为什么局部变量不初始话的值是随机的?因为若无初始化系统在内存中初始化都为0xCC,产生的随机值我们是不知道的,所以一定要初始化你定的数。
3、函数是怎么传参的?再没有传参之前我们就通过寄存器指令就已经把函数的形参已经传入寄存器中,等调用Add函数时开辟新的栈帧空间时我们再将形参形参传入进去。
4、形参和实参是什么关系?形参是我们在压榨的时候形成的,和我们实参值是相同的,但空间是独立的。所以形参只是我们实参的临时拷贝,改变形参不会影响实参的结果。
5、最后孩子码文不易,如果觉得内容有用,希望大家点赞收藏,一同学习进步!!!!

  • 47
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 53
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 53
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

️小马️

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值