> 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.
>Quoi que c'est?

     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 :)
>Interruptions et exceptions

    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 :
   0      Divide by zero
   1      Single step
   2      Non-maskable  (NMI)
   3      Breakpoint
   4      Overflow trap
   5      BOUND range exceeded (186,286,386)
   6      Invalid opcode (186,286,386)
   7      Coprocessor not available (286,386)
   8      Double fault exception (286,386)
   9      Coprocessor segment overrun (286,386)
   A      Invalid task state segment (286,386)
   B      Segment not present (286,386)
   C      Stack exception (286,386)
   D      General protection exception (286,386)
   E      Page fault (286,386)
   F      Reserved
  10      Coprocessor error (286,386)
l

    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...
>Patchage et TSR

    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.
    Documentation Kïnvôrien :
      Utilisation du Zorglub : Int 10h, Ah=8e
        Al=00               Zorglonde On
        Al=01               Zorglonde Off
        Al=02               Appel du conte De Champignac
         ...
      Exemple : alumer Zorglonde :
          MOV  AX, 8e00
          INT  10h
l

    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.
  • Sauvez l'addresse pointée par Int 10h
  • Modifier l'addresse vers la votre, de routine
  • Dans la routine, vérifier la valeur de Ah
  • Si Ah<>8e, appeler l'ancienne addresse
  • Si Ah=8e, traiter les autres registres et réagir en conséquence
  • RET


    Schématiquement, la routine ressemblera à ceci :
        CMP  ah, 8eh                            ;C'est pour nous?
        JE   Zorglub                            ;Oui? Bah go alors
        JMP  Ancienne_Addresse                  ;Non? Alors va à l'ancienne addresse
Zorglub:
        CMP  al, 00h                            ;Allumer Zorglonde?
           ...
        RET
l

    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
        MOV  ax, 3510h                          ;On va lire l'adresse de l'int 10h
        INT  21h                                ;Faisez, faisez
        MOV  OldIntOfs, bx                      ;On sauve l'offset qui est dans bx
        MOV  OldIntSeg, es                      ;On fait de même avec le segment dans es
        MOV  ax, 2510h                          ;On va écrire l'adresse de l'int 10h
        MOV  dx, Offset MyInt                   ;Dx contient l'offset
        PUSH cs                                 ;et Ds le segment
        POP ds
        INT  21h                                ;Let's go
l

    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
    MOV ax, 4C00
    INT 21h
l

    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 :
        JMP Lanceur
Ancienne_Addresse:
        OldIntOfs DW ?
        OldIntSeg DW ?
ZorgUtil:
        CMP  ah, 8eh                            ;C'est pour nous?
        JE   Zorglub                            ;Oui? Bah go alors
        JMP  Ancienne_Addresse                  ;Non? Alors va à l'ancienne addresse
Zorglub:
        CMP  al, 00h                            ;Allumer Zorglonde?
           ...
        RET
Lanceur:
        MOV  ax, 3510h                          ;On va lire l'adresse de l'int 10h
        INT  21h                                ;Faisez, faisez
        MOV  OldIntOfs, bx                      ;On sauve l'offset qui est dans bx
        MOV  OldIntSeg, es                      ;On fait de même avec le segment dans es
        MOV  ax, 2510h                          ;On va écrire l'adresse de l'int 10h
        MOV  dx, Offset ZorgUtil                ;Dx contient l'offset
        PUSH cs                                 ;et Ds le segment
        POP ds
        INT  21h                                ;Let's go
        
        MOV  dx, Lanceur Main/16+1              ;On calcul l'espace qu'il
        MOV  ax, 3100h                          ; faut réserver en
        INT  21h                                ; mémoire et on quite en TSR
l

    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.
    Lire ah
    Si ah=0 Alors al=f(al)
    Appeler ancienne routine
l

    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...
