平常我们在编写代码时,尤其是涉及到递归问题,代码有时候会出现栈溢出(Segmentation fault 或 Stack overflow )的报错,有时候野指针报错,缓冲区溢出等问题也与栈帧有关,那么栈帧是什么呢,今天就来详细了解一下。学习栈帧之前需要先了解一下电脑的存储结构是怎么样的这一预备知识,来了解下吧。

一.电脑的存储结构

如图所示是一张电脑存储结构金字塔,其结构从上到下,速度越来越慢,容量越来越大,成本减少,下面我们来一一详细介绍一下各个部分。
在这里插入图片描述

1.寄存器(第一层)

角色:CPU的“手边工作台”,位于CPU内部,属于CPU的一部分。
功能:用于存放CPU当前操作的数据和地址。我们写的代码最终都会被编译成在寄存器上进行操作的指令。
特点:速度极快,和CPU同步;容量极小,通常小于1KB;成本高。

2.高速缓存(第一层)

角色:CPU与内存间的“高速缓冲区”,位于CPU内部,通常分为L1,L2,L3三级缓存。
功能:作为内存的缓冲区,存放内存中最常用的数据和指令,因为CPU直接访问内存速度相对较慢,在缓存里访问数据时(缓存命中)速度很快,大大提高了效率。
特点:速度快,仅此于寄存器(L1>L2>L3);容量小,通常为几十KB到几十MB;成本高。

如图为现代多核CPU内部典型的缓存层级结构:

3.内存(第二层)

角色:程序的“临时工作空间”,位于主板上的内存插槽中。(我们购买的内存条就是由DRAM芯片组成的)。
功能:存放正在运行的操作系统,应用程序以及它们所需要的数据。(CPU要处理数据必须先从硬盘加载到内存,否则速度会很慢,电脑将卡到无法使用)。
特点:速度较快,但远慢于寄存器和缓存;易失性:内存一旦断电,所有数据立马全部丢失;容量较大:大小大多为16GB-64GB;成本适中。

4.辅助存储器(第三层)

角色:电脑的“永久存储仓库”,大多位于主板上的SATA或M.2接口上。
功能:永久存储所有数据,包括操作系统,应用软件,所有文件等。常见的辅存有机械硬盘、固态硬盘、U盘、光盘、磁带等。
特点:速度慢,相比内存慢了几个数量级(机械硬盘尤其慢);非易失性:断点数据不会丢失;容量巨大,目前主流是512GB-2TB;成本低廉。

我们用一个生活中的比喻来帮助理解一下:餐厅后厨
寄存器:厨师的双手
双手直接处理食材,速度快,但能拿的东西有限(一个锅铲、一把菜刀)。
缓存:厨师手边的调料架
架子上放着酱油、盐、味精等,厨师一伸手就可以拿到。
内存:厨房里的中央料理台
所有洗好的菜,腌好的肉,准备好的各种食材,这是厨师(CPU)的主要工作区域,下班后(断电)料理台立即被清空。
辅助存储器:餐厅后面的大型冷冻仓库
保存着未来几个月需要的所有食材,调料和厨具,容量很大,但取用速度慢。
在这里插入图片描述

二.栈和栈帧

1.什么是栈

与数据结构中的栈不同,在内存管理中,是程序运行时内存中的一块特定区域,属于主存储器(主存,也叫内存)。
当一个程序被操作系统调用运行时,操作系统会为它预先分配一块虚拟内存空间,这块空间通常被分为四部分:①代码区,存放程序的运行代码。 ②数据区,存放全局变量和静态变量。③堆区,用于动态申请空间(如malloc或new)。④栈区,用于管理函数调用。

2.什么是栈帧

栈帧也叫活动记录,当每个函数被调用时,系统会在上分配一块称为“栈帧”的内存空间,这块空间用来存放:函数的参数、函数的局部变量、函数的返回地址(即该函数执行完应该返回哪里继续执行)、一些上下文信息(上一个栈帧的指针)。
函数调用时,创建栈帧,当它执行结束时,它的栈帧空间会被自动回收,内存空间得到回收,供给下一个函数使用。

三.函数栈帧的创建和销毁

1.函数栈帧的创建

我们以下面这个代码片段为例来讲解一下函数栈帧具体是如何实现的

int Add(int  x, int y)
{
	return x + y;
}

int main()
{
	int a = 5, b = 10, c = 0;
	c = Add(a, b);
	printf("%d\n", c);
	return 0;
}

在了解函数栈帧创建时,需要先了解两个寄存器:
esp:栈指针寄存器–始终指向当前栈顶,push和pop会改变esp的值。
ebp:基址指针寄存器–指向当前函数栈帧的基址,
第一阶段:main函数栈帧的建立

main函数开始
push ebp ; // 1. 保存调用main的函数的ebp
mov ebp, esp ; //2. 建立main函数的新栈帧基址
sub esp, 0Ch ; //3. 为局部变量a,b,c分配12字节空间

初始化局部变量
mov dword ptr [ebp-4], 5 ; //a = 5
mov dword ptr [ebp-8], 10 ; // b = 10
mov dword ptr [ebp-0Ch], 0 ; //c = 0

此时的栈:
在这里插入图片描述
第二阶段:调用Add函数前的准备

//准备调用Add(a, b)
mov eax, [ebp-8] ; //获取b的值(10)
push eax ; //4. 参数b压栈 (从右向左)
mov ecx, [ebp-4] ; // 获取a的值(5)
push ecx ; // 5. 参数a压栈

此时的栈:
在这里插入图片描述
第三阶段:调用Add函数并为其建立栈帧

call Add ; // 6. 调用Add函数
; // call指令自动做两件事:
; // push eip ; //压入返回地址(下条指令地址)
; // jmp Add ; //跳转到Add函数

// Add函数开始
push ebp ; //7. 保存main函数的ebp
mov ebp, esp ; //8. 建立Add函数的栈帧基址
// 注意:Add函数没有局部变量,所以不需要 sub esp, X

此时的栈:
在这里插入图片描述
第四阶段:Add函数执行计算并准备返回

// 执行加法运算
mov eax, [ebp+8] ; // eax = x (5)
add eax, [ebp+12] ; // eax = eax + y (5+10=15)
// 此时 eax = 15,这就是返回值

// 开始销毁Add函数的栈帧
mov esp, ebp ; // 9. 恢复栈指针(回收局部变量空间)
pop ebp ; // 10. 恢复main函数的ebp
ret ; // 11. 返回main函数
// ret指令相当于:
// pop eip ; 弹出返回地址到指令指针

此时的栈:
在这里插入图片描述
执行ret后的栈:
在这里插入图片描述

2.函数栈帧的销毁

第五阶段:main函数清理参数并返回

// 从Add函数返回后,eax中保存着返回值15
add esp, 8 ;// 12. 清理栈上的参数(2个int,共8字节)
//将返回值存入c
mov [ebp-0Ch], eax ; //c = 15 (返回值)
//继续执行printf等代码…

此时的栈:
在这里插入图片描述
以上流程完整地展示了一个函数被调用时是栈帧是如何建立和销毁的,理解它对于调试复杂的内存问题和理解程序底层行为至关重要。
感谢阅读!^ _ ^(喜欢请一键三连)
在这里插入图片描述

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务,一站式满足开发者需求,帮助企业提升开发效率并创新开发模式。

更多推荐