目录
前言
函数栈帧详解
main函数栈帧的建立
Add函数栈帧的建立与销毁
总结
不知道大家对下述问题有没有疑问:
1.局部变量是如何创建的
2.未初始化的局部变量为什么是随机值
3.函数是如何传参的,传参顺序是怎样的
4.形参和实参是什么关系
5.函数是如何调用的
每一次函数调用都会在栈区开辟一块空间,也就是函数栈帧。
如果大家对函数栈帧不了解的话,肯度会对上述的问题有疑问,今天就让我带大家一探究竟,打开函数栈帧的大门。话不多说我们直接步入正题。由于编译器版本的不同,函数栈帧建立和销毁可能有些许差异,但大致的步骤都是一样的。我使用的环境是vs2019
在了解函数栈帧之前,我们先介绍一下寄存器,寄存器有: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函数的栈帧。
下述也就是为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上移
第二行和第三行:首先将esp的值放到ebp里面,ebp就指向esp的位置了,然后sub esp 0E4h,此时esp的地址变小了,指向上面的区域。此是ebp和esp就会维护新的空间,也就是为main函数预开辟的空间
第四五六行将ebx,esi,edi压入栈中。
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)
下面就开始创建局部变量了
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的位置
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压入栈中
从这里我们就可以看出函数传参是从右向左的,形参是实参的一份临时拷贝。
大家可以看到call指令的下一条指令地址是00c51E77,而我们在call之后,会将call指令的下一条指令压入栈中,这样当出Add函数的栈帧后就可以找到call指令的下一条指令地址了。
下面就进入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
其余的步骤和创建main函数栈帧类似,这里就不再赘述了。
`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的位置
return ret;
00C52EDE mov eax,dword ptr [ebp-8]
}
而我们是如何返回的呢?是将ebp-8的值也就是30再放到一个eax寄存器中,所以是通过寄存器存放相加后的值,这出即使出了Add函数,栈帧销毁,仍然可以返回值。
接下来又通过几条指令
00C52EE1 pop edi
00C52EE2 pop esi
00C52EE3 pop ebx00C52EF1 mov esp,ebp
00C52EF3 pop ebp
00C52EF4 ret
三次pop弹出栈顶的三个元素,esp地址+0ch
第四行:将ebp的值放到esp里面,这样Add的函数栈帧就销毁了
第五行:弹出栈顶的元素放到ebp里面,而此是栈顶元素存放的就是main函数的ebp,所以又回到了main函数栈帧
而末尾一行:ret实际上就是return的意思,弹出栈顶的元素,也就是call指令下一条指令地址,这样当我们回到main函数后就可以执行call指令的下一条指令了。
00C51E77 add esp,8
00C51E7A mov dword ptr [ebp-20h],eax
第一行:销毁形参(因为Add函数已经调用结束了,所以就不需要形参了)
第二行:将eax(存放的值30)放到[ebp-20h](c的位置),这样c的值就是30了。
函数的栈帧的创建和销毁在这里就告一段落了。
1.通过move,sub可以建立栈帧,通过move,pop也可以销毁栈帧,push 和pop可以回到栈帧。
通过push call指令的下一条指令地址,函数调用完后后可以执行call指令的下一条指令。
2.调用函数要建立栈帧,出函数要销毁栈帧,也就是把内存还给操作系统。
3.形参是实参的一份临时拷贝,改变形参的值不会改变实参,当我们要对实参进行实际操作时,就要传址,或者传引用。
4.函数传参是从右向左的,通过偏移量找到形参。
5.在创建局部变量之前,会先在栈帧中初始化一部分空间,然后再给局部变量分配部分空间。
6.我们调用函数建立新的栈帧之前,会先存放call指令下一条指令地址,再push当前函数的ebp,然后再调用函数。
了解完函数栈帧,你是不是对变量的创建,函数的调用,实参和形参的关系等等有了更深的了解。最后如果大家有任何问题,欢迎大家来下方评论区讨论。
不是看到希望才去坚持,而是坚持了才看到希望,敬每一位努力奋斗的你和学习编程的你。希望我的文章能对你有所帮助。欢迎点赞 ,评论,关注,⭐️收藏