Assembleur Intel avec NASM


précédentsommairesuivant

III. Assembleur : on continue

49 commentaires Donner une note à l'article (5)

Relu par ClaudeLELOUP.

Petit rappel : nous en sommes à avoir un programme qui affiche une chaîne de caractères à l'écran quel que soit le système d'exploitation.

C'est, mine de rien, un excellent point de départ. Mais ça manque de... comment dire... interactivité. Ce qui serait mieux, s'pas, ce serait de dire des choses à l'ordinateur, comme lui nous en dit. Or, l'ordinateur est mal fourni : il est principalement perdu dans ses propres pensées, peu lui chaut le monde extérieur. Et donc, a fortiori, moi. Mon égoïsme est blessé, mon amour-propre traîné dans la boue, et je vais remédier à cela, non de moi de bordel de moi ! Or, autant en possibilités d'actions sur le monde, l'ordinateur est bien loti, autant en possibilités de ressentir le monde, c'est lamentable. Pensez donc, un malheureux clavier et une souris ! L'ordinateur est capable de nous balancer 2000 caractères d'un coup, alors que nous ne pouvons lui donner qu'un seul caractère à la fois ! Scandaleux. Mais on va essayer de faire quelque chose quand même.

III.1. De l'entrée clavier

Monsieur BIOS a un gestionnaire de clavier, de son petit nom INT 0x16, qui a une fonction qui teste si un caractère a été entré au clavier : 0x01. On va donc boucler sur cette fonction tant qu'on n'a pas de caractère à lire. Quand on a quelque chose dans le buffer du clavier, on va le lire avec la fonction 0x00. Histoire de s'en souvenir pour un usage ultérieur, on va le stocker en mémoire. Pour voir ce qu'on tape au clavier, on affiche ce caractère lu. On décale donc le curseur à sa position suivante, et on repart tester s'il n'y aurait pas d'autres caractères à lire. Pour s'arrêter, je décide que la touche "Entrée", caractère 13, sera la marque de la fin de l'entrée clavier. A chaque caractère, on teste alors ce marqueur de fin. S'il est atteint, on passe à la ligne suivante. Puis j'affiche la chaîne entrée. Et pour ce faire, voyons les fonctions.

III.2. Des fonctions

Dans un programme, on a souvent besoin de faire la même chose plusieurs fois. Plutôt que de réécrire l'ensemble du code à chaque fois, nous avons la possibilité de le mettre à un seul endroit, et de l'appeler au besoin : c'est une fonction. Faisons une fonction qui affiche une chaîne de caractères. Elle aura besoin de savoir quelle est la chaîne à afficher : ce sera l'adresse contenue dans SI. J'appelle cette fonction "affiche_chaine" : une fois que l'affichage sera fait, il faut revenir au programme qui appelle cette fonction : instruction RET, qu'on place à la fin du code d'affichage de la chaîne. On décale tout ce code à la fin du programme, juste avant les données, histoire qu'il ne soit pas exécuté n'importe comment. Cette fonction utilise les registres AX, BX, CX et DX. Pour se faciliter le travail, nous allons sauvegarder les valeurs que ces registres avaient avant l'appel à la fonction. Comme ça, l'appel à la fonction est neutre au niveau des registres. Pas la peine de sauvegarder SI, c'est un paramètre : on doit s'attendre à ce qu'il soit modifié par la fonction, ça permettra en outre de savoir combien de caractères ont été écrits. Pour sauvegarder les registres, nous allons utiliser la pile : à nous les messages d'insulte "stack overflow". La pile est un espace mémoire dont l'adresse de fin est fixe, et dont l'adresse de début décroit à mesure qu'on y met des données. La dernière valeur qui y est stockée est donc au début, et sera récupérée en premier. La pile commence par défaut à la fin de l'espace mémoire disponible dans notre programme. Pour y mettre une valeur, c'est l'instruction PUSH, qui met 16 bits dans la pile. Pour récupérer une valeur, POP prend 16 bits de la pile pour les mettre dans un registre.

Pour appeler la fonction, c'est CALL nom_de_la_fonction.

