今日份通达“函数栈帧”的创建与销毁
前言:
在C语言中所定义的函数都是在栈上进行创建的,进入函数就是向内存的栈空间申请开辟一块空间(压栈),并在此空间里进行该函数相对应的操作,出了该函数就同时销毁释放该函数内所有的空间。
大致的理论认识即使如此,但在内存的视角上”函数栈帧“的创建与销毁是如何的呢?
补充:
这里特别提示一点:该知识点在不同的编译器底下会有一些不同,但是在大致的逻辑思维上是相同的,这里将以VS 2013进行展示。
函数:
此之前首先了解一下寄存器:eax ,ebs,ecx,edx,ebp,esp。
主要详解"ebp","esp"这两个寄存器:这两个寄存器中存放的是地址,这2个地址主要是用来维护函数的栈帧。
ebp:维护函数栈底的指针。
esp:维护函数栈顶的指针。
每一个函数调用都会在栈区创建一个空间来实现该函数,main函数同样也是被其它函数调用的。
在vs 2013 中,main函数的调用顺序:由mainCRTStartup()调用👉__tmainCRTStartup()调用👉main()。
✨以下面这段代码来演示整个函数栈帧的创建与销毁:
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int z = Add(a, b);
printf("%d\n", z);
return 0;
}
内存中逻辑表示图:
以内存的视角这样的逻辑图是如何实现的呢?
下面一一进行细致的讲解:
①打开编译器,书写这段代码
②”F11“进入调试
③光标在此位置右击进入反汇编
④便出现以下编译指令
首先根据以下指令为函数在栈中来开辟空间
函数定义前开辟空间的指令方法都是一样的。
下面将从开辟main函数空间为例:
此刻esp和ebp是维护函数"__tmainCRTStartup"。
接下来开辟main函数的空间。
①"push ebp":将ebp进行压栈
"push"压栈的同时,esp指针也相应更新
②”mov ebp,esp“:将此刻esp的地址赋给ebp
③”sub esp ,0E4h“:将esp的地址 减去0E4h为新的esp地址
④”push ebx“,”push esi“,”push edi“:在esp地址的基础上进行压栈
⑤"lea edi,[ebp-0E4h]":"lea":load effective addvess 加载。将ebp-0E4h的地址赋给lea。也就是esp”push“之前的地址位置
⑥”mov ecx,39h“,"mov eax,0CCCCCCCCh","rep stos dwors ptr es:[edi]"这段指令表示:从当前edi地址位置开始的前39个”dword“(四字节)的空间全部初始化为”0CCCCCCCCh“。
此刻main函数的空间就开辟好了。
空间开辟好后便对书写的代码进行编译
①”mov dowrd ptr [ebp-8],0Ah“:0Ah的十进制:10,将ebp-8的地址的值改为10。
同样”mov dowrd ptr [ebp-14h],14h“:14h的十进制:20,将ebp-20的地址的值改为20。
②”mov eax,dword ptr[ebp-14h]“,"push eax"
”mov ecx,dword ptr[ebp-8]“,"push ecx"
这两段指令相当于函数的传值调用,将函数的实参从右至左进行存值,并压栈保存
③”call 00D511D1“:call 指令为调用函数,并存储下一条指令的地址”add esp,8“
Add函数的实参压栈好之后,便进入main函数调用的Add函数中。同main函数首先开辟一块属于Add的函数空间。
因为同开辟main函数空间一样这里就不再重复演示,将跳过开辟空间的过程
①”mov dword ptr [ebp-8],0“:将ebp-8的地址位置的值赋0
②"mov eax,dword ptr[ebp+8]":将ebp+8的地址位置的值赋给eax
③"add eax,dword ptr[ebp+0Ch]":将eax的值加上dword ptr[ebp+0Ch]地址的值。
④ “mov dword ptr [ebp-8],eax”:将eax的值赋给ebp-8地址
⑤”mov eax,dword ptr [ebp-8]“ :将ebp-8地址的值赋给eax,。
这里将z的值计算好之后便开始返回值,此刻将要返回的值再赋值给eax进行保留,以免退出函数就找不到需要的值
00D5176B pop edi
00D5176C pop esi
00D5176D pop ebx
此刻跳出Add函数。并将adi,esi,ebx的值进行出栈释放空间。这时候”call“存的指令地址位置就能够回到之前代码的执行顺序
当函数跳出来之后,该栈空间就不需要了
”mov edp,esp“:将栈顶指针指向栈底指针
同时"pop edp":将回到之前edp存的栈底地址(main栈底的地址),且因为”pop“栈顶”esp“便往下挪一位地址
”ret“这条指令,根据之前call指令保存的下一条指令的地址,直接返回到该指令的位置。返回原来的地址esp便也往回跳一个地址
”add esp 8”因为此刻该调用函数以结束且保存形实参的值就也不需要了,直接将esp指针往回跳释放之前保存的地址即可。
此刻edp与esp又维护main函数的空间。
“mov dword ptr [ebp-20h],eax”将eax保存之前函数返回的值,再赋予一个变量地址就完成返回值的保存。
根据以上的解释,思考以下问题:
1,局部变量是怎么创建的?
2,为什么局部变量的值是随机值?
3,函数是怎么传参的?传参的顺序是怎样的?
4,形参和实参是什么关系?
5,函数调用是怎么做的?
6,函数调用是结束后怎么返回的?