函数调用栈
我们以一段代码为例子。
#include <iostream>
int g_global = 100; // ① 已初始化全局
int g_uninit; // ② 未初始化全局
const char* g_msg = "hello"; // ③ 指针在数据段,字面量在只读段
int add(int a, int b) { // ④ 函数体
int c = a + b;
return c;
}
int main() {
static int s_local = 5; // ⑤ 静态局部
int x = 10; // ⑥ 普通局部
int* p = new int(20); // ⑦ p 在栈,*p 指向堆
int r = add(x, 5); // ⑧ 调用 add,压栈帧
delete p;
return r;
}
函数中各部分存储的位置
| 内容 | 位置 | 存储期 | 栈上? |
|---|---|---|---|
g_global = 100 |
.data |
整个程序 | 否 |
g_uninit |
.bss(启动清零) |
整个程序 | 否 |
g_msg(8 字节指针本身) |
.data |
整个程序 | 否 |
"hello\0"(6 字节字面量) |
.rodata(Windows 为 .rdata) |
整个程序 | 否 |
add / main 机器码 |
.text |
整个程序 | 否 |
s_local = 5 |
.data |
整个程序 | 否(最容易错) |
x、p、r |
main 栈帧 | main 执行期间 | 是 |
a、b、c |
add 栈帧 | add 执行期间 | 是 |
new int(20) 的对象 |
堆 | 到 delete p |
否 |
参考:进程内部空间分布图

以及

