实用指南:函数的栈帧的创建和销毁(超详细版本图文丰富)
前言
嗨(^_−)☆,大家又见面啦,今天我们要讲的和前面有所不同,要深入到程序运行的“幕后舞台”——函数栈帧的创建与销毁。这是理解函数调用机制、内存管理的核心环节,每个函数执行时都会搭建专属的“临时内存舞台”,用完后再有序拆除。掌握它,你就能看透函数调用的底层逻辑,对排查栈溢出、理解递归执行等问题也会豁然开朗。可能有点难度,可是我们学会它对我们后面学习会有很大的帮助,那么接下来,我们就一起拆解该“舞台”的搭建与谢幕全过程~
我把它放在数据结构的栈后面,方便大家更好的理解
通过也能够在C语言中学习,但是我感觉在栈后面学更好
一、为什么要理解函数栈帧
我们在学这篇之前,我们是不是想要了解它能对我们有什么帮助,能帮我们消除什么问题呢,纵然我已经在前言中提到了,可是可能不够形象,所以我们在这里再具体说一下
在大家之前的学习中,是不是有这样的疑问:
1、局部变量是如何创建的?
随机值?就是2、为什么局部变量的值
3、函数是怎么进行传参的?以及传参的顺序是怎么样的?
4、形参和实参的关系是怎么样的?
5、函数调用是怎么做到的?
6、函数调用结束后又是怎样返回的?
这些挑战都与函数栈帧有关,只要理解了函数栈帧的创建和销毁,这些问题就能够很好地理解了,让我们一起走进函数栈帧的创建和销毁的过程中吧!
二、了解函数栈帧
概念:函数栈帧(stack frame)就是函数调用过程中在应用的调用栈(call stack)所开辟的空间,这些空间是用来存放:函数参数和函数返回值,临时变量,保存上下文信息等等;
栈帧的结构
栈帧是函数调用时在栈内存里的专属“小隔间”,核心结构可简单拆为4部分:
- 返回地址:函数执行完后,跳回调用者代码的位置。
- 栈基址指针(ebp/esp):ebp指向当前栈帧的底部,esp指向当前栈帧的顶部,通过这两个指针来维护函数栈帧。
- 局部变量区:存函数里的临时变量(比如
int a、数组),内存连续分配。 - 参数区:用于存放调用函数时传递给被调用函数的参数;
栈是从高地址往低地址生长的,这些部分在栈里按顺序排布,函数执行完就会销毁这个“小隔间”~
三、函数栈帧的栈帧和销毁的相关知识
1、什么是栈?
这里的栈和我们数据结构的栈不同,但核心十分相似,所以我把它放在数据结构的栈后面进行讲解
(1)栈的概念
这里的栈为程序运行中的栈(栈内存)
栈是现代计算机程序的核心概念之一,可从逻辑定义和系统实现两个维度总结:
逻辑特性:
栈是遵循“先进后出(FILO)”规则的特殊容器,支持“入栈(push,资料压入)”和“出栈(pop,数据弹出)”操作,类似“叠书”——先叠的书在最下、最后取出。系统实现:
在计算机系统中,栈是动态内存区域,压栈操作使栈内存增大,弹栈操作使栈内存减小;其内存增长方向为从高地址向低地址,在i386或x86-64架构下,由esp寄存器定位栈顶。程序意义:
栈是函数调用、局部变量存储的基础,几乎所有工具的函数、局部变量效果都依赖栈实现,是计算机语言运行的核心支撑。
(2)数据结构中的栈和程序运行的栈的对比
这里的“栈”需从两个维度理解,二者概念不同但核心逻辑(先进后出)高度关联:
| 维度 | 数据结构的“栈”(抽象逻辑) | 程序运行的“栈”(物理内存) |
|---|---|---|
| 本质 | 一种“先进后出”的逻辑数据模型 | 计算机内存中一块连续的物理区域 |
| 存在形式 | 仅在算法/代码设计中存在(如数组模拟栈) | 真实占用物理内存,由操作系统管理分配 |
| 操作主体 | 开发者手动实现 push/pop 等接口 | 编译器+CPU 自动执行(函数调用、局部变量存储时触发) |
| 核心规则 | 严格遵循“先进后出” | 同样遵循“先进后出”,是函数栈帧创建/销毁的底层支撑 |
简言之,数据结构的“栈”是逻辑规则,程序中的“栈”是该规则的物理实现。函数栈帧的创建(压栈)、销毁(出栈),就是借助“栈内存”的先进后出特性来完成的——这也是两者的核心关联。
今天我们讲的就是程序运行的栈(物理内存)
2、相关寄存器和汇编指令
(1)相关寄存器
- eax:通用寄存器,保留临时素材,常用于函数返回值。
- ebx:通用寄存器,保留临时数据。
- eip:指令寄存器,存储下一条要执行的指令的地址。
- ebp:栈底寄存器。
- esp:栈顶寄存器。
注意:
- 函数栈帧的空间。就是每一次函数调用,都要为本次函数调用开辟空间,就
- 这块空间的维护是使用了2个寄存器:esp和ebp,ebp记录的是栈底的地址,esp记录的是栈顶的地址。

