Пример программы x86-64

Aug 31 2020

Я написал следующую программу x86, чтобы убедиться, что я следую правильной практике при вызове функции и последующем выходе в ОС:

.globl _start

_start:

    # Calculate 2*3 + 7*9 = 6 + 63 = 69
    # The multiplication will be done with a separate function call

    # Parameters passed in System V ABI
    # The first 6 integer/pointer arguments are passed in:
    #   %rdi, %rsi, %rdx, %rcx, %r8, and %r9
    # The return value is passed in %rax

    # multiply(2, 3)
    # Part 1 --> Call the parameters
    mov $2, %rdi mov $3, %rsi
    # Part 2 --> Call the function (`push` return address onto stack and `jmp` to function label)
    call multiply
    # Part 3 --> Handle the return value from %rax (here we'll just push it to the stack as a test)
    push %rax

    # multiply(7, 9)
    mov $7, %rdi mov $9, %rsi
    call multiply

    # Add the two together
    # Restore from stack onto rdi for the first function
    pop %rdi
    # The previous value from multiply(7,9) is already in rax, so just add to rbx
    add %rax, %rdi

    # for the 64-bit calling convention, do syscall instead of int 0x80
    # use %rdi instead of %rbx for the exit arg
    # use $60 instead of 1 for the exit code movq $60, %rax    # use the `_exit` [fast] syscall
                      # rdi contains out exit code
    syscall           # make syscall


multiply:
    mov %rdi, %rax
    imul %rsi, %rax
    ret

Соответствует ли приведенное выше соглашениям x86-64? Я знаю, что это, вероятно, так же просто, как и есть, но что здесь можно улучшить?

Ответы

3 NateEldredge Aug 31 2020 at 21:40

Чтобы уточнить некоторые комментарии, которые вы получили по версии SO этого вопроса, главное, что вам не хватает, - это выравнивание стека , требование соглашений о вызовах ABI SysV, которое часто упускается из виду новичками.

Требование (ABI 3.2.2):

Конец области входных аргументов должен быть выровнен по границе 16 (32 или 64, если __m256или __m512передается в стек) байтовой границы.

Таким образом, это означает, что в момент перед выполнением callинструкции указатель стека %rspдолжен быть кратен 16. В вашем случае у вас есть push8 байтов без popпромежутка между вашими двумя вызовами multiply, поэтому они не могут иметь оба правильное выравнивание.

Некоторые морщины здесь появляются из-за того, что ваша родительская функция _startвместо mainили другая функция вызывается кодом C:

  • Условия входа в систему _startописаны в 3.4 ABI. В частности, в момент получения управления стек выравнивается по 16 байтам _start. Кроме того, поскольку вы не можете вернуться из _start(в стеке нет обратного адреса), вы должны выйти с помощью системного вызова, как и вы, и поэтому нет необходимости сохранять какие-либо регистры для вызывающего.

  • Для mainили любой другой функции стек был бы выровнен по 16 байтам до того, как ваша функция была вызвана, поэтому дополнительные 8 байтов для адреса возврата означают, что при входе в вашу функцию стек теперь «смещен», то есть значение rspна 8 больше или меньше числа, кратного 16. (Поскольку обычно можно манипулировать стеком только с 8-байтовыми приращениями, на самом деле он всегда находится только в двух возможных состояниях, которые я назову «выровненным» и «смещенным».) , в таких функциях вам нужно будет сохранить содержимое регистров, сохраненных вызываемым пользователем %rbx, %rbp, %r12-r15.

Итак, ваш первый вызов multiplyимеет правильное выравнивание стека, а второй - нет. Конечно, в данном случае это представляет только академический интерес, потому multiplyчто не делает ничего, что требует выравнивания стека (он даже не использует стек вообще), но делать это правильно - хорошая практика.

Один из способов исправить это - вычесть еще 8 байтов из указателя стека перед вторым вызовом либо с помощью, sub $8, %rspлибо (более эффективно), просто pushвставив любой случайный 64-разрядный регистр. Но зачем вообще использовать стек для сохранения этого значения? Мы могли бы просто поместить его, скажем %rbx, в один из регистров, сохраненных вызываемым пользователем , который, как мы знаем, multiplyдолжен сохраняться. Обычно это требует от нас сохранения и восстановления содержимого этого регистра, но, поскольку мы находимся в особом случае _start, нам не нужно этого делать.


Отдельный комментарий: у вас есть много инструкций, например, mov $7, %rdiгде вы работаете с 64-битными регистрами. Это лучше было бы написать как mov $7, %edi. Напомним, что каждая запись в 32-битный регистр обнуляет верхнюю половину соответствующего 64-битного регистра , поэтому эффект будет таким же, пока ваша константа имеет беззнаковый 32 -битный формат, а кодирование на mov $7, %ediодин байт короче, так как это не префикс REX не нужен.

Поэтому я бы пересмотрел ваш код как

.globl _start

_start:

    # Calculate 2*3 + 7*9 = 6 + 63 = 69
    # The multiplication will be done with a separate function call

    # Parameters passed in System V ABI
    # The first 6 integer/pointer arguments are passed in:
    #   %rdi, %rsi, %rdx, %rcx, %r8, and %r9
    # The return value is passed in %rax

    # multiply(2, 3)
    # Part 1 --> Load the parameters
    mov $2, %edi mov $3, %esi
    # Part 2 --> Call the function (`push` return address onto stack and `jmp` to function label)
    call multiply
    # Part 3 --> Save the return value
    mov %rax, %rbx   # could also do mov %ebx, %eax if you know the result fits in 32 bits

    # multiply(7, 9)
    mov $7, %edi mov $9, %esi
    call multiply

    # Add the two together
    add %rbx, %rax
    mov %rax, %rdi

    # for the 64-bit calling convention, do syscall instead of int 0x80
    # use %rdi instead of %rbx for the exit arg
    # use $60 instead of 1 for the exit code mov $60, %eax    # use the `_exit` [fast] syscall
                      # rdi contains out exit code
    syscall           # make syscall


multiply:
    mov %rdi, %rax
    imul %rsi, %rax
    ret

Если вы хотите полагаться на результат multiplyподгонки в 32 бита, вы можете заменить mov %rax, %rbxна, mov %eax, %ebxчтобы сэкономить один байт. И аналогично, «сложить два вместе» может использовать 32-битные инструкции, чтобы сохранить еще два байта.

Наконец, есть стилистический вопрос о том, следует ли использовать суффиксы размера операндов синтаксиса AT и T, например addqversus add. Они являются необязательными, если один операнд является регистром, поскольку размер операнда может быть выведен из размера этого регистра (например, 32 бита для %eax, 64 бита для %raxи т. Д.). Лично я предпочитаю всегда использовать их, как небольшую дополнительную проверку того, что вы действительно пишете то, что имеете в виду, но их пропуск, как вы (в основном), тоже является обычным и прекрасным; просто будьте последовательны. У вас был один случай, movq $60, %raxкогда он не нужен, поэтому для единообразия я опустил там суффикс. (Я также изменил его на %eaxпо причинам, указанным выше.)