需要记住的:
只有自动存储期的东西才在调用栈上:局部变量、函数参数、返回地址、保存的寄存器。 全局变量、静态变量、字符串字面量、函数代码、堆对象——都不在栈上。
示例代码对应进程地址空间全景:
high address 0x00007fff_ffff_ffff
+----------------------------------------------------------+
| kernel (not accessible from user mode) |
+----------------------------------------------------------+
| STACK rw- grows down v |
+----------------------------------------------------------+
| mmap r-x/rw- libc.so libstdc++.so |
+----------------------------------------------------------+
| HEAP rw- grows up ^ |
| [ int 20 ] <-- new int(20) |
+----------------------------------------------------------+
| .bss rw- g_uninit = 0 |
+----------------------------------------------------------+
| .data rw- g_global = 100 |
| g_msg -> "hello" |
| s_local = 5 |
+----------------------------------------------------------+
| .rodata r-- "hello\0" |
+----------------------------------------------------------+
| .text r-x add() main() |
+----------------------------------------------------------+
low address 0x00000000_00000000
其中栈和堆对向扩张,中间部分是两者可以共享的可用空间
其中权限位(rw-) (r-x) 是由OS加载时设置,这就说明了”字面量不能写“的问题。
栈帧长什么样?
栈,由两个寄存器所看管着。
| 寄存器 | 作用 |
|---|---|
rsp |
栈顶指针,永远指向当前的栈顶 |
rbp |
帧基址指针,指向当前函数的帧底,用固定偏移访问参数和局部变量 |
一个看着栈顶rsp,另一个·rbp看管着帧基址。
通用的栈布局规律
高地址侧
+--------------------+
| [rbp+8] 返回地址 | <- call 指令压入
| [rbp+0] 上层 rbp | <- push rbp 压入;rbp 就指在这
+--------------------+(rbp)
| [rbp-4] 局部变量 |
| [rbp-8] 局部变量 |
| ... |
+--------------------+
低地址侧(rbp之下)
那函数栈帧处于哪个区域呢?
第 1 层 · 整个进程地址空间
高地址
|-- 内核区(用户态不可访问)
|-- 栈区 Stack ← ★ 函数栈帧全住在这里
|-- 内存映射区(libc / libstdc++)
|-- 堆区 Heap
|-- BSS 段
|-- 数据段
|-- 只读数据段
|-- 代码段 Text
低地址
函数栈帧存在于栈区。
第 2 层 · 放大"栈区"
栈区 Stack 的放大视图
高地址
|-- 调用者的栈帧(__libc_start_main)
|-- main 的栈帧
|-- add 的栈帧 ← ★ 正在执行的函数,它的栈帧在最上面
|-- (下方是还没用到的空闲空间)
| <-- rsp 指着这里
低地址
栈区就是一整块内存,每次函数调用往里"占一小块",那小块就是栈帧。栈帧一个叠一个,从高地址往低地址长。
第 3 层 · 放大"add 的栈帧"(帧内部)
add 的栈帧 的放大视图
高地址
|-- [rbp+8] 返回地址 -> 回到 main
|-- [rbp+0] 保存的 main 的 rbp <-- rbp 钉在这
|-- [rbp-4] a = 10
|-- [rbp-8] b = 5
|-- [rbp-12] c = 15
| <-- rsp 在这
低地址
- 整体的包含关系:
进程地址空间
└── 栈区(一整块,几 MB)
└── 栈帧(每个函数调用占一小块)
└── 帧内条目(返回地址、旧 rbp、局部变量)
- 边界
高地址
|-- 调用者的栈帧
|-- main 的栈帧
|-- add 的栈帧
| <-- rsp:栈顶,随着调用/返回上下移动
|-- (空闲空间)
|
+------------------- 栈底边界(固定,几 MB 处)
低地址
- 高地址端(栈底):固定不动
- 低地址端(栈顶):
rsp所在位置,随函数调用伸缩 - 栈区总大小固定(Linux 默认 8MB,Windows 默认 1MB),容量用完就会爆栈
一次完整的调用流程
调用main
函数栈帧区 call main —— 压入返回地址。
【栈区】
高地址
|-- 调用者的栈帧(__libc_start_main)
|-- 返回地址(call 压入) -> _start
| <-- rsp
低地址
main被调用了,于是向下压入main
【栈区】
高地址
|-- 调用者的栈帧
|-- main 栈帧
| |-- [rbp+8] 返回地址 -> _start
| |-- [rbp+0] 保存的调用者的 rbp <-- rbp 钉在这
| |-- [rbp-4] x
| |-- [rbp-8] (填充,对齐用)
| |-- [rbp-16] p
| |-- [rbp-20] r
| <-- rsp
低地址
main内部逐句执行内容
x = 10;
| |-- [rbp-4] x = 10
p = new int(20);
【栈区】
| |-- [rbp-16] p = 0x000055aa_0000_00a0
【堆区】
|-- [整数 20] <-- p 指向这里
int r = add(x,5); 准备过程,把x变量从栈加载到寄存器。
【寄存器取值】
栈区无变化,只是寄存器取值:
| edi = 10 (第 1 个参数)
| esi = 5 (第 2 个参数)
寄存器需要通过从栈[rbp - 4] 来读到寄存器,放进edi里的。
执行add(x,5)
召唤函数add(x,5)——再压入一个返回地址
add(x,5);
【栈区】
高地址
|-- 调用者的栈帧
|-- main 栈帧
| |-- [rbp+8] 返回地址 -> _start
| |-- [rbp+0] 保存的 rbp
| |-- [rbp-4] x = 10
| |-- [rbp-16] p = 0x000055aa_0000_00a0 -> 堆
| |-- [rbp-20] r = 还没写入
|-- 返回地址(call 压入) -> 回到 main
| <-- rsp
低地址
add初始化与函数体,建立add函数帧,计算得到c
【栈区】
高地址
|-- 调用者的栈帧
|-- main 栈帧 (rbp 按 main 算)
| |-- [rbp+8] 返回地址 -> _start
| |-- [rbp+0] 保存的 rbp
| |-- [rbp-4] x = 10
| |-- [rbp-16] p = 0x000055aa_0000_00a0 -> 堆
| |-- [rbp-20] r = 还没写入
|-- add 栈帧 (rbp 按 add 算)
| |-- [rbp+8] 返回地址 -> 回到 main
| |-- [rbp+0] 保存的 main 的 rbp
| |-- [rbp-4] a = 10
| |-- [rbp-8] b = 5
| |-- [rbp-12] c = 15
| <-- rsp
低地址
【堆区】
|-- [整数 20] <-- main 的 p 仍指着
这部分属于复合栈帧,偏移是相同的,但是两个栈对应的是各自独有的rbp。
add返回
leave: ret 弹出add帧
【栈区】
高地址
|-- 调用者的栈帧
|-- main 栈帧
| |-- [rbp+8] 返回地址 -> _start
| |-- [rbp+0] 保存的 rbp
| |-- [rbp-4] x = 10
| |-- [rbp-16] p = 0x000055aa_0000_00a0 -> 堆
| |-- [rbp-20] r = 还没写入
| <-- rsp(add 帧已整个消失)
低地址
寄存器:eax = 15 (add 的返回值)
回到main里写入 r 变量
mov [rbp - 20], eax
| |-- [rbp-20] r = 15
main收尾并返回
delete p;
【栈区】
| |-- [rbp-16] p = 0x000055aa_0000_00a0 (仍存在,已成悬垂指针)
【堆区】
|-- (对象已释放,内存交还分配器)
return r;
尾部:leave; ret
【栈区】
高地址
|-- 调用者的栈帧
| <-- rsp(main 帧已整个消失)
低地址
寄存器:eax = 15 -> 交给 _start -> exit()
最终阶段:进程结束
【栈区】 全部回收
【堆区】 剩余内存由操作系统回收
【数据段】 进程结束即失效(POD 类型无析构调用)
【代码段】 随进程消失