Ejemplo de programa x86-64
Escribí el siguiente programa x86 para asegurarme de seguir las prácticas correctas al llamar a una función y luego salir al sistema operativo:
.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
¿Lo anterior sigue correctamente las convenciones x86-64? Sé que probablemente sea tan básico como parece, pero ¿qué se puede mejorar aquí?
Respuestas
Para desarrollar algunos comentarios que recibió sobre la versión SO de esta pregunta, lo principal que le falta es la alineación de la pila , un requisito de las convenciones de llamadas SysV ABI que los principiantes suelen pasar por alto.
El requisito es (ABI 3.2.2):
El final del área de argumento de entrada se alineará en un límite de 16 bytes (32 o 64, si se pasa
__m256o__m512en la pila).
Eso significa que, en el instante antes de ejecutar una callinstrucción, el puntero de la pila %rspdebe ser un múltiplo de 16. En su caso, tiene un valor pushde 8 bytes sin un popintermedio entre sus dos llamadas a multiply, por lo que ambos no pueden tener alineación correcta.
Aquí se introducen algunas arrugas por el hecho de que su función principal es en _startlugar de mainotra función llamada por código C:
Las condiciones de entrada a
_startse describen en 3.4 del ABI. En particular, la pila se alinea a 16 bytes en el instante en que_startobtiene el control. Además, dado que no puede regresar desde_start(no hay una dirección de retorno en la pila), debe salir con una llamada al sistema como lo hace, por lo que no es necesario guardar ningún registro para la persona que llama.Para
maino cualquier otra función, la pila se habría alineado a 16 bytes antes de que se llamara a su función, por lo que los 8 bytes adicionales para la dirección de retorno significan que al ingresar a su función, la pila ahora está "desalineada", es decir, el valor derspes 8 más o menos que un múltiplo de 16. (Dado que uno normalmente solo manipularía la pila en incrementos de 8 bytes, en realidad solo está en dos estados posibles, que llamaré "alineado" y "desalineado"). , en tales funciones, necesitaría preservar el contenido de los registros guardados por el destinatario%rbx, %rbp, %r12-r15.
Entonces, tal como está, su primera llamada a multiplytiene una alineación de pila correcta, pero la segunda no. Por supuesto, es solo de interés académico en este caso, porque multiplyno hace nada que necesite alineación de pila (ni siquiera usa la pila en absoluto), pero es una buena práctica hacerlo bien.
Una forma de solucionarlo sería restar otros 8 bytes del puntero de la pila antes de la segunda llamada, ya sea con sub $8, %rspo (más eficientemente) simplemente pushingresando cualquier registro aleatorio de 64 bits. Pero, ¿por qué deberíamos molestarnos en usar la pila para guardar este valor? Podríamos simplemente ponerlo en uno de los registros de llamadas guardadas, digamos %rbx, que sabemos que multiplydebemos preservar. Normalmente, esto requeriría que guardemos y restauremos el contenido de este registro, pero como estamos en el caso especial de ser _start, no tenemos que hacerlo.
Un comentario aparte es que tiene muchas instrucciones, como mov $7, %rdidónde opera en registros de 64 bits. Esto sería mejor escribirlo como mov $7, %edi. Recuerde que cada escritura en un registro de 32 bits pondrá a cero la mitad superior del registro de 64 bits correspondiente , por lo que el efecto es el mismo siempre que su constante sea de 32 bits sin signo y la codificación de mov $7, %edisea un byte más corta, ya que no lo hace. no necesita un prefijo REX.
Así que revisaría tu 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
Si desea confiar en el resultado del multiplyajuste en 32 bits, puede reemplazar mov %rax, %rbxcon mov %eax, %ebxpara guardar un byte. Y del mismo modo, la opción "Sumar los dos" podría usar instrucciones de 32 bits en su lugar para ahorrar dos bytes más.
Finalmente, hay un punto estilístico sobre si se deben usar los sufijos de tamaño de operando de sintaxis AT & T, como addqversus add. Son opcionales cuando un operando es un registro, ya que el tamaño del operando se puede deducir del tamaño de ese registro (por ejemplo, 32 bits para %eax, 64 bits para %rax, etc.). Mi preferencia personal es usarlos siempre, como una pequeña verificación adicional de que realmente estás escribiendo lo que quieres decir, pero omitirlos como lo hiciste (en su mayoría) también es común y está bien; solo sé consistente. Tuviste una instancia de movq $60, %raxdonde no era necesario, así que por coherencia omití el sufijo allí. (También lo cambié a %eaxpor las razones mencionadas anteriormente).