目录
1.相关寄存器和汇编指令
1.1相关寄存器
1.2相关汇编命令
2.函数栈帧的创建和销毁
2.1函数栈帧的创建
2.2函数栈帧的销毁
eax:通用寄存器,保留临时数据,常用于返回值。
ebx:通用寄存器,保留临时数
ebp:栈底寄存器。
esp:栈顶寄存器。
eip:指令寄存器,保存当前指令的下一条指令的。
mov:数据转移指令。
push:数据入栈,同时esp栈顶寄存器也要发生改变。
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变。
sub:减法命令。
add:加法命令。
call:函数调用,1. 压入返回地址2.转入目标函数。
jump:通过修改eip,转入目标函数,进行调用。
ret:恢复返回地址,压入eip,类似popeip命令。
首先我们要知道,每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。
这块空间的维护是使用了2个寄存器:esp和ebp,ebp和esp存放的是地址,用来维护函数栈帧的
如下图所示:
函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异,本次演示以VS2019为例。
演示代码:
#include
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;
}
那接下来我们从main函数的栈帧创建开始讲解:
调试到main函数开始执行的第一行,右击鼠标转到反汇编。
我们接下来一行行解析汇编代码
在main函数之前也是有函数去调用main函数的,这个函数可能根据编译器而定,这里不细说明。
(1)00BE1820 push ebp
push ebp就是把ebp的值放到函数顶,也就是压栈,ebp里面的值就是调用main函数的函数的ebp的值,而esp就指向了ebp的顶部。
(2)00BE1821 mov ebp , esp
move指令会将esp的值存放到ebp中,就相当于ebp和esp指向同一个地址。
(3)00BE1823 sub esp , 0E4h
sub指令会让esp的地址减去逗号后面的0E4h(16进制),产生一个新的esp,然后新的esp就指向了之前操作的地址,现在ebp和esp之间的空间就是为了main函数开辟的,也就是main函数的栈帧空间。
(4)00BE1829 push ebx
同(1) 将ebx的值进行压栈,ebx - 4.
(5)00BE182A push esi
同(1)将esi的值进行压栈,esi - 4.
(6)00BE182B push edi
同(1)将edi的值进行压栈,dei - 4.
(7)00BE182C lea edi , [ebp-24h]
将 edi - 24的值放到 lea 中。
(8)00BE182F mov ecx , 9
将9放到 ecx 中
(9)00BE1834 mov eax , 0CCCCCCCCh
将0CCCCCCCCh放到 eax 中
(10)00BE1839 rep stos dword ptr es : [edi]
从edi(dbp - 24h)到ebp之间所有的元素赋值为0CCCCCCCCh,也就是将为main函数开辟的空间全部赋值为0CCCCCCCCh。
(11)int a = 3;
00BE183B movd word ptr [ebp - 8] , 3
将3储存到 ebp - 8 的地址处,也就是存放到变量a的空间里。
(12)int b = 5;
00BE1842 movd word ptr [ebp - 14h] , 5
将5储存到 ebp - 14h的地址处,也就是存放到变量b的空间里。
(13)int ret = 0;
00BE1849 movd word ptr [ebp - 20h] , 0
将0储存到 ebp - 20h的地址处,也就是存放到变量ret的空间里。
(11)~ (13)的代码其实就是为变量 a , b , ret 做初始化,也就是说局部变量是在函数的栈帧空间里创建的。
(14)ret = Add(a, b);
调用Add函数,并且进行传参。
(15)00BE1850 mov eax , dword ptr [ebp - 14h]
将 ebp - 14h 地址中的数据存放到 eax 中,也就是传递b的值。
(16)00BE1853 push eax
对 eax 进行压栈,eax - 4.
(17)00BE1854 mov ecx , dword ptr [ebp - 8]
将 ebp - 8 地址中的数据存放到 ecx 寄存器中,也就是传递a的值。
(18)00BE1857 push ecx
对 ecx 进行压栈,ecx - 4.
(19)00BE1858 call 00BE10B4
调用函数 地址为 00BE10B4。
(20)00BE185D add esp , 8
call指令的下一条地址
(21)00BE1860 mov dword ptr [ebp - 20h] , eax
call指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。
接下来嗯f11就进入Add函数里面去了
代码执行到Add函数的时候,就要开始创建Add函数的栈帧空间了。
在Add函数中创建栈帧的方法和在main函数中是相似的,只是在栈帧空间的大小上可能略有差异。
图片中的a'和b'其实就是Add函数的形参x,y。这里的分析很好的说明了函数的传参过程,以及函数在进行值传递调用的时候,形参其实是实参的一份拷贝。对形参的修改不会影响实参。
当函数调用要结束返回的时候,前面创建的函数栈帧也开始销毁。
那具体是怎么销毁的呢?我们看一下反汇编代码。
(1)00BE177F pop edi
在栈顶弹出一个值存放到 edi 中,esp+4。
(2)00BE1780 pop esi
同(1),在栈顶弹出一个值存放到 esi 中,esp+4。
(3)00BE1781 pop ebx
同(1)在栈顶弹出一个值存放到ebx中,esp+4。
(4)00BE1782 mov esp , ebp
再将 Add 函数的 ebp 的值赋值给 esp ,相当于回收了 Add 函数的栈帧空间
(5)00BE1784 pop ebp
弹出栈顶的值存放到 ebp ,栈顶此时的值恰好就是main函数的 ebp ,esp+4,此时恢复了main函数的栈帧维护,esp 指向main函数栈帧的栈顶,ebp 指向了main函数栈帧的栈底。
(6)00BE1785 ret
ret 指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是 call 指令下一条指令的地址,此时esp+4,然后直接跳转到 call 指令下一条指令的地址处,继续往下执
回到了call指令的下一条指令的地方:
但调用完Add函数,回到main函数的时候,继续往下执行,可以看到:
到这里就给大家完整的演示了main函数栈帧的创建,Add函数栈帧的创建和销毁的过程 。