【问题标题】:MASM is it possible to create a recursive function without conditional statement?MASM 是否可以在没有条件语句的情况下创建递归函数?
【发布时间】:2018-04-12 04:02:45
【问题描述】:

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

问题来了:

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

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

仅使用 LOOP 指令,不使用其他条件 声明。

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

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

【问题讨论】:

  • cmov?你能用吗?
  • @SeverinPappadeux 教授没有明确说我们不能,但我在我们的书中没有看到,所以我会说不。
  • 我们这里似乎有一些矛盾的要求。 ECX 是否应该保持 recProc supposed 运行的次数?还是它确实运行的数字?我想理论上它可以同时用于两者,但这似乎很愚蠢。此外,loop 条件语句。图片loop notdone.
  • 提示:回想一下 LOOP 递减 ECX。如果 ECX 等于 0,则控制进入下一条语句,否则进入标签参数。

标签: assembly x86 conditional-statements masm


【解决方案1】:

loop is exactly like dec ecx / jnz(除了不修改状态标志、地址大小前缀的怪异和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

如果您的递归终止条件不是递减计数器,则很容易滥用loop 将其用作通用if (n != 0) 而无需修改n

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

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

这是一个糟糕的调用约定,会在您调用的每个函数上施加大量工作。允许函数破坏标志和 eax/ecx/edx 是完全正常的,因此它们可以使用一些寄存器而无需保存/恢复。

POPAD 使用起来真的很不方便,因为它会覆盖所有寄存器,包括您想要放置返回值的 EAX。因此,您要么必须在 POPAD 周围保存/恢复 EAX,要么必须将结果存储到堆栈上的正确位置,以便 POPAD 将其加载到 EAX。

此外,不使用pushad/popad 也可以不修改寄存器和FLAGS,因此该语句是错误的。除非您将其视为单独的要求,否则也会像 loop 要求那样给您带来不便。


MASM可以不用条件语句创建递归函数吗?

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

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

但是,如果您只是想出一些愚蠢的计算机技巧,例如滥用 loop,那么您还应该考虑自我修改代码,或指针查找表,或其他影响流控制的方法,而无需标准条件指令喜欢jcc

【讨论】:

  • inc ecx \ loop 在 32 位模式下是 3 个字节,inc cx \ loop 在 16 位模式下是 3 个字节。 64 位模式缺少单字节递增指令,因此 inc ecx \ loop (2 + 2) 是 A. 与 test \ jnz (2 + 2) 一样大,B. 你需要 @987654355 @ (3) 或 test rcx, rcx (也是 3) 因为 loop 默认为 a64。或inc ecx \ a32 loop (2 + 3)。即在 64 位模式下,loop 替代方案从不保存任何字节(只是不同的状态标志)。
  • @ecm: 哦,笨蛋,在查看您的编辑时没有充分考虑该语句的上下文,只是在我的脑海中,test ecx,ecx 在两者中的大小相同在 32 位和 64 位模式下,loop 在所有模式下都比dec ecx/jnz 节省了字节数,这两者都不是重点。谢谢,已修复。
  • 我忘记了,a32 loop 在 64 位模式下是否会清除 rcx 的上半部分?这将在inc ecx \ a32 loop 或使用o32 test ecx, ecx 之间产生进一步的差异。
  • @ecm:是的,每次写入 32 位寄存器都会零扩展至完整的 64 位寄存器。 (我没有仔细检查这个事实,但我很确定这是真的。)
  • 哎呀,o32 inc 已经为零扩展到 rcx 无论如何。所以我的问题在这里无关紧要。
【解决方案2】:

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

考虑到任务描述和最小的努力,我可能会产生这个(没有调试它,因为我没有 MASM 或任何能够运行 MASM 的操作系统,所以自己修复语法问题,但原则应该是从 cmets 中清除)

; 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,..,original ecx


编辑:

关于主题中的一般问题“是否可以在没有条件语句的情况下创建递归函数” ...

好吧,递归函数必须调用自己,否则它就不是递归函数,所以 1) 函数体中的某处是自调用。

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

1+2 => 无限循环。

您可以争论什么是条件语句,例如,您可以创建内部函数跳转,将目标地址计算为数学表达式,有时跳转到包含自调用的代码路径,有时跳转到不包含调用的代码路径,使得特定条件下的递归终端,但没有使用显式 Jccloop 条件分支,但对我来说,即使这也符合“条件”语句。

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

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

【讨论】:

  • 我的第一个想法是在没有任何条件分支的情况下结束递归是自修改代码:P 请参阅 The Story of Mel 以获取经典示例。
  • @PeterCordes 是的,但这对我来说在数学意义上还不够纯粹。但是你有一些观点,因为如何称它为“有条件的”并不明显。我宁愿将它标记为无效,因为它正在调用不同的函数(不是自调用),所以它不是递归。 :) ;)
  • 当然,要使用它,您需要覆盖函数中的一条指令,使其返回而不是调用自身。而不是调用不同的函数。顺便说一句,loop 可以向前分支;你把这个复杂化了。 8 位位移是标准的rel8,而不是+0..+255。
  • @PeterCordes 我不明白过度复杂化.. 你的变体有两个 ret 相反的路径?我相信两者都具有非常相似的复杂性,并且哪个更简单可能纯粹是个人喜好,因为我有额外的入口点进入“正常有序的 do{}while”循环比中间返回更容易阅读,如我的代码我经常有一些堆栈/寄存器恢复和多返回路径对我来说有点乏味。除非我遗漏了一些明显的东西,否则我认为这与个人喜好有关,很高兴您也提供了其他选项。 :)
  • 在我接受 OP 分配使用pushad / pushf 的所有愚蠢要求之前,我认为在我真正写下我的答案之前,我认为你的过于复杂了。就个人喜好而言:不,这是在尾部复制成本额外代码大小与在通过函数的执行路径上有额外 jmp 之间的选择。我更喜欢针对较少执行的指令进行优化,但您可能会出于客观原因选择一个而不是另一个。
【解决方案3】:

这是我的挑战代码

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

感谢收看

【讨论】:

  • 请翻译所有个非英语的cmets和标识符(在代码中)。本网站仅假设读者会说英语。
猜你喜欢
  • 1970-01-01
  • 2016-11-01
  • 2020-05-15
  • 2014-06-16
  • 1970-01-01
  • 2021-04-07
  • 2012-11-13
  • 2019-04-06
  • 1970-01-01
相关资源
最近更新 更多