Come si stampa un numero intero in Programmazione a livello di assieme senza printf dalla libreria c?

Qualcuno può dirmi il codice puramente di assemblaggio per la visualizzazione del valore in un registro in formato decimale? Si prega di non suggerire l’uso di printf hack e quindi di compilare con gcc.

Descrizione:

Bene, ho fatto alcune ricerche e alcuni esperimenti con NASM e ho immaginato che avrei potuto usare la funzione printf dalla libreria c per stampare un intero. Ho fatto questo compilando il file object con il compilatore GCC e tutto funziona correttamente.

Tuttavia, ciò che voglio ottenere è stampare il valore memorizzato in qualsiasi registro nel formato decimale.

Ho fatto qualche ricerca e ho calcolato che il vettore di interrupt 021h per la riga di comando DOS può visualizzare stringhe e caratteri mentre 2 o 9 si trovano nel registro ah e i dati sono nel dx.

Conclusione:

Nessuno degli esempi che ho trovato mostra come visualizzare il valore del contenuto di un registro in forma decimale senza utilizzare la funzione printf della libreria C. Qualcuno sa come farlo in assemblea?

È necessario scrivere una routine di conversione da binario a decimale, quindi utilizzare le cifre decimali per produrre “caratteri numerici” da stampare.

Devi presumere che qualcosa, da qualche parte, stamperà un personaggio sul tuo dispositivo di output preferito. Chiama questa subroutine “print_character”; presume che prenda un codice di carattere in EAX e conservi tutti i registri .. (Se non si dispone di una subroutine, si ha un ulteriore problema che dovrebbe essere alla base di una domanda diversa).

Se si dispone del codice binario per una cifra (ad esempio, un valore compreso tra 0 e 9) in un registro (ad esempio, EAX), è ansible convertire tale valore in un carattere per la cifra aggiungendo il codice ASCII per il carattere “zero” al registro. Questo è semplice come:

add eax, 0x30 ; convert digit in EAX to corresponding character digit 

È quindi ansible chiamare print_character per stampare il codice del carattere numerico.

Per generare un valore arbitrario, è necessario selezionare cifre e stamparle.

Il prelievo di cifre richiede fondamentalmente di lavorare con poteri di dieci. È più facile lavorare con una potenza di dieci, ad es. 10 se stessa. Immaginiamo di avere una routine di divisione per 10 che ha un valore in EAX e ha prodotto un quoziente in EDX e un resto in EAX. Lascio che sia un esercizio per te per capire come implementare una tale routine.

Quindi una semplice routine con l’idea giusta è quella di produrre una cifra per tutte le cifre che il valore potrebbe avere. Un registro a 32 bit memorizza valori fino a 4 miliardi, quindi è ansible ottenere 10 cifre stampate. Così:

  mov eax, valuetoprint mov ecx, 10 ; digit count to produce loop: call dividebyten add eax, 0x30 call printcharacter mov eax, edx dec ecx jne loop 

Funziona … ma stampa le cifre in ordine inverso. Oops! Bene, possiamo sfruttare lo stack pushdown per memorizzare le cifre prodotte e quindi saltarle in ordine inverso:

  mov eax, valuetoprint mov ecx, 10 ; digit count to generate loop1: call dividebyten add eax, 0x30 push eax mov eax, edx dec ecx jne loop1 mov ecx, 10 ; digit count to print loop2: pop eax call printcharacter dec ecx jne loop2 

Lasciato come esercizio al lettore: sopprimere gli zeri iniziali. Inoltre, dal momento che stiamo scrivendo caratteri numerici in memoria, invece di scriverli nello stack potremmo scriverli in un buffer e quindi stampare il contenuto del buffer. Anche lasciato come esercizio al lettore.

La maggior parte dei sistemi operativi / ambienti non ha una chiamata di sistema che accetta numeri interi e li converte in valori decimali. Devi farlo tu stesso prima di inviare i byte al sistema operativo, o copiarli direttamente nella memoria video, o disegnare i glifi dei caratteri corrispondenti nella memoria video …

Di gran lunga il modo più efficiente consiste nel fare una singola chiamata di sistema che fa l’intera stringa in una sola volta, perché una chiamata di sistema che scrive 8 byte è fondamentalmente lo stesso costo della scrittura di 1 byte.

Ciò significa che abbiamo bisogno di un buffer, ma questo non aggiunge molto alla nostra complessità. 2 ^ 32-1 è solo 4294967295, che è solo 10 cifre decimali. Il nostro buffer non ha bisogno di essere grande, quindi possiamo semplicemente usare lo stack.

