news 2026/7/18 18:11:00

学习笔记:函数栈帧的创建与销毁

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
学习笔记:函数栈帧的创建与销毁

在学习 C/C++ 的过程中,我们经常会将独立的功能抽象为函数,可以说 C 程序就是以函数为基本单位的。但你是否想过:

  • 函数调用时参数是如何传递的?传参的顺序是怎样的?

  • 形参和实参到底是什么关系?

  • 为什么局部变量不初始化,打印出来的值是随机的(或者是经典的“烫烫烫”)?

  • 函数的返回值又是如何带回的?

想要拔高编程内功,彻底弄透这些问题,我们必须沉下心来,走进底层内存,去看看函数栈帧(Stack Frame)到底是怎么回事。

1. 什么是函数栈帧与核心寄存器

函数栈帧,就是函数调用过程中在程序的调用栈(Call Stack)里所开辟的专属内存空间。这块空间主要用来存放:

  • 函数参数和函数返回值

  • 临时变量(包括函数的非静态局部变量以及编译器自动生成的其他临时变量)。

  • 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

在经典的计算机操作系统中,栈总是向下增长(由高地址向低地址)的。

为了维护这块动态的栈帧空间,CPU 主要依赖两个极其关键的寄存器:

  • ebp(栈底寄存器):记录当前栈帧底部的地址。

  • esp(栈顶寄存器):记录当前栈帧顶部的地址。

2. 揭秘:main 函数栈帧的创建与“烫烫烫”的由来

每一次函数调用,都要为本次函数调用开辟空间。我们以 VS2019 环境下的一段简单代码为例,从main函数的视角切入。

实际上,在main函数执行之前,是由一个名为invoke_main的函数来调用main函数的。因此,在进入main之前,栈区已经有了invoke_main的栈帧。

进入main函数后,编译器会执行以下反汇编代码来创建新栈帧:

代码段

// main函数栈帧的创建 00BE1820 push ebp // 把ebp寄存器中的值(invoke_main的栈底)进行压栈保存 00BE1821 mov ebp, esp // 将esp的值赋给ebp,相当于产生了main函数的ebp 00BE1823 sub esp, 0E4h // esp减去一个16进制数字0xe4,为main函数开辟出一块全新的栈帧空间 00BE1829 push ebx // 保存 ebx 寄存器原值 00BE182A push esi // 保存 esi 寄存器原值 00BE182B push edi // 保存 edi 寄存器原值

紧接着,编译器会执行一段神操作——初始化栈帧空间。它会将新开辟的从ebp-0x24ebp的这一段内存,每个字节都强制初始化为0xCC

💡硬核科普:为什么会输出“烫烫烫”?如果我们在代码中定义了一个数组char arr[20];但没有初始化,它恰好分配在这块被填满0xCC的空间上。两个连续排列的0xCC(即0xCCCC)在汉字编码中对应的字符刚好就是“烫”!这就是那个困扰无数初学者的乱码的物理真相。

3. 核心实战:Add 函数的传参汇编全过程

搞懂了栈帧的创建,我们来看看局部变量是怎么传递给另一个函数的。假设我们在main函数中定义了a=3b=5,并调用ret = Add(a, b);

在底层汇编中,参数的传递其实就是把参数 push 到栈帧空间中。请看下面这段极其精彩的汇编流解:

代码段

// 1. 局部变量的创建与初始化 00BE183B mov dword ptr [ebp-8], 3 // 将 3 存储到 ebp-8 的地址处(变量 a) 00BE1842 mov dword ptr [ebp-14h], 5 // 将 5 存储到 ebp-14h 的地址处(变量 b) 00BE1849 mov dword ptr [ebp-20h], 0 // 将 0 存储到 ebp-20h 的地址处(变量 ret) // 2. 调用 Add 函数时的传参(从右向左入栈) 00BE1850 mov eax, dword ptr [ebp-14h] // 传递b,将存放的 5 拿出来放到 eax 寄存器中 00BE1853 push eax // 将 eax 的值(5)压栈,栈顶 esp 自动 -4 00BE1854 mov ecx, dword ptr [ebp-8] // 传递a,将存放的 3 拿出来放到 ecx 寄存器中 00BE1857 push ecx // 将 ecx 的值(3)压栈,栈顶 esp 自动 -4 // 3. 跳转调用函数 00BE1858 call 00BE10B4 // 执行 call 指令跳转

