Quelqu'un peut-il me dire le code purement assemblage pour afficher la valeur dans un registre au format décimal? Veuillez ne pas suggérer d'utiliser le hack printf puis de le compiler avec gcc.
La description:
Eh bien, j’ai fait quelques recherches et expérimenté avec NASM et j’ai pensé que je pouvais utiliser la fonction printf de la bibliothèque c pour imprimer un entier. Je l'ai fait en compilant le fichier objet avec le compilateur GCC et tout fonctionne assez bien.
Cependant, ce que je veux réaliser est d’imprimer la valeur stockée dans n’importe quel registre sous forme décimale.
J'ai fait des recherches et j'ai pensé que le vecteur d'interruption 021h pour la ligne de commande DOS pouvait afficher des chaînes et des caractères alors que 2 ou 9 se trouvaient dans le registre ah et que les données se trouvaient dans le fichier dx.
Conclusion:
Aucun des exemples que j'ai trouvés n'a montré comment afficher la valeur de contenu d'un registre sous forme décimale sans utiliser printf de la bibliothèque C. Est-ce que quelqu'un sait comment faire cela en assemblée?
Vous devez écrire une routine de conversion binaire en décimal, puis utiliser les chiffres décimaux pour produire des "caractères numériques" à imprimer.
Vous devez supposer que quelque chose, quelque part, imprimera un caractère sur le périphérique de sortie de votre choix. Appelez ce sous-programme "print_character"; suppose qu’il faut un code de caractère dans EAX et conserve tous les registres. (Si vous n’avez pas un tel sous-programme, vous avez un problème supplémentaire qui devrait être à la base d’une autre question).
Si vous avez le code binaire pour un chiffre (par exemple, une valeur de 0 à 9) dans un registre (par exemple, EAX), vous pouvez convertir cette valeur en un caractère pour le chiffre en ajoutant le code ASCII. pour le caractère "zéro" au registre. C'est aussi simple que:
add eax, 0x30 ; convert digit in EAX to corresponding character digit
Vous pouvez ensuite appeler print_character pour imprimer le code de caractère numérique.
Pour générer une valeur arbitraire, vous devez extraire les chiffres et les imprimer.
Choisir des chiffres nécessite fondamentalement de travailler avec des puissances de dix. Il est plus facile de travailler avec une puissance de dix, par exemple 10, elle-même. Imaginez que nous ayons une routine de division par 10 qui prend une valeur dans EAX et génère un quotient dans EDX et un reste dans EAX. Je vous laisse le soin de comprendre comment mettre en œuvre une telle routine.
Ensuite, une routine simple avec la bonne idée consiste à produire un chiffre pour tous les chiffres que la valeur peut avoir. Un registre 32 bits stocke des valeurs de 4 milliards, vous pouvez donc imprimer 10 chiffres. Alors:
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
Cela fonctionne ... mais affiche les chiffres dans l'ordre inverse. Oops! Eh bien, nous pouvons tirer parti de la pile de pile pour stocker les chiffres produits, puis les extraire dans l’ordre inverse:
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
Exercice laissé au lecteur: supprimer les zéros non significatifs. De plus, puisque nous écrivons des caractères numériques dans la mémoire, au lieu de les écrire dans la pile, nous pourrions les écrire dans un tampon, puis imprimer le contenu du tampon. Aussi laissé comme un exercice au lecteur.
La plupart des systèmes d'exploitation/environnements n'ont pas d'appel système acceptant les entiers et les convertissant en décimal pour vous. Vous devez le faire vous-même avant d'envoyer les octets au système d'exploitation, de les copier vous-même dans la mémoire vidéo ou de dessiner les glyphes de police correspondants dans la mémoire vidéo ...
De loin, le moyen le plus efficace est de faire un seul appel système qui traite toute la chaîne en une fois, car un appel système qui écrit 8 octets a essentiellement le même coût que l’écriture d’un octet.
Cela signifie que nous avons besoin d'un tampon, mais cela n'ajoute rien à notre complexité. 2 ^ 32-1 correspond à 4294967295, soit 10 chiffres décimaux. Notre tampon n'a pas besoin d'être volumineux, nous pouvons donc simplement utiliser la pile.
L'algorithme habituel produit les chiffres LSD-first. Etant donné que l'ordre d'impression est d'abord MSD, nous pouvons simplement commencer à la fin du tampon et travailler en arrière. Pour imprimer ou copier ailleurs, il suffit de garder une trace du début et de ne pas s’embêter à le placer au début d’un tampon fixe. Pas besoin de jouer avec Push/pop pour inverser quoi que ce soit, il suffit de le produire à l'envers en premier.
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 fait un excellent travail, en utilisant un multiplicateur de constante magique au lieu de div
pour diviser efficacement par 10. ( Explorateur du compilateur Godbolt pour la sortie asm).
Voici une version NASM commentée simple, utilisant div
(code lent mais plus court) pour les entiers non signés 32 bits et un appel système Linux write
. Il devrait être facile de porter cela en code 32 bits en modifiant simplement les registres en ecx
au lieu de rcx
. (Vous devez également enregistrer/restaurer esi
pour les conventions d'appel 32 bits habituelles, à moins que vous ne le transformiez en une macro ou une fonction à usage interne uniquement.)
La partie appel système est spécifique à Linux 64 bits. Remplacez-le par ce qui convient à votre système, par exemple. Appelez la page VDSO pour des appels système efficaces sous Linux 32 bits ou utilisez directement int 0x80
pour les appels système inefficaces. Voir Conventions d’appel pour les appels système 32 et 64 bits sous 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
Domaine public. N'hésitez pas à copier/coller ceci dans tout ce sur quoi vous travaillez. Si ça casse, vous devez garder les deux pièces.
Et voici le code pour l'appeler dans une boucle décomptant jusqu'à 0 (y compris 0). Le mettre dans le même fichier est pratique.
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)
Assemblez et reliez avec
yasm -felf64 -Worphan-labels -gdwarf2 print-integer.asm &&
ld -o print-integer print-integer.o
./print_integer
100
99
...
1
0
Utilisez strace
pour voir que les seuls appels système effectués par ce programme sont write()
et exit()
. (Voir aussi les astuces gdb/debugging au bas du wiki x86 tag, et les autres liens.)
J'ai posté une version AT & T de cette syntaxe pour les entiers 64 bits en réponse à Impression d'un entier sous forme de chaîne avec la syntaxe AT & T, avec des appels système Linux à la place de printf . Voir cela pour plus de commentaires sur les performances, et une référence de div
par rapport au code généré par le compilateur en utilisant mul
.
Je ne peux pas commenter, alors je poste la réponse de cette façon. @ Ira Baxter, réponse parfaite Je veux juste ajouter qu'il n'est pas nécessaire de diviser 10 fois la valeur affichée pour que la valeur de registre cx soit définie sur 10. Juste diviser le nombre en ax jusqu'à "ax == 0"
loop1: call dividebyten
...
cmp ax,0
jnz loop1
Vous devez également enregistrer combien de chiffres étaient présents dans le numéro d'origine.
mov cx,0
loop1: call dividebyten
inc cx
Quoi qu'il en soit, Ira Baxter m'a aidé à optimiser le code de plusieurs manières :)
Ce n’est pas seulement une question d’optimisation, mais aussi de formatage. Lorsque vous souhaitez imprimer le numéro 54, vous souhaitez imprimer 54 et non 0000000054 :)