【问题标题】:Converting a basic recursive C function into assembly将基本的递归 C 函数转换为程序集
【发布时间】:2021-03-25 03:49:59
【问题描述】:

我在汇编中编写递归函数时遇到了一些麻烦。我想在这里为 C 函数建模:

int power(int base, int exponent)
{
    if (exponent==0)
        return 1;
    else {
        /* return base * power(base, --exponent); -- in normal form, writing in long-form below*/
        exponent--;
        int tmp1 = power(base, exponent);
        int tmp2 = base * tmp1;
        return tmp2;
    }
}

到目前为止,我在汇编中的内容如下:

power:
    // int power(int base=rdi, int exponent=rsi)
    push %rbp
    mov %rsp, %rbp

    // if (exponent==0) return 1
    cmp $0, %rsi
    jnz exponentiate

 return_one:
    mov $1, %eax
    jmp cleanup

 exponentiate:
    dec %rsi
    push %rax
    call power
    pop %rbx
    imul %rbx, %rax

 cleanup:
    mov %rbp, %rsp
    pop %rbp
    ret

这会产生结果为零。我认为我的错误出在exponentiate 函数中,但我很难调试它。我知道我可以使用编译器资源管理器“找到答案”,但我想知道是否有人可以帮助指出我的代码中的错误。


我已经能够将其简化为以下,我什至不必为递归情况设置堆栈框架:

 exponentiate:
    dec %rsi
    call power
    imul %rdi, %rax

为什么允许这样做?我认为对于所有递归函数,您都需要设置自己的堆栈框架,但为什么在上述情况下不需要?

【问题讨论】:

  • 你第一次做push %rax,你期望%rax保持什么价值?
  • @JosephSible-ReinstateMonica 我认为 1 如果这是 return_one 的回报?
  • 你认为你的代码运行的顺序是什么?
  • Of course it is,你忽略了我所说的关于使用-O1 的所有内容,在这种情况下,它是一个非常干净的 asm,它只执行一次推送,没有其他堆栈空间。 godbolt.org/z/EKbcTf15a。事实证明,-O1 不需要任何 noinline 技巧,仅此一项即可制作递归 asm 而无需将其转换为迭代。 How to remove "noise" from GCC/clang assembly output?
  • re:堆栈对齐与否:是GCC sometimes reserves 16 bytes more than it needs for alignment。但这并不重要,它保留 一些 并使用它才是真正重要的。当您编写自己的函数时,您可以对堆栈框架进行自己的布局,并选择何时在寄存器中包含什么。使用 -fverbose-asm 让它用 var 名称注释它的 asm。

标签: assembly x86-64


【解决方案1】:

这是我能想出的最简洁的转换来保持递归大小写:

// int power(int base=rdi, int exponent=rsi)
power:
    cmp $0, %esi                  # if (exponent==0)
    jnz exponentiate                # // 
    mov $1, %eax                    # tmp = 1
    ret                             # return tmp
  exponentiate:                  # else
    dec %esi                       # exponent--
    call power                     # int tmp = power(base, exponent)
    imul %edi, %eax                # tmp = tmp * base
    ret                            # return tmp

它最终看起来与您拥有的 C 代码比较接近。

【讨论】:

  • 哈,离我很近。您通过保持mov $1, %eax 有条件将一条指令保存在公共路径上,但我通过仅在一个地方使用ret 来保存一个字节:-)
  • @NateEldredge 哈哈,你和我的唯一区别是我花了五个小时才想出;)
  • 哦,我看到你已经在你的问题中提到了没有保存%edi的版本......我不是故意抄袭它,实际上我只是没有阅读你问题的那部分小心。但我现在已经编辑指出这确实是你的想法。
  • @NateEldredge 哦,不用担心,实际上我有一个版本,然后我使用你的建议从rxxexxcmp $0 %esi 这样的东西。再次感谢您的帮助!
  • @samuelbrody1249: What are good examples that actually motivate the study of recursion? (以及关于该问题的其他答案)有一些很好的例子,仅仅编写一个循环并不容易。在这个问题中,通过利用power 不修改 EDI,您已经完成了循环的一半。可以说它仍然是纯递归,只是使用保留 RDI 的调用约定,显然迭代编写会更容易。
【解决方案2】:

您想将递归调用的返回值乘以base 的值。那是%edi。出于某种原因,在您的原始版本中,您推送%rax,即使它只包含函数的(第一个)条目上的垃圾。 (mov $1, %eax 之后是跳转到cleanup,因此它不会在exponentiate 之前运行。)然后你会跳到%rbx,这是一个保留调用的寄存器;如果你想修改它,你必须保存它的值并在以后恢复它。

更自然的做法是在调用周围简单地推送和弹出%rdi,因为它包含您之后需要的值。但是,正如您最终注意到的那样,您实际上不需要修改 base,因此您不妨保持其完整,并将 %rdi 在整个函数中视为“只读”。那么你根本不需要使用堆栈,除了隐式的返回地址。 (在这里你可以击败编译器,我猜它必须假设 %rdi 被任何调用破坏,包括对其正在编译的函数的调用,即使它知道自己的寄存器使用情况......)

因此,您确实可以将exponentiate 部分写为:

exponentiate:
    dec %esi
    call power
    imul %edi, %eax

(您使用 32 位寄存器是正确的,因为在 C 示例中您将所有内容声明为 int。如果您希望它在 int64_t 上工作,请随意改回 64 位寄存器.)

其他说明:

  • 从不真正需要堆栈框架设置,尤其是当您根本不需要堆栈做任何事情时。特别是没有这样的规则,它是递归函数所必需的(如果你认为你会看到它不会做任何事情)。你可以跳过它。最糟糕的情况是调试器无法为您提供回溯。

  • 如果使用 32 位值,您希望将 %esi 与零进行比较,而不是 %rsi;它还保存了一个字节的 REX 前缀。您可以通过写test %esi, %esi 代替cmp $0, %esi 来节省另一个字节,因为您只需要查看零标志。

  • 你可以稍微简化一下跳跃

power:
    mov $1, %eax
    test %esi, %esi
    jz cleanup
    // former label exponentiate was here, no longer needed
    dec %esi
    call power
    imul %edi, %eax
cleanup:
    ret

如果您运行 exponentiate 部分,它只会覆盖 eax 中的 1,因此不会造成任何伤害。无条件运行一个非常便宜的mov 指令可能比额外跳转要好。

【讨论】:

  • 因为您只需要查看零标志。 - test reg,regall 标志设置为与 cmp $0, reg 相同,除了可能AF,半进位标志,您只能通过pushf 阅读。 Test whether a register is zero with CMP reg,0 vs OR reg,reg?。所以不管你想检查什么条件,你总是可以使用test来比较一个寄存器和零。
  • 利用power 保持RDI 未修改这一事实是将递归转换为迭代的一半。在某些方面这很好,因为递归对于这个函数是 100% 没有意义的,这使得一切都变得更加复杂,以实现零收益。 (与树遍历或 Ackermann 不同,它实际上比替代方案更自然且更容易,这将涉及手动堆栈数据结构 - What are good examples that actually motivate the study of recursion?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-03-18
  • 2019-03-15
  • 2023-04-05
  • 2016-01-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多