【问题标题】:Implement recursion in ASM without procedures在没有过程的 ASM 中实现递归
【发布时间】:2016-12-11 23:52:49
【问题描述】:

我正在尝试用一种类似 ASM 且没有过程的简化语言来实现函数和递归。只有简单的 jumpz、jump、push、pop、add、mul 类型的命令。

命令如下:
(所有变量和文字都是整数)

  • set(设置已存在变量的值或声明并初始化新变量)例如(设置 x 3)
  • push(将一个值推入堆栈。可以是变量或整数),例如(按 3)或(按 x)
  • pop(将堆栈弹出到变量中)例如(pop x)
  • add(将第二个参数添加到第一个参数)例如(加 x 1) 或 (加 x y)
  • mul(与 add 相同,但用于乘法)
  • 跳转(跳转到特定的代码行)例如(jump 3) 将跳转到第 3 行或 (jump x) 将跳转到等于 x 值的 # 行
  • jumpz(如果第二个参数等于 0,则跳转到行号)例如(jumpz 3 x) 或 (jumpz z x)

变量“IP”是程序计数器,等于当前正在执行的代码行的行号。

在这种语言中,函数是程序底部的代码块,通过从堆栈中弹出一个值并跳转到该值来终止。 (使用堆栈作为调用堆栈)然后可以在程序中的任何其他位置调用函数,只需将指令指针压入堆栈,然后跳转到函数的开头。

这适用于非递归函数。

如何修改它来处理递归?

我读过用堆栈实现递归是将参数和局部变量压入堆栈的问题(在这种较低级别的情况下,我认为也是指令指针)

我将无法执行 x = f(n) 之类的操作。为此,我需要一些变量 y(也用于 f 的主体),将 y 设置为 n,调用 f 将其“返回值”分配给 y,然后将控制跳转回调用 f 的位置,然后我们将 x 设置为等于 y。

(对定义从第 36 行开始的数字求平方的函数)

1 - set y 3
2 - set returnLine IP
3 - add returnLine 2
4 - push returnLine
5 - jump 36
6 - set x y
...
36 - mul y 2
37 - pop returnLine
38 - jump returnLine

这似乎不适合递归。参数和中间值需要放在堆栈上,我认为堆栈上相同地址的多个实例将由递归调用产生,这很好。

【问题讨论】:

  • 好的,我放一个示例程序
  • 是的,从第 36 行到第 38 行的块基本上是 f
  • 但理想情况下我希望能够做类似递归阶乘的事情
  • 比较都必须使用 jumpz 命令完成
  • 现在正在学习 :)

标签: recursion assembly callstack


【解决方案1】:

Margaret 的 pastebin 稍作修改,以便在我的该语言的解释器中运行:(无限循环问题,可能是由于我的转录错误)

set n 3
push n
set initialCallAddress IP
add initialCallAddress 4
push initialCallAddress
jump fact
set finalValue 0
pop finalValue
print finalValue
jump 100
:fact
set rip 0
pop rip
pop n
push rip
set temp n
add n -1
jumpz end n
push n
set link IP
add link 4
push link
jump fact
pop n
mul temp n
:end
pop rip
push temp
jump rip

成功转录彼得的斐波那契计算器:

        String[] x = new String[] {
            //n is our input, which term of the sequence we want to calculate
                "set n 5",
            //temp variable for use throughout the program
                "set temp 0",
            //call fib
                "set temp IP",
                "add temp 4",
                "push temp",
                "jump fib",
            //program is finished, prints return value and jumps to end
                "print returnValue",
                "jump end",
            //the fib function, which gets called recursively
                ":fib",
            //if this is the base case, then we assert that f(0) = f(1) = 1 and return from the call
                "jumple base n 1",
                "jump notBase",
                ":base",
                "set returnValue n",
                "pop temp",
                "jump temp",
                ":notBase",
            //we want to calculate f(n-1) and f(n-2)
            //this is where we calculate f(n-1)
                "add n -1",
                "push n",
                "set temp IP",
                "add temp 4",
                "push temp",
                "jump fib",
            //return from the call that calculated f(n-1)
                "pop n",
                "push returnValue",
            //now we calculate f(n-2)
                "add n -1",
                "set temp IP",
                "add temp 4",
                "push temp",
                "jump fib",
            //return from call that calculated f(n-2)
                "pop n",
                "add returnValue n",
            //this is where the fib function ultimately ends and returns to caller
                "pop temp",
                "jump temp",
            //end label
                ":end"
        };

