L'assembleur par les plantes

 

La fois dernière, je vous avais promis de vous expliquer quelque chose qui vous intrigue peut-être : les Input/Output (ou Entrées/Sorties).

Malheureusement, il n'est pas facile de trouver un exemple concret et simple mettant en oeuvre des I/O, comme vous le comprendrez en lisant la suite.

C'est pourquoi le programme d'exemple de ce texte n'aura rien à voir avec les I/O.

J'avais tout d'abord commencé par me lancer dans l'explication du 8242, fermement convaincu de l'existence d'un profond intérêt ludique dans un programme qui ferait clignoter les diodes du clavier. Dommage pour les techno freaks qui perdent ainsi un merveilleux stroboscope psychedélique qui aurait assuré une super ambiance planante à leur prochaine soirée rave (à condition de se tenir le nez collé contre le clavier), mais j'ai finalement écarté ce projet.

J'avais ensuite essayé de faire un petit jeu d'aventure tout en texte, mais le programme présentait trente fois plus de données que de code.

Finalement, je suis arrivé à un consensus assez comique qui égaiellera sans doute vos séances d'informatique à supelec, à la fac, au lycée, au collège, a la maternelle, ou au bureau.

Mais ca c'est pour la seconde partie de soirée. Pour l'instant...

 

Le champ d'entrées/sorties

Comme vous vous en souvenez surement, le processeur et les divers composants de votre PC sont reliés par le bus. Lorsque le processeur veut lire ou écrire en mémoire, par exemple, il doit envoyer sur le bus un signal identifiant la mémoire comme destinataire, l'adresse concernée, et éventuellement lors d'une écriture les données à écrire.

Et inversement la mémoire peut renvoyer des données sur le bus pour le processeur.

Mais de nombreux autres composants ont aussi besoin de communiquer avec le processeur, comme les cartes d'extension et tout un tas d'autres trucs. C'est cette communication que l'on appelle entrées/sorties (I/O pour faire plus court).

Périphériques accédés en I/O

Il y a deux façons d'accéder aux périphériques pour le processeur. La plus simple à expliquer (et donc à comprendre) est l'accés en I/O.

C'est le mode d'accés naturel aux périphériques. Avec cette méthode, le processeur envoit sur le bus comme pour la mémoire une adresse puis éventuellement une valeur de donnée s'il s'agit d'une opération d'écriture.

La différence c'est qu'ici un signal prévient les composants reliés au bus que le message ne s'applique pas à la mémoire (il est très schématique mon bus). Le composant qui se reconnaîtra dans l'adresse (que l'on appelle aussi "un port" lorsque c'est une adresse en I/O) prendra les données sur le bus et enverra éventuelement d'autres données au processeur (pour une opération de lecture).

Vous avez déjà entendu parler de ça lorsque vous avez configuré votre carte sonore par exemple : il faut lui attribuer une adresse (220h généralement). Si deux cartes utilisent les mêmes adresses, il y a conflit.

Les instructions susceptibles de provoquer une telle opération en I/O sont IN et OUT.

IN sert à lire une adresse I/O. La destination est toujours l'accumulateur. L'adresse peut être 8 ou 16 bits, et doit être contenue dans DX. Dans le cas d'une adresse 8 bits, il est également possible d'écrire l'adresse directement en valeur immédiate.

Exemples :

         IN AL,5Fh  /  IN EAX,DX  /  IN AX,60h  /  etc...

