linux assembly x86 x86-64 att

Imprimir un número entero como una cadena con sintaxis de AT&T, con llamadas al sistema Linux en lugar de printf



assembly x86 (2)

Como señala @ ped7g, está haciendo varias cosas mal: utilizando la ABI int 0x80 32 bits en el código de 64 bits y pasando valores de caracteres en lugar de punteros a la llamada al sistema write() .

Aquí le mostramos cómo imprimir un número entero en Linux de 64 bits, la forma simple y algo eficiente. Vea ¿Por qué GCC usa la multiplicación por un número extraño en la implementación de la división de enteros? para evitar div r64 para la división por 10, porque eso es muy lento ( 21 a 83 ciclos en Intel Skylake ). Un inverso multiplicativo haría que esta función sea realmente eficiente, no solo "algo". (Pero, por supuesto, todavía habría espacio para optimizaciones ...)

Las llamadas al sistema son costosas (probablemente miles de ciclos para write(1, buf, 1) ), y hacer una syscall dentro de los pasos del bucle en los registros, por lo que es inconveniente, torpe e ineficiente. Deberíamos escribir los caracteres en un búfer pequeño, en orden de impresión (el dígito más significativo en la dirección más baja), y hacer una sola llamada al sistema write() al respecto.

Pero entonces necesitamos un búfer. La longitud máxima de un entero de 64 bits es de solo 20 dígitos decimales, por lo que podemos usar algo de espacio en la pila. En x86-64 Linux, podemos usar el espacio de pila debajo de RSP (hasta 128B) sin "reservarlo" modificando RSP. Esto se llama la red-zone .

En lugar de codificar los números de llamada del sistema, el uso de GAS facilita el uso de las constantes definidas en los archivos .h . Tenga en cuenta el mov $__NR_write, %eax cerca del final de la función. El x86-64 SystemV ABI pasa argumentos de llamada al sistema en registros similares a la convención de llamada a la función . (Por lo tanto, es un registro totalmente diferente del 32 bits int 0x80 ABI).

#include <asm/unistd_64.h> // This is a standard glibc header file // It contains no C code, only only #define constants, so we can include it from asm without syntax errors. .p2align 4 .globl print_integer #void print_uint64(uint64_t value) print_uint64: lea -1(%rsp), %rsi # We use the 128B red-zone as a buffer to hold the string # a 64-bit integer is at most 20 digits long in base 10, so it fits. movb $''/n'', (%rsi) # store the trailing newline byte. (Right below the return address). # If you need a null-terminated string, leave an extra byte of room and store ''/n/0''. Or push $''/n'' mov $10, %ecx # same as mov $10, %rcx but 2 bytes shorter # note that newline (/n) has ASCII code 10, so we could actually have used movb %cl to save code size. mov %rdi, %rax # function arg arrives in RDI; we need it in RAX for div .Ltoascii_digit: # do{ xor %edx, %edx div %rcx # rax = rdx:rax / 10. rdx = remainder # store digits in MSD-first printing order, working backwards from the end of the string add $''0'', %edx # integer to ASCII. %dl would work, too, since we know this is 0-9 dec %rsi mov %dl, (%rsi) # *--p = (value%10) + ''0''; test %rax, %rax jnz .Ltoascii_digit # } while(value != 0) # If we used a loop-counter to print a fixed number of digits, we would get leading zeros # The do{}while() loop structure means the loop runs at least once, so we get "0/n" for input=0 # Then print the whole string with one system call mov $__NR_write, %eax # SYS_write, from unistd_64.h mov $1, %edi # fd=1 # %rsi = start of the buffer mov %rsp, %rdx sub %rsi, %rdx # length = one_past_end - start syscall # sys_write(fd=1 /*rdi*/, buf /*rsi*/, length /*rdx*/); 64-bit ABI # rax = return value (or -errno) # rcx and r11 = garbage (destroyed by syscall/sysret) # all other registers = unmodified (saved/restored by the kernel) # we don''t need to restore any registers, and we didn''t modify RSP. ret

Para probar esta función, pongo esto en el mismo archivo para llamarlo y salir:

.p2align 4 .globl _start _start: mov $10120123425329922, %rdi # mov $0, %edi # Yes, it does work with input = 0 call print_uint64 xor %edi, %edi mov $__NR_exit, %eax syscall # sys_exit(0)

Construí esto en un binario estático (sin libc):

$ gcc -Wall -nostdlib print-integer.S && ./a.out 10120123425329922 $ strace ./a.out > /dev/null execve("./a.out", ["./a.out"], 0x7fffcb097340 /* 51 vars */) = 0 write(1, "10120123425329922/n", 18) = 18 exit(0) = ? +++ exited with 0 +++ $ file ./a.out ./a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=69b865d1e535d5b174004ce08736e78fade37d84, not stripped

Relacionado: Linux x86-32 bucle de precisión extendida que imprime 9 dígitos decimales de cada "extremidad" de 32 bits: vea .toascii_digit: en mi respuesta de código de golf Extreme Fibonacci . Está optimizado para el tamaño del código (incluso a expensas de la velocidad), pero está bien comentado.

