我们假设,这块长方形区域就是栈区。
当调用函数的时候,栈区里面将会产生一系列动作,接下来我们一起研究一下:
以一个简单的加法函数为例:
#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函数,然后调用Add函数。
但是,main函数也不是直接就调用的,它是被一个叫做mainCRTStartup函数所调用的,而这个函数又是被__tmainCRTStartup的函数所调用的,这里面的逻辑略显繁琐。
如图:
接着,我们需要了解两个寄存器:esp和ebp,它们分别是指向栈顶(esp)和栈底(ebp)的指针,用来维护函数的运行。也就是说,函数的活动范围就看他们两个的位置距离,空间越大,调用函数时使用的空间就越大,每次当函数创建时,他们都会一上一下开辟空间,当函数需要销毁或者缩小范围时,他们也会调整自己的位置,从而达到调用和销毁的目的。
接下来我们分析一下反汇编代码:
int main() {
002718A0 push ebp
002718A1 mov ebp,esp
002718A3 sub esp,0E4h
002718A9 push ebx
002718AA push esi
002718AB push edi
002718AC lea edi,[ebp-24h]
002718AF mov ecx,9
002718B4 mov eax,0CCCCCCCCh
002718B9 rep stos dword ptr es:[edi]
002718BB mov ecx,27C003h
002718C0 call 0027131B
int a = 10;
002718C5 mov dword ptr [ebp-8],0Ah
int b = 20;
002718CC mov dword ptr [ebp-14h],14h
int c = 0;
002718D3 mov dword ptr [ebp-20h],0
我们逐句分析:
002718A0 push ebp
push是压栈的意思,就是说把ebp放到了之前__tmainCRTStartup的函数的栈帧的上面
002718A1 mov ebp,esp
mov是move的缩写,也就是把xx(后面)的值赋到了xx(前面)里面,在这里的意思是把esp的值赋到了ebp里面,也就是把原来__tmainCRTStartup函数的栈顶指针变成了main函数的栈底指针(意义上是这样的,实际而言寄存器还是自己本身,只是数字变了)
002718A3 sub esp,0E4h
sub是减去的意思,把esp减去0E4h(这是一个数字)而因为在栈区里面,一般是从高地址向低地址消耗的(就是地址名门牌号由高向低),所以,esp减去一个数字就相当于是向低地址扩张这么多空间,如图:
002718A9 push ebx
002718AA push esi
002718AB push edi
上面三条都是压栈的操作,将ebx、esi、edi三个具有不同作用的寄存器逐个压到最上面去,这三个寄存器的具体作用在这里不展开说了。
002718AC lea edi,[ebp-0E4h]
002718AF mov ecx,9
002718B4 mov eax,0CCCCCCCCh
002718B9 rep stos dword ptr es:[edi]
002718BB mov ecx,27C003h
这五行的意思是:在ebp-24h处,重复27C003次,赋值0CCCCCCCCh,每次赋值开辟dword,也就是4个字节。
int a = 10;
002718C5 mov dword ptr [ebp-8],0Ah
int b = 20;
002718CC mov dword ptr [ebp-14h],14h
int c = 0;
002718D3 mov dword ptr [ebp-20h],0
前面说过,mov意思是把后者的值赋到前者
所以这三行汇编语言的意思是,将0Ah,也就是十六进制的10;14h,也就是十六进制的20;0,这三个数字分别赋到ebp-8;ebp-14h;ebp-20h上
接着是传参,这里将a和b进行传值调用,也就是传形参,一起看看他的逻辑:
ret = Add(a, b);
00BE1850 mov eax,dword ptr [ebp-14h]
00BE1853 push eax
00BE1854 mov ecx,dword ptr [ebp-8]
00BE1857 push ecx
00BE1858 call 00BE10B4
00BE185D add esp,8
00BE1860 mov dword ptr [ebp-20h],eax
首先,将ebp-14h(b)这个地址所存放的数字赋值到eax中
接着把eax压栈上来
同理,ebp-8(a)这个地址所存放的数字赋值到ecx中
接着压栈ecx
接着用call调用会调用Add函数,并把下一行指令的地址进行压栈
(图中最上面的ebp在下一段有讲解)
如图:
接下来我们看调用Add函数的逻辑:
int Add(int x, int y)
{
00BE1760 push ebp
00BE1761 mov ebp,esp
00BE1763 sub esp,0CCh
00BE1769 push ebx
00BE176A push esi
00BE176B push edi
首先将ebp(栈底指针)进行压栈操作,放到最上面,这样做方便最后销毁函数的时候找得到main函数;
接着将esp的值赋到ebp里面,也就是把栈底指针移到和栈顶指针一样的位置上;
然后将esp减去0CCh,栈顶指针减去数字,很明显是要开辟0CCh这么多的空间;
最后就是进行三个压栈的操作,esp也要-4-4-4,共减去12,来容纳ebx、esi、edi这三个寄存器。
如图:
接着我们来看Add内部的逻辑
int z = 0;
00BE176C mov dword ptr [ebp-8],0
z = x + y;
00BE1773 mov eax,dword ptr [ebp+8]
00BE1776 add eax,dword ptr [ebp+0Ch]
00BE1779 mov dword ptr [ebp-8],eax
第一行给z赋值,和之前的赋值操作同理,直接找到ebp-8的位置,把0赋值进去;
重点是如何进行加法的操作:
接下来我们看一下Add函数是如何销毁的:
return z;
00BE177C mov eax,dword ptr [ebp-8]
}
00BE177F pop edi
00BE1780 pop esi
00BE1781 pop ebx
00BE1782 mov esp,ebp
00BE1784 pop ebp
00BE1785 ret
ebp-8我们知道是z的位置,那么他所存放的值赋到eax后,因为eax是寄存器,不会随着函数的销毁而消失,所以就可以带回main函数。
pop:弹出,即esp+4,把最上面空间的东西销毁,并将最上面的空间还给内存
可以看到,弹出了edi、esi、ebx三个寄存器,esp+4+4+4
接着把ebp的值赋到esp上,也就是将栈顶指针拉下来,拉到和栈底指针一样的位置
最后弹出ebp,并进行ret(返回)
这里注意,虽然弹出了ebp,但是之前压栈的有main的ebp地址,看图:
在回到函数之后,我们再看一下,既然函数已经销毁了,那它是怎么把z的值带回来的呢?
00BE185D add esp,8
00BE1860 mov dword ptr [ebp-20h],eax
可以看到,首先esp直接加了8,跳过了原来的a和b的形参
接着把eax的值放到了ebp-20h的地址上,也就是ret中,到此为止,整个调用函数并销毁的过程就完成了,之后还有main函数的销毁,道理是一样的,故在此不展开说明。
如果本文对您有帮助,可不可以给我一个小小的点赞呀❤~您的支持是我最大的动力。
博主小白一枚,才疏学浅,难免有所纰漏,欢迎大家讨论和提出问题,博主一定第一时间改正。
谢谢观看嘿嘿(๑•̀ㅂ•́)و✧~!