当前位置: 首页 > news >正文

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

在学习 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++ 的高级特性,是不可或缺的底层基石。

http://www.cnnetsun.cn/news/3494050.html

相关文章:

  • 数字化管控原料保质期,助力烘焙企业实现库存降本增效
  • AI Agent可靠性如何验证?:3类致命缺陷+4步自动化检测法,错过=线上事故
  • 佳通轮胎携手仰望U9X亮相古德伍德,以轮胎科技挑战性能边界
  • 负电压原理与应用:从基础概念到电路设计实践
  • 多层PCB板层设计与EMC优化全解析
  • 想找全链路AI数字人厂商?2026年品牌众多,到底怎么选更适配业务
  • 电感与电流关系解析及工程应用
  • HBM4技术解析:AI算力需求下的高带宽内存革新
  • 精密电阻选型与应用全指南:从参数解析到工程实践
  • 推荐2款Windows实用干货软件,完全免费、无广告
  • 三极管放大电路设计:从基础到工程实践
  • 揭秘RPA流程设计失败率高达73%的真相:5个被90%团队忽略的AI融合关键点
  • ChatGPT 模型选择指南:日常工作与代码场景
  • AI 辅助编程时代,程序员应该如何正确使用代码助手?
  • 锂电池内阻与电压关系解析及性能优化
  • 深入解析TI EDMA事件控制机制:从寄存器操作到链式传输实战
  • 深入解析MibSPI传输组:硬件自动化SPI通信的核心配置
  • 2025年Q1智能手机市场预测与供应链分析
  • 三维建模:SolidWorks 2025保姆级下载与安装教程指南
  • 工程办公系统核心功能优势 解决建筑企业异地办公与审批繁琐问题
  • 减少展会和B2B平台投入后,这家工厂的咨询量反而翻了一倍
  • 2026济南黄金回收白银回收铂金回收中检持证鉴定师铂金银饰高价回收门店联系方式推荐
  • BLE模块PCB射频走线避坑指南:如何确保50Ω阻抗与天线净空?
  • 目前短视频评论发布已经达到极限-----需要添加一个短视频平台
  • APK逆向与源码重构实战:从零到一掌握Android应用逆向工程
  • 家政数字化:万亿蓝海的智能革命-家政小程序APP
  • 如何买回被抢注的品牌域名
  • 材料星与豆包写材料全流程对比:从提示词到成稿
  • 深度解析:如何选择PDRN护肤品代工厂?以水信生物为例的全面指南
  • 2026呼伦贝尔黄金回收白银回收铂金回收市民首选无隐形扣费正规备案回收门店联系方式推荐