--在C语言中,函数栈帧是指在函数调用过程中,在内存栈中为该函数分配的一块空间,用于存储函数的局部变量,参数,返回地址等信息。
栈帧的结构:
--在前期的学习中,我们可能会产生很多困惑
比如:
当我们理解函数栈帧的创建和销毁后,我们就可以更好的去解决这些问题,如同修练自己的内功,也方便在后期能搞懂更多的知识。
相关寄存器:
相关汇编指令:
代码如下:
#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 c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}这段代码我们在vs2022上调试的话,调试进入add函数后,我们就可以观察到函数的调用堆栈(右击勾选,显示外部代码) ,如下图所示

函数调用堆栈是可以反馈函数调用逻辑的,我们可以清晰的观察到,是由invoke_main函数来调用main函数的 ,在此之间的我们就不过多的去考虑了,我们接下来直接从main函数的栈帧创建开始。
--当函数每次被调用时,系统都会在栈上为该函数分配一块栈帧空间。首先将调用函数的相关信息,如参数,返回地址等压入栈中,然后调整ebp和esp,为局部变量分配空间
我们先将main函数转到反汇编--调试到main函数第一行时,右键鼠标转到反汇编,反汇编代码如下
int main()
{
//函数栈帧的创建
005518D0 push ebp
005518D1 mov ebp,esp
005518D3 sub esp,0E4h
005518D9 push ebx
005518DA push esi
005518DB push edi
005518DC lea edi,[ebp-24h]
005518DF mov ecx,9
005518E4 mov eax,0CCCCCCCCh
005518E9 rep stos dword ptr es:[edi]
005518EB mov ecx,55C008h
005518F0 call 0055132F
005518F5 nop
//main函数中的主要代码
int a = 10;
005518F6 mov dword ptr [ebp-8],0Ah
int b = 20;
005518FD mov dword ptr [ebp-14h],14h
int c = 0;
00551904 mov dword ptr [ebp-20h],0
c = Add(a, b);
0055190B mov eax,dword ptr [ebp-14h]
0055190E push eax
0055190F mov ecx,dword ptr [ebp-8]
00551912 push ecx
00551913 call 005510B9
00551918 add esp,8
0055191B mov dword ptr [ebp-20h],eax
------------------------------------------------------------
printf("%d\n", c);
0055191E mov eax,dword ptr [ebp-20h]
00551921 push eax
00551922 push 557B30h
00551927 call 005510D7
0055192C add esp,8
return 0;
0055192F xor eax,eax
}我们可以将上面main函数的函数栈帧创建过程的主要部分单独拆解出来看看,代码如下
005518D0 push ebp
//把ebp寄存器中的值进行压栈,到了esp-4的位置,此时的ebp中存放的是invoke_main函数栈帧的ebp
005518D1 mov ebp,esp
//将esp的值存放到ebp中,相当于ebp来到了invoke_main函数栈帧的esp位置,产生了main函数的ebp
005518D3 sub esp,0E4h
//将esp中的地址减去一个16进制数字0E4h,esp向上移动,产生了新的esp,也就是main函数的esp
//结合上面产生的ebp之后,ebp和esp之间就维护了一块为main函数开辟的栈帧空间
005518D9 push ebx
//将寄存器ebx中的值压栈,esp-4,esp向上移动
005518DA push esi
//将寄存器epi中的值压栈,esp-4,esp继续向上移动
005518DB push edi
//将寄存器edi中的值压栈,esp-4,esp接着向上移动
005518DC lea edi,[ebp-24h]
005518DF mov ecx,9
005518E4 mov eax,0CCCCCCCCh
005518E9 rep stos dword ptr es:[edi]
//以上这四串代码是在初始化main函数的栈帧空间
//1.先将ebp-24h的地址加载到edi中
//2.将9放入ecx中
//3.将0xCCCCCCCC放入eax中
//4.将从ebp-24h到ebp之间ecx个4个字节的数字初始化为0xCCCCCCCC
接下来再来分析main函数中的主要代码
int a = 10;
005518F6 mov dword ptr [ebp-8],0Ah
//将0Ah存储到ebp-8这个地址中,ebp-8的位置其实就是变量a
int b = 20;
005518FD mov dword ptr [ebp-14h],14h
//将14h存储到ebp-14h这个地址中,ebp-14h的位置其实就是变量b
int c = 0;
00551904 mov dword ptr [ebp-20h],0
//将0存储到ebp-20h这个地址中,ebp-20h的位置其实就是变量c
////以上就是局部变量在其所在函数的栈帧空间中创建和初始化的过程
c = Add(a, b);
0055190B mov eax,dword ptr [ebp-14h]
// 先传参b,将ebp-14h位置中b的值存储到eax中
0055190E push eax
//将eax的值压栈,esp-4,向上移动
0055190F mov ecx,dword ptr [ebp-8]
//再传参a,将ebp-8位置中a的值存储到ecx中
00551912 push ecx
//将ecx的值压栈,esp-4,继续向上移动
//跳转调用函数
00551913 call 005510B9
//call指令会将call指令的下一条指令的地址进行压栈操作
//这样做可以让函数调用结束后回到call的下一条指令地址后继续执行
00551918 add esp,8
0055191B mov dword ptr [ebp-20h],eax 
call指令会执行函数调用逻辑,这个时候我们会跳转到Add函数中,我们再来观察Add函数的反汇编代码
int Add(int x, int y)
{
00551790 push ebp
//将ebp压栈,到了esp-4 的位置,此时的ebp是main函数中的ebp
00551791 mov ebp,esp
//将esp的值给了ebp,ebp来到了原来main函数esp的位置,形成了Add函数的ebp
00551793 sub esp,0CCh
//将esp-00ch,形成了Add函数的esp,结合前面的ebp,ebp和esp维护了一块为Add函数开辟的栈帧空间
00551799 push ebx
//将ebx压栈,esp-4,向上移动
0055179A push esi
//将esi压栈,esp-4,向上移动
0055179B push edi
//将edi压栈,esp-4,向上移动
0055179C lea edi,[ebp-0Ch]
//将ebp-0ch的地址加载到edi中
0055179F mov ecx,3
//将3放入exc中
005517A4 mov eax,0CCCCCCCCh
//将0CCCCCCCCh放入eax中
005517A9 rep stos dword ptr es:[edi]
//将ebp-0ch到ebp之间ecx个4个字节的数字初始化为0CCCCCCCCh
005517AB mov ecx,55C008h
005517B0 call 0055132F
005517B5 nop
int z = 0;
005517B6 mov dword ptr [ebp-8],0
//将0的值给到ebp-8中
z = x + y;
005517BD mov eax,dword ptr [ebp+8]
005517C0 add eax,dword ptr [ebp+0Ch]
005517C3 mov dword ptr [ebp-8],eax
return z;
005517C6 mov eax,dword ptr [ebp-8]
}
005517C9 pop edi
005517CA pop esi
005517CB pop ebx
005517CC add esp,0CCh
005517D2 cmp ebp,esp
005517D4 call 00551253
005517D9 mov esp,ebp
005517DB pop ebp
005517DC ret 代码执行到Add函数的时候就要开始创建Add函数的栈帧空间了,与前面main函数的栈帧空间创建过程差不多,这里就不详细讲述了,主要是计算求和的时候我们通过偏移访问,访问到了函数调用前压栈进去的参数,这就是形参访问,很好说明了形参就是实参的一份临时拷贝,最后将求出的和通过eax寄存器中带回。
z = x + y;
005517BD mov eax,dword ptr [ebp+8]
// 将ebp+8地址处的数字存储到eax中
005517C0 add eax,dword ptr [ebp+0Ch]
// 将ebp+12地址处的数字加到eax寄存中
005517C3 mov dword ptr [ebp-8],eax
//将eax的结果保存到ebp-8的地址处,其实就是放到z中
return z;
005517C6 mov eax,dword ptr [ebp-8]
//将ebp-8地址处的值放在eax中,其实就是把z的值存储到eax寄存器中,通过eax寄存器带回计算的结果
--函数执行完毕后,栈帧被销毁,通过恢复ebp和esp的值,释放栈帧空间,将控制权返回给调用函数,继续执行调用函数中调用之后的代码
当函数调用结束后,前面创建的函数栈帧要销毁,我们来看看Add函数的这部分反汇编代码吧
005517C9 pop edi //在栈顶弹出一个值,存放到edi中,esp+4,向下移
005517CA pop esi //在栈顶弹出一个值,存放到esi中,esp+4,向下移
005517CB pop ebx //在栈顶弹出一个值,存放到ebx中,esp+4,向下移
005517CC add esp,0CCh //esp+0cch,向下移
005517D2 cmp ebp,esp //esp的值给ebp,ebp来到了esp的位置
005517D4 call 00551253
005517D9 mov esp,ebp //再将ebp的值给了esp,回收了Add函数的栈帧空间
005517DB pop ebp
//弹出栈顶的值存放到ebp,栈顶此时的值恰好就是main函数的ebp,esp+4,恢复了main函数栈帧空间的维护
005517DC ret
//ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令下一条指令的地址,此时esp+4,向下移动,然后直接跳转到call指令下一条指令的地址处,继续往下执行回到了call指令下一条指令的地方

调用完后继续回到main函数后继续执行这两串代码,函数的返回值通过eax带了出来,其中就是x+y的和z,也就是a+b的和c。
结语:本篇文章就到此结束了,对于函数栈帧的创建与销毁,个人能力有限,欢迎大家进行补充,一起交流学习,感谢大家的支持!