Assembly 如何使用循环终止递归函数?

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

我正在尝试编写一个程序,它使用循环作为递归过程,而不是使用条件,如何使用循环作为“中断”来防止调用无限运行

我知道循环将自动使用ecx作为计数器,当ecx为0时,循环将终止,然而,由于循环中的递归调用,我的程序似乎无限运行。我还尝试使用jmp指令并多次将循环定位到其他位置,但程序仍在无限期运行

.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