首页 > 解决方案 > 寄存器分配 --- 如何利用和溢出调用者保存的寄存器

问题描述

我了解到,如果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. read来自print一些外部编译单元。main因此,当我们编译当前编译单元时,特别是在为函数分配寄存器期间,我们并不真正了解它们的调用者保存寄存器的使用情况。

godbolt中,-O3它编译为以下

main:
  pushq %r15
  pushq %r14
  pushq %r13
  pushq %r12
  pushq %rbp
  pushq %rbx
  subq $24, %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 $24, %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. 对于像这样的被调用者,read并且print由于我们不知道他们的寄存器使用情况,我们应该如何为caller saved registers?

谢谢

标签: x86compiler-constructionx86-64calling-conventionregister-allocation

解决方案


看起来令人困惑且不直观的“调用者保存/被调用者保存”术语误导您认为每个寄存器都应该始终由某个地方的某个人保存。请参阅什么是被调用者和调用者保存的寄存器?- “呼叫保留”与“呼叫破坏”在易于记忆和作为心理模型方面更有用。让值被破坏是正常的,就像函数 arg 一样。

看起来我们真的不需要处理溢出调用者保存的寄存器,如果我们不使用它们。

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

如果它在调用破坏寄存器中有一个值需要在函数调用中存活,GCC 选择mov将该值保存到调用保留寄存器。例如,当read()返回时,它的返回值在 EAX 中,它会被下一次调用销毁。因此,它使用mov %eax, %ebp或其他任何方式将其保存到保留调用的寄存器中,或者将一个溢出到12(%rsp).

(请注意,GCC 使用 push/pop 来保存/恢复它使用的调用保留寄存器的调用者值。)

GCC 的默认代码生成策略是保存/恢复调用保留的寄存器以在调用之间保存值,而不是溢出到该函数内部的内存中。对于不那么琐碎的情况,这通常是一件好事,尤其是对于循环内的调用。请参阅为什么编译器在这里坚持使用被调用者保存的寄存器?有关更多信息。

而且我们不能同时使用 rbp 或 rsp,因为必须使用其中任何一个来寻址堆栈内存。

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


推荐阅读