函数栈帧的创建和销毁(详细!图解!)

1、函数的创建

1.1 main函数的调用

image.png
我们假设,这块长方形区域就是栈区
当调用函数的时候,栈区里面将会产生一系列动作,接下来我们一起研究一下:
以一个简单的加法函数为例:

#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;
}

在运行这个函数的时候,显然我们需要先调用main函数,然后调用Add函数。
但是,main函数也不是直接就调用的,它是被一个叫做mainCRTStartup函数所调用的,而这个函数又是被__tmainCRTStartup的函数所调用的,这里面的逻辑略显繁琐。
如图:
image.png

1.2 esp和edp寄存器

接着,我们需要了解两个寄存器:esp和ebp,它们分别是指向栈顶(esp)和栈底(ebp)的指针,用来维护函数的运行。也就是说,函数的活动范围就看他们两个的位置距离,空间越大,调用函数时使用的空间就越大,每次当函数创建时,他们都会一上一下开辟空间,当函数需要销毁或者缩小范围时,他们也会调整自己的位置,从而达到调用和销毁的目的。
image.png

1.3 开辟main函数的空间

接下来我们分析一下反汇编代码

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函数的栈底指针(意义上是这样的,实际而言寄存器还是自己本身,只是数字变了)
image.png

002718A3  sub         esp,0E4h  

sub是减去的意思,把esp减去0E4h(这是一个数字)而因为在栈区里面,一般是从高地址向低地址消耗的(就是地址名门牌号由高向低),所以,esp减去一个数字就相当于是向低地址扩张这么多空间,如图:image.png

1.4 初始化main函数内部

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个字节。
image.png

1.5 创造变量并赋值(main函数内)

	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上

  1. ebp-8,减去8就是往低地址上去2个双字节型空间
  2. ebp-14h,减去20就是往低地址上去5个双字节型空间
  3. ebp-20h,减去32就是往低地址上去8个双字节型空间
    如图所示:图 1
    image.png
    位置找到了之后,值也会赋到相应的空间:图2
    image.png

1.6 函数的传参(Add函数)

接着是传参,这里将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在下一段有讲解)
如图:image.png

1.7 Add函数的调用

接下来我们看调用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这三个寄存器。
如图:

1.8 Add函数的定义

接着我们来看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赋值进去;
重点是如何进行加法的操作:

  • 可以看到,里面有一个叫做eax的寄存器,而这个寄存器就是用来进行累加的,它可以在未来函数销毁的时候数据安全带回main函数~
  • ptr的意思是指针,也就是指他的地址
    有了这个知识点,我们就很容易可以理解下面的三行是干什么的了:
    首先将 ebp+8 所在地址的数字(即原来的形参b)保存到eax寄存器中(之前是赋值用,现在是需要累加)
    接着将 ebp+0Ch 所在地址的数字(即原来的形参a)累加到eax中
    最后把eax中存放的数字放到ebp-8里面,而ebp-8,不就是z所在的地址嘛~
    也就是说,eax把自己存储的相加结果放到了z里面
    如图:
    image.png

2、函数的销毁

2.1 Add函数的销毁

接下来我们看一下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地址,看图:
image.png

2.2 销毁后如何带回数值

在回到函数之后,我们再看一下,既然函数已经销毁了,那它是怎么把z的值带回来的呢?

00BE185D  add     esp,8 
00BE1860  mov     dword ptr [ebp-20h],eax  

可以看到,首先esp直接加了8,跳过了原来的a和b的形参
接着把eax的值放到了ebp-20h的地址上,也就是ret中,到此为止,整个调用函数并销毁的过程就完成了,之后还有main函数的销毁,道理是一样的,故在此不展开说明。

写在最后

如果本文对您有帮助,可不可以给我一个小小的点赞呀❤~您的支持是我最大的动力。

博主小白一枚,才疏学浅,难免有所纰漏,欢迎大家讨论和提出问题,博主一定第一时间改正。

谢谢观看嘿嘿(๑•̀ㅂ•́)و✧~!

猜你喜欢

转载自blog.csdn.net/weixin_70218204/article/details/131755727