【问题标题】:How to restore x86-64 register saving conventions如何恢复 x86-64 寄存器保存约定
【发布时间】:2020-07-21 00:04:30
【问题描述】:
fibonacci:
         cmpq    $1, %rdi
         ja      .recursive
         movl    $1, %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 是调用者保存的寄存器,因此需要在调用其他方法之前和之后保存。在这种情况下,只有在递归调用斐波那契之后使用 %r10 时,我们才需要保存它。不过,我不确定在哪里添加指令以恢复寄存器保存约定。

【问题讨论】:

    标签: assembly x86-64 cpu-registers calling-convention


    【解决方案1】:

    %r10 是调用者保存的寄存器,所以需要在调用其他方法前后保存。

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

    选择一个像 RBX 这样的调用保留寄存器,用于保存您希望在调用中保留的值,并在函数的开始/结束时保存/恢复它一次。就像您对 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    $1, %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 放入您之前使用 sub $16, %rsp 保留的空间中一样,或者使用实际推送/弹出。 如果大型函数中的调用保留寄存器用完,则需要这样做。

    (但希望您可以选择溢出主要读取的内容:重新读取内存很便宜,更新内存会造成存储/重新加载延迟瓶颈。这就是编译器具有智能寄存器分配算法的原因)

    (您只需要 8 个字节的空间,但编译器会在调用之前维护 x86-64 System V 的 16 字节 RSP 对齐。这意味着在另一个调用之前任何函数中的总 RSP 偏移量是 8 + 16*x,如果你以 push %rbp 开头,然后你需要留下 16 个字节。手动知道你只是在调用你自己,并且这个函数不关心堆栈对齐。)


    顺便说一句,对于使用最后两个值的更简单的循环,递归斐波那契需要 O(Fib(N)) 时间与 O(N)。有趣的事实是,xadd %rdx, %rax 在循环中实现了斐波那契,或者使用 2 个 add 指令展开。 Assembly Language (x86): How to create a loop to calculate Fibonacci sequence

    【讨论】:

      猜你喜欢
      • 2016-09-24
      • 2014-01-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-12
      • 2011-11-04
      • 2012-08-26
      • 1970-01-01
      相关资源
      最近更新 更多