Сборка - Функция свопинга - Почему это не сработает? - PullRequest
0 голосов
/ 15 декабря 2011

Мне нужно создать функцию, которая меняет значение & x на значение & y (что означает swap * (& 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 ]

4 голосов
/ 15 декабря 2011

На самом деле вы не резервируете память в стеке, которую используете при обращении к двойному мечу в [EBP-4].Он может быть перезаписан такими вещами, как подпрограммы прерывания, обработчики сигналов, асинхронно вызываемые процедуры, независимо от того, что применяется в вашей ОС.

Код должен выглядеть следующим образом:

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

Также убедитесь, чточто вы передаете в качестве параметров адреса переменных x и y, а не значения переменных.push x + push y передаст адреса x и y в NASM, но они передадут значения x и y в TASM и MASM.

1 голос
/ 29 октября 2018

Помимо исправления Алексея, вы можете сделать это значительно более эффективным. (Конечно, встраивание свопа и оптимизация на сайте вызовов еще лучше.)

Нет необходимости в локальном временном хранилище в стеке: вместо этого можно дважды перезагрузить один из адресов или сохранить / восстановить ESI и использовать его в качестве временного.

Вы фактически уничтожаете EBX, который сохраняется во всех обычных соглашениях C о вызовах. В большинстве 32-битных соглашений о вызовах x86 EAX, ECX и EDX - это три регистра с закрытыми вызовами, которые вы можете использовать без сохранения / восстановления, в то время как остальные сохраняются при вызове. (То есть ваш вызывающий ожидает, что вы не уничтожите их значения, поэтому вы можете использовать их, только если вернете исходное значение. Вот почему EBP должен быть восстановлен после использования его для указателя кадра.)


То, что gcc -O3 -m32 делает при компиляции автономного (не встроенного) определения для функции подкачки, это сохранение / восстановление EBX, поэтому он имеет 4 регистра для работы. Clang выбирает ESI.

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

В проводнике компилятора Godbolt :

# 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 к опциям компилятора, чтобы увидеть code-gen с указателем кадра, если хотите. (Godbolt перекомпилирует и покажет вам asm. Очень удобный сайт для изучения параметров компилятора и изменений кода.)

64-битные соглашения о вызовах уже содержат аргументы в регистрах и имеют достаточно чистых регистров, поэтому мы просто получаем 4 инструкции, гораздо более эффективные.


Как я уже говорил, другой вариант - перезагрузить один из аргументов указателя дважды:

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. Загрузка одного и того же значения дважды - это очень дешево, и выполнение вне очереди означает, что не проблема быстро подготовить адрес магазина, хотя в программном порядке это только инструкция прямо перед магазином, который загружает адрес.

...