无程序在ASM中实现递归

Implement recursion in ASM without procedures

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

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

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

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

这适用于非递归函数。

如何修改它以处理递归?

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

我不能做像 x = f(n) 这样的事情。为此,我需要一些变量 y(也在 f 的主体中使用),将 y 设置为 n,调用 f 将其 "return value" 分配给 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

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

下一个代码在 "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.

您的 asm 确实 提供了足够的工具来实现通常的过程调用/return 序列。您可以压入一个 return 地址并作为 call 跳转,弹出一个 return 地址(进入临时位置)并作为 ret 间接跳转到它。我们可以只制作 callret 宏。 (除了在宏中生成正确的 return 地址很棘手;我们可能需要一个标签 (push ret_addr),或者类似 set tmp, IP / add tmp, 4 / push tmp / jump target_function)。简而言之,这是可能的,我们应该将其包裹在一些语法糖中,这样我们在研究递归时就不会陷入困境。

使用正确的语法糖,您可以在汇编中实现 Fibonacci(n),实际上 assemble 适用于 x86 和您的玩具机。


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

然后你像在普通调用约定中一样实现递归,其中调用者或被调用者使用 push / pop 到 save/restore 堆栈上的寄存器所以它可以重复使用。函数 return 值进入寄存器。函数参数应该放在寄存器中。一个丑陋的替代方案是将它们推送到 after return 地址(创建一个 caller-cleans-the-args-from-the-stack 调用约定),因为你不没有堆栈相关的寻址模式来像 x86 那样访问它们(在堆栈的 return 地址之上)。或者您可以像大多数 RISC call 指令一样在 link register 中传递 return 地址(对于分支与 link,通常称为 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 arg 也保存在堆栈中,成为后来恢复了。 因此每个函数的堆栈帧包含需要在递归调用中存活的状态,以及一个 return 地址。 这正是具有堆栈的机器上递归的全部内容。

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


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


显然你可以非递归地更快地实现 Fib(n) much,具有 O(n) 时间复杂度和 O(1) space 复杂度,而不是O(Fib(n)) 时间和 space(堆栈使用)复杂度。这是一个糟糕的例子,但它是微不足道的。

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"
        };