C语言的函数栈帧究竟是什么?你知道吗?
Posted 未见花闻
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言的函数栈帧究竟是什么?你知道吗?相关的知识,希望对你有一定的参考价值。
前面的话:
作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!
博主的码云gitee,平常博主写的程序代码都在里面。
1.寄存器
寄存器
是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和地址。在中央处理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序计数器(PC)。在中央处理器的算术及逻辑部件中,寄存器有累加器(ACC)。
本文不过多深入了解寄存器,只要知道寄存器集成在CPU之中和以下几个寄存器就可以了。
2.函数栈帧
2.1函数栈帧的概述
C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。
栈帧
也叫过程活动记录
,是编译器用来实现过程/函数调用的一种数据结构
。
函数栈帧的创建和销毁是基于栈所实现的。
所谓栈,是一种数据结构,具有先进后出的特点。在函数栈帧创建过程中,内存从高地址开始使用,越后面创建的函数栈帧或压栈数据,所存储的空间地址越低。
想要更深入了解栈
这一数据结构,欢迎访问博主另一篇文章:栈和队列介绍和基本功能从理论到实践
2.2函数栈帧创建过程
2.2.1被调用的main函数
main函数是会被其他函数调用的,在不同编译器中调用main的函数也不同。
在VS2019中,main函数会被下面几个编译器内置的函数链式访问。
首先,这个invoke_main函数会返回main函数的返回值。
static int __cdecl invoke_main()
{
return main(__argc, __argv, _get_initial_narrow_environment());
}
然后会有一个名叫main_result的int const类型变量接收,invoke_main函数的返回值,也就是main函数的返回值,最后这个main_result会被编译器其他函数所使用。
int const main_result = invoke_main();
函数栈帧的结构如下
esp为栈顶指针
ebp为栈底指针
它们共同维护函数栈帧
2.2.2函数栈帧创建与销毁的过程
对于函数栈帧的创建与销毁,我们以一个简单的程序为例。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int add(int a, int b)
{
int d = a + b;
return d;
}
int main()
{
int a = 2;
int b = 6;
int c = 0;
c = add(a, b);
printf("%d\\n",c);
return 0;
}
由于编译器中有其他函数调用main,所以在main函数栈帧创建前,编译器中调用main的函数栈帧就已经创建了,esp,ebp会在如图位置
00892580 push ebp //ebp压栈
00892581 mov ebp,esp //将esp的值赋给ebp
00892583 sub esp,0E4h //将esp的值减0E4h,也就是为main函数栈帧分配空间
00892589 push ebx //ebx压栈
0089258A push esi //esi压栈
0089258B push edi //edi压栈
0089258C lea edi,[ebp-24h]
0089258F mov ecx,9
00892594 mov eax,0CCCCCCCCh
00892599 rep stos dword ptr es:[edi] //将main初始函数栈帧全部初始化为0CCCCCCCCh
0089259B mov ecx,89C003h
008925A0 call 0089130C //进入main函数
int a = 2;
008925A5 mov dword ptr [ebp-8],2 //ebp - 8就是a的位置,将a赋值为2
int b = 6;
008925AC mov dword ptr [ebp-14h],6 //同理ebp - 14h为b的地址将b赋值为6
int c = 0;
008925B3 mov dword ptr [ebp-20h],0 //ebp - 20h为c的地址,c赋值为0
c = add(a, b);
008925BA mov eax,dword ptr [ebp-14h] //传参,将b值传给add函数 ,先将b值传给eax
008925BD push eax //eax压栈
008925BE mov ecx,dword ptr [ebp-8] //传参,将a值传给add函数,先将a值传给ecx
008925C1 push ecx //ecx压栈
008925C2 call 00891023 //进入add
//带符号:008925C2 call _add (0891023h)
int add(int a, int b)
{
008917B0 push ebp //记录上一个ebp的地址
008917B1 mov ebp,esp //将ebp赋值成esp地址
008917B3 sub esp,0CCh //add函数栈帧
008917B9 push ebx
008917BA push esi
008917BB push edi
008917BC lea edi,[ebp-0Ch]
008917BF mov ecx,3
008917C4 mov eax,0CCCCCCCCh
008917C9 rep stos dword ptr es:[edi] //与main函数栈帧初始化同理,将add函数初始化为CC CC CC CC
008917CB mov ecx,offset _18BA86EA_test@c (089C003h)
008917D0 call @__CheckForDebuggerJustMyCode@4 (089130Ch)
//008917C9 rep stos dword ptr es:[edi]
//008917CB mov ecx,89C003h
//008917D0 call 0089130C
// int d = a + b;
//008917D5 mov eax,dword ptr [a]
//008917D8 add eax,dword ptr [b]
//008917DB mov dword ptr [d],eax
// return d;
//008917DE mov eax,dword ptr [d]
int d = a + b;
008917D5 mov eax,dword ptr [ebp+8] //将a赋值给eax
008917D8 add eax,dword ptr [ebp+0Ch] //将eax加上b,即2+6 = 8
008917DB mov dword ptr [ebp-8],eax //将eax=8赋值给d
return d;
008917DE mov eax,dword ptr [ebp-8] //将d的值赋值给寄存器eax
}
008917E1 pop edi //出栈edi
008917E2 pop esi //出栈esi
008917E3 pop ebx //出栈ebx
008917E4 add esp,0CCh //将add函数销毁,esp回到ebp的位置
008917EA cmp ebp,esp
008917EC call 00891235 //回到main
008917F1 mov esp,ebp //将ebp的地址给esp
008917F3 pop ebp //出栈ebp,让ebp指向上一次地址位置
008917F4 ret
008925C7 add esp,8 // 销毁两个形参,esp指向main函数栈顶
008925CA mov dword ptr [ebp-20h],eax //将eax(返回)值8赋值给ebp - 20h 也就是c
printf("%d\\n",c);
008925CD mov eax,dword ptr [ebp-20h] //将c值赋给eax
008925D0 push eax
008925D1 push 897BCCh
008925D6 call 008913A2
008925DB add esp,8
return 0;
008925DE xor eax,eax
}
//和add函数销毁一样,main函数销毁,结束程序
008925E0 pop edi
008925E1 pop esi
008925E2 pop ebx
008925E3 add esp,0E4h
008925E9 cmp ebp,esp
008925EB call 00891235
008925F0 mov esp,ebp
008925F2 pop ebp
008925F3 ret
以上是关于C语言的函数栈帧究竟是什么?你知道吗?的主要内容,如果未能解决你的问题,请参考以下文章
图解C/C++语言底层:函数调用过程之函数栈帧的创建和销毁(上)