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语言的函数栈帧究竟是什么?你知道吗?的主要内容,如果未能解决你的问题,请参考以下文章