OUT fait tout le contraire, et réalise une écriture dans un port. Une fois encore le numéro du port (l'adresse) doit être contenu dans DX sauf s'il est inférieur à 256 auquel cas il peut figurer dans l'instruction en tant que valeur immédiate, et l'accumulateur contient la valeur à envoyer (AL, AX ou EAX).

Exemples :

         OUT 5Fh,AL  /  OUT DX,EAX  /  OUT 60h,AX  /  etc...

Notez que, comme pour la mémoire, les I/O se font en mode little endian, c'est à dire avec les octets les moins significatifs sur les adresses les plus petites. En d'autres termes, si vous envoyez le double-mot 12345678h à l'adresse 1F0h (ne le faites pas surtout, à cette adresse il y à le contrôleur de votre disque dur !), l'adresse 1F0h recevra l'octet 78h, l'adresse 1F1h l'octet 56h, l'adresse 1F2h l'octet 34, et l'adresse 1F3h l'octet 12h.

Je vous rappelle une dernière fois que si cette norme vous semble illogique c'est que l'on écrit les nombres en notation Arabe dans un sens illogique. Na. C'est comme de représenter la mémoire de bas en haut : usuel mais idiot.

Au passage, cet exemple nous apprend une seconde chose : il ne faut pas envoyer n'importe quoi n'importe où ! Par exemple, faites joujou avec les ports 1F0 à 1F8 et vous courrez le risque d'écrire un cylindre de votre disque dur, de reformater une piste, voire pire (quelqu'un se vantait un jour d'avoir par mégarde "planté" les têtes de lecture sur la surface de son disque). Evidement j'exagère le danger, mais bon, faites gaffe, il y a des composants qu'il vaut mieux ne pas trop chatouiller (principalement le contrôleur de disque et la CMOS).

Pour finir, veillez à ne pas mélanger le concept d'adresse quand il s'agit de mémoire et quand il s'agit de ports, car les deux n'ont rien à voir. Les numéros de ports sont toujours au maximum sur 16 bits, et il n'est évidemment pas question de segmentation. Pour vous éviter des malentendus, j'appelerais dorénavent toujours "port" une adresse I/O. D'autant qu'au prochain paragraphe le risque de cafouillage augmente...

 

Périphériques accédés comme de la mémoire

Cette méthode est appliquée lorsque le nombre d'information à échanger est très important. Dans ce cas les 16 pauvres fils d'adresses utilisés lors des accés en I/O sont insuffisants (par exemple les cartes VGA utiliseraient toutes les adresses I/O possibles rien que pour l'écran) et la carte d'extension peut être accédée par le processeur comme de la mémoire.

On dit dés lors que la carte est mappée en mémoire car elle est vue par le processeur dans son espace mémoire. L'interêt est de pouvoir affecter plus simplement le travail du composant en question, car on peut alors utiliser toutes les instructions qui habituellement modifient la mémoire (MOV et toute autre instruction pouvant avoir une opérande de type mémoire).

Deux choses que vous connaissez probablement fonctionnent comme ceci : le BIOS de la carte vidéo, qui est une puce contenant de la mémoire morte (qui garde son contenu après extinction de l'alimentation), et la mémoire écran de la carte vidéo. Certains contrôleurs de disques durs peuvent aussi utiliser ce principe.

Du point de vue du processeur, c'est de la mémoire tout à fait normale et il peut parfois même y exécuter du code. Malheureusement, la plupart des composants périphériques mappés en mémoire sont plus lents à réagir que la mémoire centrale et c'est pourquoi tous les BIOS proposent de se recopier en mémoire centrale pour accélerer les routines qu'ils contiennent (principe de la "shadow RAM").

Les cartes vidéos, pour la grande majorités, fonctionnent aussi suivant ce principe de mapping (CGA, EGA, VGA, SVGA, et d'autres). Dans ce cas il est interressant de noter une différence avec le BIOS : ici, les valeurs envoyées et recues subissent de nombreux traitements, car pensez qu'une simple VGA doit gerer 256Ko en n'adressant que 64Ko d'adresses. Comme pour le BIOS, la plupart des cartes vidéos sont plus lentes que la mémoire centrale.

 

LE GADGET DU MOIS

Vous devez commencer à entrevoir pourquoi j'avais du mal à trouver un exemple simple d'utilisation d'I/O. En effet, utiliser les I/O c'est converser avec un composant particulier qui ne produit généralement pas d'effet sympatique apte à interresser les néophytes que nous sommes.

J'ai bien vu tout à l'heure que cela ne vous excitait pas outre mesure que l'on se tape la discut' avec le contrôleur de votre disque dur...

C'est pourquoi nous allons recommencer à programmer un petit truc plus sympa. Mais après un super calculateur de nombres premiers dans le Rep6 et la sensationnelle simulation de moto du Rep7, on comprend que j'ai eu du mal à trouver un programme qui fasse transpirer, à travers cet article et par delà lui, et ceci entre nous en toute modestie, un cheminement qui fait parvenir la recherche en analyse de systèmes informatiques à une nouvelle étape radicalement décisive.

Je veux parler, naturellement, d'un générateur aléatoire de fautes de frappes.

Oui, bon, je vous l'accorde, ce n'est pas d'un interêt national, mais l'avantage d'un tel programme est qu'il n'y a pas de graphismes mais plutôt de la programation système. Pour une initiation à l'assembleur, c'est préférable, même si je sais pertinemment bien que la seule chose qui interesse une majorité d'entre vous c'est d'effectuer quelques débilités graphiques.

ENCORE UN DETOURNEMENT DE CLAVIER

Pour générer des fautes de frappes, il y a trois solutions :

S'il vous manque du vocabulaire, suivez le guide :

Evidement, de ces trois solutions, nous retiendrons la dernière, car gérer soi-même le buffer clavier est trop complexe, et qui dit complexe sur PC dit risque d'incompatibilité.

Nous allons donc prendre subrepticement la place de l'int 16h du BIOS, si bien qu'à chaque fois qu'un programme fera appel à cette interruption, il fera en fait appel, sans s'en douter, à notre programme.

Cependant, nous n'allons détourner que deux des sous-fonctions de l'interruption 16h : la 0 et la 10h. Ces deux fonctions sont identiques et renvoient dans AL le code ASCII de la touche en attente, et dans AH le scan code de cette touche (la différence entre la fonction 0 et 10h tient à ce que cette dernière reconnait d'avantage de touches).

Lorsque cette fonction sera solicitée, nous renverrons éventuellement un caractère bidon. "Eventuelement" et "bidon" sont synonymes d'"aléatoire" en informatique, nous commencerons donc par tirer un nombre au sort.

Mais avant cela...

 

ON VA SQUATTER LA MEMOIRE !

Avant toute chose, si ce n'est pas déjà fait, relisez le texte d'Olivier Parisy dans le Rep2 qui vous apprendra tout le nécessaire, et même d'avantage, sur la mise en TSR d'un morceau de code.

La première chose à faire dans un cas comme ça, c'est de prévoir la désinstallation du TSR. Genre : quand on lance le programme pour la première fois ça l'installe, et lorsqu'on le lance une seconde fois ça désinstalle le TSR précédement installé.

Pour ça, c'est simple car le TSR installé détourne une interruption ; on connait donc l'adresse du TSR, qui se trouve dans la table d'interruption et on peut donc déterminer si le TSR est déjà résident ou pas, et le désinstaller si le cas se présente.

Première instruction : un JMP vers la fin. Pourquoi ? Parceque on préfère avoir le code résident en premier. Pourquoi ? Vous êtes trop curieux, attendez la suite.

A l'adresse du JMP, après le label "Install", on commence par initialiser une variable avec quelquechose qui ressemble à la date actuelle. Ce n'est pas pour calculer l'age du capitaine, mais plutôt pour initialiser le générateur de nombres aléatoires.

Ensuite, il faut savoir si oui ou non le TSR est déjà résident (auquel cas on l'enlève) ou pas (auquel cas on ne l'enlève pas (je précise juste au cas où)).

Pour ça, comme prévu, on va voir ce qui se trouve à l'adresse de l'interruption 16h (en notant dans un coin cette adresse, au cas où il faudrait installer le TSR).

Puis on compare le mot qui se trouve juste avant cette adresse au mot qui se trouve juste avant notre portion de code résident (qui ne l'est pas encore (résident)), et qui se trouve être le fameux JMP de tout à l'heure. En effet, si le programme à déjà installé ce morceau de code en mémoire lors d'une exécution précédente il va se trouver ce JMP juste avant le code de l'interruption. Il y a en effet bien peu de chance que le même JMP soit présent avant l'int 16h du BIOS (tellement peu que je me demande pourquoi j'en parle).

Donc, si le JMP est présent, on saute à "Desinstall:". Sinon, on installe le TSR. Pour commencer, on change l'adresse de l'int 16h dans la table d'interruption pour que celle ci vienne pointer le debut de notre int 16h a nous (pas de panique : on avait sauvegardé l'ancienne adresse), puis on affiche un petit message histoire de rassurer celui qui le lira, on libère le segment d'environnement (Cf. le petit aparte quelques lignes plus bas), et on quitte le programme en signalant au DOS qu'il serait aimable de ne pas nous désallouer totalement cette partie de mémoire.

Je suis sur que vous êtes emballé ; mais le segment d'environnement, "caisse"(sic) ? C'est un petit segment que réserve le DOS à chaque programme et qui est censé contenir (si votre interpréteur de commande fait correctement son boulot) l'environnement de la machine.

Ce segment n'étant absolument pas nécessaire au TSR, on le vire de la mémoire. Comment, y'en a qui ne savent pas ce que c'est que l'environnement ? L'environnement, ce sont les fûts radioactifs recyclables cachés dans les forêts, les bombes atomiques biodégradables immergées dans les lagons, et les chaînes de variables que vous rentrez en général dans votre autoexec.bat (ces machins qui ressemblent à des SET ULTRASND=220,1,1,11,7 ou à des SET BLASTER=A_LA_POUBELLE).

Revenons au cas où on découvre le fameux JMP deux octets avant le début de l'int 16h. On à donc sauté au label "Desinstall:". Là, il faut tout d'abord restaurer l'ancienne int 16h à son poste dans la table d'interruption. On va donc chercher son adresse, non pas dans notre code à nous (car nous on ne connait que l'adresse de l'int actuelle, c'est à dire une copie de notre programme qui s'est installé résident lors d'une précedente exécution), mais dans le code de l'int 16h courante (vous suivez ?).

C'est ce que font les deux premières instructions. Ensuite, on libère simplement la mémoire utilisée par le TSR (qui n'a donc plus rien d'un TSR), puis on quitte tranquillement, c'est à dire en affichant un message compatissant.

 

COMMENT CAUSER DES DESAGREMENTS

Etudions maintenant le code résident de l'int 16h.

On commence naturellement par tester la fonction demandée. Si ce n'est pas la fonction 0 ni 10h, on passe le relais à l'ancienne int 16h pour qu'elle fasse le boulot à notre place. Notez qu'elle se chargera de rendre la main à l'appelant puisque nous n'avons pas touché à la pile.

Sinon, c'est là qu'on s'ammuse enfin. Et qu'on va avoir besoin de notre nombre aléatoire. Pour avoir ce nombre, on fait Rand32=Rand32*22695477+1. Ca ne s'invente pas, ca se lit. Dans l'article de Pierre Larbier sur les nombres aléatoires, par exemple. Vous remarquez qu'on sauvegarde (PUSH/POP) tous les registres que l'on modifie, ce qui est la moindre des politesses lorsque l'on interrompt du code.

Ensuite, on va tester si on fait une crasse à l'utilisateur ou pas. Pour ca, on teste 4 bits (quelconques) de Rand32, et si ces 4 bits sont nuls on passe à l'action. Si l'un au moins ne l'est pas, on rend la main sagement. Ca nous fait une chance sur 16 (en supposant que Rand32 soit vraiment aléatoire) pour qu'on importune l'utilisateur à chaque pression d'une touche. A ce rythme, on démontre que l'utilisateur devient fou au bout d'une centaine de touches.

Pour trouver quelle vacherie on va faire, on utilise une petite table de saut qui, en fonction de Rand32, nous conduit à l'une des quatres routines possibles. La première de ces routines renvoit dans AX le code pour la touche DELETE, la seconde renvoit un code aléatoire, la seconde passe la couleur du fond de l'écran à son negatif, et la dernière inverse l'état du speaker pour jouer une note de frequence aléatoire.

Vous pouvez sans mal rajouter des routines déplaisantes en agrandisant la table de saut (et en choisissant un index plus large pour sauter dedans (attention de ne pas sauter n'importe où !)).

Sinon pour voir le source, il faut cliquer ici !

RIXED, MAGNE TOI, T'ES A LA BOURRE

J'espere que ce petit programme vous à bien plu et que vous avez découvert un peu comment s'ammuser avec les TSR et les valeurs aléatoires. J'avoue que j'ai été petit peu limite question timing pour remettre ce texte en temps et en heure pour qu'il parraisse dans le Rep'9, alors je ne vais pas m'attarder plus longtemps, et comme de votre côté vous avez certainement d'autres articles forts interressants à lire tout va bien.

Alors peut être à une prochaine fois, les codeurs...

 

Remerciements à Pierre Larbier pour son aide et ses conseils concernant la rédaction de cet article.