正如您所说,阻止它工作的错误是在递归调用后缺乏修复堆栈。但这不是唯一的错误
_multiply_fact
不是真正的功能。它只是_factorial
's 代码的一部分,它在您不采用提前n <= 1
返回路径时运行。因此,您不应该在其中制作堆栈框架。
pop ebp
顶部的完全_multiply_fact
是假的。它之所以有效,是因为运行时堆栈的顶部已经有调用者的ebp
. 如果你直接将它作为一个函数调用,你会ebp
用返回地址破坏调用者的地址。
首先,您根本不需要制作堆栈帧,因此完全浪费堆栈空间和指令。(虽然它确实有助于调试器进行回溯,因为没有它它们需要编译器通常添加的信息,但是除非您手动添加,否则手写的 asm 函数没有。)
您_factorial
还破坏了调用者的ebx
,这违反了 ABI。我认为您的函数实际上不会获得正确的值,因为它取决于在ebx
对_factorial
. 在所有递归调用返回之后,ebx=1
,因为每个嵌套调用都用它的 arg 破坏了 ebx。
但是,由于您是用 asm 编写的,因此您可以让递归自调用假设哪些寄存器没有被破坏,或者如果您愿意,甚至可以在 regs 中传递 args。不过,您仍然需要以n
某种方式保存递归调用。一种选择是利用您知道_factorial
不会在堆栈上破坏其 arg 的事实(即使 ABI 允许它)。
不过,您仍然需要可公开访问的包装函数来遵循 ABI。
显然,与循环相比,递归首先对于阶乘是浪费时间,但这是一个尽可能少的版本。
global _factorial
_factorial:
; one arg: int n
push ebp
mov ebp, esp ; make a stack frame
mov edx, [ebp + 8] ; load first arg into a scratch reg
dec edx
jg .multiply_fact ; if ((n-1) > 0). Note that dec doesn't set CF, so just use jnz instead of ja if you want to convert this to unsigned
;; base case
mov eax, 1 ;if (n <= 1) ret 1
pop ebp
ret
.multiply_fact: ; not a separate function, still part of _factorial
; in NASM, .labels are local labels that don't show up as symbols in the object file. MASM uses something different.
; at this point: edx = n-1
push edx
call _factorial ; eax = fac(n-1)
pop edx ; get n-1 back off the stack. Taking advantage of extra knowledge of our own internals, since the ABI allows functions to clobber their args on the stack
; add esp, 4 not needed, because we popped instead
inc edx ; undo the earlier dec before we pushed
imul eax, edx ; n * fac(n-1). 2-arg imul runs faster
pop ebp
ret