MASM 是否可以创建没有条件语句的递归函数?

MASM is it possible to create a recursive function without conditional statement?

对于我的汇编程序中的作业 class,我们得到了以下问题。我尝试了几种不同的方法来解决它,但我只能想到使用条件语句的解决方案。我没有在网上或 class 中从其他人那里找到答案。来自 c++/javascript 风格的语言,我知道必须有一个基本语句(否则函数可能永远调用自己)。那不需要条件语句吗?

问题如下:

**直接递归是过程调用自身时使用的术语。您不希望让过程永远调用自己,因为运行时堆栈会填满。您需要以某种方式限制递归。

编写调用递归过程的 MASM 程序。标记过程 recProc。在此过程中,将计数器加 1,以便您可以验证它的执行次数。 运行 您的程序带有调试器,并在程序结束时检查计数器的值。在 ECX 中放置一个数字,指定您希望允许递归继续的次数。

Use only the LOOP instruction and no other other conditional statements.

找到一种递归过程调用自身固定次数的方法。

使用过程时,需要确保保留寄存器和标志。您需要使用 PUSHFD/POPFD 和 PUSHAD/POPAD。**

loop 本身就是条件分支的东西,整个任务描述有点......嗯......符合我的口味。嗯...

考虑到任务描述和最小的工作量,我可能会生成这个(没有调试它,因为我没有 MASM 或任何 OS 能够 运行ning MASM,所以自己修复语法问题,不过原理应该看评论就清楚了)

; in data section
counter DWORD 0

; in code section

; recursive function to call itself recursively ECX-1 many times
; ECX should be above zero (for zero it will end with 4 billions
; of recursive depth exhausting stack memory quickly)
recProc PROC
    ; preserve flags and all register values
    pushfd
    push   ecx      ; only ECX is modified in procedure body
    ; (no need for weapons of mass destruction like PUSHAD)

    ; don't call recursively with the same ECX, decrement it first
    jmp    recProc_test_if_call_is_needed

    ; main loop calling recProc ECX-1 many times
recProc_loop:
    call   recProc
recProc_test_if_call_is_needed:
    ; count every iteration, even ones not leading to recursion
    inc    DWORD PTR [counter]
    loop   recProc_loop

    ; restore all registers and flags
    pop    ecx
    popfd
    ret
ENDP

如果我在脑海中正确调试,这应该会在 [counter] == 6 中产生 ECX=3 参数值。 (而且总体[counter] == ∑i, i=1,..,原ecx


编辑:

关于主题 "is it possible to create a recursive function without conditional statement" 中的一般问题 ...

嗯,递归函数要调用自己,否则就不是递归函数,所以1)函数体某处是自调用。

2) 如果没有影响代码流的条件语句,该函数将始终运行以相同的方式(控制流明智),即进行自我调用。

1+2 => 无限循环。

你可以争论什么是条件语句,例如你可以创建内部函数跳转,将目标地址计算为数学表达式,有时跳转到包含自调用的代码路径,有时跳转到不包含调用的代码路径,使递归终端在某些条件下,但不使用显式 Jccloop 条件分支,但对我来说,即使是 "conditional" 语句。

所以简单的回答是:不,这是不可能的。

但是在你的任务中这是可能的,因为你得到了 loop 指导。顺便说一句,不要在 "production" 代码中使用 loop 指令:Why is the loop instruction slow? ... pushad/popad 也是如此(这也是一个可怕的想法——尤其是在递归函数中,因为这意味着深度递归的数量将受到用于保存不改变的寄存器的浪费堆栈 space 的严重限制,但即使在非递归调用中,在 99% 的情况下(性能方面)更好的是只保存特定的寄存器,而不是使用 pushad/popad),并在调用之间保留 flags 也很荒谬。这就是为什么我找到你的任务描述 "meh",因为它实际上是在强迫你编写愚蠢的代码。 :/

(except for not modifying the status flags, weirdness with the address-size prefix, and being much slower on most CPUs).

