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.