如何恢复 x86-64 寄存器保存约定

How to restore x86-64 register saving conventions

fibonacci:
         cmpq    , %rdi
         ja      .recursive
         movl    , %eax
         ret
.recursive:
         push    %rbp
         push    %r10
         movq    %rdi, %r10
         leaq    -2(%rdi), %rdi
         call    fibonacci
         movq    %rax, %rbp
         leaq    -1(%r10), %rdi
         call    fibonacci
         addq    %rbp, %rax
         pop     %r10
         pop     %rbp
         ret

如何恢复 x86-64 寄存器保存约定。

我不确定该怎么做,但这是我的思路。 %r10是caller-saved寄存器,所以在调用其他方法前后都需要保存。在这种情况下,只有在递归调用斐波那契之后使用 %r10 时,我们才需要保存它。不过,我不确定在哪里添加说明来恢复寄存器保存约定。

%r10 is a caller-saved register, so it needs to be saved before and after calls to other methods.

如果你避免在里面放任何珍贵的东西,就不会!您正在陷入由笨拙的“调用者保存”术语所造成的陷阱,就好像这意味着您 必须 来保存它而不是让 tmp 值消亡。我更喜欢call-preserved vs. call-clobbered

选择一个像 RBX 这样的调用保留寄存器来保存你希望在调用中保留的值,save/restore它一次在start/end 你的函数。就像您对 RBP 所做的一样。您 可以 使用 R10 做到这一点,因为您的函数只调用它自己。但是,如果您想完全遵循 x86-64 System V 调用约定(即只假设调用自己保留调用保留寄存器),请使用 RBX、RBP 或 R12..R15。 What registers are preserved through a linux x86-64 function call

作为优化,您可以只使用 1 个调用保留寄存器,首先将其用于 N,然后用于第一个 Fib 结果。

为了好玩,我将输入 N 设为 32 位整数;无需使用 64 位整数输入; Fib(94) 溢出一个 64 位整数!这应该使我在处理 N(32 位寄存器)与 Fib(N)(64 位寄存器)时更容易看到。请记住,写入 32 位寄存器会隐式零扩展到完整的 64 位寄存器。

## Recursive is a terribly inefficient way to implement Fibonacci
## but if you must do it this way (without memoization or anything), this may be less bad than most, only using 16 bytes of stack per depth of call.
recursive_fib:    # unsigned long fib(unsigned N)
    cmp    , %edi 
    ja    .Lnon_basecase
    mov    %edi, %eax                ## bugfix: Fib(0) = 0, Fib(1) = 1
    ret

.Lnon_basecase:
    push   %rbx              ###### save a call-preserved register
    mov    %edi, %ebx        # keep a copy of N for later
    dec    %edi
    call   recursive_fib            # Fib(N-1)
    lea    -2(%rbx), %edi    # read N
    mov    %rax, %rbx        # replace RBX with Fib(N-1)
    call   recursive_fib            # Fib(N-2)
    add    %rbx, %rax        # retval = Fib(N-2) + Fib(N-1)
    pop    %rbx              ###### restore caller's RBX
    ret

可以 当然可以将 RDI 溢出/重新加载到第一个 call 的堆栈中,就像编译器将 mov 放入 space 您之前使用 sub , %rsp 或实际 push/pop 保留。 如果您 运行 超出大型函数中的调用保留寄存器,则需要这样做。

(但希望你可以选择溢出一些读取最多的东西:重新读取内存很便宜,更新内存会造成 store/reload 延迟瓶颈。这就是编译器具有智能寄存器分配算法的原因)

(您只需要 space 的 8 个字节,但编译器会在调用之前保持 x86-64 系统 V 的 16 字节 RSP 对齐。这意味着在另一个调用之前任何函数中的总 RSP 偏移量是8 + 16*x,如果你以 push %rbp 开头,那么你需要保留 16 个字节。通过手写你知道你只是在调用你自己,并且这个函数不关心堆栈对齐。)


顺便说一句,递归 Fibonacci 需要 O(Fib(N)) 时间,而 O(N) 对于使用最后 2 个值的更简单的循环。有趣的是,循环中的 xadd %rdx, %rax 实现斐波那契数列,或者用 2 add 指令展开。