您的asm
做
提供足够的设施来实现通常的过程调用/返回序列
。您可以按回信地址并跳转为
call
,并弹出一个返回地址(到暂存位置),并作为
ret
.我们可以
呼叫
和
ret(雷特)
宏。(除了在宏中生成正确的返回地址是很棘手的;我们可能需要一个标签(
push ret_addr
),或者类似的东西
set tmp, IP
/
add tmp, 4
/
push tmp
/
jump target_function
). 简而言之,这是可能的,我们应该用一些语法糖把它包装起来,这样我们在研究递归时就不会陷入这种困境。
使用正确的语法糖,您可以实现
Fibonacci(n)
您考虑的是修改静态(全局)变量的函数。递归需要局部变量,因此对函数的每个嵌套调用都有自己的局部变量副本。您的机器没有使用寄存器,而是使用(显然是无限的)命名的静态变量(例如
x
和
y
). 如果您想像MIPS或x86那样编程,并复制现有的调用约定,
只需使用一些命名变量,如
eax
,
ebx
或
r0
r31
寄存器体系结构使用寄存器的方式。
然后,以与普通调用约定相同的方式实现递归,其中调用方或被调用方使用
push
/
pop
在堆栈上保存/恢复寄存器,以便可以重用。函数返回值进入寄存器。函数参数应放入寄存器中。另一个糟糕的选择就是推他们
之后
返回地址(创建调用者将清除堆栈调用约定中的参数),因为您没有堆栈相对寻址模式来像x86那样访问它们(位于堆栈返回地址之上)。或者您可以在
link register
像大多数RISC一样
呼叫
指令(通常称为
bl
或类似的分支和链接),而不是像x86那样推送
呼叫
.(因此非叶被叫方必须推送传入
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)
(使用
n-1
在里面
edi
n-1个
因此,每个函数的堆栈框架都包含递归调用所需的状态和返回地址。
这正是递归在有堆栈的机器上的意义所在。
国际海事组织,何塞的例子也不能证明这一点,因为没有一个国家需要在呼吁
pow
因此,它只会推送一个返回地址和参数,然后弹出参数,只建立一些返回地址。然后在最后,遵循返回地址链。它可以扩展为在每个嵌套调用中保存本地状态,但实际上并没有说明这一点。
我的实现与gcc为x86-64编译同一个C函数的方式略有不同(使用edi中的第一个参数和eax中的ret值的相同调用约定)。gcc6.1与
-O1
保持简单,并实际执行两个递归调用,如您所见
on the Godbolt compiler explorer
. (
-O2
-O3
进行一些积极的转变)。gcc保存/恢复
rbx
跨整个功能,并保持
n
在里面
电子束x
所以在
纤维(n-1)
呼叫(并保持
纤维(n-1)
在里面
电子束x
在第二次通话中幸存下来)。System V调用约定指定
重组合异种骨
rbi
as调用被阻塞(并用于传递参数)。
显然,您可以实现Fib(n)
很
更快的非递归,具有O(n)时间复杂性和O(1)空间复杂性,而不是O(Fib(n))时间和空间(堆栈使用)复杂性。这是一个可怕的例子,但它是微不足道的。