5 Activation Records¶
活动记录 (Activation Record / Stack Frame) 是栈上的一块内存区域,在函数调用时创建、返回时销毁,用于存储该次调用的参数、局部变量、返回地址等运行时状态。整个机制的核心是调用约定 (Calling Convention / ABI)——一套二进制层面的协议,规定参数怎么传、寄存器怎么用、栈帧怎么布局。
5.1 Memory Layout¶
程序运行时地址空间从低到高依次为:
- Code(代码段):只读,存放指令。
- Static Data(静态数据段):全局变量、静态变量、字符串常量。
- Heap(堆):动态分配 (
malloc/new),向高地址增长。 - Stack(栈):函数调用栈,向低地址增长。
两个关键寄存器定位栈帧:
- SP (Stack Pointer):指向栈顶,随 push/pop 变化。
- FP (Frame Pointer):指向当前栈帧的基址(固定锚点),用于访问局部变量和参数。在 x86-64 中通常用
rbp。
Calling Convention¶
调用约定由硬件平台和操作系统定义,以 x86-64 System V 为例:
参数传递:前 6 个整型/指针参数依次通过 rdi, rsi, rdx, rcx, r8, r9 传递,浮点参数通过 xmm0~xmm7 传递。超出部分通过栈传递,由调用者压入。
返回地址:call 指令自动将下一条指令地址压入栈,ret 指令弹出并跳转回去。
返回值:整型/指针放在 rax,浮点放在 xmm0。
寄存器保存约定:
| 类型 | 寄存器 | 规则 |
|---|---|---|
| Caller-Save | rax, rcx, rdx, rsi, rdi, r8-r11 |
调用者若关心这些值,需在调用前自行保存 |
| Callee-Save | rbx, rbp, r12-r15 |
被调用者若使用这些寄存器,必须先保存原值、返回前恢复 |
叶子过程优化 (Leaf Procedure):不调用其他函数的函数称为叶子过程。若其只用 caller-save 寄存器且不超出寄存器数量,则无需保存任何寄存器,也无需建立完整栈帧——可省略 push rbp 等开销。
Frame-Resident Variables¶
以下情况变量必须分配在栈帧中,无法放入寄存器:
- 传引用 (Passed by reference):必须有内存地址指向它。
- 取地址 (Address taken):如 C 语言的
&a操作。 - 嵌套过程访问 (Nested access):内部函数引用外部函数的变量。
- 数据过大 (Too large):超过寄存器宽度(如大结构体、大数组)。
- 数组 (Arrays):元素通过基址+偏移访问,需要地址运算。
- 寄存器溢出 (Spill):活跃变量数超过寄存器数,部分被迫写入内存。
满足 1、2、3 的变量称为逃逸变量 (Escape Variables)——它们"逃出"了寄存器的约束,必须驻留在栈帧中。
Static Scoping & Nested Functions¶
C 语言不允许嵌套函数,但 Pascal、Tiger 等语言支持。问题:内部函数如何访问外层函数的变量?
静态作用域 (Lexical Scoping):变量的可见性由代码的嵌套结构决定(写代码时就能确定),而非运行时调用链。
静态深度 (Static Depth):函数定义所在的嵌套层数。最外层为 0,每嵌套一层 +1。
Static Link¶
每个栈帧包含一个静态链 (Static Link) 指针,指向其静态父函数(词法上直接包含它的函数)的栈帧。访问深度差为 \(d\) 的非局部变量时,沿静态链追踪 \(d\) 步到达目标帧。
- 优点:实现简单,每帧多一个指针。
- 缺点:访问时间为 \(O(d)\),每次访问需遍历链表。
Static Link vs Dynamic Link
动态链 (Dynamic Link) 指向调用者的栈帧(即上一帧,FP 通常保存它的值),用于函数返回时恢复栈。静态链指向定义者,用于变量访问。两者通常不同。
Display¶
维护全局数组 Display[i],指向当前深度为 \(i\) 的最新活跃栈帧。访问深度为 \(d\) 的变量时,直接取 Display[d] 即可定位目标帧。
- 优点:访问 \(O(1)\),直接寻址。
- 缺点:函数调用/返回时需保存和恢复 Display 数组项(深度为 \(d\) 的函数进入时覆盖
Display[d],退出时恢复),维护成本高。
Example — 沿用上面 P/Q/call_it 的嵌套结构(静态深度:P=0, Q=1, call_it=1)。Display 数组在调用过程中的演变:
初始 (main 调用 P 前): P 进入后:
Display[0] = 空 Display[0] = P的帧 ──→ 覆盖 0 号槽
Display[1] = 空 Display[1] = 空
Display[2] = 空 Display[2] = 空
call_it 进入后 (深度 1): Q 进入后 (深度 1):
Display[0] = P的帧 Display[0] = P的帧
Display[1] = call_it的帧 ──→ 覆盖 Display[1] = Q的帧 ──→ 覆盖 1 号槽
Display[2] = 空 Display[2] = 空
每次进入深度为 \(d\) 的函数时,先保存 Display[d] 的旧值到该函数的栈帧中,再覆盖写入当前帧地址。退出时从帧中恢复旧值。
栈帧 保存/恢复机制
──── ────────────
┌─────────────────┐
│ P 的帧 (d=0) │ 进入 P: save old Display[0] → P 的帧内
│ [saved D[0]] │ Display[0] = P 的帧
├─────────────────┤
│ call_it (d=1) │ 进入 call_it: save old Display[1] → call_it 帧内
│ [saved D[1]] │ Display[1] = call_it 的帧
├─────────────────┤
│ Q 的帧 (d=1) │ 进入 Q: save old Display[1] → Q 的帧内
│ [saved D[1]] │ ← call_it 的帧 Display[1] = Q 的帧
└─────────────────┘
Q 返回: Display[1] = saved (恢复为 call_it 的帧)
call_it 返回: Display[1] = saved (恢复为空)
P 返回: Display[0] = saved (恢复为空)
当 Q 执行 print(x) 访问深度为 0 的 x 时:直接取 Display[0] → P 的帧,一步到位,无需遍历链表。与 static link 需要沿链追踪 \(d\) 步相比,Display 将变量访问从 \(O(d)\) 降为 \(O(1)\),代价是函数调用/返回时需要额外维护。
Lambda Lifting¶
将嵌套函数"提升"到外层,所有非局部变量转换为显式的额外参数。
// 提升前
void outer(int x) {
void inner() { print(x); }
}
// 提升后
void inner(int x) { print(x); }
void outer(int x) { /* x 显式传给 inner */ }
- 优点:无需复杂栈帧结构,数据流显式。
- 缺点:增加参数数量,调用开销增大;闭包转发复杂。
Stack Frame Layout¶
以 Tiger 编译器为例,典型栈帧从高地址(帧底)到低地址(栈顶)的布局:
FP 指向 Static Link 上方(即 Return Address 的下方),SP 随参数压栈/弹出动态移动。编译时,每个局部变量和参数通过 [FP + offset] 即可定址。



