linux gcc assembly x86-64 gas

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