Problèmes liés aux interruptions
21 avril 2009 – 20:52Dans le cadre de mon stage, je m’intéresse au fonctionnement des interruptions de l’architecture x86. Il s’agit d’un mécanisme complexe mais extrêmement important pour comprendre comment un système d’exploitation arrive à fonctionner. Si j’écris ce post, c’est parce que je me suis pris la tête sur des problèmes qui y sont liés… Je viens d’en trouver les causes, et je pense que cela pourra en intéresser plus d’un.
Généralités sur les interruptions
Une interruption est un événement particulier qui peuvent se produire lors de l’exécution d’un programme. A chaque type d’interruption est associé un numéro appelé vecteur d’interruption, connu du ou des processeur(s). Pour simplifier, les interruptions peuvent provenir de deux sources : le logiciel et le matériel. Les interruptions matérielles sont générées par les périphériques (clavier, souris, cartes, timers, disques durs…) et sont transmises au processeur par l’APIC (Advanced Programmable Interrupt Controller). Les interruptions logicielles peuvent être quant à elles déclenchées volontairement par une instruction (int) ou de façon accidentelle par une erreur arithmétique, logique, de protection, etc. Dans ce cas, on parle souvent d’exception et de fautes.
Généralement, les interruptions (aussi bien matérielles que logicielles) sont susceptibles de se déclencher à n’importe quel moment dans l’exécution d’un programme, et chaque processeur doit savoir comment traiter chacune d’entre elles. A chaque vecteur d’interruption on associe alors un handler d’interruption, qui est une fonction qui sera appelée automatiquement quand l’interruption sera générée. On place ces fonctions dans une table, l’IDT, pour Interrupt Descriptor Table. Il est important de noter qu’il y a une IDT par processeur, afin que chacun puisse avoir si besoin un comportement différent pour chaque interruption.
Il existe deux types d’interruptions matérielles, les masquables (les IRQ) et les non masquables (la NMI et la SMI) qu’on ne traitera pas ici en raison de leur grande particularité. Chaque interruption matérielle possède une priorité, qui est gérée par l’APIC. Pour faire simple, ce composant est relié aux périphériques par des lignes d’IRQ (Interrupt Requests) et se charge de faire le médiateur entre tous ces périphériques et le ou les processeur(s). Son rôle est en gros de transformer ces IRQ en demandes d’interruptions avec le vecteur associé. Comme plusieurs IRQ peuvent intervenir en même temps, l’APIC définit des priorités afin de transmettre les plus urgentes d’abord.
Dans certains cas, le processeur peut ne pas vouloir être dérangé par une interruption. C’est le cas par exemple lorsqu’il est en train de modifier des structures globales en mémoire. Il a alors la possibilité d’ignorer les interruptions qu’il va recevoir de deux manières. La première, que je ne détaillerai pas, est de ne masquer que celles dont la priorité est inférieure à un certain seuil (appelé IRQL sous Windows) en communiquant avec l’APIC. La deuxième est de masquer la totalité des interruptions et c’est celle que nous allons détailler.
Subtilité sur le masquage
C’est le registre EFLAGS et plus précisément son bit IF (bit 9) qui contrôle si les interruptions masquables sont activées ou non. Ce bit est modifiable par l’intermédiaire des instructions cli
pour masquage et sti
pour démasquage, ou bien en utilisant pushf / popf associés à des masques.
Attention, le masquage des interruptions n’affecte que les interruptions matérielles ! Les interruptions logicielles (ainsi que les interruptions NMI et SMI, très particulières) ne sont pas affectées par ces opérations. Autrement dit, vous aurez beau baisser l’IRQL ou faire un cli
, une exception due à une division par zéro ou à un défaut de page pourra toujours survenir.
Problème n°1 : DbgPrint(), c’est le mal
J’ai fait tous mes tests en machine virtuelle. Comme le développement de driver n’est jamais sûr à 100% et que je voulais tout de même avoir quelque chose de fonctionnel, j’ai réalisé un petit script qui charge le driver et le décharge 100 fois de suite, en lançant en plus des programmes divers pour solliciter l’OS un maximum. Le tout étant de prouver que le driver n’explose pas à la première interruption, bien entendu.
Dans mon cas, je devais hooker l’interruption n°14 correspondant au défaut de page (page fault), en insérant du code avant de rappeler le handler par défaut de Windows. Je rappelle que cette exception (logicielle) se produit quand une page virtuelle n’est mappée à aucune adresse physique (soit parce qu’elle a été swappée sur disque, soit parce qu’elle n’existe tout simplement pas). Cela se produit quand on tente d’accéder à une page dont le descripteur PTE a son premier bit (bit P) à 0.
J’ai donc commencé par concevoir un handler minimal qui ne faisait rien à part empiler tous les registres, les dépiler puis appeler le handler de Windows, KiTrap0E
. Jusqu’ici, tout fonctionne, sauf que je n’ai pas vraiment de preuve que ma routine est appelée (vu que je n’ai aucune trace).
Je me décide donc à insérer des DbgPrint()
dans le code de la routine. Et là, c’est le drame : j’obtiens des traces certes, mais j’en obtiens tellement que la VM freeze ou affiche un écran bleu au bout de quelques secondes. Il faut croire que les défauts de page sont très nombreux sous Windows… Mais pourquoi ça plante maintenant et pas avant ? L’explication est liée au visualisateur de traces (j’utilise DebugView). Lorsque DbgPrint()
est appelé, le message est récupéré par le noyau puis par DebugView. Je ne sais pas exactement pourquoi, mais apparemment quand il y a beaucoup de messages de récupérés, DebugView commence à provoquer des défauts de page (probablement à cause du fait qu’il se fait swapper par l’OS). Comme ces défauts de pages sont à leur tour catchés par mon handler, ils entraînent d’autres appels à DbgPrint()
… Bref, une belle boucle qui finit par exploser tôt ou tard.
Au final, je renonce donc à afficher des traces, vu que cela perturbe plus le système qu’autre chose. En plus j’ai ma preuve que mon handler est appelé, vu que ça plante
Problème n°2 : sti, c’est aussi le mal
Le réel but de mon handler est de modifier une structure assez cruciale du noyau sous certaines conditions. Afin d’éviter que cette modification entraîne des problèmes de concurrence, je décide de masquer les interruptions avant le traitement (cli
), et de les démasquer ensuite à l’aide de et sti
. Je lance le driver. Pas de crash. Je commence à crier victoire, et au moment ou je lance mon script de déchargement, j’ai le droit à un écran bleu. Je regarde le code de déchargement, il ne fait absolument rien de méchant. Je relance, idem. Je finis par comprendre que ce n’est pas le déchargement du driver qui foire, mais juste le fait de lancer un programme, car cela provoque un défaut de page… C’est donc bien mon handler qui pose problème, mais où ?
Je supprime ma modification de variable globale, ça plante toujours. Pourtant je n’ai plus que cli
et sti
dans mon handler (en plus de l’appel au handler par défaut). Je retire sti
et je constate que ça marche ! C’est donc cette instruction qui est responsable du plantage du driver… mais pourquoi ? En fait, il se trouve que lors des appels au handler de défaut de page, les interruptions sont déjà masquées. C’est dû au fait que l’interruption correspondant au défaut de page possède un descripteur dans l’IDT qui précise que l’IF doit être mis à 0 lors de l’appel du handler (c’est une interrupt gate, selon Intel). Ainsi, lors de l’appel au handler, l’instruction cli
ne fait absolument rien (le bit IF d’EFLAGS est déjà à 0)… Le problème, c’est que sti
le passe à 1, ce qui autorise les interuptions ! Si jamais le défaut de page s’est produit dans une zone critique de l’OS et qu’une interruption survient précisément à ce moment, le kernel se vautre. Cela ne se produit peut-être pas à tous les coups, mais croyez moi, ça arrive (faites le test pour vous en convaincre).
Comment patcher cela ? Ne pas masquer les interruptions dans un handler de défaut de page, puisqu’elles le sont déjà. La solution, c’est tout simplement de ne rien faire
Conclusion
Morale de l’histoire : utiliser DbgPrint()
ou sti
dans un handler de défaut de page est une mauvaise idée. Pour déboguer, préférez utiliser des variables globales plutôt que des fonctions à effet de bords incontrôlables. Et ne cherchez pas à masquer (et surtout à démasquer) les interruptions qui le sont déjà . J’espère que ce post vous aura épargné un long moment de galère. En tout cas, j’aurais aimé en lire un du même type plus tôt !
3 réponses à “Problèmes liés aux interruptions”
Hello,
J’avoue ne pas avoir toutes les connaissances requises pour bien tout comprendre mais soit le sujet me passionne et depuis un moment deja, donc voici ma question:
tu hooks une interruption, cela sous entend que lorsque l’interruption est declanchee, tu fais un « crocher » par une routine que tu a écrite avant éventuellement exécuter la routine qui était censée s’executer de base, j’ai bon?
Du coup j’aimerai savoir quand le masquage des interruptions s’effectue?
De mon point de vue, si le masquage des interruptions doit se faire, c’est par la routine de l’OS que tu hook justement (et donc tu passes avant elles).
bref, merci de repondre a cette question si le coeur t’en dit.
Par sloshy le 23 avril 2009
Salut,
En effet le principe d’un hook est bien de détourner une interruption en exécutant du code à la place ou avant d’appeler la routine (handler) de l’OS.
En ce qui concerne le masquage des interruptions, j’ai envie de dire « ça dépend ». Dans certains cas, elles sont masquées automatiquement par le processeur lorsque la routine d’interruption (celle que tu as écrite) est appelée. Cela dépend en fait du descripteur que tu place dans l’IDT. Un descripteur contient non seulement le segment et l’adresse de ta routine, mais aussi des flags décrivant comment gérer l’IF. Il y a trois grands types de descripteurs : les portes d’interruption (interrupt gates), les portes de trappe (trap gates), et les portes de tâche task gates. Ceux-ci sont décrits au chapitre 5.11 du manuel 3A d’Intel. Ces trois types de descripteurs définissent comment le masquage des interruptions doit être traité lors de l’appel à la routine. Sous Windows, l’interruption 14 correspondant au défaut de page possède un descripteur de type porte d’interruption, ce qui signifie que les interruptions sont masquées automatiquement quand la routine de traitement du défaut de page est appelée.
Sinon, dans le cas ou les interruptions ne sont pas marquées automatiquement, il est toujours possible de les masquer temporairement à la main avec cli et sti. Cela peut se faire avant l’appel à la routine de l’OS. Mais dans tous les cas, il faut bien veiller à restaurer la valeur du flag d’interruption (IF). Ce que je préconise est de faire un pushf au début ainsi qu’un popf à la fin de la routine (avant d’appeler celle de l’OS) pour être sûr de tout restaurer.
Par Emilien Girault le 23 avril 2009
Hello,
Merci pour les explications, c’est deja un peu plus claire pour moi … je penserai au manuel intel juste âpres la bloc de juin.
Par sloshy le 23 avril 2009