esp指向栈顶,也叫栈顶指针;ebp指向栈底,也叫栈底指针。
所以,esp维护的是栈顶(顶部),ebp维护的是栈底(底部)。
(2)相关汇编指令(后面要用)
- push:将操作数压入栈中,同时调整栈顶指针esp。
- pop:从栈中弹出数据到指定位置,同时调整栈顶指针esp。
- mov:数据传送指令,在寄存器之间、寄存器与内存之间传送数据。
- add:加法指令,将两个操作数相加,结果存于指定寄存器。
- sub:减法指令,将两个操作数相减,结果存于指定寄存器。
- call:过程调用,压入返回地址或转入调用函数。
- lea:加载有效地址指令,将操作数的地址加载到指定寄存器。
- ret:返回地址指令,回到调用位置。
四、函数的栈帧的创建和销毁
1、 函数的调用堆栈
下面我们来实现函数的调用堆栈,我们先来一个简单的代码来观察
代码如下:
#include<stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
这段代码对应的栈区的图如下
大家进行调试窗口中的调用堆栈来观察
按F10进行调试,大家知道Add是被main调用的但我们发现main也是被调用的,是被谁调用的呢
我们继续按F10,我们会发现我们到这个页面
我们可以从这个页面向上翻,可以看出main是被 __tmainCRTStartup()函数调用的,再按F10可以看出 __tmainCRTStartup()函数也是被mainCRTStartup调用的,如下图
在这里我们只需要理解它的调用逻辑就行啦,
总的来说可以用箭头表示,箭头表示被调用,表示main被调用过程
main --> __tmainCRTStartup --> mainCRTStartup
由高地址到低地址,所以函数的栈帧分配如下图:就是大家每次调用函数都要分配相应的空间,而程序的运行是从高地址到底地址,所以函数分配的空间也

我们现在已经有了大概的轮廓啦,接下来我们来看一下它的具体调用过程
2、函数的栈帧的创建和销毁的具体过程
可能大家还有所疑惑,那么接下来我们来看一下具体的调用过程
这里还是F10调试,直接右键点反汇编
(1)观察前的操作

通过这里屏幕右边就出现了C语言的汇编代码,能够看清每一步的操作啦
我们这里还要把符号名关掉,便于我们观察

(2)开始观察
因为main是由_tmainCRTStartup调用的,所以刚开始内存布局如图:
1、push ebp
图形理解
这里相当于压栈,加个元素进去,将_tmainCRTStartup的ebp元素压栈

由于esp维护栈顶元素。所以esp的地址也要向上移动,如图
地址理解
我们也允许通过看一下esp的地址变化
F10
esp的地址-4,所以向上走了
内存块观察
我们还许可通过内存块来观察
地址搜索esp可看出
这里根据大小端字节序,由于VS是小端,所以要倒着读,地址就是与008ffbf4相同,说明我们把该元素给压进去啦

2、mov ebp,esp
接下来是mov,这里就把图形和地址观察放在一起啦
mov 就是把后面的值赋给前面去·

3、(创建main的栈帧)sub esp,0E4h
sub就是将前一个元素的值-去一个值
这里0E4h是16进制数字,我们可以借助监视来显示

我们发现它是228
下面进行操作
esp-228,esp向上移动
这里就相当于esp和ebp预开辟出main函数的栈帧,如图:
4、push ebx 、push esi 、push edi
这里再压3个元素这里ebx,esi,edi不用管他们是什么
5、(初始化)lea edi,[ebp+FFFFFF1Ch]、mov ecx,39、eax,0CCCCCCCCh,ret stos dword ptr es:[edi]
lea加载有效地址
这里我们把显现符号名再加上就好啦

- 就是将后面的地址放在edi中,这里放的这个地址就是esp没有进行上一步操作时的地址
- mov ecx,39h中39h次是一会儿在这进行复制的时候准确的一个次数,ecx,39h就是把39h的值放到ecx这个寄存器里面。
- 而ecx,OCCCCCCCh表示一会复制的内容
- 这一次的所以操作,主要是末了一步起作用,将edi(ebp-0E4h)向下39h次dword(4个字节)的数据全部初始化成这样的素材
(word为2个字节,dword(double word)4个字节)
大家可能从内存中看到的确初始化成了,这里只展示一部分
我们还用图来表示一下
6、3个mov,正式开始执行代码
这里我们还是把显示符号名去掉
其中0Ah=10,14h=20,0=0,
所以这三步是对a,b,c进行赋值
其中dword ptr 表示是指明操作数类型为指向 4 字节数据的指针
ebp为栈低指针,其中ebp-8,-14h,-20h表示对应a,b,c三个变量的地址
这里用图来表示,main中一个小格子表示4个字节
7、进入函数调用前
先讲传参过程
这里Add函数传参数,从右向左读取

