函数栈帧的创建和销毁(程序员必了解内容)

目录

前言

函数栈帧详解

main函数栈帧的建立

Add函数栈帧的建立与销毁

总结


前言

不知道大家对下述问题有没有疑问:

1.局部变量是如何创建的

2.未初始化的局部变量为什么是随机值

3.函数是如何传参的,传参顺序是怎样的

4.形参和实参是什么关系

5.函数是如何调用的

每一次函数调用都会在栈区开辟一块空间,也就是函数栈帧。

如果大家对函数栈帧不了解的话,肯度会对上述的问题有疑问,今天就让我带大家一探究竟,打开函数栈帧的大门。话不多说我们直接步入正题。由于编译器版本的不同,函数栈帧建立和销毁可能有些许差异,但大致的步骤都是一样的。我使用的环境是vs2019

函数栈帧的创建和销毁(程序员必了解内容)_第1张图片

                               

函数栈帧详解

在了解函数栈帧之前,我们先介绍一下寄存器,寄存器有:eax,abx,ecx,edx等等,而我们今天要通过ebp,esp这两个寄存器来维护函数栈帧。

ebp:基址指针寄存器,其中存放这一个指针,指向当前栈帧中栈底的元素

bsp:栈指针寄存器,其中存放着一个指针,指向当前栈帧中栈顶的元素,且会随着栈帧的扩充指向栈顶。

栈是先进后出的,栈支持两种基本操作,push和pop。push将数据压入栈中,pop将栈中的数据弹出并存储到指定寄存器或者内存中。

我们通过一个简单的加法程序来分析函数栈帧。

#include 

int Add(int x, int y)
{    
    int ret = x + y;
    return ret;
}
nt main()
{    
    int a = 10, b = 20;
    int c = 0;
    c = Add(a, b);
    printf("%d\n", c);

    return 0;

}

实际上main函数也是被其他函数调用的,我们这里暂且称为__tmain,而这个函数也是被另外一个函数调用的,这里就不作深究了,重点放在main函数的栈帧。

函数栈帧的创建和销毁(程序员必了解内容)_第2张图片

main函数栈帧的建立

下述也就是为main函数创建栈帧做的准备