【讨论】:

    【解决方案2】:

    您的 asm 确实提供了足够的设施来实现通常的过程调用/返回序列。您可以推送一个返回地址并作为call 跳转,然后弹出一个返回地址(到临时位置)并作为ret 间接跳转到它。我们可以只创建callret 宏。 (除了在宏中生成正确的返回地址很棘手;我们可能需要一个标签(push ret_addr),或类似set tmp, IP/add tmp, 4/push tmp/jump target_function)。简而言之,这是可能的,我们应该用一些语法糖把它包装起来,这样我们在查看递归时就不会陷入困境。

    使用正确的语法糖,您可以在汇编中实现Fibonacci(n),该汇编实际上将为 x86 和您的玩具机器进行汇编。


    您正在考虑修改静态(全局)变量的函数。递归需要局部变量,因此对函数的每个嵌套调用都有自己的局部变量副本。您的机器没有寄存器,而是(显然是无限的)命名静态变量(如xy)。如果您想像 MIPS 或 x86 一样对其进行编程,并复制现有的调用约定,只需使用一些命名变量,例如 eaxebx、... 或 r0 .. r31寄存器架构使用寄存器。

    然后,您可以像在正常调用约定中一样实现递归,其中调用者或被调用者使用push / pop 保存/恢复堆栈上的寄存器,以便可以重用。函数返回值进入寄存器。函数 args 应该放在寄存器中。一个丑陋的替代方法是将它们 返回地址之后推送(创建 caller-cleans-the-args-from-the-stack 调用约定),因为您没有堆栈相对寻址模式以 x86 的方式访问它们(在堆栈上的返回地址之上)。或者你可以在link register 中传递返回地址,就像大多数RISC call 指令(通常称为bl 或类似的,用于分支和链接),而不是像x86 的call 那样推送它。 (所以非叶子被调用者必须自己将传入的lr 推入堆栈,然后再进行另一个调用)


    一个(愚蠢而缓慢的)天真实现的递归 Fibonacci 可能会执行以下操作:

    int Fib(int n) {
        if(n<=1) return n;          // Fib(0) = 0;  Fib(1) = 1
        return Fib(n-1) + Fib(n-2);
    }
    
    ## valid implementation in your toy language *and* x86 (AMD64 System V calling convention)
    
    ### Convenience macros for the toy asm implementation
    # pretend that the call implementation has some way to make each return_address label unique so you can use it multiple times.
    # i.e. just pretend that pushing a return address and jumping is a solved problem, however you want to solve it.
    %define call(target)   push return_address; jump target; return_address:
    %define ret            pop rettmp; jump rettmp    # dedicate a whole variable just for ret, because we can
    # As the first thing in your program,  set eax, 0  / set ebx, 0 / ...
    
    global Fib
    Fib:
        # input: n in edi.
        # output: return value in eax
          # if (n<=1) return n;  // the asm implementation of this part isn't interesting or relevant.  We know it's possible with some adds and jumps, so just pseudocode / handwave it:
        ... set eax, edi and ret  if edi <= 1 ... # (not shown because not interesting)
        add     edi, -1
        push    edi        # save n-1 for use after the recursive call
        call    Fib        # eax = Fib(n-1)
        pop     edi        # restore edi to *our* n-1
        push    eax        # save the Fib(n-1) result across the call
        add     edi, -1
        call    Fib        # eax = Fib(n-2)
        pop     edi        # use edi as a scratch register to hold Fib(n-1) that we saved earlier
        add     eax, edi   # eax = return value = Fib(n-1) + Fib(n-2)
        ret
    

    在递归调用Fib(n-1) 期间(edi 中的n-1 作为第一个参数),n-1 参数也保存在堆栈中,以便稍后恢复。 所以每个函数的堆栈帧都包含递归调用所需的状态和返回地址。这正是具有堆栈的机器上递归的全部内容。

    Jose 的示例也没有证明这一点,IMO,因为没有任何状态需要在pow 的调用中幸存下来。所以它最终只是推送一个返回地址和 args,然后弹出 args,只建立一些返回地址。然后在最后,跟随返回地址链。它可以扩展为在每个嵌套调用中保存本地状态,实际上并没有说明它。


    我的实现与 gcc 为 x86-64 编译相同的 C 函数的方式有点不同(在 edi 中使用相同的第一个 arg 调用约定,在 eax 中使用 ret 值)。带有-O1 的gcc6.1 保持简单,实际上进行了两次递归调用,如您所见on the Godbolt compiler explorer。 (-O2 尤其是-O3 做了一些激进的转换)。 gcc 在整个函数中保存/恢复rbx,并将n 保留在ebx 中,以便在Fib(n-1) 调用后可用。 (并将Fib(n-1) 保留在ebx 中以在第二次通话中存活)。 System V 调用约定将 rbx 指定为调用保留寄存器,但将 rbi 指定为调用破坏(并用于传递参数)。


    显然,您可以以 O(n) 时间复杂度和 O(1) 空间复杂度,而不是 O(Fib(n)) 时间和空间(堆栈使用)复杂度。这是一个糟糕的例子,但它是微不足道的。

    【讨论】:

    • “一个丑陋的选择”,你伤害了我的感情 :)
    • @JoseManuelAbarcaRodríguez:直到我完成我的回答之后,我才真正理解了你的代码。我看着它,但它似乎过于复杂。直到后来我才意识到这就是你在做什么。我认为堆栈参数对于具有无限命名寄存器的机器来说是一个错误,只要它不能是多线程的。它有效,但我发现更难说服自己它是真正的递归,而不仅仅是一个溢出/重新加载的循环。您的函数会从堆栈中吃掉 args,只留下返回地址链,因此每个堆栈帧都没有任何已保存的局部变量。
    • 我认为像“call Fib”这样的事情超出了我的语言可以轻松做到的范围
    • @JohnSmith:阅读宏定义几行。这只是 push IP / jump Fib 的简写,以保持代码可读性。 ret 也一样:它会临时弹出并进行间接跳转。
    • 哦,我明白了。我正在逐步理解它以尝试理解
    【解决方案3】:

    接下来的代码在“John Smith Assembly”中递归地将数字“base”提升到“exponent”的幂:

    1 - set base 2            ;RAISE 2 TO ...
    2 - set exponent 4        ;... EXPONENT 4 (2^4=16).
    3 - set result 1          ;MUST BE 1 IN ORDER TO MULTIPLY.
    4 - set returnLine IP     ;IP = 4.
    5 - add returnLine 4      ;RETURNLINE = 4+4.
    6 - push returnLine       ;PUSH 8.
    7 - jump 36               ;CALL FUNCTION.
    .
    .
    .
    ;POWER FUNCTION.
    36 - jumpz 43 exponent    ;FINISH IF EXPONENT IS ZERO.
    
    37 - mul result base      ;RESULT = ( RESULT * BASE ).
    38 - add exponent -1      ;RECURSIVE CONTROL VARIABLE.
    39 - set returnLine IP    ;IP = 39.
    40 - add returnLine 4     ;RETURN LINE = 39+4.
    41 - push returnLine      ;PUSH 43.
    42 - jump 36              ;RECURSIVE CALL.
    
    43 - pop returnLine
    44 - jump returnLine
    ;POWER END.
    

    为了测试它,让我们手动运行它:

     LINE | BASE EXPONENT RESULT RETURNLINE STACK
    ------|---------------------------------------
       1  |   2
       2  |         4
       3  |                  1
       4  |                           4
       5  |                           8
       6  |                                   8
       7  |
      36  |
      37  |                  2
      38  |         3
      39  |                          39
      40  |                          43
      41  |                                  43(1)
      42  |
      36  |
      37  |                  4
      38  |         2
      39  |                          39
      40  |                          43
      41  |                                  43(2)
      42  |
      36  |
      37  |                  8
      38  |         1
      39  |                         39
      40  |                         43
      41  |                                  43(3)
      42  |
      36  |
      37  |                 16
      38  |         0
      39  |                         39
      40  |                         43
      41  |                                  43(4)
      42  |
      36  |
      43  |                         43(4)
      44  |
      43  |                         43(3)
      44  |
      43  |                         43(2)
      44  |
      43  |                         43(1)
      44  |
      43  |                          8
      44  |
       8  |
    

    编辑:函数参数现在在堆栈上(没有手动运行):

    1 - set base 2            ;RAISE 2 TO ...
    2 - set exponent 4        ;... EXPONENT 4 (2^4=16).
    3 - set result 1          ;MUST BE 1 IN ORDER TO MULTIPLY.
    4 - set returnLine IP     ;IP = 4.
    5 - add returnLine 7      ;RETURNLINE = 4+7.
    6 - push returnLine       ;PUSH 11.
    7 - push base             ;FIRST PARAMETER.
    8 - push result           ;SECOND PARAMETER.
    9 - push exponent         ;THIRD PARAMETER.
    10 - jump 36              ;FUNCTION CALL.
    ...
    ;POWER FUNCTION.
    36 - pop exponent         ;THIRD PARAMETER.
    37 - pop result           ;SECOND PARAMETER.
    38 - pop base             ;FIRST PARAMETER.
    39 - jumpz 49 exponent    ;FINISH IF EXPONENT IS ZERO.
    
    40 - mul result base      ;RESULT = ( RESULT * BASE ).
    41 - add exponent -1      ;RECURSIVE CONTROL VARIABLE.
    42 - set returnLine IP    ;IP = 42.
    43 - add returnLine 7     ;RETURN LINE = 42+7.
    44 - push returnLine      ;PUSH 49.
    45 - push base
    46 - push result
    47 - push exponent
    48 - jump 36              ;RECURSIVE CALL.
    
    
    49 - pop returnLine
    50 - jump returnLine
    ;POWER END.
    

    【讨论】:

    • 这不是基本迭代吗?我认为它所做的更多的是“重复乘以二”而不是“将 2 乘以 2 ^(n-1)的结果”。不过我可能会走得很远
    • 我在想当你到达第一个 2 * f(n-1) 时,你把 2 放在堆栈上.. 可能还有 n-1 在堆栈上..以及地址。但是我很难理解如何使用这种原始语言使递归“递归”
    • 我想这可能就是我在这里寻找的东西
    • @JohnSmith 你的意思是像this这样的东西吗?
    • 谢谢。我一边走一边编造了这种语言。我现在了解您的解决方案。这绝对是我正在寻找的 100%。我刚刚用 C# 为该语言编写了一个解释器,并将您的代码放入其中,然后弹出了一个不错的 16。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-28
    相关资源
    最近更新 更多