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

发布时间:2026/7/18 18:11:26
学习笔记:函数栈帧的创建与销毁 在学习 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-0x24到ebp的这一段内存每个字节都强制初始化为0xCC。硬核科普为什么会输出“烫烫烫”如果我们在代码中定义了一个数组char arr[20];但没有初始化它恰好分配在这块被填满0xCC的空间上。两个连续排列的0xCC即0xCCCC在汉字编码中对应的字符刚好就是“烫”这就是那个困扰无数初学者的乱码的物理真相。3. 核心实战Add 函数的传参汇编全过程搞懂了栈帧的创建我们来看看局部变量是怎么传递给另一个函数的。假设我们在main函数中定义了a3和b5并调用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时它所占据的栈帧也迎来了销毁时刻。带回结果Add函数会将计算结果如 xy 的和存入通用的eax寄存器中作为返回值带回。恢复现场连续执行pop edi、pop esi、pop ebx将在函数开头保存的寄存器原值弹出来恢复。空间回收执行mov esp, ebp将栈顶指针直接拉回到当前栈底瞬间回收掉Add函数的整个栈帧空间。归还掌控权执行pop ebp将栈顶弹出的值赋给ebp由于之前压栈的就是main函数的ebp此时便完美恢复了main函数的栈帧维护。最后执行ret指令弹出之前留下的“面包屑”返回地址程序跳转回去继续执行。回到main函数后编译器会执行add esp, 8直接将esp向下移动 8 个字节相当于销毁了刚才传参时压入栈中的形参a和b的拷贝。紧接着执行mov dword ptr [ebp-20h], eax将eax寄存器里历经千辛万苦带回来的计算结果存入ret变量中。至此一次完美的函数调用彻底落幕。总结学习编程语法只是皮毛内存才是灵魂。只有真正看懂了函数栈帧的创建与销毁我们才能从底层的上帝视角去审视代码彻底明白变量的生老病死与数据的流转奥秘。这对于后续深入学习复杂数据结构以及 C 的高级特性是不可或缺的底层基石。