3.16 调用者与被调用者保存寄存器

x86-64 的调用约定将通用寄存器分为两类:调用者保存(caller-saved)和 被调用者保存(callee-saved)。这一约定确保了函数调用前后寄存器的状态一致性。

基本概念

问题背景

当函数 A 调用函数 B 时,如果 B 修改了 A 正在使用的寄存器,A 返回后继续执行就会出错。解决方案有两种:

策略谁负责保存保存时机
调用者保存调用者 Acall 之前保存自己关心的寄存器,调用后恢复
被调用者保存被调者 B在使用前保存寄存器原始值,返回前恢复

调用者保存寄存器

寄存器说明
%rax返回值寄存器,通常被函数修改
%rcx第 4 个参数,常用于临时计算
%rdx第 3 个参数
%rsi第 2 个参数
%rdi第 1 个参数
%r8第 5 个参数
%r9第 6 个参数
%r10临时寄存器
%r11临时寄存器

调用者保存的规则

如果调用者希望在 call 之后仍保留这些寄存器的值,它必须:

  1. call 之前 将这些寄存器的值保存到栈上
  2. 从被调函数返回后 从栈上恢复
# 调用者视角
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通用寄存器

被调用者保存的规则

如果被调函数想要使用这些寄存器:

  1. 在函数入口 将当前值保存到栈上
  2. 在函数返回前 从栈上恢复原始值
# 被调函数视角
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 会弹出错误地址)
  • 如果分配了局部变量空间,必须在返回前通过 addqmovq %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 → 程序错误!

相关笔记

    • 通用寄存器的分类与用途
    • 栈帧中的寄存器保存区域
    • 参数寄存器的传递规则

链接到