web-dev-qa-db-fra.com

Comment imprimer un entier dans la programmation au niveau de l'assemblage sans printf à partir de la bibliothèque c?

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?

14
Kaustav Majumder

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.

12
Ira Baxter

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.

2
Peter Cordes

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

1
Gondil