Exemplo de programa x86-64
Escrevi o seguinte programa x86 para ter certeza de que estou seguindo as práticas corretas ao chamar uma função e, em seguida, sair para o sistema operacional:
.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
O acima segue as convenções x86-64 corretamente? Eu sei que provavelmente é o mais básico possível, mas o que pode ser melhorado aqui?
Respostas
Para elaborar alguns comentários que você obteve sobre a versão do SO desta questão, a principal coisa que está faltando é o alinhamento da pilha , um requisito das convenções de chamada SysV ABI que geralmente é esquecido pelos iniciantes.
O requisito é (ABI 3.2.2):
O final da área de argumento de entrada deve ser alinhado em um limite de 16 (32 ou 64, se
__m256ou__m512for passado na pilha).
Então isso significa que, no instante antes de você executar uma callinstrução, o ponteiro da pilha %rspprecisa ser um múltiplo de 16. No seu caso, você tem um pushde 8 bytes sem um popentre suas duas chamadas para multiply, então eles não podem ter alinhamento correto.
Algumas rugas são introduzidas aqui pelo fato de que sua função pai é em _startvez de mainou outra função chamada pelo código C:
As condições de entrada
_startestão descritas em 3.4 da ABI. Em particular, a pilha é alinhada a 16 bytes no instante em que_startobtém o controle. Além disso, como você não pode retornar de_start(não há endereço de retorno na pilha), você precisa sair com uma chamada de sistema como faz e, portanto, não há necessidade de salvar nenhum registro para o chamador.Para
mainou qualquer outra função, a pilha teria sido alinhada a 16 bytes antes de sua função ser chamada, então os 8 bytes extras para o endereço de retorno significam que na entrada de sua função, a pilha agora está "desalinhada", ou seja, o valor derspé 8 mais ou menos que um múltiplo de 16. (Uma vez que normalmente só se manipularia a pilha em incrementos de 8 bytes, ela só está realmente em dois estados possíveis, que chamarei de "alinhados" e "desalinhados".) Também , em tais funções, você precisaria preservar o conteúdo dos registradores salvos pelo callee%rbx, %rbp, %r12-r15.
Assim como está, sua primeira chamada para multiplytem o alinhamento correto da pilha, mas a segunda não. Claro, é apenas de interesse acadêmico neste caso, porque multiplynão faz nada que precise do alinhamento da pilha (nem mesmo usa a pilha), mas é uma boa prática fazer isso da maneira certa.
Uma maneira de corrigir isso seria subtrair outros 8 bytes do ponteiro da pilha antes da segunda chamada, com sub $8, %rspou (mais eficientemente) simplesmente pushinserindo qualquer registro de 64 bits aleatório. Mas por que deveríamos nos preocupar em usar a pilha para salvar esse valor? Poderíamos simplesmente colocá-lo em um dos registros salvos pelo callee, digamos %rbx, que sabemos que multiplydeve ser preservado. Normalmente, isso exigiria que salvássemos e restaurássemos o conteúdo desse registro, mas como estamos no caso especial de ser _start, não precisamos fazer isso.
Um comentário separado é que você tem várias instruções, como mov $7, %rdiquando você opera em registradores de 64 bits. Seria melhor escrever como mov $7, %edi. Lembre-se de que cada gravação em um registro de 32 bits zera a metade superior do registro de 64 bits correspondente , de modo que o efeito é o mesmo, desde que sua constante seja de 32 bits sem sinal e a codificação de mov $7, %ediseja um byte menor, pois não não precisa de um prefixo REX.
Então, eu revisaria seu código como
.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
Se você quiser confiar no resultado do multiplyajuste de 32 bits, pode substituir mov %rax, %rbxpor mov %eax, %ebxpara salvar um byte. E da mesma forma, o "Adicionar os dois juntos" poderia usar instruções de 32 bits para salvar mais dois bytes.
Finalmente, há um ponto estilístico sobre o uso dos sufixos de tamanho de operando da sintaxe AT&T, como addqversus add. Eles são opcionais quando um operando é um registro, uma vez que o tamanho do operando pode ser deduzido do tamanho desse registro (por exemplo, 32 bits para %eax, 64 bits para %rax, etc). Minha preferência pessoal é sempre usá-los, como uma pequena verificação extra de que você está realmente escrevendo o que quer dizer, mas omiti-los como você (principalmente) fez também é comum e correto; apenas seja consistente. Você tinha uma instância de movq $60, %raxonde não era necessário, portanto, para consistência, omiti o sufixo lá. (Eu também mudei para %eaxpelos motivos mencionados acima.)