Il solito algoritmo produce prima le cifre LSD. Poiché l’ordine di stampa è MSD, possiamo iniziare alla fine del buffer e lavorare all’indietro. Per stampare o copiare altrove, tieni traccia di dove inizia e non preoccuparti di portarlo all’inizio di un buffer fisso. Non c’è bisogno di pasticciare con il push / pop per invertire qualcosa, basta produrlo all’indietro in primo luogo.

 char *itoa_end(unsigned long val, char *p_end) { const unsigned base = 10; char *p = p_end; do { *--p = (val % base) + '0'; val /= base; } while(val); // runs at least once to print '0' for val=0. // write(1, p, p_end-p); return p; // let the caller know where the leading digit is } 

gcc / clang fa un lavoro eccellente, usando un moltiplicatore di costanti magiche invece di div per dividere per 10 in modo efficiente. ( Explorer del compilatore Godbolt per output asm).

Ecco una semplice versione NASM commentata, usando div (codice lento ma più breve) per interi senza segno a 32 bit e una chiamata di sistema di write Linux. Dovrebbe essere facile ecx codice in modalità a 32 bit semplicemente cambiando i registri in ecx invece di rcx . (Dovresti anche salvare / ripristinare esi per le consuete convenzioni di chiamata a 32 bit, a meno che tu non stia facendo di questo una macro o una funzione di solo uso interno).

La parte chiamata di sistema è specifica per Linux a 64 bit. Sostituiscilo con ciò che è appropriato per il tuo sistema, ad esempio chiama la pagina VDSO per chiamate di sistema efficienti su Linux a 32 bit o usa int 0x80 direttamente per chiamate di sistema inefficienti. Vedi le convenzioni di chiamata per le chiamate di sistema a 32 e 64 bit su Unix / Linux .

 ALIGN 16 ; void print_uint32(uint32_t edi) ; x86-64 System V calling convention. Clobbers RSI, RCX, RDX, RAX. global print_uint32 print_uint32: mov eax, edi ; function arg mov ecx, 0xa ; base 10 push rcx ; newline = 0xa = base mov rsi, rsp sub rsp, 16 ; not needed on 64-bit Linux, the red-zone is big enough. Change the LEA below if you remove this. ;;; rsi is pointing at '\n' on the stack, with 16B of "allocated" space below that. .toascii_digit: ; do { xor edx, edx div ecx ; edx=remainder = low digit = 0..9. eax/=10 ;; DIV IS SLOW. use a multiplicative inverse if performance is relevant. add edx, '0' dec rsi ; store digits in MSD-first printing order, working backwards from the end of the string mov [rsi], dl test eax,eax ; } while(x); jnz .toascii_digit ;;; rsi points to the first digit mov eax, 1 ; __NR_write from /usr/include/asm/unistd_64.h mov edi, 1 ; fd = STDOUT_FILENO lea edx, [rsp+16 + 1] ; yes, it's safe to truncate pointers before subtracting to find length. sub edx, esi ; length, including the \n syscall ; write(1, string, digits + 1) add rsp, 24 ; undo the push and the buffer reservation ret 

Dominio pubblico. Sentiti libero di copiare / incollare tutto ciò su cui stai lavorando. Se si rompe, puoi tenere entrambi i pezzi.

Ed ecco il codice per chiamarlo in un ciclo che conta fino a 0 (incluso 0). Metterlo nello stesso file è conveniente.

 ALIGN 16 global _start _start: mov ebx, 100 .repeat: lea edi, [rbx + 0] ; put whatever constant you want here. call print_uint32 dec ebx jge .repeat xor edi, edi mov eax, 231 syscall ; sys_exit_group(0) 

Assemblare e colbind con

 yasm -felf64 -Worphan-labels -gdwarf2 print-integer.asm && ld -o print-integer print-integer.o ./print_integer 100 99 ... 1 0 

Usa strace per vedere che le uniche chiamate di sistema che questo programma fa sono write() e exit() . (Vedi anche i suggerimenti gdb / debug nella parte inferiore del wiki del tag x86 , e gli altri collegamenti lì.)


Ho postato una versione di syntax AT & T di questo per interi a 64 bit come risposta a Stampare un intero come una stringa con syntax AT & T, con chiamate di sistema Linux invece di printf . Vedi questo per ulteriori commenti sulle prestazioni e un benchmark tra div e il codice generato dal compilatore usando mul .

Non posso commentare quindi postare una risposta in questo modo. @Ira Baxter, risposta perfetta Voglio solo aggiungere che non hai bisogno di dividere 10 volte come hai postato impostando register cx sul valore 10. Basta dividere il numero in ax fino a “ax == 0”

 loop1: call dividebyten ... cmp ax,0 jnz loop1 

Devi anche memorizzare quante cifre ci sono state nel numero originale.

  mov cx,0 loop1: call dividebyten inc cx 

Ad ogni modo, Ira Baxter mi ha aiutato in pochi modi su come ottimizzare il codice 🙂

Questo non riguarda solo l’ottimizzazione ma anche la formattazione. Quando si desidera stampare il numero 54, si desidera stampare 54 non 0000000054 🙂

Suppongo che vuoi stampare il valore su stdout? se questo è il caso
devi usare una chiamata di sistema per farlo. Le chiamate di sistema dipendono dal sistema operativo.

per esempio Linux: Linux Call Table

Il programma Hello World in questo tutorial può darti alcuni spunti.

1 -9 sono 1 -9. dopo, ci dev’essere anche una conversione che non conosco. Supponiamo che tu abbia 41H in AX (EAX) e che tu voglia stampare un 65, non “A” senza fare alcune chiamate di servizio. Penso che tu debba stampare una rappresentazione di carattere di un 6 e di un 5 qualunque cosa possa essere. Ci deve essere un numero costante che può essere aggiunto per arrivarci. È necessario un operatore modulo (tuttavia lo si fa in assembly) e il ciclo per tutte le cifre.

Non sono sicuro, ma questa è la mia ipotesi.