Stampa di un intero come una stringa con syntax AT & T, con chiamate di sistema Linux anziché printf

Ho scritto un programma Assembly per visualizzare il fattoriale di un numero dopo la syntax AT & t. Ma non funziona. C’è il mio codice

.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 

Questo programma non sta stampando nulla, ho anche usato gdb per visualizzarlo funziona bene fino alla funzione loop, ma quando entra in qualche valore casuale inizia a entrare in vari register. Aiutami a fare il debug in modo che possa stampare fattoriale.

Molte cose:

0) Immagino che questo sia l’ambiente Linux 64b, ma dovresti averlo indicato (se non lo è, alcuni dei miei punti non saranno validi)

1) int 0x80 è una chiamata 32b, ma stai usando i registri 64b, quindi dovresti usare syscall (e diversi argomenti)

2) int 0x80, eax=4 richiede che ecx contenga l’indirizzo della memoria, dove il contenuto è memorizzato, mentre gli dai il carattere ASCII in ecx = accesso alla memoria illegale (la prima chiamata dovrebbe restituire errore, cioè eax è un valore negativo) . O usare strace dovrebbe rivelare gli argomenti errati + l’errore restituito.

3) perché addq $4, %rsp ? Non ha senso per me, stai danneggiando rsp , quindi il prossimo pop rcx mostrerà un valore sbagliato, e alla fine ti ritroverai a “salire” nello stack.

… forse un po ‘di più, non ho fatto il debug, questa lista è solo leggendo la fonte (quindi potrei anche sbagliarmi su qualcosa, anche se sarebbe raro).

A proposito il tuo codice funziona . Semplicemente non fa quello che ti aspettavi. Ma funziona bene, proprio come la CPU è progettata e precisamente ciò che hai scritto nel codice. Che sia in grado di raggiungere ciò che volevi o avesse senso, è un argomento diverso, ma non incolpare l’HW o l’assemblatore.

… Posso fare una rapida intuizione su come la routine può essere risolta (solo hack-fix parziale, ha ancora bisogno di riscrivere per syscall sotto 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 

Di nuovo non ho provato me stesso, semplicemente scrivendolo dalla testa, quindi fammi sapere come è cambiata la situazione.

Come sottolinea @ ped7g, stai facendo diverse cose sbagliate: utilizzando l’ int 0x80 32 bit nel codice a 64 bit e passando i valori dei caratteri invece dei puntatori alla chiamata di sistema write() .

Ecco come stampare un intero in Linux a 64 bit, il modo semplice e un po ‘efficiente. Vedi Perché GCC usa la moltiplicazione per un numero strano nell’implementazione della divisione integer? per evitare div r64 per divisione per 10, perché è molto lento (da 21 a 83 cicli su Intel Skylake ). Un inverso moltiplicativo renderebbe questa funzione effettivamente efficiente, non solo “un po ‘”. (Ma ovviamente ci sarebbe ancora spazio per le ottimizzazioni …)

Le chiamate di sistema sono costose (probabilmente migliaia di cicli per write(1, buf, 1) ) e fanno un syscall all’interno dei passi del ciclo sui registri, quindi è scomodo, goffo e inefficiente. Dovremmo scrivere i caratteri in un piccolo buffer, in ordine di stampa (cifra più significativa all’indirizzo più basso) e fare una singola chiamata di sistema write() su quello.

Ma poi abbiamo bisogno di un buffer. La lunghezza massima di un intero a 64 bit è di soli 20 cifre decimali, quindi possiamo semplicemente usare dello spazio di stack. In x86-64 Linux, possiamo usare lo stack space sotto RSP (fino a 128B) senza “riservarlo” modificando RSP. Questo è chiamato la zona rossa .

Invece di codificare i numeri di chiamata di sistema, l’utilizzo di GAS semplifica l’uso delle costanti definite nei file .h . Nota mov $__NR_write, %eax vicino alla fine della funzione. L’ABI SystemV x86-64 passa gli argomenti di chiamata di sistema in registri simili alla convenzione di chiamata di funzione . (Quindi è totalmente diverso registri dal int 0x80 32 bit.)

 #include  // 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 

Per testare questa funzione, inserisco questo nello stesso file per chiamarlo e uscire:

 .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) 

Ho costruito questo in un binario statico (senza 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 

Correlato: loop x86-32 esteso a precisione di Linux che stampa 9 cifre decimali da ciascun “arto” a 32 bit: vedi .toascii_digit: nella mia risposta al codice-golf Extreme Fibonacci . È ottimizzato per le dimensioni del codice (anche a scapito della velocità), ma ben commentato.

Usa div come fai tu, perché è più piccolo che usare un inverso moltiplicativo veloce). Utilizza il loop per il loop esterno (su un numero intero multiplo per una precisione estesa), sempre per la dimensione del codice al costo della velocità .

Usa l’ int 0x80 32 bit e stampa in un buffer che conteneva il “vecchio” valore di Fibonacci, non quello corrente.


Un altro modo per ottenere un’efficiente asm è da un compilatore C. Per solo il ciclo su cifre, guarda cosa produce gcc o clang per questa sorgente C (che è fondamentalmente ciò che sta facendo asm). Il Godbolt Compiler explorer rende facile provare diverse opzioni e diverse versioni del compilatore.

Vedi gcc7.2 -O3 asm output che è quasi una sostituzione drop-in per il ciclo in print_uint64 (perché ho scelto gli argomenti per andare negli stessi registri):

 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); } 

Ho testato le prestazioni su uno Skylake i7-6700k commentando le istruzioni syscall e inserendo un ciclo ripetuto attorno alla chiamata di funzione. La versione con mul %rcx / shr $3, %rdx è circa 5 volte più veloce rispetto alla versione con div %rcx per la memorizzazione di una stringa 10120123425329922 lunga ( 10120123425329922 ) in un buffer. La versione div correva a 0,25 istruzioni per orologio, mentre la versione mul correva a 2,65 istruzioni per orologio (anche se richiede molte più istruzioni).

Potrebbe valere la pena srotolare per 2, e fare una divisione per 100 e dividere il resto in 2 cifre. Ciò fornirebbe un parallelismo molto più efficace a livello di istruzioni, nel caso in cui i colli di bottiglia della versione più semplice shr + shr +. La catena di operazioni di moltiplicazione / spostamento che porta val a zero sarebbe lunga la metà, con più lavoro in ogni breve catena di dipendenze indipendenti per gestire un resto 0-99.