Recursion 在ASM中实现递归,无需过程

Recursion 在ASM中实现递归,无需过程,recursion,assembly,callstack,Recursion,Assembly,Callstack,我试图用一种类似ASM的简化语言实现函数和递归,这种语言没有过程。只有简单的jumpz、jump、push、pop、add和mul类型的命令 以下是命令: (所有变量和文字都是整数) set(设置现有变量的值或声明并初始化新变量),例如(set x 3) push(将值推送到堆栈上。可以是变量或整数),例如(push 3)或(push x) pop(将堆栈弹出到变量中)例如(pop x) 添加(将第二个参数添加到第一个参数),例如(添加x1)或(添加xy) mul(与加法相同,但用于乘法) 跳

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

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

  • set(设置现有变量的值或声明并初始化新变量),例如(set x 3)
  • push(将值推送到堆栈上。可以是变量或整数),例如(push 3)或(push x)
  • pop(将堆栈弹出到变量中)例如(pop x)
  • 添加(将第二个参数添加到第一个参数),例如(添加x1)或(添加xy)
  • mul(与加法相同,但用于乘法)
  • 跳转(跳转到特定的代码行),例如,(跳转3)将跳转到第3行或(跳转x)将跳转到#等于x值的行
  • jumpz(如果第二个参数等于零,则跳到行号),例如(jumpz 3 x)或(jumpz x)
变量“IP”是程序计数器,等于正在执行的当前代码行的行号

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

这适用于非递归函数

如何修改它来处理递归

我读过,用堆栈实现递归就是将参数和局部变量推到堆栈上(在这个较低级别的例子中,我认为也是指令指针)

我不能做像x=f(n)这样的事情。要做到这一点,我需要一些变量y(也在f的主体中使用),将y设置为n,调用f,将其“返回值”赋给y,然后将控制权跳回到调用f的位置,然后将x设置为y

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


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

下一个代码在“John Smith Assembly”中递归地将数字“base”提升为幂“exponent”:

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

 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确实提供了足够的工具来实现通常的过程调用/返回序列。您可以推送一个回信地址并作为
呼叫跳转,也可以弹出一个回信地址(跳转到临时位置)并作为
ret
间接跳转到该地址。我们可以只做
call
ret
宏。(除了在宏中生成正确的返回地址很棘手之外;我们可能需要一个标签(
push ret_addr
),或者类似于
set tmp,IP
/
add tmp,4
/
push tmp
/
跳转目标函数
)。简言之,这是可能的,我们应该用一些语法糖来包装它,这样我们在研究递归时就不会陷入这种困境

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


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

然后,您可以像在普通调用约定中一样实现递归,即调用方或被调用方使用
push
/
pop
在堆栈上保存/恢复寄存器,以便可以重用。函数返回值进入寄存器。函数参数应该放在寄存器中。另一个丑陋的选择是在返回地址之后推送它们(创建调用方会从堆栈调用约定中清除arg),因为您没有堆栈相对寻址模式来像x86那样(在堆栈上的返回地址之上)访问它们。或者,您可以像大多数RISC
call
指令(对于分支和链接,通常称为
bl
或类似的指令)一样,在a中传递返回地址,而不是像x86的
call
那样推送它。(因此,非叶调用方在进行另一次调用之前必须将传入的
lr
推送到堆栈上)


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

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
intfib(intn){

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

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

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

好的,我放了一个示例程序。是的,从第36行到第38行的块基本上是fB,但理想情况下,我希望能够做一些类似于递归因子的事情。所有的比较都必须使用jumpz命令来完成。现在研究一下:)这不是基本上是迭代吗?它做的更多的是“重复乘以2”,而不是“用2^(n-1)的结果乘以2”我想。我可能想得太远了。我在想,当你到达第一个2*f(n-1)时,你把2放在堆栈上..也许还有堆栈上的n-1..还有地址。但是我很难理解如何用这个pri让递归“递归”
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
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"
        };