1. 栈
1.1 什么是栈
什么是栈?在内存中栈是一种特殊的数据结构,它遵循后进先出的规则。内存中的栈通常用于存储临时变量,函数调用的上下文(每一次函数调用,都会在内存上创建空间,用来存放函数参数,函数返回值,临时变量等),返回的地址,栈是由操作系统管理的。
1.2 栈在内存中的表示
在内存中,栈通常被实现为一个连续的内存区域。在经典的操作系统中,栈总是向下增长的(由高地址向低地址)的。
1.3 寄存器
什么是寄存器?寄存器是CPU内部用来存放数据的小型存储区域,常用来存放参与运算的数据和结果。通俗来讲,寄存器是用来存放一些数据的。
ebp:栈底寄存器
esp:栈顶寄存器,esp会随程序的运行不断改变
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
eip:指令寄存器,保存当前指令的下一条指令的地址
1.4 常见的汇编指令
1.4.1 数据传输指令
mov:将数据从一个位置移动到另一个位置
push:将数据压入栈中
pop:从栈中弹出数据
1.4.2 算术指令
add:加法指令
sub:减法指令
mul:乘法指令
div:除法指令
1.4.3 其他的一些相关指令
call:函数调用,用于压入返回地址和转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip
2. 函数栈帧
简单来说,函数栈帧是函数在调用过程中,程序调用栈所开辟的一块空间。一个函数的栈帧就是函数在内存中栈上的一块空间。这些空间用来存放函数中的临时变量,函数的参数,函数的返回值,以及函数调用前后需要保持不变的寄存器和保存的上下文信息。
3. 函数栈帧的创建与销毁
3.1 演示代码
#include<stdio.h>
int Add(int x, int y) {
int z = 0;
z = x + y;
return z;
}
int main() {
int a = 1;
int b = 2;
int ret = 0;
ret = Add(a, b);
printf("%d\n", ret);
return 0;
}
3.2 通过反编译观察
在C语言中,反编译是将翻译后的二进制代码还原为高级语言代码。我们可以通过反编译看到汇编指令(以Visual Studio 2019演示),main函数转化的汇编代码如下所示:
3.3 函数栈帧的创建
初步拆解每行汇编代码
00AB18B0 push ebp
表示把ebp栈底寄存器中的值进行压栈操作。
00AB18B1 mov ebp,esp
move指令会把esp中的值存放到ebp中,这就是main函数的ebp。
00AB18B3 sub esp,0E4h
sub指令会让esp中的地址剪去一个16进制的数字0xe4,产生新的esp,此时esp是main函数栈帧的esp,结合上面的指令的ebp,esp和ebp之间就维护了一块栈空间,这个栈空间就是为main函数开辟的,也就是main函数的栈帧,后续add函数栈帧的创建与它几乎一样,着一块栈空间用来存放main函数中的局部变量和临时数据等。
00AB18B9 push ebx
00AB18BA push esi
00AB18BB push edi
将寄存器ebx,esi,edi的值依次压入栈中,esp进行三次esp-4操作,这三条指令保存了3个寄存器的值在栈区,这三个寄存器的值在函数随后执行中会被修改,所以先保存寄存器原来的值,以便退出函数时恢复。
00AB18BC lea edi,[ebp-24h]
把ebp-24h的地址,放在edi中。
00AB18BF mov ecx,9
把9放在ecx中。
00AB18C4 mov eax,0CCCCCCCCh
把0xCCCCCCCC的值放在eax中。
00AB18C9 rep stos dword ptr es:[edi]
将从edp-0x2h到ebp这一段内存中的每一个字节都赋值为0xCC,如下图所示:
0xCCCCCCC表示内存中4个字节,可以看成4个一个字节的0xCC
接下来分析main函数中的核心代码:
右击,关闭显示符号名,更方便我们查看变量在内存中的位置。
00AB18D5 mov dword ptr [ebp-8],1
将1存储到ebp-8的地址处,ebp-8的位置就是a变量。
00AB18DC mov dword ptr [ebp-14h],2
将2存储到ebp-14h的地址处,ebp-14h的位置就是b变量。
00AB18E3 mov dword ptr [ebp-20h],0
将0存储到ebp-20h的地址处,ebp-20h的位置就是ret变量。
00AB18EA mov eax,dword ptr [ebp-14h]
00AB18ED push eax
00AB18EE mov ecx,dword ptr [ebp-8]
00AB18F1 push ecx
传递b,将ebp-14h处的值(2)放入eax寄存器中
将eax的值压栈,esp-4
传递a,将ebp-8处的值(1)放入ecx寄存器中
将ecx的值压栈,esp-4
上面的四步操作实际上就是表示函数的传参
00AB18F2 call 00AB10B4
00AB18F7 add esp,8
00AB18FA mov dword ptr [ebp-20h],eax
call指令是要执行函数调用的逻辑,在执行call指令之前会先把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当前函数调用结束时要返回到call指令的下一条指令的地方,然后往后执行。
代码执行到Add函数时就要开始创建Add函数的栈帧空间了
在Add函数中创建栈帧的方法和在main函数中是相似的,只是栈帧空间大小不同
00AB1770 push ebp
00AB1771 mov ebp,esp
00AB1773 sub esp,0CCh
00AB1779 push ebx
00AB177A push esi
00AB177B push edi
将main函数栈帧的ebp保存,压栈,esp-4
将main函数的esp赋值给新的ebp,ebp现在是Add函数的ebp
将esp-0xCC,求出Add函数的esp
将ebx,esi,edi的值进行压栈,并进行3次esp-4操作
00AB177C lea edi,[ebp-0Ch]
00AB177F mov ecx,3
00AB1784 mov eax,0CCCCCCCCh
00AB1789 rep stos dword ptr es:[edi]
先将ebp-0Ch的地址放入edi中
把3放在ecx中
将0xCCCCCCCC放入eax中
将从edp-0x0Ch到ebp这段的内存中的每个字节都初始化为0xCC
Add函数的核心部分
int z = 0;
00AB1795 mov dword ptr [ebp-8],0
将0放在ebp-8的地址处,其实就是创建z
z = x + y;
00AB179C mov eax,dword ptr [ebp+8]
将ebp+8地址处的数字存储在eax中
00AB179F add eax,dword ptr [ebp+0Ch]
将ebp+0Ch地址处的数字加到eax寄存器中
00AB17A2 mov dword ptr [ebp-8],eax
将eax的结果保存在ebp-8的地址处,其实就是放在z中
return z;
00AB17A5 mov eax,dword ptr [ebp-8]
将ebp-8地址处的值放在eax中,其实就是把z的值保存在寄存器eax中,这里是想通过寄存器带回计算的结果,作为函数的返回值。
3.4 函数栈帧的销毁
Add函数栈帧的销毁
00AB17A8 pop edi
00AB17A9 pop esi
00AB17AA pop ebx
依次在栈顶弹出一个值,放入edi,esi,ebx中,并进行3次esp+4的操作
00AB17B8 mov esp,ebp
将Add函数的ebp的值赋值给esp,相当于回收了Add函数的栈帧空间
00AB17BA pop ebp
弹出栈顶的值存放在ebp中,栈顶此时的值恰好就是main函数的ebp,esp+4,此时就恢复了main函数的栈帧维护,esp指向main函数栈帧的栈顶,ebp指向了main函数栈帧的栈底
00AB17BB ret
ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令下一条指令的地址,此时esp+4,然后直接跳转到call指令下一条指令的地址处。
00AB18F7 add esp,8
esp直接+8,相当于跳过了main函数中压栈的x,y
00AB18FA mov dword ptr [ebp-20h],eax
将eax中的值,存档在ebp-20h的地址处,其实就是把eax、寄存器中存储的结果存储到main函数中的变量ret中,eax中存储的值就是x和y的和。
至此,完成演示了main函数的创建和Add函数的创建和销毁的过程,希望对大家有所帮助,也感谢大家的阅读。
标签:销毁,函数,esp,mov,eax,ebp,main,栈帧 From: https://blog.csdn.net/2301_76928097/article/details/144447668