它的分支位移范围是标准的rel8 [-128 .. +127],所以没有要求它用于后向分支

因此,您可以将它用作条件语句而无需跳过任何环节,只需使用它来跳过递归基本情况的代码(可能只是一个 ret):

; first arg in ECX = recursion count
my_func:
  loop  @keep_descending
  ;; fall-through path: ecx was 1 before loop, and is now 0
  ... do something here ...
  ret

@keep_descending:
  ; ecx has been decremented by 1
  push   ebx           ; save a call-preserved reg; use it to save state across the recursive call

  ... main body of function here ...

  call   my_func       ; clobbers ecx; save it if needed

  ... more stuff
  ; If your function way was tail-recursive, you should have just made a loop
  
  lea    eax, [edx+ecx]    ; return in EAX
  pop    ebx
  ret

不修改n就很容易滥用loop作为通用if (n != 0),如果你的递归终止条件是什么除了递减计数器。

  inc   ecx               ; 1 byte in 32-bit mode
  loop  ecx_was_non_zero
  ;; fall-through path: ecx was zero before inc, and is now zero again

这比 test ecx, ecx / jnz(在 32 位模式下)短 1 个字节,因此对 a code-golf GCD loop 很有用,其中唯一重要的是代码大小,而不是性能。 (CX 的等效技巧适用于 16 位模式,但 64 位模式没有 1 字节 inc。)

When using procedures, you need to ensure that registers and flags are preserved. You need to use PUSHFD/POPFD and PUSHAD/POPAD.

这是一个蹩脚的调用约定,它对您调用的每个函数都强加了很多工作。允许函数破坏标志和 eax/ecx/edx 是完全正常的,因此它们可以在没有 saving/restoring.

的情况下使用一些寄存器

POPAD用起来真的很不方便,因为它覆盖了所有的寄存器,包括你想放一个return值的EAX。因此,您要么必须 save/restore POPAD 周围的 EAX,要么必须将结果存储到堆栈中正确的位置,以便 POPAD 将其加载到 EAX。

此外,可以在不使用 pushad/popad 的情况下保持寄存器和 FLAGS 不变,因此该陈述是错误的。除非你把它作为一个单独的要求,否则也会像 loop 要求那样给你带来不便。


MASM is it possible to create a recursive function without conditional statement?

这是一个不同的问题(因为 loop 是一个条件分支),但答案仍然是肯定的。您的选项包括自修改代码。 (有关示例,请参见 The Story of Mel。)在您的情况下,您可能会在函数的开头将 call 指令覆盖为 nop 或其他内容。

您可以使用 cmov 无分支地执行此操作,以获取指向该指令或寄存器中某个无害位置的指针。或者您可能会调用 cmov 条件指令。它不是分支,但名称中有条件。无论如何,自修改代码对现代 CPU 的性能来说是可怕的,所以你也不应该使用它。

但是如果你只是想出一些愚蠢的计算机技巧,比如滥用 loop,那么你还应该考虑自修改代码,或者查找 table 指针,或者其他方法在没有像 jcc.

这样的标准条件指令的情况下影响流量控制

这是我的挑战代码

include Irvine32.inc
.386
.model flat, stdcall
.stack 4096
ExitProcess Proto, dwExitCode:dword
.data
    sum dword ?
.code
main proc
    mov ecx, 5                         ; counter loop in recursive
    mov eax, 0                          ; sum =0
    call recursive
    mov sum , eax
    mov eax, sum
    call WriteDec                       ; if you want display to console window: use Lib or write code
invoke ExitProcess, 0
main endp
; Recusive
; receive : ECX : counter
; eax : accumulator
recursive proc 
    loop l1                     ;loop first , so it alway used, to dec ECX, 
    ret                         ; when ecx=0, no loop, ret is used... back to main
    l1:
        add eax, 1          ;add 1 to accumulator
        call recursive
        ret
recursive endp
end main

感谢收看