3.16 调用者与被调用者保存寄存器
x86-64 的调用约定将通用寄存器分为两类:调用者保存(caller-saved)和 被调用者保存(callee-saved)。这一约定确保了函数调用前后寄存器的状态一致性。
基本概念
问题背景
当函数 A 调用函数 B 时,如果 B 修改了 A 正在使用的寄存器,A 返回后继续执行就会出错。解决方案有两种:
| 策略 | 谁负责保存 | 保存时机 |
|---|---|---|
| 调用者保存 | 调用者 A | 在 call 之前保存自己关心的寄存器,调用后恢复 |
| 被调用者保存 | 被调者 B | 在使用前保存寄存器原始值,返回前恢复 |
调用者保存寄存器
| 寄存器 | 说明 |
|---|---|
%rax | 返回值寄存器,通常被函数修改 |
%rcx | 第 4 个参数,常用于临时计算 |
%rdx | 第 3 个参数 |
%rsi | 第 2 个参数 |
%rdi | 第 1 个参数 |
%r8 | 第 5 个参数 |
%r9 | 第 6 个参数 |
%r10 | 临时寄存器 |
%r11 | 临时寄存器 |
调用者保存的规则
如果调用者希望在 call 之后仍保留这些寄存器的值,它必须:
- 在
call之前 将这些寄存器的值保存到栈上 - 从被调函数返回后 从栈上恢复
# 调用者视角
func_caller:
movq $42, %rax # %rax = 42(调用者关心这个值)
pushq %rax # 1. 保存 %rax 到栈(即将调用子函数)
call child_func # 调用子函数(可能修改 %rax)
popq %rax # 3. 恢复 %rax = 42
# ... 使用 %rax ...
ret调用者可以不保存
如果调用者在函数调用后不再使用该寄存器的值,就不需要保存:
func:
movq $42, %rdi # 设置参数
call other_func # 调用后不再需要 %rdi 的值
# 不需要保存 %rdi
ret被调用者保存寄存器
| 寄存器 | 说明 |
|---|---|
%rbx | 通用寄存器 |
%rbp | 基址指针(也可用作通用寄存器) |
%r12 | 通用寄存器 |
%r13 | 通用寄存器 |
%r14 | 通用寄存器 |
%r15 | 通用寄存器 |
被调用者保存的规则
如果被调函数想要使用这些寄存器:
- 在函数入口 将当前值保存到栈上
- 在函数返回前 从栈上恢复原始值
# 被调函数视角
child_func:
pushq %rbx # 1. 保存 %rbx(被调用者保存寄存器)
pushq %r12 # 保存 %r12
movq (%rdi), %rbx # 2. 使用 %rbx
movq %rbx, %r12 # 使用 %r12
# ... 函数体 ...
popq %r12 # 3. 恢复 %r12
popq %rbx # 恢复 %rbx
ret完整示例
// C 代码
long callee(long x) {
long t = x * 3;
return t + 5;
}
long caller(long a, long b) {
long sum = a + b;
long result = callee(sum);
return result + a;
}如果所有寄存器都是调用者保存
caller:
pushq %rdi # 保存 a(调用者保存)
pushq %rsi # 保存 b(调用者保存)
addq %rsi, %rdi # sum = a + b
movq %rdi, %rdi # 参数已经就位
call callee
# %rax = result
popq %rsi # 恢复 b(不再需要)
popq %rdi # 恢复 a
addq %rdi, %rax # return result + a
ret使用被调用者保存寄存器优化
caller_opt:
pushq %rbp # 使用被调用者保存寄存器存储 a
pushq %rbx # 使用被调用者保存寄存器存储 b
movq %rdi, %rbp # rbp = a (被调用者保存,无需在 call 前保存)
movq %rsi, %rbx # rbx = b (被调用者保存)
addq %rbx, %rdi # sum = a + b
call callee # call 会破坏调用者保存寄存器,但 %rbp/%rbx 安全
addq %rbp, %rax # return result + a
popq %rbx # 恢复
popq %rbp
ret优势:无需在 call 前后反复保存/恢复 %rbp 和 %rbx,它们由被调函数 callee 负责维护。
两种策略的对比
| 特性 | 调用者保存 | 被调用者保存 |
|---|---|---|
| 谁负责 | 调用者 | 被调用者 |
| 保存次数 | 在每次 call 前(可能多次保存同一个寄存器) | 在函数入口保存一次 |
| 恢复时机 | 每次 call 后 | 函数返回前 |
| 适用于 | 临时值、不跨函数调用使用的值 | 需跨函数调用保持的值、频繁使用的值 |
| 典型寄存器 | %rax, %rcx, %rdx, %rsi, %rdi, %r8~%r11 | %rbx, %rbp, %r12~%r15 |
%rsp 和 %rbp 的特殊管理
%rsp(栈指针)
%rsp 有严格的隐式管理规则:
%rsp不需要”保存”,因为 push/pop/call/ret 自动维护- 被调函数必须保证返回时
%rsp与进入时相同(否则ret会弹出错误地址) - 如果分配了局部变量空间,必须在返回前通过
addq或movq %rbp, %rsp恢复
%rbp(基址指针)
%rbp 是被调用者保存的,但同时有额外约定:
- 通常用作栈帧基址(可选)
- 用作基址时,在函数入口通过
pushq %rbp; movq %rsp, %rbp设置 - 在函数返回前通过
movq %rbp, %rsp; popq %rbp恢复
编译器对寄存器保存的优化
编译器通过寄存器分配(register allocation)算法决定分配哪些寄存器,并自动插入保存/恢复代码:
# 优化编译器可能生成的代码
optimized_func:
pushq %rbx # 只有一个被调用者保存寄存器
pushq %rbp
# 函数体使用 %rbx, %rbp
# 多次调用其他函数,但无需反复保存/恢复
popq %rbp
popq %rbx
ret违反约定的后果
如果函数不遵守寄存器保存约定:
# 违反约定的函数(错误的实现)
bad_func:
movq $0, %rbx # 修改了被调用者保存寄存器但不恢复
ret # 调用者将看到 %rbx 被清零
# 调用者的代码
caller:
movq $100, %rbx # 假设 %rbx 在调用后仍然为 100
call bad_func
# %rbx 现在为 0,不是期望的 100 → 程序错误!相关笔记
-
- 通用寄存器的分类与用途
-
- 栈帧中的寄存器保存区域
-
- 参数寄存器的传递规则