前沿:
可能很多人也是第一次听说函数栈帧这个词,想问什么是函数栈帧,理解函数栈帧有什么作用,函数栈帧的创建销毁是什么呢?这章节我们就来了解一下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.2 栈:
1.3寄存器:
~同样的我们也需要了解一下什么是寄存器。
1.3.1 寄存器的概念是:
1、寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。其实寄存器就是一种常用的时序逻辑电路,但这种时序逻辑电路只包含存储电路。寄存器的存储电路是由锁存器或触发器构成的,因为一个锁存器或触发器能存储1位二进制数,所以由N个锁存器或触发器可以构成N位寄存器。寄存器是中央处理器内的组成部分。寄存器是有限存储容量的高速存储部件,它们可用来暂存指令、数据和位址。
1.3.2寄存器的功能:
①清除数码:将寄存器里的原有数码清除。
②接收数码:在接收脉冲作用下,将外输入数码存入寄存器中。
③存储数码:在没有新的写入脉冲来之前,寄存器能保存原有数码不变。
④输出数码:在输出脉冲作用下,才通过电路输出数码。
1.3.3相关的寄存器为:
⑤ eip:指令寄存器,保存当前指令的下一条指令的地址。
1.3.4 相关汇编命令:
二、函数栈帧的创建和销毁
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-400 BE1821 mov ebp , esp //move 指令会把 esp 的值存放到 ebp 中,相当于产生了 main 函数的 ebp,这个值就是 invoke_main 函数栈帧的 esp00 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-400 BE182A push esi // 将寄存器 esi 的值压栈, esp-400 BE182B push edi // 将寄存器 edi 的值压栈, esp-4// 上面 3 条指令保存了 3 个寄存器的值在栈区,这 3 个寄存器的在函数随后执行中可能会被修改,所以先保存寄存器原来的值,以便在退出函数时恢复。// 下面的代码是在初始化 main 函数的栈帧空间。//1. 先把 ebp-24h 的地址,放在 edi 中//2. 把 9 放在 ecx 中//3. 把 0xCCCCCCCC 放在 eax 中//4. 将从 edp-0x2h 到 ebp 这一段的内存的每个字节都初始化为 0xCC00 BE182C lea edi ,[ ebp - 24 h ]00 BE182F mov ecx , 900 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-400 BE1854 mov ecx , dword ptr [ ebp - 8 ] // 传递 a ,将 ebp-8 处放的 3 放在 ecx 寄存器中00 BE1857 push ecx // 将 ecx 的值压栈, esp-4//跳转调用函数00 BE1858 call 00 BE10B400 BE185D add esp , 800 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-400 BE1854 mov ecx , dword ptr [ ebp - 8 ] // 传递 a ,将 ebp-8 处放的 3 放在 ecx 寄存器中00 BE1857 push ecx // 将 ecx 的值压栈, esp-4// 跳转调用函数00 BE1858 call 00 BE10B400 BE185D add esp , 800 BE1860 mov dword ptr [ ebp - 20 h ], eax
函数调用过程:
00 BE1858 call 00 BE10B400 BE185D add esp , 800 BE1860 mov dword ptr [ ebp - 20 h ], eax
int Add(int x, int y)
{00 BE1760 push ebp // 将 main 函数栈帧的 ebp 保存 ,esp-400 BE1761 mov ebp , esp // 将 main 函数的 esp 赋值给新的 ebp , ebp 现在是 Add 函数的 ebp00 BE1763 sub esp , 0 CCh // 给 esp-0xCC ,求出 Add 函数的 esp00 BE1769 push ebx // 将 ebx 的值压栈 ,esp-400 BE176A push esi // 将 esi 的值压栈 ,esp-400 BE176B push edi // 将 edi 的值压栈 ,esp-4int z = 0 ;00 BE176C mov dword ptr [ ebp - 8 ], // 将 0 放在 ebp-8 的地址处,其实就是创建 zz = 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 edi00 BE1780 pop esi00 BE1781 pop ebx00 BE1782 mov esp , ebp00 BE1784 pop ebp00 BE1785 ret
2.2.4函数栈帧的销毁:
1、要知道我们创建的栈就好比我们住的宾馆,当不用的时候要退房间,栈也一样,当调用完成后要进行销毁。而如何销毁呢我们可以看一下。
00BE177F pop edi //在栈顶弹出一个值,存放到edi中,esp+4
00 BE1780 pop esi // 在栈顶弹出一个值,存放到 esi 中, esp+400 BE1781 pop ebx // 在栈顶弹出一个值,存放到 ebx 中, esp+400 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 指令下一条指令的地址处,继续往下执行。
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 中去读取返回值的。
到这里函数栈帧的创建和销毁就结束了。文章来源:https://www.toymoban.com/news/detail-466391.html
三、总结:
学完想必我们就好理解一下几个问题啦:文章来源地址https://www.toymoban.com/news/detail-466391.html
到了这里,关于c语言(函数栈帧的创建和销毁)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!