装配 - 交换功能 - 为什么它不起作用?

时间:2011-12-14 20:36:23

标签: assembly x86 swap

我需要创建一个函数,将& x的值与& y的值交换(意味着交换*(& y)和*(& x)。

Swap:

    push EBP
    mov EBP,ESP
    mov EBX, [EBP+12] ; ebx = *x
    mov EAX, DWORD [EBX] ;eax = ebx = *x
    mov DWORD [EBP-4], EAX ; [ebp-4] = eax =*x
    mov EDX, [EBP+8] ; edx = *y
    mov EAX, DWORD [EDX] ; eax = *edx = *y
    mov DWORD [EBX], EAX ; ebx = eax = *y
    mov EAX, DWORD [EBP-4] ; eax = *x
    mov DWORD [EDX], EAX ; edx = *x
    pop EBP ; ebx = *y and edx = *x
    ret

我称之为:

    // call Swap
    push x
    push y
    call swap

我不明白为什么它不起作用。我添加了评论,解释了我对它的理解。我的实施有什么问题?我该如何解决?

2 个答案:

答案 0 :(得分:4)

在[EBP-4]上访问dword时,实际上并没有在堆栈上保留内存。它可以被诸如中断例程,信号处理程序,异步调用程序之类的东西覆盖,无论你的操作系统是什么。

代码应该是这样的:

swap:
    push  EBP
    mov   EBP,ESP           ; make a traditional stack frame

    sub   ESP, 4         ; reserve memory for a local variable at [EBP-4]

    mov   EBX, [EBP+12]        ; ebx = &x
    mov   EAX, DWORD [EBX]     ; eax = x
    mov   DWORD [EBP-4], EAX   ; [ebp-4] = eax = x
    mov   EDX, [EBP+8]         ; edx = &y
    mov   EAX, DWORD [EDX]     ; eax = y
    mov   DWORD [EBX], EAX     ; *&x = y
    mov   EAX, DWORD [EBP-4]   ; eax = x reloaded from the local
    mov   DWORD [EDX], EAX     ; *&y = x

    leave          ; remove locals (by restoring ESP), restore EBP

    ret

此外,请确保您将变量xy的地址作为参数传递,而不是变量的值。 push x + push y会在NASM中传递xy的地址,但会在TASM和MASM中传递xy的值

答案 1 :(得分:1)

除了Alexey的错误修正外,您还可以使其效率大大提高。 (当然,在呼叫站点内联交换和优化效果更好。)

堆栈上不需要本地临时文件:您可以重新加载其中一个地址两次,或保存/还原ESI并将其用作临时文件。

您实际上正在破坏EBX,在所有常规C调用约定中都保留了该调用。在大多数32位x86调用约定中,EAX,ECX和EDX是无需保存/恢复即可使用的三个调用占据寄存器,而其他寄存器则保留调用。 (因此,您的调用方希望您不要破坏它们的值,因此只有在放回原始值后才能使用它们。这就是为什么在将EBP用于帧指针之后必须还原EBP的原因。)


为交换功能编译独立(非内联)定义时,gcc -O3 -m32的作用是保存/恢复EBX,因此它具有4个寄存器。 clang选择ESI。

void swap(int *px, int *py) {
    int tmp = *px;
    *px = *py;
    *py = tmp;
}

On the Godbolt compiler explorer

# gcc8.2 -O3 -m32 -fverbose-asm
# gcc itself emitted the comments on the following instructions
swap:
        push    ebx     #
        mov     edx, DWORD PTR [esp+8]    # px, px
        mov     eax, DWORD PTR [esp+12]   # py, py
        mov     ecx, DWORD PTR [edx]      # tmp, *px_3(D)
        mov     ebx, DWORD PTR [eax]      # tmp91, *py_5(D)
        mov     DWORD PTR [edx], ebx      # *px_3(D), tmp91
        mov     DWORD PTR [eax], ecx      # *py_5(D), tmp
        pop     ebx       #
        ret  

# DWORD PTR is the gas .intel_syntax equivalent of NASM's DWORD
# you can just remove them all because the register implies an operand size

这也避免了制作遗留的堆栈框架。如果需要,可以将-fno-omit-frame-pointer添加到编译器选项中以查看带有帧指针的代码生成。 (Godbolt将重新编译并向您显示asm。非常方便的网站,供您探索编译器选项和代码更改。)

64位调用约定已经在寄存器中包含args,并且具有足够的暂存寄存器,因此我们只得到4条指令,效率更高。


正如我提到的,另一种选择是重新加载一个指针args两次:

swap:
       # without a push, offsets relative to ESP are smaller by 4
        mov     edx, [esp+4]    # edx = px   reused later
        mov     eax, [esp+8]    # eax = py   also reused later
        mov     ecx, [edx]      # ecx = tmp = *px   lives for the whole function

        mov     eax, [eax]      # eax = *py   destroying our register copy of py
        mov    [edx], eax       # *px = *py;  done with px, can now destroy it

        mov     edx, [esp+8]   # edx = py
        mov    [edx], ecx       # *py = tmp;
        ret  

仅7条指令,而不是8条指令。非常便宜,两次加载相同的值非常便宜,而且乱序执行意味着即使按程序顺序快速准备好商店地址也不是问题。只是商店前面的指令会加载地址。