x86 программа для выполнения экспоненты

Sep 11 2020

Для продолжения, мой последний вопрос касался сложения чисел вместе с вызовом функции: Пример программы x86-64 . В этом я попытался применить уроки, указанные в предыдущем ответе (выравнивание стека, использование edxвместо того, rdxгде будут соответствовать результаты). Я попытался прокомментировать встроенный код:

# We are going to calculate 7^2 + 2^4 = 49 + 16 = 65
# The exponent function supports whole numbers (long) >= 0

.section .data
base_1: .long   7
exp_1:  .long   2
base_2: .long   2
exp_2:  .long   4

.section .text
.globl _start
_start:


    # We will do the first function call, 7^2
    mov base_1(%rip),   %edi
    mov exp_1(%rip),    %esi
    call exp

    # needs to be 16-byte aligned before the next function call so do two pushes
    pushq $0
    pushq %rax

    # Now do the second function call
    mov base_2(%rip),   %edi
    mov exp_2(%rip),    %esi
    call exp

    # We have the return value in %eax so let's add this with our previous function's value
    popq %rdi
    
    add %eax,   %edi
    mov $60,    %eax
    syscall

exp:
    # Initialize %eax to 1
    mov $1,     %eax

exp_op:
    cmp $0,     %esi
    je exp_ret
    imul %edi,  %eax
    dec %esi
    jmp exp_op

exp_ret:
    ret

У меня есть несколько конкретных вопросов по коду:

  • Делает сырое push/ popобычное? Или это принято всегда делать push rbp mov rsp rbp... pop rbp. Почему один метод предпочтительнее другого?
  • Что, если бы я хотел сохранить результат первого вычисления в регистре: есть ли какие-нибудь регистры, которые гарантированно сохраняются между вызовами функций? Или поэтому push...popметод так часто используется?
  • Наконец, мне expкажется, все в порядке? Немного странно иметь три метки, составляющие одну функцию. Как это можно было улучшить?

Ответы

3 Quuxplusone Sep 11 2020 at 13:45

Я не эксперт, но какого черта, вот комментарий к обзору:

# needs to be 16-byte aligned before the next function call so do two pushes
pushq $0
pushq %rax

Комментарий хороший, но я бы его перефразировал. Два восьмибайтовых нажатия составляют 16 байтов и не меняют выравнивания стека. Поэтому я предполагаю, что один из этих толчков является значительным, а другой - несущественным, но в вашем комментарии мне не сказано, что есть что! Так что вместо этого вы можете сказать

# one extra push to preserve 16-byte stack alignment
pushq $0

# push the result of `exp`
pushq %rax

Вы можете уменьшить размер сгенерированного кода, исключив незначительную константу $0:

# push the result of `exp`, plus one extra push to preserve 16-byte stack alignment
pushq %rax
pushq %rax

Теперь читателю даже не нужно выяснять, какой толчок является значительным, потому что оба толчка делают одно и то же!


Но почему важно сохранять 16-байтовое выравнивание вызовов? Это не требование машины . Похоже, вы пытаетесь следовать определенному ABI , например, для взаимодействия с C или C ++. В вашей внешней документации должно быть более четко указано, какой ABI вы пытаетесь придерживаться.

И потом, если будут пытаться взаимодействовать с кодом C, вы можете улучшить свой код, указав , какие из его меток подразумеваются внешними точками входа и какими из них являются только внутренними локальными метками. Похоже, вы намереваетесь expбыть вызванным из другого кода - это точка входа - но, например exp_op, не exp_retвызывается и технически вызывается, но просто действует как бездействие . Вы можете как-то пометить их как «детали местной реализации, а не для внешнего потребления».

Да, технически вы уже делаете это путем экспорта, .globl _startа не путем экспорта, .globl expно все же существует большая разница между вызываемой функцией expи локальной меткой, exp_opкоторая не отражена в вашей схеме именования. Если бы я делал это, я бы добавил .globl expи переименовал exp_op, exp_retбы что-нибудь вроде Lexp1, Lexp2или L1_looptop, L2_loopend.