<-Précédent Retour à l'accueil Contact : etienne"point"sauvage"at"gmail.com Suivant->
  1. De l'entrée clavier
  2. Des fonctions
  3. De la machine
  4. De l'affichage des nombres

    Assembleur : on continue

    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.

  1. De l'entrée clavier
  2. 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.

  3. Des fonctions.
  4. 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

  5. De la machine
  6. 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, toussa.

    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 met 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, ie 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.

  7. De l'affichage des nombres
  8. 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