【问题标题】:Recursive fibonacci Assembly递归斐波那契大会
【发布时间】:2018-04-10 15:25:55
【问题描述】:

今天我在汇编中写了一个递归斐波那契,它不起作用。我使用 NASM 将其编译为目标文件,然后使用 gcc 使其成为精灵。
当我输入 1 或 2 时,该功能可以正常工作,但是当我输入 3、4、5、6 或更多时,该功能不起作用。 我认为函数调用自身存在问题。

这是代码:

SECTION .data ;init data




str: db "This equal: %d",10,0

SECTION .text   ;asm code


extern printf
global main

main:
push ebp
mov ebp,esp
;--------------------


push 03  ; the index 
call _fibonacci
add esp,4

push DWORD eax
push str
call printf


;---------------------

mov esp,ebp
pop ebp
ret

这是函数:

_fibonacci:

push ebp
mov ebp,esp


mov ebx, [ebp+8] ;; param n 
cmp ebx,0
jne CHECK2

mov eax,0
jmp _endFIbofunc        

CHECK2: 
    cmp ebx,0x1
    jne ELSE3
    mov eax,1
jmp _endFIbofunc

ELSE3:

mov ebx,[ebp+8] 
dec ebx  ;; n-1


;;  FIRST call
push ebx
call _fibonacci
add esp,4
mov edx,eax

;;  SEC CALL
dec ebx
push ebx
call _fibonacci
add esp,4 
add eax,edx


mov eax,[ebp-4]

_endFIbofunc:

mov esp,ebp
pop ebp
ret

我在 Ubuntu 16.04 上运行它后,它发送错误:

分段错误(核心转储)

可能是什么问题?

