Assembly 如何使用循环终止递归函数?
我正在尝试编写一个程序,它使用循环作为递归过程,而不是使用条件,如何使用循环作为“中断”来防止调用无限运行 我知道循环将自动使用ecx作为计数器,当ecx为0时,循环将终止,然而,由于循环中的递归调用,我的程序似乎无限运行。我还尝试使用jmp指令并多次将循环定位到其他位置,但程序仍在无限期运行Assembly 如何使用循环终止递归函数?,assembly,masm,irvine32,Assembly,Masm,Irvine32,我正在尝试编写一个程序,它使用循环作为递归过程,而不是使用条件,如何使用循环作为“中断”来防止调用无限运行 我知道循环将自动使用ecx作为计数器,当ecx为0时,循环将终止,然而,由于循环中的递归调用,我的程序似乎无限运行。我还尝试使用jmp指令并多次将循环定位到其他位置,但程序仍在无限期运行 .data count DWORD ? ;the counter for the # of times the loop ran userVal DWORD ? ;the # of time
.data
count DWORD ? ;the counter for the # of times the loop ran
userVal DWORD ? ;the # of times the loop will run according to the user
.code
main PROC
mov count, 0
call ReadDec ;read userinput for userVal, and stores it in ecx as counter
mov userVal, eax
mov ecx, userVal
mov eax,0
call recur ;call the recursion procedure
;call DumpRegs ;for showing registers
exit
main ENDP
recur PROC USES ecx eax ;the recursion Proc(objective: terminate the procedure with decrementing ecx
; so the recursion will run ecx # of times)
mov eax, count ;store the count (starts with 0)
call WriteInt ;prints the count in consol
inc count ;increment count everytime recursion is called
L2: ;the loop
call recur ; recursive call
loop L2
ret
recur ENDP
END main
预期的输出是10 0 1 2 3 4 5 6 7 8 9
(0
到9
是打印的,因为递归过程应该运行10次,10
是userVal),但是,我得到了10 0 1 2 3 4 5 6 8 9 10 12 14…
(无限运行)
。。。为递归过程编写一个使用循环
而不是使用条件的程序
我觉得这是一个有趣的挑战,因为它需要一些开箱思考。它可能没有实际用途,但仍然有一些概念证明的优点
循环
指令使用有符号8位位移编码,这意味着条件跳转可以向后跳转,向前跳转!在大多数(如果不是所有)情况下,loop
至今仍在使用,我们只会向后跳。将
循环
指令放在上面,看起来很不自然,但效果很好
下面的代码分两个阶段工作
- 准备阶段在堆栈上推送一组返回地址(递归调用)
- 生产阶段弹出所有这些返回地址并打印当前编号
inc count
放在call WriteInt
之前,将call WriteInt
公开,这样我就可以用jmp WriteInt
替换它了生产阶段开始时,
ECX
将为0。因此,我没有在内存中使用count变量,而是为此使用了ECX
寄存器。通过
jecxz Done
指令,可以防止代码进入无限循环并引发堆栈溢出
jmp Start
; ----------------------
Recur:
loop .a ; Jumps N-1 times
jmp .b ; Jumps 1 time
.a: call Recur
.b: mov eax, ecx
inc ecx
jmp WriteInt ; Returns N times
; ----------------------
Start:
call ReadDec ; -> EAX (valid input is assumed)
mov ecx, eax ; The unsigned number N is [0,4GB-1]
jecxz Done ; In case N == 0
call Recur
Done:
有趣的是,以正常方式使用loop
编写此代码同样容易,因此可以向后跳。然而,它需要在计数器上增加一个额外的增量,并且它会跳得更多(参见下面的比较)
下面是我如何比较这两种方法的。我删除了对WriteInt的调用,原因很明显
LOOP FORWARD LOOP BACKWARD
jmp Start jmp Start
; ---------------------- ; ----------------------
Recur: Recur:
loop .a jmp .b
jmp .b .a: call Recur
.a: call Recur mov eax, ecx
.b: mov eax, ecx inc ecx
inc ecx ret
ret .b: loop .a
ret
; ---------------------- ; ----------------------
Start: Start:
mov ecx, 25000 mov ecx, 25000
call Recur inc ecx
call Recur
左边的代码段执行时间为282微秒,右边的代码段执行时间为314微秒。这就慢了11%。ecx永远不会递减(先调用,然后循环)。您的代码一直运行到堆栈溢出。可能在调用之前减少它,或者不管您的意图是什么。我在循环之前添加了
dec ecx
,直到overflow@rAndom69:循环
指令本身递减ecx
,它相当于不修改标志的dec ecx/jnz
。但是recur
不会保存/还原ECX,因此它会破坏其父循环计数器。@然而,由于只有无条件递归,所以永远不会到达循环。ecx销毁可能是(也可能不是)真实的,因为我们没有看到WriteInt。也就是说,如果你想让它结束,你必须在递归之前终止循环。如果ecx是您的递归计数器,那么dec ecx,jz结束函数。无论如何,明显的终止条件是在继续之前检查count
,而不使用向下计数器。这两个值都可以保存在寄存器中,因为无论哪种方式,它都相当于一个迭代实现。为此使用递归是没有意义的,这样做不会教会您在调用中保存/恢复任何本地状态,因为不需要任何本地状态。您的迭代版本可能不应该被称为Recur
。还值得一提的是,对于可能需要运行0次的循环,使用loop
的“正常”方法是在循环之前放置一个jecxz
,以跳过它。您的反向jmp“正常循环”是人为地避免它,而是使用jmp
到循环条件,但为了使其工作,您在调用程序中增加ECX,以将0..2^32-1映射到1..0,并且出于某种原因,还过滤掉一个0的输入,我认为在将其映射到1后您可以正确处理该输入。在调用方中放入inc ecx
之前,您可能添加了该检查?有趣的是,在正常路径中包含额外的jmp
(不仅仅是循环
失败的基本情况)只会慢11%。我猜循环
在您的CPU上太慢了,以至于它(连同调用
/ret
)几乎隐藏了额外的jmp
成本!我猜这是英特尔,不是AMD;AMD因为推土机有快速的循环
@PeterCordes,你的猜测是对的。这是一个英特尔奔腾双核处理器T2080。@PeterCordes代码已更正。我误读了文档中的“ReadDec”,认为输入仅限于[0,2GB-1],并且(在第二段代码中)不可能将计数器包装为0。关于jecxz
(在两个代码段中):如果用户输入为0,则无需执行任何操作(任务描述)。这就是为什么我在早期过滤掉一个0的输入。当递归函数能够自己处理它的时候,把它转移给它的调用者似乎很奇怪。但是,为了提高效率,在可以运行多次迭代的实际部分之前,排除这种特殊情况是有意义的。(至少对于额外处理在热路径上的情况。)
LOOP FORWARD LOOP BACKWARD
jmp Start jmp Start
; ---------------------- ; ----------------------
Recur: Recur:
loop .a jmp .b
jmp .b .a: call Recur
.a: call Recur mov eax, ecx
.b: mov eax, ecx inc ecx
inc ecx ret
ret .b: loop .a
ret
; ---------------------- ; ----------------------
Start: Start:
mov ecx, 25000 mov ecx, 25000
call Recur inc ecx
call Recur