Utiliza div como lo haces, porque es más pequeño que usar un inverso multiplicativo rápido). Utiliza el loop para el bucle externo (sobre un entero múltiple para una precisión extendida), nuevamente para el tamaño del código a costa de la velocidad .

Utiliza el int 0x80 ABI de 32 bits, e imprime en un búfer que contenía el valor de Fibonacci "antiguo", no el actual.

Otra forma de obtener un asm eficiente es desde un compilador de C. Solo para el ciclo sobre los dígitos, observe lo que producen gcc o clang para esta fuente C (que es básicamente lo que está haciendo el asm). El explorador del compilador Godbolt facilita la prueba con diferentes opciones y diferentes versiones del compilador.

Vea la salida de gcc7.2 -O3 asm, que es casi un reemplazo print_uint64 para el bucle en print_uint64 (porque elegí los argumentos para ir en los mismos registros):

void itoa_end(unsigned long val, char *p_end) { const unsigned base = 10; do { *--p_end = (val % base) + ''0''; val /= base; } while(val); // write(1, p_end, orig-current); }

Probé el rendimiento en un Skylake i7-6700k comentando la instrucción syscall y poniendo un ciclo de repetición alrededor de la llamada a la función. La versión con mul %rcx / shr $3, %rdx es aproximadamente 5 veces más rápida que la versión con div %rcx para almacenar una cadena numérica larga ( 10120123425329922 ) en un búfer. La versión div corrió a 0.25 instrucciones por reloj, mientras que la versión mul corrió a 2.65 instrucciones por reloj (aunque requiere muchas más instrucciones).

Puede valer la pena desenrollar por 2, y dividir por 100 y dividir el resto en 2 dígitos. Eso daría mucho mejor paralelismo a nivel de instrucción, en caso de que la versión más simple cuellos de botella en latencia mul + shr . La cadena de operaciones de multiplicación / cambio que lleva val a cero sería la mitad del tiempo, con más trabajo en cada cadena de dependencia corta e independiente para manejar un resto 0-99.

He escrito un programa de ensamblaje para mostrar el factorial de un número siguiendo la sintaxis de AT&T. Pero no funciona. Aquí está mi código

.text .globl _start _start: movq $5,%rcx movq $5,%rax Repeat: #function to calculate factorial decq %rcx cmp $0,%rcx je print imul %rcx,%rax cmp $1,%rcx jne Repeat # Now result of factorial stored in rax print: xorq %rsi, %rsi # function to print integer result digit by digit by pushing in #stack loop: movq $0, %rdx movq $10, %rbx divq %rbx addq $48, %rdx pushq %rdx incq %rsi cmpq $0, %rax jz next jmp loop next: cmpq $0, %rsi jz bye popq %rcx decq %rsi movq $4, %rax movq $1, %rbx movq $1, %rdx int $0x80 addq $4, %rsp jmp next bye: movq $1,%rax movq $0, %rbx int $0x80 .data num : .byte 5

Este programa no imprime nada, también utilicé gdb para visualizar que funciona bien hasta la función de bucle, pero cuando viene el siguiente valor aleatorio comienza a ingresar en varios registros. Ayúdame a depurar para que pueda imprimir factorial.


Varias cosas:

0) Supongo que este es un entorno Linux 64b, pero debería haberlo dicho (si no lo es, algunos de mis puntos no serán válidos)

1) int 0x80 es una llamada de 32b, pero está utilizando registros de 64b, por lo que debe usar syscall (y diferentes argumentos)

2) int 0x80, eax=4 requiere que ecx contenga la dirección de la memoria, donde se almacena el contenido, mientras le da el carácter ASCII en ecx = acceso ilegal a la memoria (la primera llamada debe devolver un error, es decir, eax es un valor negativo) . O usar strace <your binary> debería revelar los argumentos incorrectos + error devuelto.

3) ¿ addq $4, %rsp qué addq $4, %rsp ? No tiene sentido para mí, está dañando rsp , por lo que el siguiente pop rcx mostrará un valor incorrecto, y al final correrá "hacia arriba" en la pila.

... tal vez un poco más, no lo depuré, esta lista es solo leyendo la fuente (por lo que incluso puedo estar equivocado sobre algo, aunque eso sería raro).

Por cierto, su código está funcionando . Simplemente no hace lo que esperabas. Pero funciona bien, precisamente porque la CPU está diseñada y precisamente lo que escribió en el código. Si eso logra lo que quería o tiene sentido, ese es un tema diferente, pero no culpe al HW o al ensamblador.

... puedo adivinar rápidamente cómo se puede solucionar la rutina (solo una corrección parcial de syscall , aún necesita reescribirse para syscall en 64b linux):

next: cmpq $0, %rsi jz bye movq %rsp,%rcx ; make ecx to point to stack memory (with stored char) ; this will work if you are lucky enough that rsp fits into 32b ; if it is beyond 4GiB logical address, then you have bad luck (syscall needed) decq %rsi movq $4, %rax movq $1, %rbx movq $1, %rdx int $0x80 addq $8, %rsp ; now rsp += 8; is needed, because there''s no POP jmp next

Una vez más no lo intenté, solo escribí desde la cabeza, así que avíseme cómo cambió la situación.