linux - Llamar a printf en x86_64 usando el ensamblador GNU
gcc assembly (2)
He escrito un programa usando la sintaxis de AT&T para usar con el ensamblador GNU:
.data
format: .ascii "%d/n"
.text
.global main
main:
mov $format, %rbx
mov (%rbx), %rdi
mov $1, %rsi
call printf
ret
Utilizo GCC para ensamblar y vincularme con:
gcc -o main main.s
Lo ejecuto con este comando:
./principal
Cuando ejecuto el programa me sale un error seg.
Al usar gdb, dice
printf
no encontrado.
He intentado ".extern printf", que no funciona.
Alguien sugirió que debería almacenar el puntero de la pila antes de llamar a
printf
y restaurar antes de
RET
. ¿Cómo hago eso?
Hay varios problemas con este código. La convención de llamadas AMD64 System V ABI utilizada por Linux requiere algunas cosas. Requiere que justo antes de una LLAMADA la pila esté alineada al menos con 16 bytes (o 32 bytes):
El final del área de argumento de entrada se alineará en un límite de 16 bytes (32, si se pasa __m256 en la pila).
Después de que el tiempo de ejecución
C
llame a su función
main
, la pila está desalineada por 8 porque
CALL
colocó el puntero de retorno en la pila.
Para realinear el límite de 16 bytes, simplemente puede
EMPUJAR
cualquier
registro de uso general en la pila y
POP
al final.
La convención de llamada también requiere que AL contenga el número de registros vectoriales utilizados para una función de argumento variable:
% al se usa para indicar el número de argumentos vectoriales pasados a una función que requiere un número variable de argumentos
printf
es una función de argumento variable, por lo que
AL
debe configurarse.
En este caso, no pasa ningún parámetro en un registro vectorial, por lo que puede establecer
AL
en 0.
También desreferencia el puntero de formato $ cuando ya es una dirección. Entonces esto está mal:
mov $format, %rbx
mov (%rbx), %rdi
Esto toma la dirección del formato y la coloca en RBX . Luego, toma los 8 bytes en esa dirección en RBX y los coloca en RDI . RDI debe ser un puntero a una cadena de caracteres, no los caracteres en sí. Las dos líneas podrían reemplazarse con:
lea format(%rip), %rdi
Esto utiliza el direccionamiento relativo de RIP.
También debe
NUL
terminar sus cadenas.
En lugar de usar
.ascii
, puede usar
.asciz
en la plataforma x86.
Una versión funcional de su programa podría verse así:
# global data #
.data
format: .asciz "%d/n"
.text
.global main
main:
push %rbx
lea format(%rip), %rdi
mov $1, %esi # Writing to ESI zero extends to RSI.
xor %eax, %eax # Zeroing EAX is efficient way to clear AL.
call printf
pop %rbx
ret
Otras recomendaciones / sugerencias
También debe ser consciente de la ABI de Linux de 64 bits, que la convención de llamada también requiere funciones que escriba para honrar la preservación de ciertos registros. La lista de registros y si deben conservarse es la siguiente:
Cualquier registro que diga
Yes
en la columna
Conservado en el registro
es uno que debe asegurarse de conservar en su función.
La función
main
es como cualquier otra función
C.
Si tiene cadenas / datos que sabe que serán de solo lectura, puede colocarlos en la sección
.section .rodata
con
.section .rodata
lugar de
.data
En modo de 64 bits: si tiene un operando de destino que es un registro de 32 bits, la CPU extenderá a cero el registro en todo el registro de 64 bits. Esto puede ahorrar bytes en la codificación de instrucciones.
Es posible que su ejecutable se esté compilando como código independiente de la posición. Puede recibir un error similar a:
la reubicación R_X86_64_PC32 contra el símbolo `printf @@ GLIBC_2.2.5 ''no se puede usar al hacer un objeto compartido; recompilar con -fPIC
Para solucionar esto, deberá llamar a la función externa
printf
esta manera:
call printf@plt
Esto llama a la función de biblioteca externa a través de la Tabla de vinculación de procedimientos (PLT)
Puede ver el código de ensamblaje generado a partir de un archivo c equivalente.
Ejecutando
gcc -o - -S -fno-asynchronous-unwind-tables test.c
con test.c
#include <stdio.h>
int main() {
return printf("%d/n", 1);
}
Esto genera el código de ensamblaje:
.file "test.c"
.section .rodata
.LC0:
.string "%d/n"
.text
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
movl $1, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
popq %rbp
ret
.size main, .-main
.ident "GCC: (GNU) 6.1.1 20160602"
.section .note.GNU-stack,"",@progbits
Esto le proporciona una muestra de un código de ensamblaje que llama a printf que luego puede modificar.
En comparación con su código, debe modificar 2 cosas:
-
% rdi debe apuntar al formato, no debe hacer referencia a% rbx, esto podría hacerse con el
mov $format, %rdi
-
printf tiene un número variable de argumentos, entonces debe agregar
mov $0, %eax
La aplicación de estas modificaciones dará algo como:
.data
format: .ascii "%d/n"
.text
.global main
main:
mov $format, %rdi
mov $1, %rsi
mov $0, %eax
call printf
ret
Y luego ejecutarlo imprimir:
1