寄存器分配 --- 如何利用和溢出调用者保存的寄存器

register allocation --- how to utilize and spill the caller saved registers

我了解到,如果被调用者使用了任何 caller saved registers (rax rdx rcx rsi rdi r8 r9 r10 r11),那么它必须在调用者的 call 指令之前保存并在调用者指令之后恢复。

通过下面的例子,

int read();
void print(int i);

int main()
{
    int a = read();
    int b = read();
    int c = read();
    int d = read();
    int e = read();
    int f = read();
    int g = read();
    print(a);
    print(b);
    print(c);
    print(d);
    print(e);
    print(f);
    print(g);
}

备注

  1. 变量a - g应该使用全部callee saved registers (rbp rsp rbx r12 r13 r14 r15)。而且我们不能同时使用 rbprsp,因为其中一个必须用于寻址堆栈内存。

  2. readprint 来自一些外部编译单元。因此,当我们编译当前编译单元时,特别是在 main 函数的寄存器分配期间,我们并不真正了解它们的调用者保存寄存器的使用情况。

godbolt-O3 中编译为以下内容

main:
  pushq %r15
  pushq %r14
  pushq %r13
  pushq %r12
  pushq %rbp
  pushq %rbx
  subq , %rsp # spill here
  call read()
  movl %eax, 12(%rsp) # spill here
  call read()
  movl %eax, %ebx
  call read()
  movl %eax, %r15d
  call read()
  movl %eax, %r14d
  call read()
  movl %eax, %r13d
  call read()
  movl %eax, %r12d
  call read()
  movl 12(%rsp), %edi
  movl %eax, %ebp
  call print(int)
  movl %ebx, %edi
  call print(int)
  movl %r15d, %edi
  call print(int)
  movl %r14d, %edi
  call print(int)
  movl %r13d, %edi
  call print(int)
  movl %r12d, %edi
  call print(int)
  movl %ebp, %edi
  call print(int)
  addq , %rsp
  xorl %eax, %eax
  popq %rbx
  popq %rbp
  popq %r12
  popq %r13
  popq %r14
  popq %r15
  ret

备注

  1. 变量a溢出到12(%rsp)

  2. 我们不需要溢出 caller saved registers 中的任何一个,因为它们根本不被使用,事实证明这里效率更高。

我的问题

  1. 看起来我们真的不需要处理 caller saved registers 的溢出问题,如果我们不使用它们的话。因此,我们什么时候应该使用 caller saved registers?

  2. 对于readprint这样的被调用者,由于我们不知道他们的寄存器使用情况,我们应该如何为caller saved registers做溢出?

谢谢

I have learned that if any of the caller saved registers (rax rdx rcx rsi rdi r8 r9 r10 r11) is used by the callee, then it has to be saved before and restored after a call instruction by the caller.

应该是

I have learned that if any of the caller saved registers (rax rdx rcx rsi rdi r8 r9 r10 r11) is used by the caller, then it has to be saved before and restored after a call instruction by the caller.

调用者保存寄存器是那些可能被任何被调用函数破坏的寄存器。您不确定任何给定的被调用者是否使用它们,因此您必须假设最坏的情况。但是,调用者只需要在调用者使用它们时保存它们。如果你不是,你不在乎他们可能会被破坏。

看起来令人困惑和不直观的“调用者已保存/被调用者已保存”术语误导了您,让您认为每个寄存器都应该始终由某个地方的某人保存。请参阅 What are callee and caller saved registers? - “保留调用”与“破坏调用”在便于记忆和作为心智模型方面更有用。让值被销毁是正常的,比如函数arg。

Look like we don't really need to deal with spilling the caller saved registers, if we don't use them.

请注意,您的函数 确实 使用一对 call-clobbered(“调用者已保存”)寄存器:它使用 RDI 将 arg 传递给 print(int),并将 RAX 归零为 main 的 return 值。

如果它在 call-clobbered 寄存器 中有一个值需要在函数调用中存活 ,GCC 选择 mov 该值到一个 call-preserved 寄存器。例如read() returns时,它的return值在EAX中,会被下一次调用销毁。所以它使用 mov %eax, %ebp 或其他任何东西将其保存到 call-preserved 寄存器中,或者将一个溢出到 12(%rsp).

(请注意,GCC 使用 push/pop 到 save/restore 它使用的 call-preserved 寄存器的调用者值。)

GCC 的默认 code-gen 策略是 save/restore call-preserved 寄存器来保存调用中的值,而不是溢出到内存 内部 这个函数.对于不那么琐碎的情况,这通常是一件好事,尤其是对于循环内的调用。有关详细信息,请参阅

And we cannot use both rbp or rsp, since either has to be used for addressing the stack memory.

也是错误的:对于 -fomit-frame-pointer(在大多数优化级别上),RBP 只是另一个 call-preserved 寄存器。您的函数使用它来保存 read return 值。 (EBP为RBP的低32位)