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++语言底层:函数调用过程之函数栈帧的创建和销毁(上)

函数栈帧与可变参数列表 p1可变参数列表(完结)( C语言从入门到入土(进阶篇)

深入理解C语言从函数栈帧角度理解return关键字

学好C语言,还需要掌握这个内功——函数栈帧的创建与销毁

C语言中的Write函数