>IRQ et ISR

    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
        IRQ     INT     Description
        IRQ0     8      timer (55ms intervals, 18.2 per second)
        IRQ1     9      keyboard service required
        IRQ2     A      slave 8259 or EGA/VGA vertical retrace
        IRQ8    70      real time clock  (AT,XT286,PS50+)
        IRQ9    71      software redirected to IRQ2  (AT,XT286,PS50+)
        IRQ10   72      reserved  (AT,XT286,PS50+)
        IRQ11   73      reserved  (AT,XT286,PS50+)
        IRQ12   74      mouse interrupt  (PS50+)
        IRQ13   75      numeric coprocessor error  (AT,XT286,PS50+)
        IRQ14   76      fixed disk controller (AT,XT286,PS50+)
        IRQ15   77      reserved  (AT,XT286,PS50+)
        IRQ3     B      COM2 or COM4 service required, (COM3-COM8 on MCA PS/2)
        IRQ4     C      COM1 or COM3 service required
        IRQ5     D      fixed disk or data request from LPT2
        IRQ6     E      floppy disk service required
        IRQ7     F      data request from LPT1 (unreliable on IBM mono)
l

    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 :
  • Vous devez préserver TOUS LES REGISTRES sauf les flags. La raison en est évidente : votre programme peut être appelé à n'importe quel endroit et n'importe quel moment, entre deux instructions d'un programme quelconque, ce dernier ne s'attendant certainement pas à voir le contenu de ax modifié sans crier gare...
  • La fin d'un ISR ne se termine pas par RET mais bien par IRET (interrupt return, toujours aussi subtil). Pourquoi cette différence? Bah c'est juste que le cpu, avant de faire un CALL FAR sur l'ISR, push le flag (c'est d'ailleurs la raison pour laquelle on peut le modifier), ce qui n'est pas le cas lors d'une Int classique.


    Le code de ce petit programme (assemblé avec a86)
IntSet          SEGMENT
                JMP  main                       ;Vers le principal
Resident:
                PUSH ax                         ;Sauver les registres que
                PUSH bx                         ;  l'on va modifier
                MOV ah, 0eh                     ;Write text in teletype mode
                MOV al, '*'                     ;Ché pas moi, zavez essayé '°'?
                XOR bx, bx                      ;0+1 caractères à afficher
                INT 10h                         ;Et hop
                POP bx                          ;Restauration des registres
                POP ax                          ;  précédemment sauvés
                IRET                            ;Fin de l'interruption
Main:                            
                MOV  ax, 251ch                  ;Changement de INT 1c
                MOV  dx, offset Resident        ;Ds:Dx pour l'appel
                INT  21h                        ;Change-la
                MOV  dx, offset Main            ;On garde en mémoire => Main
                INT  27h                        ;Quitte et reste résident
IntSet          ENDS
l

    Oksébôôô!
l
  • Pour nos amis français : Enz=Etc, voila, pasque c du vlaams une fois...
  • En fait il n'y a que 15 IRQ disponibles, car le controleur IRQ (appelé le PIC, programable interrupt controler) ne possède en réalité que 8entrées, ce qui fait que pour en avoir plus, il faut les brancher en cascade, et que ce branchement du second controleur se fait sur une des entrées du premier. On peut ainsi avoir autant d'irq que l'on désire, a condition bien sur que la carte mère (et le bios) les acceptent. Le second controleur est mis en Slave via l'IRQ 2 du premier. Le fait que certains périph peuvent être mis sur l'IRQ 2 (des Sound Blaster sous dos, sisi j'en ai vu) reste pour moi un grand mystère, je pense en fait que cette irq est une fausse irq qui est générée par une autre, l'irq 9, car la documentation dit :
            IRQ2     A      slave 8259 or EGA/VGA vertical retrace
            IRQ9    71      software redirected to IRQ2  (AT,XT286,PS50+)
    
    l
    Le 8259 est le petit nom du PIC. Je ne suis pas sur de ce que j'avance car j'avoue n'avoir fait aucun test pour le vérifier, mais ça se tient, non? Quand a savoir quelle int il faut patcher pour utiliser des périphs sur IRQ2, je sais juste que la A ne marche pas, alors essayez la 71h pour voir :)
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.