【问题标题】:Translating recursivity to assembly将递归性转化为汇编
【发布时间】:2017-05-25 14:08:41
【问题描述】:

我正在尝试在汇编中翻译 C++ 递归代码。我在 32 位模式下工作。

这是 C++ 代码。

1 extern "C" void output (unsigned);
2 extern "C" void parcours (unsigned[], unsigned, unsigned = 0);
3
4 void parcours (unsigned v[], unsigned n, unsigned k) {
5     while (k < n) {
6         output(v[k]);
7         parcours(v, n, 2*k+1);
8         k = 2*k+2;
9     }
10 }

这是我翻译它的尝试。我正在努力将递归 C++(或任何其他高级语言)翻译成汇编。如果您能更正此代码并指出我的错误,我将不胜感激。

%include "io.inc"

section .text
global start

start:    
    mov edi, [ebp+8]    ;address of the first element of the array
    mov eax, [ebp+12]   ;n
    mov edx, [ebp+16]   ;k

parcours:
    enter 0,0           ;stack frame creation
    push edx            ;we save k before the modification

    shl edx,1
    add edx,1 ;2*k+1


loop:
    cmp edx,eax
    jb endloop          ;if k < n

    push [edi+edx]      ;we push for the output
    call output
    pop ebx

    call parcours
    shl edx,1           ;2*k
    add edx,2           ;2*k+2
    jmp loop

endloop:
    pop edx
    leave               ;stack frame destruction
    ret

【问题讨论】:

  • 将非递归和递归 C++ 转换为程序集没有区别,从 CPU 的角度来看,实际上很难在每个可能的源中可靠地检测递归性,也不会影响生成的程序集以任何方式。编译器将按照配置的调用约定将其编译为任何其他函数。您可以使用 C++ 编译器来检查编译器会做什么(但设置合理的优化量以获得一些不错的程序集可能会很棘手)。您当前的程序集可能会使寄存器中的值杂乱无章,应该保留这些值。
  • 实际上,您不会将ebx 用于任何事情,只是作为草稿,但如果我猜对了您的调用约定,您应该保存它(或避免完全弄乱它)。而eax, edx 可能无法生存call output。否则,该代码对我来说看起来相当不错,请在调试器中尝试以查看自己,如果它按预期工作(使用一些仅执行无递归的参数,然后 depth=1,然后 depth=2+)。
  • 只需遵循编译器调用约定,它就可以工作,没有理由设计递归与非递归,它们可以是相同的代码。
  • 重点是我想锻炼自己并正确理解它是如何工作的。 @Ped7g 我会试试你说的。
  • enter 0,0 - 好的,但我建议push ebp \ mov ebp, esp(如果编译器使用帧指针,你也会看到编译器这样做)

标签: assembly x86 nasm


【解决方案1】:

碰巧有专门设计用于将 C++ 代码转换为汇编的工具。此类工具被称为“编译器”,这实际上是它们的主要用途。

您所做的就是通过编译器运行 C++ 代码,并使用适当的标志来为您提供中间汇编语言表示。在 GCC 和 Clang 等 Gnu 风格的编译器中,这将是 -S-fverbose-asm 也很有帮助)。对于 Microsoft 编译器,选项是 /FA

或者,您可以编译代码,然后使用objdump (Gnu) 或dumpbin (MS) 之类的工具来显示目标代码的汇编列表。 (如果您使用调试符号进行编译,这具有在汇编指令中交错相应 C++ 语句的良好效果,至少在编译器的能力范围内,因为不一定存在 1 对 1 映射,更不用说为优化目的而重新排序指令的问题)。

两者都具有相同的效果。你转这个:

 extern "C" void output (unsigned);
 extern "C" void parcours (unsigned[], unsigned, unsigned = 0);

 void parcours (unsigned v[], unsigned n, unsigned k) {
     while (k < n) {
         output(v[k]);
         parcours(v, n, 2*k+1);
         k = 2*k+2;
     }
 }

进入这个:

parcours:
        push    edi
        push    esi
        push    ebx
        mov     esi, DWORD PTR [esp+20]
        mov     ebx, DWORD PTR [esp+24]
        mov     edi, DWORD PTR [esp+16]
        cmp     ebx, esi
        jnb     .L1
.L3:
        sub     esp, 12
        push    DWORD PTR [edi+ebx*4]
        add     ebx, ebx
        call    output
        lea     eax, [ebx+1]
        add     esp, 12
        add     ebx, 2
        push    eax
        push    esi
        push    edi
        call    parcours
        add     esp, 16
        cmp     esi, ebx
        ja      .L3
.L1:
        pop     ebx
        pop     esi
        pop     edi
        ret

这是优化编译器为您的parcours 函数生成的汇编语言代码。从这里开始,您需要做两件事:

  1. 浏览代码并确保您了解每条指令的作用、编译器发出指令的原因以及函数作为一个整体是如何工作的。跟踪代码,就像您是一台执行它的计算机一样,确保您理解它。

  2. 围绕它编写一个包装器,其中包含汇编程序通常需要的东西,例如section .text 和一个入口点。

请注意,递归只是通过从parcours 函数内再次调用parcours 函数来实现的。这里 C++ 和汇编的唯一区别是 C++ 使用parcours(v, n, 2*k+1);,而汇编使用一系列push 指令(用于参数),然后是call parcours

与您编写的代码相比,这段代码有什么不同?

首先,正如 cmets 中所讨论的,它遵循标准的调用约定,其中非易失性寄存器保存在顶部(带有一系列 push 指令)并恢复在底部(带有相应的一系列pop 指令)。参数以标准方式在堆栈上从右到左传递。

此外,优化编译器(不足为奇)在生成最佳代码序列方面比您更好。特别是,他们知道使用enterleave 来创建/销毁堆栈帧很慢,因此他们直接操作堆栈指针(esp),并且仅在需要时/在需要时才这样做。再举一个例子,他们知道shl reg, 1 等价于add reg, reg,但后者更快。他们知道一些可爱的小技巧,例如 lea eax, [ebx+1] 是一种将 ebx 的值加 1 的有效方法,同时将结果存储在 eax 中,而无需修改 ebx

不过,最重要的是,它们具有簿记算法,可以在执行递归函数调用时保持堆栈正确平衡,而您在尝试中无法正确执行此操作,这就是导致分段错误的原因。

我会让您对您的尝试与 C++ 编译器生成的内容进行详细的逐行比较。这是学习如何编写汇编代码的一种非常有效的方法。就像使用调试器单步执行您的代码一样,以准确查看它偏离轨道的位置。现在尝试还为时不晚,并将结果与​​正确的代码进行比较。

【讨论】:

  • 我不是很明白为什么我们减去 esp 的 12,然后我们在调用之后加上 16。
  • push32 位寄存器相当于从堆栈指针中减去 4 个字节。因此,当您有sub esp, 12 后跟push DWORD … 时,您必须通过adding 16 字节到esp 来清理堆栈。我的意思是,你可以完成pop reg+add esp, 12,但这样做的缺点是(1)破坏寄存器和(2)指令比必要的多,所以编译器只做一个add esp, 16。 @carlhou
猜你喜欢
  • 2013-08-03
  • 1970-01-01
  • 2016-02-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多