Voici maintenant notre programme :

 
Sélectionnez

				org 0x0100 ; Adresse de début .COM
 
				;Ecriture de la chaîne hello dans la console
				mov si, hello; met l'adresse de la chaîne à afficher dans le registre SI
				call affiche_chaine
				mov si, hello; met l'adresse de la chaîne à lire dans le registre SI
				mov ah, 0x03
				int 0x10; appel de l'interruption BIOS qui donne la position du curseur, stockée dans dx
				mov cx, 1
				attend_clavier:
				mov ah, 0x01;on teste le buffer clavier
				int 0x16
				jz attend_clavier
				;al contient le code ASCII du caractère
				mov ah, 0x00;on lit le buffer clavier
				int 0x16
				mov [si], al;on met le caractère lu dans si
				inc si
				cmp al, 13
				je fin_attend_clavier
				;al contient le code ASCII du caractère
				mov ah, 0x0A;on affiche le caractère courant cx fois
				int 0x10
				inc dl; on passe à la colonne suivante pour la position du curseur
				mov ah, 0x02;on positionne le curseur
				int 0x10
				jmp attend_clavier
				fin_attend_clavier:
				inc dh; on passe à la ligne suivante pour la position du curseur
				xor dl, dl
				mov ah, 0x02;on positionne le curseur
				int 0x10
				mov byte [si], 0;on met le caractère terminal dans si
				mov si, hello; met l'adresse de la chaîne à afficher dans le registre SI
				call affiche_chaine
				ret
 
				affiche_chaine:
				push ax
				push bx
				push cx
				push dx
				xor bh, bh; RAZ de bh, qui stocke la page d'affichage
				mov ah, 0x03
				int 0x10; appel de l'interruption BIOS qui donne la position du curseur, stockée dans dx
				mov cx, 1; nombre de fois  l'on va afficher un caractère
				affiche_suivant:
				mov al, [si];on met le caractère à afficher dans al
				or al, al;on compare al à zéro pour s'arrêter
				jz fin_affiche_suivant
				cmp al, 13
				je nouvelle_ligne
				mov ah, 0x0A;on affiche le caractère courant cx fois
				int 0x10
				inc dl; on passe à la colonne suivante pour la position du curseur
				positionne_curseur:
				inc si; on passe au caractère suivant
				mov ah, 0x02;on positionne le curseur
				int 0x10
				jmp affiche_suivant
				fin_affiche_suivant:
				pop dx
				pop cx
				pop bx
				pop ax
				ret
				nouvelle_ligne:
				inc dh; on passe à la ligne suivante
				xor dl, dl; colonne 0
				jmp positionne_curseur
				;fin de affiche_chaine
 
				hello: db 'Bonjour papi.', 13, 0
				

III.3. De la machine

On arrive à faire des choses, mais ce qui serait bien, maintenant, c'est d'avoir une idée de la machine sur laquelle le programme s'exécute. C'est plus compliqué, parce que toutes ces informations ne vont pas nécessairement être stockées aux mêmes endroits selon, j'imagine, le constructeur, le type de matériel, ce genre de choses.

III.3.a. Des drapeaux

Le processeur dispose d'un registre de drapeaux. Il faut voir un drapeau comme celui d'un arbitre ou d'un commissaire aux courses. Si le drapeau est invisible, il n'y a rien à signaler. Un drapeau levé signifie qu'il y a quelque chose : le vert à pois bleus signifie qu'un martien atterrit, le jaune que le n°13 vient de taper le n°6 d'en face, celui à damier que Senna a rencontré un mur, etc. En informatique, un drapeau est représenté par un bit. Ce bit à 0 signifie qu'il n'y a rien à signaler, le drapeau est invisible. Ce bit à 1 signifie que le drapeau est visible, levé. Non, malheureusement, le processeur n'a pas de drapeau pour les aliens. Voici des exemples de drapeaux dont le processeur dispose :

  • CF, Carry Flag, drapeau de retenue : cela symbolise la retenue, comme à l'école ;
  • ZF, Zero Flag, drapeau de zéro : vous venez de calculer la tête à toto ;
  • PF, Parity Flag, drapeau de parité : votre résultat est pair. Le virus Parity Boot changeait la valeur de ce drapeau, le canaillou ;
  • SF, Sign Flag, drapeau de signe : votre résultat est négatif ;
  • DF, Direction Flag, drapeau de direction : vous allez dans le sens des adresses décroissantes, c'est le voyant de recul de votre voiture ;
  • OF, Overflow Flag, drapeau de dépassement de capacité : votre résultat ne tient plus dans le type que vous avez choisi.

