目录
1.什么是函数栈帧
我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。函数栈帧是内存中存储执行特定函数所需信息的一块区域,就是函数在使用过程中在栈区上所开辟的空间。它包含了函数执行过程中所需的各种数据,如局部变量,返回地址,传递给函数的参数等。
2.了解函数栈帧的好处
加深我们对栈区以及函数的理解
- 局部变量是怎么创建的?
- 为什么局部变量的值是随机值?
- 函数是怎么传参的?传参的顺序是怎样的?
- 形参和实参是什么关系?
- 函数调用是怎么做的?
- 函数调用结束后怎么返回
当你将这篇文章看完之后你就会得到答案
那我们开始进入函数栈帧的海洋中
3.函数栈帧的创建与销毁
3.1什么是栈
这里就需要介绍内存中是如何划分区域
由图可以看出从低地址往高地址依次分配:
- 代码区(code segment):你写的代码在这个区域中,这个区域的代码可以共享,用来节省内存空间,代码区通常是只读的
- 常量区(constant):常量区是用来存储字符串常量,数字常量,#define的常量和其他静态常量数据,其内容在运行时不能被修改
- 静态区(全局区)(static):用于存放全局变量和静态变量,存放在静态区的变量,整个程序运行期间都存在
- 堆区(heap):用于动态内存分配,通常用malloc手动申请和释放
- 栈区(stack):栈区是由编译器自动分配和释放的内存区域,用于存放局部变量,函数,函数参数和返回值。栈区的内存使用规则是:先使用高地址在使用低地址
这次我们的主角是栈区(stack)
3.2 认识相关寄存器和汇编指令
相关寄存器
- eax:通用寄存器,保留临时数据,常用于返回值
- ebx:通用寄存器,保留临时数据
- ebp:栈底寄存器
- esp:栈顶寄存器
- eip:指令寄存器,保存当前指令的下一条指令的地址
虽然有很多寄存器但是我们需要记住两个即可esp,ebp
相关汇编指令
- mov:数据转移指令
- push:数据入栈,同时esp栈顶寄存器也要发生改变
- pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
- sub:减法命令
- add:加法命令
- call:函数调用,1. 压入返回地址 2. 转入目标函数
- jump:通过修改eip,转入目标函数,进行调用
- ret:恢复返回地址,压入eip,类似pop eip命令
3.3解析函数栈帧的创建与销毁
3.3.1预备知识
首先我们达成一些预备知识才能有效的帮助我们理解,函数栈帧的创建和销毁。
- 每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。
- 这块空间的维护是使用了2个寄存器: esp 和 ebp , ebp 记录的是栈底的地址, esp 记录的是栈顶的地址。
如图所示:
函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异,本次演示以VS2022为例。
3.3.2 函数的调用堆栈
演示代码:
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a,b);
return 0;
}
这段代码我们调试以后查看调用堆栈,勾选【显示外部代码】
如下图:
我们可以看到mian函数在调用之前也是被调用的,main是由invoke_main函数来调用的,invoke_main前面的几个函数我们就不考虑了
每个栈帧建立的时候都有esp,ebp来维护栈帧空间
那就开始吧!
3.3.3 准备环境
为了让我们研究函数栈帧的过程足够清晰,不要太多干扰,我们可以关闭下面的选项,让汇编代码中排除一些编译器附加的代码:
3.3.4 转到反汇编
建议用32位的系统,这样汇编代码更多细节,调试到main函数开始执行的第一行,右击鼠标转到反汇编
这段是mian函数中所有的汇编代码
int main()
{
008B1840 55 push ebp
008B1841 8B EC mov ebp,esp
008B1843 81 EC E4 00 00 00 sub esp,0E4h
008B1849 53 push ebx
008B184A 56 push esi
008B184B 57 push edi
008B184C 8D 7D DC lea edi,[ebp-24h]
008B184F B9 09 00 00 00 mov ecx,9
008B1854 B8 CC CC CC CC mov eax,0CCCCCCCCh
008B1859 F3 AB rep stos dword ptr es:[edi]
int a = 10;
008B185B C7 45 F8 0A 00 00 00 mov dword ptr [ebp-8],0Ah
int b = 20;
008B1862 C7 45 EC 14 00 00 00 mov dword ptr [ebp-14h],14h
int c = 0;
008B1869 C7 45 E0 00 00 00 00 mov dword ptr [ebp-20h],0
c = Add(a,b);
008B1870 8B 45 EC mov eax,dword ptr [ebp-14h]
008B1873 50 push eax
008B1874 8B 4D F8 mov ecx,dword ptr [ebp-8]
008B1877 51 push ecx
008B1878 E8 3C F8 FF FF call 008B10B9
008B187D 83 C4 08 add esp,8
008B1880 89 45 E0 mov dword ptr [ebp-20h],eax
}
int Add(int x, int y)
{
008B1780 55 push ebp
008B1781 8B EC mov ebp,esp
008B1783 81 EC CC 00 00 00 sub esp,0CCh
008B1789 53 push ebx
008B178A 56 push esi
008B178B 57 push edi
int z = 0;
008B178C C7 45 F8 00 00 00 00 mov dword ptr [ebp-8],0
z = x + y;
008B1793 8B 45 08 mov eax,dword ptr [ebp+8]
008B1796 03 45 0C add eax,dword ptr [ebp+0Ch]
008B1799 89 45 F8 mov dword ptr [ebp-8],eax
return z;
008B179C 8B 45 F8 mov eax,dword ptr [ebp-8]
}
008B179F 5F pop edi
008B17A0 5E pop esi
008B17A1 5B pop ebx
008B17A2 8B E5 mov esp,ebp
008B17A4 5D pop ebp
008B17A5 C3 ret
先看这一段,这里就是创建栈帧的过程
->1.
当这段汇编代码结束之后如图:
上面的这段代码最后4句,等价于下面的伪代码:
edi = ebp-0x24;
ecx = 9;
eax = 0xCCCCCCCC;
for(; ecx = 0; --ecx,edi+=4)
{
*(int*)edi = eax;
}
我们还可以看内存中是怎么操作的,调试-窗口-内存
可以看到ebp上面的内容都被初始化为了cccccccc
小知识:烫烫烫~
之所以上面的程序输出“烫”这么一个奇怪的字,是因为main函数调用时,在栈区开辟的空间的其中每一个字节都被初始化为0xCC,而arr数组是一个未初始化的数组,恰好在这块空间上创建的,0xCCCC(两个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC被当作文本就是“烫”。
->2.
->3.
->4.
销毁栈帧
->5.
3.3.5 整体结果图
标签:00,销毁,函数,int,mov,ebp,栈帧 From: https://blog.csdn.net/m0_73634434/article/details/141452613