【问题讨论】:

    标签: linux recursion assembly fibonacci


    【解决方案1】:

    您必须存储(推送)要在递归调用中更改的寄存器。然后恢复它们的原始值(pop)。这应该可以解决问题。

    类似这样的:

    • 推送你将在你的函数中使用的所有寄存器(除了你将获得返回值的 eax)
    • 推送 ebx,因为那是你的参数
    • 调用函数
    • 添加 esp,4
    • 弹出您在第一步中推送的所有寄存器,现在以相反的顺序进行

    【讨论】:

    • “这应该可以解决问题。” 恐怕这有点过于乐观了。 OP 正在使用未初始化的内存来获取函数结果。
    【解决方案2】:
    mov eax,[ebp-4]
    

    您正在使用[ebp-4] 的内存而没有在其中放入有用的东西! 您需要在函数序言中保留此空间:

    _fibonacci:
        push ebp
        mov  ebp, esp
        sub  esp, 4
    

    从第一个递归调用返回时,您将 EAX 的结果放入此内存双字中。
    从第二个递归调用返回,您将这个内存双字的内容添加到EAX
    这样做,EDX 寄存器将不再被破坏。


    您为什么要使用EBX 寄存器?如果您使用它,则必须按照Al Kepp 的答案中的说明保留它。
    如果您首先将参数放在EAX 中,您就会知道对于两个低于 2 的值(即 0 和 1),结果正好等于参数。很简单。

        mov  eax, [ebp+8] ;; param n 
        cmp  eax, 2
        jb   _endFIbofunc        
    

    如果您没有在第一次递归调用后立即平衡堆栈,您可以只减少已经存在的 dword 并进行第二次递归调用。

        dec  eax              ; n-1
        push eax              ;(*)
        call _fibonacci
        mov  [ebp-4], eax
        dec  dword ptr [esp]  ; n-2
        call _fibonacci
        add  esp,4            ;(*)
        add  eax, [ebp-4]
    

    整个过程:

    _fibonacci:
        push ebp
        mov  ebp, esp
        sub  esp, 4           ;(*)
        mov  eax, [ebp+8] ;; param n 
        cmp  eax, 2
        jb   _endFIbofunc        
        dec  eax              ; n-1
        push eax              ;(*)
        call _fibonacci
        mov  [ebp-4], eax
        dec  dword ptr [esp]  ;(*) n-2
        call _fibonacci
        add  esp,4            ;(*)
        add  eax, [ebp-4]
    _endFIbofunc:
        mov  esp, ebp
        pop  ebp
        ret
    

    【讨论】:

    • 感谢您改进我的过程,但是:在您写的行中:dec dword ptr [esp] ;(*) n-2。 nasm 大喊:错误:操作数后应有逗号、冒号、装饰符或行尾
    • @OrShemesh:NASM 语法只使用dworddword ptr 语法适用于 MASM。
    • 请注意,在大多数调用约定中,函数在堆栈中“拥有”它们的参数。您已选择让 _fibonacci 保留 arg 未修改并让调用者在函数返回后重用它。这行得通,但我认为如果你使用自己的 arg 作为溢出槽,你可以保存sub esp,4。此外,当您拥有ebp 时,使用[esp] 寻址模式有点令人困惑。我猜你正在这样做而不是 EBP 相对模式以表明它是下一个函数调用的 arg。
    • 如果使用用户堆栈的中断(或上下文切换)发生在 add esp,4add eax, [ebp-4] 之间会发生什么?为什么有add esp,4,因为这是稍后由mov esp,ebp 处理的?不相关 - 可以选择将函数更改为根本不使用 ebp。
    • @rcgldr: add esp,4 正在清理堆栈中的push eax。它的[ebp-4] 仍然高于esp(它 [esp])。你是对的,如果使用堆栈框架是没有意义的。 (同意使用ebp 比它的价值更麻烦,除了初学者。保存/恢复ebx 并使用保留调用的寄存器可能比溢出到堆栈槽更好。这就是编译器会做的。 (如果它没有把递归变成循环))
    【解决方案3】:

    除了提供的其他答案之外,还有一个替代解决方案:

    _fibonacci:
            mov     eax,[esp+4]             ;eax = n
            cmp     eax,2                   ;br if n < 2
            jb      _endFIbofunc
            dec     eax                     ;push n-1
            push    eax
            call    _fibonacci              ;returns eax = fib(n-1)
            xchg    eax,[esp]               ;eax = n-1, [esp] = fib(n-1)
            dec     eax                     ;push n-2
            push    eax
            call    _fibonacci              ;returns eax = fib(n-2)
            add     eax,[esp+4]             ;eax = fib(n-1)+fib(n-2)
            add     esp,8
    _endFIbofunc:
            ret
    

    琐事 - fib(47) 是最大的

     n     fib(n)      # calls
    
     0          0            1
     1          1            1
     2          1            3
     3          2            5
     4          3            9
     5          5           15
     6          8           25
     7         13           41
     8         21           67
     9         34          109
    10         55          177
    11         89          287
    12        144          465
    13        233          753
    14        377         1219
    15        610         1973
    16        987         3193
    17       1597         5167
    18       2584         8361
    19       4181        13529
    20       6765        21891
    21      10946        35421
    22      17711        57313
    23      28657        92735
    24      46368       150049
    25      75025       242785
    26     121393       392835
    27     196418       635621
    28     317811      1028457
    29     514229      1664079
    30     832040      2692537
    31    1346269      4356617
    32    2178309      7049155
    33    3524578     11405773
    34    5702887     18454929
    35    9227465     29860703
    36   14930352     48315633
    37   24157817     78176337
    38   39088169    126491971
    39   63245986    204668309
    40  102334155    331160281
    41  165580141    535828591
    42  267914296    866988873
    43  433494437   1402817465
    44  701408733   2269806339
    45 1134903170   3672623805
    46 1836311903   5942430145
    47 2971215073   9615053951
    48 4807526976   ...
    

    【讨论】:

    • xchg eax,[esp] 有一个隐含的 lock 前缀。这对代码大小有好处,但对性能非常不利。加载到 ecx,然后将 eax 存储回该位置。
    • OP 在 [tag:linux] 上调用 NASM 中的 printf,而他们的 main 不使用 ecx / edx,因此您应该假设 i386 SysV ABI 没问题。 (虽然递归调用是“私有的”,所以不保持 16B 堆栈对齐是可以的。)
    • 异或交换内存?嗯,是的,我认为这会更快,尤其是在 2 个负载和 1 个内存目标的情况下,它只是一次存储转发往返,因此它可以从存储缓冲区中受益,而不必刷新它并直接访问缓存。执行两个 memory-dest xors 和一个 memory source 将在第二条指令之后准备好寄存器结果,但对吞吐量来说更糟(并且仍然等待存储转发往返。)
    • 递归斐波那契是愚蠢的。对于初学者来说简单高效就是简单的迭代add eax, edx/add edx, eax。 (对于未来的读者,请参阅 rcgldr 和我在 stackoverflow.com/questions/32659715/… 上的回答以了解有效循环,以及在少于 O(log2(n)) 时间但具有更高常数的 Lucas 序列中获得 Fib(n)。更好的递归函数是 Ackermann 的,它需要某种堆栈,因为它增长得非常快。 (但对于小型输入仍然适合 32 位)
    • 在两个地方需要预测:获取块预测(它只需要预测下一个要获取的块,因为我们刚刚获取了这个块)。而且还有确切的目标地址。 RIP 会根据预测进行推测性更新,显然它必须指向正确的指令;当分支实际执行以验证预测时,纠正错误的唯一机制是回滚到分支之前的已知良好状态,而不是通过尝试调整架构状态来解决丢失或错误执行的指令。
    猜你喜欢
    • 2010-12-03
    • 2014-04-02
    • 2017-11-18
    • 2014-01-08
    • 2011-12-14
    • 2012-11-29
    • 2016-02-21
    • 2012-02-15
    相关资源
    最近更新 更多