这里先把b元素的数据传到eax这个寄存器中,再进行压栈
a的同理
进行完后
8、进入函数调用 call
接下来我们就要正式进入Add函数中

call函数为调用函数,这里我们就不要按F10啦,要按F11
按之前
通过按之后,大家能够发现call在调用时call又把下一个地址压进去了
就形成了这样,这是为了在执行完调用函数时,方便继续执行代码

再按F11,就真正进到调用函数里面了

我们发现下面绿色框起来的,和我们之前执行过的一样,这里我就不再解释了
执行后
接下来,我们来执行Add函数中的指令

先初始化z=0

再将ebp+8所对应的值传给eax,而ebp+8对应的就是当时大家第7步是进行的传参过程对应的a的值,下一个add将eax(a)的值与ebp+0ch(b)相加,相当于eax+=b;
最后把eax得到的值传给对应的z,但是程序结束要销毁,但还得返回z的值,所以我们再把z的值传给寄存器eax
所以我们发现
传参数时参数从左向右读取,调用时参数从左向右调用
完整示意图
9、调用函数的销毁

这里就不做解释了,其中mov操作就把调用函数给销毁啦
pop ebp
这个处理就是把ebp栈底指针,回到main的栈底
ret
这个就是从栈顶弹出call的下一条指令
再按F10我们发现,回来main了
10、继续main函数的操作

add esp,8就是将esp向下移动8个字节,也就是把传参的临时变量销毁

这个操作将eax的值给ebp-20h对应的,也就是将返回值给C
函数栈帧的创建和创建的具体过程就讲的差不多了,后面的操作就不进行讲解啦
五、简单回顾
现在大家来简单回顾一下函数的栈帧的创建和销毁
先是函数的调用堆栈
了解函数调用栈帧的简单过程
再是转到反汇编来看函数栈帧的创建
创建:
存“返回地址”(函数执行完回哪里);
存“调用者的栈底(ebp)”;
设“当前函数的栈底(ebp)”;
为局部变量“开辟栈空间”。
销毁:
释放局部变量的栈空间;
恢复“调用者的栈底(ebp)”;
跳回“返回地址”继续执行。
关于参数
传参数时参数从左向右读取,调用时参数从左向右调用
核心就是**“先建环境执行函数,再清环境回归调用者”**,全程靠栈“先进后出”的规则保障。
六、总结
我们来回顾先前的疑问
1. 局部变量是如何创建的?
首先为函数分配好栈帧空间,栈帧空间里面我们初始化完一部分空间之后,再给局部变量在栈帧里面分配空间。
2. 为什么局部变量的值是随机值?
如果局部变量不初始化,那么它就会得到随机值,我们设置变量自动赋的随机值,如下:
如果初始化就会把随机值覆盖掉
什么样的?就是3. 函数调用时参数是如何传递的?传参的顺序
函数调用时,参数通常按约定顺序(如从右到左)压入栈中,被调用函数通过栈地址偏移读取参数。
4. 形参和实参的关系是怎么样的?
形参是函数定义时用于接收数据的临时变量,实参是调用时传入的实际数据,调用时实参的值传递给形参,形参仅为实参的副本,对形参的修改(值传递下)不影响实参。
5. 函数调用是怎么做到的?
如上文
6. 函数调用结束后又是怎样返回的?
函数调用结束后,会先将返回值存入指定寄存器(如x86的eax),再销毁当前栈帧(mov esp, ebp释放局部变量、pop ebp恢复调用者栈底),最后执行ret指令弹出返回地址,跳回调用者的下一条指令继续执行。
7、注意
传参数时参数从左向右读取,调用时参数从左向右调用
通过我们创建的函数栈帧理论上能够被占满,但是我们的编译器会解决这个障碍不会让他慢的
八、结束语
我觉得学完数据结构的栈再来学更容易理解,两个栈的核心十分相同。本篇有点难度,不用全部掌握,只需要理解就行啦!本篇如果博主有写的不好的地方,欢迎大家在评论区建议或讨论!感谢大家的支持啦!就是嗨|ू・ω・` ),本文到这里就结束啦,我把它放在数据结构和C语言中,但
浙公网安备 33010602011771号