Ces drapeaux sont utilisés par les instructions de saut conditionnel. JZ, par exemple, signifie Jump if Zero et saute (à l'adresse donnée en paramètre) si le drapeau ZF est levé. Certains appels de fonction changent l'état des drapeaux selon une logique qui leur est propre, et les drapeaux perdent ainsi leur signification première, un peu comme si l'arbitre sortait, au lieu du carton rouge attendu, un ananas violet. Ca met le bazar, c'est drôle, les informaticiens adorent. Oui, nous aussi on jouera avec. Des drapeaux, le processeur en a tout un registre, ce qui fait, au maximum ...

...

...

...

16 ! Nos registres font 16 bits, ce qui fait 16 drapeaux possibles au maximum. Et le registre des drapeaux s'appelle, je vous le donne en mille : FLAGS. Quelque chose me dit que le processeur est anglo-saxon.

III.3.b. De la configuration système

Alors, comme point de départ, j'ai trouvé l'interruption 0x11, qui remplit AX avec des flags et des nombres. Appelons donc 0x11 et analysons AX. On utilise l'instruction TEST, qui lève des drapeaux en fonction du résultat du ET logique entre les opérateurs. TEST ne change pas les valeurs des opérandes, ce qui est pratique, ça évite d'appeler l'interruption à chaque fois qu'on teste une partie de la valeur de retour. On teste avec un nombre binaire, préfixé 0b : ça permet de voir facilement le bit qui nous intéresse. Par exemple, si on teste le premier bit, on teste avec 0b0001 : le résultat sera zéro, soit ZF à 1, ou bien un, soit ZF à 0. Une instruction de saut conditionnel fera le reste, et AX sera prêt à être testé avec le deuxième bit, soit 0b0010.

Les troisième et quatrième bits commencent à être tordus : il s'agit de la mémoire disponible, comptée en nombre de blocs de 16 kilooctets. Mais comme un ordinateur sans mémoire vive ne sert à rien (merci monsieur Turing), le premier bloc n'est pas compté : il y a au moins 16 ko (kilooctets) de mémoire dans un ordinateur. Il faut donc ajouter 1 au nombre de pages de 16 ko. Les valeurs disponibles sur 2 bits sont : 0, 1, 2 et 3. Notre nombre de pages est alors de 1, 2, 3 ou 4, soit 16, 32, 48 ou 64 ko.

Il n'y a pas de test à faire, il faut récupérer le nombre. Pour le récupérer, il faut transformer notre retour afin qu'il soit notre nombre de pages, i.e. décaler ses bits de deux bits vers la droite (après suppression des bits qui ne nous intéressent pas par un AND). Il s'agit de l'instruction SHR, pour SHift Right (décalage à droite). Après, on incrémente pour prendre en compte la page gratis, et il faudrait multiplier par 16 pour avoir le nombre de ko disponibles. Une multiplication, c'est bien. Mais mieux, c'est le décalage de bits. Pour la faire courte, un décalage de 1 bit à droite correspond à une division par 2, tandis qu'un décalage de 1 bit à gauche fait une multiplication par 2. Pour multiplier par 16, il faut multiplier par 2 quatre fois, donc décaler de 4 bits vers la gauche, soit SHL ax, 4.

Ayant récupéré notre quantité de mémoire disponible, nous souhaiterions l'afficher. Or, il s'agit d'un nombre, pas d'une chaîne de caractères. "R" ne nous apprendrait rien. Il faut transformer notre nombre en chaîne de caractères.

III.4. De l'affichage des nombres

Faisons une fonction de transformation de nombre en chaîne de caractères. AX contient le nombre à transformer, et SI l'adresse du début de la chaîne. On utilise l'algorithme appelé "algorithme des divisions successives". On va diviser notre nombre par 10 jusqu'à ce que le quotient soit 0, et stocker chaque reste comme étant un chiffre à afficher. Les chiffres ASCII ayant le bon goût d'être dans l'ordre, il suffit d'ajouter "0" à un chiffre pour avoir son caractère.

Alors, à la main : prenons 32. Divisons 32 par 10 : quotient 3, reste 2. 2 est le chiffre à afficher. Stockons 2 + "0". Divisons 3 par 10 : reste 3, quotient 0. Stockons 3 + "0". Le quotient est 0, fin de l'algorithme. La chaîne est dans l'ordre inverse. Si on l'a stockée dans la pile, en dépilant, on sera dans le bon ordre. Ne reste plus qu'à ajouter le zéro terminal et à l'afficher.

Une division se fait par l'instruction DIV suivie du diviseur, qui doit être dans un registre. DIV divise AX par le diviseur, et stocke le résultat dans AH pour le reste et AL pour le quotient. Note : avec ça, on ne pourra pas traiter de nombre plus grand que 2550.

Le code complet :

 
Sélectionnez

				org 0x0100 ; Adresse de début .COM
 
				;Ecriture de la chaîne hello dans la console
				mov si, hello; met l'adresse de la chaîne à afficher dans le registre SI
				call affiche_chaine
				mov si, hello; met l'adresse de la chaîne à lire dans le registre SI
				mov ah, 0x03
				int 0x10; appel de l'interruption BIOS qui donne la position du curseur, stockée dans dx
				mov cx, 1
				attend_clavier:
				mov ah, 0x01;on teste le buffer clavier
				int 0x16
				jz attend_clavier
				;al contient le code ASCII du caractère
				mov ah, 0x00;on lit le buffer clavier
				int 0x16
				mov [si], al;on met le caractère lu dans si
				inc si
				cmp al, 13
				je fin_attend_clavier
				;al contient le code ASCII du caractère
				mov ah, 0x0A;on affiche le caractère courant cx fois
				int 0x10
				inc dl; on passe à la colonne suivante pour la position du curseur
				mov ah, 0x02;on positionne le curseur
				int 0x10
				jmp attend_clavier
				fin_attend_clavier:
				inc dh; on passe à la ligne suivante pour la position du curseur
				xor dl, dl
				mov ah, 0x02;on positionne le curseur
				int 0x10
				mov byte [si], 0;on met le caractère terminal dans si
				mov si, hello; met l'adresse de la chaîne à afficher dans le registre SI
				call affiche_chaine
 
				int 0x11
				test ax, 0b0001
				jnz lecteurs_disquette
				mov si, pas_disquette
				call affiche_chaine
				test_coprocesseur:
				test ax, 0b0010
				jnz coprocesseur_present
				mov si, pas_coprocesseur
				call affiche_chaine
				test_memoire:
				and ax, 0b1100
				shr ax, 2
				inc ax; une zone mémoire est donnée gratis.
				shl ax, 4; les zones mémoires sont comptées par paquets de 16 ko
				mov si, hello
				call nombre_vers_chaine
				mov si, hello
				call affiche_chaine
				mov si, memoire_dispo
				call affiche_chaine
				ret
 
				lecteurs_disquette:
				mov si, disquettes
				call affiche_chaine
				jmp test_coprocesseur
 
				coprocesseur_present:
				mov si, coprocesseur
				call affiche_chaine
				jmp test_memoire
 
				nombre_vers_chaine:
				push bx
				push cx
				push dx
				mov bl, 10
				mov cx, 1
				xor dh, dh
				stocke_digit:
				div bl
				mov dl, ah
				push dx ;sauve le reste dans la pile
				inc cx
				xor ah, ah
				or al, al
				jne stocke_digit
 
				;Affichage du chiffre
				boucle_digit:
				loop affiche_digit
				mov byte [si], 0
				pop dx
				pop cx
				pop bx
				ret
 
				affiche_digit:
				pop ax
				add ax, '0'
				mov [si], al
				inc si
				jmp boucle_digit
				;fin nombre_vers_chaine
 
				affiche_chaine:
				push ax
				push bx
				push cx
				push dx
				xor bh, bh; RAZ de bh, qui stocke la page d'affichage
				mov ah, 0x03
				int 0x10; appel de l'interruption BIOS qui donne la position du curseur, stockée dans dx
				mov cx, 1; nombre de fois  l'on va afficher un caractère
				affiche_suivant:
				mov al, [si];on met le caractère à afficher dans al
				or al, al;on compare al à zéro pour s'arrêter
				jz fin_affiche_suivant
				cmp al, 13
				je nouvelle_ligne
				mov ah, 0x0A;on affiche le caractère courant cx fois
				int 0x10
				inc dl; on passe à la colonne suivante pour la position du curseur
				positionne_curseur:
				inc si; on passe au caractère suivant
				mov ah, 0x02;on positionne le curseur
				int 0x10
				jmp affiche_suivant
				fin_affiche_suivant:
				pop dx
				pop cx
				pop bx
				pop ax
				ret
				nouvelle_ligne:
				inc dh; on passe à la ligne suivante
				xor dl, dl; colonne 0
				jmp positionne_curseur
				;fin de affiche_chaine
 
				disquettes: db 'Lecteur(s) de disquette', 13, 0
				pas_disquette: db 'Pas de lecteur de disquette', 13, 0
				coprocesseur: db 'Coprocesseur arithmétique', 13, 0
				pas_coprocesseur: db 'Pas de coprocesseur', 13, 0
				memoire_dispo: db ' ko.', 13, 0
				hello: db 'Bonjour papi.', 13, 0
				

précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2011 Etienne Sauvage. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.