III. Assembleur : on continue▲
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 :
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 où 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 :
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 où 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