我正在尝试以类似ASM的简化语言实现函数和递归,这种语言没有过程。只有简单的jumpz,jump,push,pop,add,mul type命令。
以下是命令:
(所有变量和文字都是整数)
变量'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
这似乎不适合递归。参数和中间值需要进入堆栈,我认为同一地址的堆栈上的多个实例将来自递归调用,这很好。
答案 0 :(得分:1)
下一个代码会提高数字" base"对权力"指数"递归地在约翰史密斯大会":
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.
答案 1 :(得分:1)
您的asm 提供了足够的工具来实现通常的过程调用/返回序列。您可以按下返回地址并跳转为call
,然后弹出一个返回地址(进入临时位置)并以ret
间接跳转到该地址。我们可以制作call
和ret
个宏。 (除非在宏中生成正确的返回地址很棘手;我们可能需要标签(push ret_addr
),或类似set tmp, IP
/ add tmp, 4
/ push tmp
/ {{ 1}})。简而言之,它是可能的,我们应该将它包含在一些语法糖中,这样我们就不会在查看递归的过程中陷入困境。
使用正确的语法糖,您可以在程序集中实现jump target_function
,它将为x86和您的玩具机器实际组装。
您正在考虑修改静态(全局)变量的函数。递归需要局部变量,因此每次对函数的嵌套调用都有自己的局部变量副本。您的机器具有(显然无限制的)命名静态变量(如Fibonacci(n)
和x
),而不是具有寄存器。如果要像MIPS或x86一样对其进行编程,并复制现有的调用约定,只需使用一些命名变量,如y
,eax
,...或ebx
。 。r0
寄存器架构使用寄存器的方式。
然后以与普通调用约定相同的方式实现递归,其中调用者或被调用者使用r31
/ push
来保存/恢复堆栈上的寄存器,以便可以重用它。函数返回值进入寄存器。函数args应该进入寄存器。一个丑陋的替代方案是在返回地址之后将它们推送到(创建一个调用者 - 清除-gsgs从堆栈调用约定),因为你没有堆栈 - 相对寻址模式以x86的方式访问它们(在堆栈上的返回地址之上)。或者您可以在link register中传递返回地址,就像大多数RISC pop
指令(通常称为call
或类似指令一样,用于分支和链接),而不是像x86&#39那样推送它; s bl
。 (因此,非叶子被调用者必须先将传入的call
推入堆栈,然后再拨打电话)
一个(愚蠢和缓慢)天真实现的递归Fibonacci可能会做类似的事情:
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
(Fib(n-1)
中的n-1
作为第一个参数)期间,edi
arg也会保存在堆栈中,以便稍后恢复。 因此每个函数的堆栈帧包含需要在递归调用中存活的状态和返回地址。这正是具有堆栈的机器上的递归。
何塞的例子也没有证明这一点,IMO,因为没有一个州需要在n-1
的号召之后存活下来。所以它最终会推送一个返回地址和args,然后弹出args,只建立一些返回地址。然后在最后,跟随返回地址链。它可以扩展为在每个嵌套调用中保存本地状态,但实际上并没有说明它。
我的实现与gcc如何为x86-64编译相同的C函数(使用edi中第一个arg的相同调用约定,eax中的ret值)略有不同。带有pow
的gcc6.1保持简单并实际执行两次递归调用,如您所见on the Godbolt compiler explorer。 (-O1
,特别是-O2
做了一些积极的转变)。 gcc会在整个功能中保存/恢复-O3
,并将rbx
保留在n
中,以便在ebx
通话后可以使用Fib(n-1)
。 (并Fib(n-1)
保持ebx
以便在第二次通话中幸存下来。 System V调用约定将rbx
指定为调用保留寄存器,但将rbi
指定为call-clobbered(并用于arg-passing)。
显然,你可以非递归地实现更快的Fib(n) ,具有O(n)时间复杂度和O(1)空间复杂度,而不是O(Fib(n))时间和空间(堆栈使用)复杂性。它是一个可怕的例子,但它是微不足道的。
答案 2 :(得分:0)
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
Peter的Fibonacci计算器的成功转录:
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"
};