int main()
{    
00851E30  push        ebp  
00851E31  mov         ebp,esp  
00851E33  sub         esp,0E4h  
00851E39  push        ebx  
00851E3A  push        esi  
00851E3B  push        edi

第一行:push ebp,将ebp压入栈中。此时esp上移

函数栈帧的创建和销毁(程序员必了解内容)_第3张图片

第二行和第三行:首先将esp的值放到ebp里面,ebp就指向esp的位置了,然后sub esp 0E4h,此时esp的地址变小了,指向上面的区域。此是ebp和esp就会维护新的空间,也就是为main函数预开辟的空间

函数栈帧的创建和销毁(程序员必了解内容)_第4张图片

第四五六行将ebx,esi,edi压入栈中。

函数栈帧的创建和销毁(程序员必了解内容)_第5张图片

00851E3C  lea         edi,[ebp-24h]  
00851E3F  mov         ecx,9  
00851E44  mov         eax,0CCCCCCCCh  
00851E49  rep stos    dword ptr es:[edi]

而这四步,是将ebp-24h向下重复9次,dword双字的内存全部改为0CCCCCCCCh,这就是为什么我们有时变量未初始化会打印烫烫烫之类的原因(有些编译器会将main函数初始开辟的栈帧全都初始化为0CCCCCCCCh)

函数栈帧的创建和销毁(程序员必了解内容)_第6张图片

函数栈帧的创建和销毁(程序员必了解内容)_第7张图片

下面就开始创建局部变量了

int a = 10, b = 20;
00851E55  mov         dword ptr [ebp-8],0Ah  
00851E5C  mov         dword ptr [ebp-14h],14hint c = 0;

int c = 0;
00671E63  mov         dword ptr [ebp-20h],0 

第一行:将0Ah(也就是10)放在ebp-8的位置

第二行:将14h(也就是20)放在ebp-14h的位置

第三行:将0放在ebp-14h的位置

函数栈帧的创建和销毁(程序员必了解内容)_第8张图片

c = Add(a, b);
00C51E6A  mov         eax,dword ptr [ebp-14h]  
00C51E6D  push        eax  
00C51E6E  mov         ecx,dword ptr [ebp-8]  
00C51E71  push        ecx  
00C51E72  call        00C510B4  
00C51E77  add         esp,8 

下面就要调用Add函数了,而在call Add函数之前我们又执行了几条指令:

一二行:实际上就是push eax(里面存放的是b的值20),将eax压入栈中

三四行:实际上就是push eax(里面存放的是a的值10),将eax压入栈中

函数栈帧的创建和销毁(程序员必了解内容)_第9张图片

从这里我们就可以看出函数传参是从右向左的,形参是实参的一份临时拷贝。

大家可以看到call指令的下一条指令地址是00c51E77,而我们在call之后,会将call指令的下一条指令压入栈中,这样当出Add函数的栈帧后就可以找到call指令的下一条指令地址了。

函数栈帧的创建和销毁(程序员必了解内容)_第10张图片

         函数栈帧的创建和销毁(程序员必了解内容)_第11张图片

Add函数栈帧的建立与销毁

下面就进入Add函数内部了

int Add(int x, int y)
{    
00C52EB0  push        ebp  
00C52EB1  mov         ebp,esp  
00C52EB3  sub         esp,0CCh  
00C52EB9  push        ebx  
00C52EBA  push        esi  
00C52EBB  push        edi  
00C52EBC  lea         edi,[ebp-0Ch]  
00C52EBF  mov         ecx,3  
00C52EC4  mov         eax,0CCCCCCCCh  
00C52EC9  rep stos    dword ptr es:[edi]  
00C52ECB  mov         ecx,0C5C003h  
00C52ED0  call        00C5130C 

第一行:是将main函数的ebp压入栈中。因为esp是始终指向函数的栈顶的,实际上是方便出Add函数后找到main函数的ebp

函数栈帧的创建和销毁(程序员必了解内容)_第12张图片

其余的步骤和创建main函数栈帧类似,这里就不再赘述了。

函数栈帧的创建和销毁(程序员必了解内容)_第13张图片

`int ret = x + y;
00C52ED5  mov         eax,dword ptr [ebp+8]  
00C52ED8  add         eax,dword ptr [ebp+0Ch]  
00C52EDB  mov         dword ptr [ebp-8],eax  `

ebp+8实际上就是存放a的临时变量10的地址,ebp+0ch就是存放b的临时变量20的地址,此时eax存放相加后的值30,

放到ebp-8的位置

函数栈帧的创建和销毁(程序员必了解内容)_第14张图片

return ret;
00C52EDE  mov         eax,dword ptr [ebp-8]  
}

而我们是如何返回的呢?是将ebp-8的值也就是30再放到一个eax寄存器中,所以是通过寄存器存放相加后的值,这出即使出了Add函数,栈帧销毁,仍然可以返回值。

接下来又通过几条指令

00C52EE1  pop         edi  
00C52EE2  pop         esi  
00C52EE3  pop         ebx

00C52EF1  mov         esp,ebp  
00C52EF3  pop         ebp  
00C52EF4  ret 

三次pop弹出栈顶的三个元素,esp地址+0ch

第四行:将ebp的值放到esp里面,这样Add的函数栈帧就销毁了

第五行:弹出栈顶的元素放到ebp里面,而此是栈顶元素存放的就是main函数的ebp,所以又回到了main函数栈帧

函数栈帧的创建和销毁(程序员必了解内容)_第15张图片

而末尾一行:ret实际上就是return的意思,弹出栈顶的元素,也就是call指令下一条指令地址,这样当我们回到main函数后就可以执行call指令的下一条指令了。

函数栈帧的创建和销毁(程序员必了解内容)_第16张图片

00C51E77  add         esp,8  
00C51E7A  mov         dword ptr [ebp-20h],eax

第一行:销毁形参(因为Add函数已经调用结束了,所以就不需要形参了)

函数栈帧的创建和销毁(程序员必了解内容)_第17张图片

第二行:将eax(存放的值30)放到[ebp-20h](c的位置),这样c的值就是30了。

函数栈帧的创建和销毁(程序员必了解内容)_第18张图片

函数的栈帧的创建和销毁在这里就告一段落了。

总结

1.通过move,sub可以建立栈帧,通过move,pop也可以销毁栈帧,push 和pop可以回到栈帧。

通过push call指令的下一条指令地址,函数调用完后后可以执行call指令的下一条指令。

2.调用函数要建立栈帧,出函数要销毁栈帧,也就是把内存还给操作系统。

3.形参是实参的一份临时拷贝,改变形参的值不会改变实参,当我们要对实参进行实际操作时,就要传址,或者传引用。

4.函数传参是从右向左的,通过偏移量找到形参。

5.在创建局部变量之前,会先在栈帧中初始化一部分空间,然后再给局部变量分配部分空间。

6.我们调用函数建立新的栈帧之前,会先存放call指令下一条指令地址,再push当前函数的ebp,然后再调用函数。

了解完函数栈帧,你是不是对变量的创建,函数的调用,实参和形参的关系等等有了更深的了解。最后如果大家有任何问题,欢迎大家来下方评论区讨论。

不是看到希望才去坚持,而是坚持了才看到希望,敬每一位努力奋斗的你和学习编程的你。希望我的文章能对你有所帮助。欢迎点赞 ,评论,关注,⭐️收藏

函数栈帧的创建和销毁(程序员必了解内容)_第19张图片

你可能感兴趣的:(c/c++,c语言)