茜莉娅 Serelia
← Back to the beginning

关于 "函数调用栈"

八股回忆录
cpp

函数调用栈

我们以一段代码为例子。

#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 类型无析构调用)
【代码段】   随进程消失
© 泠時月 2026,采用 CC BY 4.0 许可,转载保留署名。

留言 · 0 段对话

© 2026 茜莉娅 · Powered by Serelia 茜莉娅 88x31 徽章

扫码分享

二维码