> Hardware |
PrograZine issue #10 - http://www.citeweb.net/discase/10/int.htm | Edito | Sommaire | Contribution | Contacts |
Les interruptions
par Plouf - plouf@win.be http://www.citeweb.net/pmplayer |
Initié | DOS | ASM |
Ce tutoriel explique le fonctionnement des interruptions (INT) en mode réel. Il suppose une connaissance moyenne de l'assembleur.
Vous savez certainement que pour afficher une chaine de caractère sous Dos, vous pouvez vous contenter d'appeler dos via l'int 21h, service 09h. Vous mettez le numéro du service dans ah, remplissez les registres (ici ds:dx), et entrez la fameuse instruction INT 21h. Mais qu'est-ce que c'est que ce truc, INT 21h? Il faut en fait voir ça comme les API Windows. Il est rare qu'un programme ouvre et gère sa fenêtre tout seul, ce qu'il fait, c'est qu'il appelle une fonction de Windows qui va faire tout ça pour lui et basta. Pour cela, le programme doit connaitre le nom de la dll (ici user32.dll), et le nom de la fonction de cette dll (ici CreateWindowEx). Sous dos, il n'y a pas de DLL, et en fait toutes ces routines ne se trouvent pas dans un fichier, mais en mémoire (en fait, en partie chargées en mémoire au démarage de Dos), ce qui fait que si un programme veut y accéder, il doit au final connaître l'addresse mémoire de cette routine, qui est susceptible de changer d'un système à un autre et même d'un boot à un autre. Il faut donc un endroit (qui lui ne change pas) où il est justement écrit à quel endroit se trouve telle ou telle routine. Cet endroit existe, il est sous forme d'un tableau de DWORD de 256entrés, fait donc 1k tout rond, et se trouve à l'addresse 0000:0000 (heu wé, c'est pas trop compliqué à retenir :)) Le premier k de la mémoire conventionelle est donc rempli par un tableau de DWORD. Ce DWORD se présente sous la forme Segment:Offset et contient donc l'adresse de la routine X, où X est l'indice de l'élément. On voit donc qu'il y a une limitation à 256routines possibles. Quand vous appelez INT 21h, le processeur va rechercher l'élément 21h (=33) de ce tableau, et fait un CALL FAR dessus, c'est tout. Le code se trouvant à cette adresse se présente donc comme une sous-routine qui sera terminée par un RET, ce qui provoquera le retour vers votre programme. En général, cette routine commence par regarder le contenu de ah pour voir quelle sous-routine appeler, mais cela n'est pas une obligation. Ces routines portent le beau nom d'interruption, sans doute parce qu'elles interrompent l'excécution en cours du programme pour faire une tâche bien précise, mais sans doute parce qu'elles font plus encore, gardons le suspense pour la partie suivante :)
Question : vous êtes le responsable du dévelopement du premier processeur chez Intel. C'est cela, vous devez inventer le PC. Vous avez déjà créé les interruptions (mettons), et venez de mettre la dernière main à l'instruction DIV, quand tout à coup vous vous posez cette bête question : et si il y a division par zéro? Car après tout, la question n'est pas si inocente qu'il n'y parrait, car d'un côté le processeur ne doit pas imploser, et de l'autre, il faudrait que le programme en soit avertit pour qu'il agisse en conséquence, donc il ne faut pas se contenter d'ignorer le probleme en mettant ax à 0 (par exemple). Comme nous somme donc dans un cas de figure où le cpu ne sait plus ce qu'il doit faire, il lui reste une dernière carte : l'exception (qui porte bien son nom : situation exceptionelle). Le processeur va "tout simplement" appeler une interruption (qu'est-ce qui l'en empeche après tout), mettons l'interruption 0 (c'est pratique a retenir, division par 0 entraine l'int 0). Ce qui fait que dès lors, un programme peut être avertit de cette erreur, il lui suffit de détourner le premier DWORD de la table des interruptions (le tableau dans le premier k) vers une routine à lui, et le tour est joué. Et le pire, c'est qu'il n'y a rien de plus simple, il suffit de taper Ofs Routine dans le premier Word, et CS dans le second (le second avant le premier, ordre inverse des octet, Intel oblige...), et pouet! Et le responsable de chez Intel, tout content de ce coup de génie, décide donc qu'à toute erreur du processeur correspondra une interruption, qu'on renomera pour l'occasion "exception", et pour faire joli et bien aligné, on dira que les interruptions de 0 à 16 seront des exceptions. 16 et pas 15, me demandez pas pourquoi, moi je lis ma documentation sur les exceptions qui dit ceci :
On reconnait d'ailleurs quelques classiques : la 06, 0D et 0E, qui sont des vieilles copines des utilisateurs Windows : vous connaissez surement cette petite phrase : "Cette application va être arrêtée car elle a causé l'exception 06h dans le module bidule à l'addresse machin". Normalement on maudit ce message, et bien moi, en vérité je vous le dit : bénissez-le, car le programme qui avait complètement déconné (c'est le moins qu'on puisse dire vu qu'il a pointé CS:EIP vers une zone non excécutable) à été évincé par Win, ce dernier décidant d'arreter les frais. Tout autre comportement aurait été dangereux (Sous DOS : reboot assuré) Ce tableau permet d'ailleurs de comprendre pourquoi, jamais au grand jamais, vous n'aurez de service Dos ou Bios qui s'appelle par une INT plus petite que 11h...
Il faut à présent se poser la question : qu'est-ce qu'on peut faire de drole avec tout ça? Vous avez vu que c'est un outil très puissant pour tout qui veut fournir des services à une application tournant sous Dos (et en premier lieu, le Bios et Dos). Imaginez (grand fou) que vous alliez créer un hardware magnifique : le Zorglub. (Note : en me relisant je me dis que j'aurais du prendre un autre nom à la con, car il est difficile à écrire, j'en ai bavé :)) Vous aimeriez que les programmes Dos puissent tirer partit du Zorglub, et donc vous voulez leur fournir des fonctions du genre "Alumer zorglonde". Pour cela, il vous faut une Int, ou au moins un numéro de service dans une Int déjà utilisée. Par exemple si le Zorglub fonctionne en étroite liaison avec l'écran, il serait normal que vous utilisiez un service non utilisé du Bios gaphique (10h), histoire de mettre tout ce qui est graphique dans une seule et même Int. Pour cela vous conpulsez votre documentation, voyez que (j'invente) le service ah=8eh n'est utilisé par personne, et décidez que vous allez vous y placer.
Le problème pour vous est de savoir comment vous y placer. Bien sur, vous pouriez mettre l'addresse CS:Ofs de votre routine dans l'élément 10h de la table des interruptions, mais cela empêcherait les anciennes commandes Bios d'être appelées, ce qui n'est pas une très bonne idée sauf si vous êtes le seul à utiliser cette int, comme par exemple le gestionnaire de souris est le seul à utiliser l'int 33h. Mais comme on à pas le choix (on à pas encore inventé mieux que ces 4octets à modifier), c'est ce que l'on va faire mais attention : on va d'abord sauvegarder l'addresse actuelle, que l'on appelera si Ah<>8e, et ainsi tout marchera sans problème.
Schématiquement, la routine ressemblera à ceci :
Bon évidemment il reste un petit problème à résoudre : comment modifier l'addresse de l'INT 10h? Réponse : on peut le faire sois-même mais comme plusieurs considérations sont à prendre en compte (les ISR voir plus loin), il vaut mieux demander à dos de le faire pour nous : services DOS (INT 21h) 35h, qui LIT l'addresse de l'INT contenue dans AL et la retourne dans ES:BX, et 25h, qui ECRIT l'addresse de l'INT contenue dans AL avec l'addresse DS:DX
Tralala, nous avons ce qu'on appelle couramment "patché" l'int 10h. Bon c'est bien joli tout ça, mais à présent il serait temps de quitter le programme et de rendre la main à Dos pour qu'il lance (par exemple) le programme qui utilise le Zorglub. Mais si on quitte bêtement notre programme, Dos va libérer la mémoire qui lui était allouée, notre fameuse routine avec! Ce qui fait que notre routine va se faire écrabouiller avec n'importe quoi, et dès que le nouveau prog voudra utiliser le zorglub et appeler INT 10,8e l'odri va planter. En fait ce qu'il faudrait pouvoir dire à dos, c'est "je veux quitter, mais ce que je ne veux surtout pas, c'est que tu libère la zone mémoire de ma routine". Autrement dit : "quitte mais laisse moi résident en mémoire", ou bien en anglais "Terminate but Stay Resident" ou TSR. C'est facile à faire? Oui monsieur! Pour quitter un programme, vous faites tous
Ben ici on va juste faire autre chose, voilà tout... C'est plus 4C, c'est 31, qui fonctionne de la même facon, sauf qu'il prend un parametre en plus de al (qui contient le Return Code), c'est Dx, qui contient la quantité de mémoire à réserver en paragraphes (=16octets) pour le programme en partant du début (autrement dit l'addresse de votre première instruction) En clair, un TSR se compose toujours de deux parties : un lanceur et une routine d'éxcecution (wais, comme Ariane), la routine se trouvant en tête du programme et le lanceur étant détruit après la mise en orbite (de plus en plus comme Ariane, ces gars là n'ont rien inventé :)) Le lanceur se contente de patcher l'INT et de quitter en précisant qu'il doit rester résident, et comme il est inutile de conserver le lanceur en mémoire, on ne donne que la taille de la routine (et c'est pourquoi elle doit se trouver au début du programme, vu que Dos comment à sauver à partir du début) Schémas d'un TSR :
A titre d'excercice on pourra par exemple essayer de remplacer des services existants, comme par exemple se mettre dans INT 10,0 pour inverser les modes d'écrans (le 13 vers le 12, le 12 vers le 2...), résultats amusants garantis. Pour cela il faut patcher l'INT 10h, vérifier dans le TSR la valeur de ah, si ah=0, modifier la valeur de al qui contient le mode d'écran, et puis appeler l'ancienne routine.
Notons au passage qu'un virus ne fonctionne pas autrement, il patche les INT les plus courantes, ce qui lui permet de se faire appeler à peu près tout le temps et d'avoir ainsi le controle absolu de la machine. Et comme l'antivirus fait également la même chose, il est parfois difficile de les distinguer des virus. Après tout, un antivirus considère souvent un autre antivirus comme un virus...
Puisque vous ête imaginatifs ce soir, on va continuer dans ce sens : Imaginons que vous deviez programmer un modem. Ce modem envoie des données vers le port Com1 (mettons), et vous, vous les lisez. Bon, imaginons un moment qu'un abrutit lance Photoshop5 (ou bien l'émul de la Neogeo parrait que ça rame au chargement). Votre programme ne va plus avoir la "main" pendant un laps de temps considérable (d'un point de vue du modem), ce qui fait que vous risquez de "rater" des octets avec les conséquences que cela entraine (fini le download de la chanson de windows en mp3...). Il faudrait trouver un truc pour que le modem vous avertisse (et vous rende la main par la même occasion) qu'il a du travail pour vous. Ca serait pratique à plus d'un titre, car vous n'auriez plus à attendre bêtement à l'avant-plan que les octets n'arrivent, dès qu'ils sont là, vous êtes appelés immédiatement, ce qui vous permet de traiter, en avant-plan cette fois, ce beau compteur de progression 3d avec Z-Buffer et tout et tout (je vais donner des idées a microsoft pour msie6 moi)... Bon bah cessez de l'imaginer et d'y rêver parce que ça existe et c'est comme ça que marchent la plupart des périph sur votre PC (le clavier, pour pas rater des touches, la carte son pour avertir qu'elle a fini son buffer, le modem, l'imprimante, enz(1)..) Comment qu'on fait? Bah pardis, avec les Int, tient! Tout à l'heure, le processeur avait décidé d'appeler des Int précises lors d'évenements insolites. Bah à nouveau, le processeur va appeler certains Int lorsque certains hardwares le désirent. Comment? Chaque périph fonctionant avec ce système est branché sur un système (sur la carte mère) qui possède 16entrées(2). Le périph se connecte sur une de ces 16entrées, et quand il désire avertir le programme de quelque chose, il émet un signal sur sa petite entrée, le système sur lesquelles sont ces entrées avertit le processeur que le périph se trouvant sur l'entrée unetelle l'a avertit, et le processeur excécute une Int associée au numéro de cette entrée. Bon, vous l'avez compris, ces "entrées", ce sont les IRQ, ce système c'est le contrôleur des IRQ, on ne peut vraiment rien vous cacher... La table suivante donne l'int correspondant à l'IRQ
Vous allez me dire "m'sieur, c'est pas normal, ya des INT qu'elles sont déjà définies comme exceptions". Si vous regardez bien, ces INT "doubles" sont les int définies uniquement avec un (286,386), autrement dit ce sont des exceptions du mode protégé, lequel ne fonctionne pas vraiment comme le mode réel, et donc ne vous en souciez pas (autrement dit : vous ne pouvez pas avoir ces exceptions en mode réel, ce qui prouve en passant que windows tourne en mode protégé, et comme les services dos ne fonctionnent qu'en mode réel, les idiots qui vous disent que windows tourne sous dos ne sont que des pauvres taches :)) Un TSR qui "patche" une INT associée à une IRQ est appelé un ISR (I comme interrupt, subtil). Ce qui fait que, en gros, ce sont des INT qui sont appelées "toutes seules". Un petit jeu amusant consiste à patcher l'int 1C, qui est en fait appelée par l'int 08h, le timer, et d'y créer un code qui affiche (par exemple) un astérisque avec avancement du curseur. Vous le tapez en TSR, rendez la main à dos, et ooh comme c'est amusant, on voit apparaître sur l'écran du dos une suite de **** à la vitesse de 18.206 * par seconde. Ce dernier n'en a d'ailleurs que faire vu qu'il continue à fonctionner correctement en ignorant ces parasites. (Remarquez que dans edit ca marche aussi et c'est encore plus drole, surtout dans les menus). Pourquoi l'INT 1c et pas directement la 08h? Parce que l'int 08h est patchée par des périphs "vitaux" au système, et que modifier cette adresse risque fort de provoquer un crash. C'est d'ailleurs pour ça que la 08h appelle la 1C, pour assouvir le besoin en tests idiots des programmeurs du dimanches que nous sommes tous :) Quelques considérations importantes relatives à la programmation des ISR :
Le code de ce petit programme (assemblé avec a86)
Oksébôôô!
|
Cet article est la propriété de Plouf. La copie et la diffusion sont libres sauf dans un but lucratif sans accord explicite de l'auteur. |