在这里我们要特别注意call指令。当执行call指令去调用Add函数时,系统在跳转之前,会把call指令的下一条指令的物理地址进行压栈操作。这就像是在森林里留下一个面包屑标记,确保Add函数执行完之后,CPU 还能顺着地址找回来继续往下执行。

这也从底层证明了一个核心结论:在进行值传递调用时,形参其实是实参的一份拷贝,位于完全不同的物理内存地址上,对形参的修改绝对不会影响实参

4. 栈帧的销毁与结果带回

天下没有不散的宴席,当Add函数执行到return时,它所占据的栈帧也迎来了销毁时刻。

  1. 带回结果Add函数会将计算结果(如 x+y 的和)存入通用的eax寄存器中,作为返回值带回。

  2. 恢复现场:连续执行pop edipop esipop ebx,将在函数开头保存的寄存器原值弹出来恢复。

  3. 空间回收:执行mov esp, ebp,将栈顶指针直接拉回到当前栈底,瞬间回收掉Add函数的整个栈帧空间。

  4. 归还掌控权:执行pop ebp,将栈顶弹出的值赋给ebp,由于之前压栈的就是main函数的ebp,此时便完美恢复了main函数的栈帧维护。最后执行ret指令,弹出之前留下的“面包屑”返回地址,程序跳转回去继续执行。

回到main函数后,编译器会执行add esp, 8,直接将esp向下移动 8 个字节,相当于销毁了刚才传参时压入栈中的形参ab的拷贝。紧接着,执行mov dword ptr [ebp-20h], eax,将eax寄存器里历经千辛万苦带回来的计算结果,存入ret变量中。至此,一次完美的函数调用彻底落幕。

总结

学习编程,语法只是皮毛,内存才是灵魂。只有真正看懂了函数栈帧的创建与销毁,我们才能从底层的上帝视角去审视代码,彻底明白变量的生老病死与数据的流转奥秘。这对于后续深入学习复杂数据结构以及 C++ 的高级特性,是不可或缺的底层基石。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/18 18:09:48

数字化管控原料保质期,助力烘焙企业实现库存降本增效

在烘焙生产、门店零售全链路运营中,原料保质期管理是库存运营的核心短板。奶油、酵母、果馅、乳制品等原料保质期长短不一,存储标准严格,过期原料直接报废,临期原料未及时消耗会持续拉高经营损耗。传统人工盘点、通用进销存工具均…

作者头像 李华
网站建设 2026/7/18 18:01:18

佳通轮胎携手仰望U9X亮相古德伍德,以轮胎科技挑战性能边界

7月中旬,全球顶级汽车文化盛会——古德伍德速度节(Festival of Speed)在英国举行。作为全球最具影响力的汽车盛事之一,古德伍德速度节汇聚世界顶尖汽车品牌与性能车型,通过登山赛等形式展现汽车工业的速度激情与技术进…

作者头像 李华
网站建设 2026/7/18 18:01:14

负电压原理与应用:从基础概念到电路设计实践

1. 负电压的基本概念与常见误解 我第一次接触负电压这个概念是在大学电子实验室里。当时看到示波器上显示的波形居然能"沉"到零线以下,感觉特别神奇。后来才知道,负电压并不是什么神秘的东西,它只是相对于某个参考点而言的电位差。…

作者头像 李华
网站建设 2026/7/18 17:59:35

多层PCB板层设计与EMC优化全解析

1. PCB板层设计基础与电磁兼容性概述PCB(Printed Circuit Board)作为电子设备的核心载体,其设计质量直接影响产品的电磁兼容性(EMC)。电磁兼容性是指电子设备在电磁环境中既能正常工作,又不会对其他设备产生…

作者头像 李华
网站建设 2026/7/18 17:59:32

想找全链路AI数字人厂商?2026年品牌众多,到底怎么选更适配业务

选数字人平台这件事,正在变得越来越让人头疼。2026年中国AI数字人市场规模已攀升至54.4亿元。市场膨胀的同时,平台之间的技术路线差异也越来越大——有的主打极速克隆、有的专注多语言、有的深耕行业场景。对于需要数字人服务的企业来说,选错…

作者头像 李华