Les interruptions
Cet article traite du fonctionnement globale des interruptions sur notre architecture x86, en partant de la table des interruptions jusqu'au Programmable Interrupt Controller (PIC).
Types d'interruptions
L'architecture x86 possède plusieurs classes d'interruptions :
- Les exceptions, qui sont générées par le processeur lui-même quand il rencontre un problème d'état, par exemple les Page Fault si on accède à une adresse invalide, ou encore l'exception Invalid Opcode si le processeur ne parvient pas à décoder une instruction de notre code. Il y a 32 exceptions, la liste peut être trouvée sur le wiki OSDev. Les exceptions gèrent également les non-maskable interrupts (NMI);
- Les interrupt requests (IRQ), elles sont générées par un composant externe au processeur, qui déclenche lui-même les interruptions sur le processeur en lui envoyant des signaux électriques. Ce composant est appelé PIC (Programmable Interrupt Controller);
- Les software interrupts, elles sont déclenchées par des instructions (comme certaines exceptions) telles que INT, et sont typiquement utilisées pour la réalisation des System Calls, où une fonction du kernel est "appelée" depuis du code utilisateur.
Interrupt Descriptor Table (IDT)
Cette table est une structure stockée en mémoire constituée de 256 entrées (appelées gates) de 8 octets. Cette structure fait donc 256 * 8 = 2048 octets. L'adresse de cette structure est donnée au processeur via l'instruction lidt
au démarrage du kernel.
Lorsque le processeur doit traiter une interruption (exception, IRQ ou software interrupt), il parcours alors cette table et déclenche la gate associée si possible.
Chaque entrée définie l'offset de l'interrupt handler, ou Interrupt Service Routine (ISR), son segment ainsi que des flags, tels que la présence ou non d'une ISR. Chaque gate peut être de type task, interrupt ou trap, comme on peut le voir sur la figure suivante tirée du Intel Manual:
Les interrupt et trap gates sont identiques dans leur structure, et nous n'aborderons pas les task gates. La différence entre une interrupt et une trap gate est qu'une interrupt gate va masquer les interruptions avant d'exécuter l'ISR avant de restorer l'ancienne valeur.
Le code suivant permet de comprendre comment configurer une gate en mode interrupt (le mode utilisé dans le projet système). Nous partons du principe que notre IDT démarre à l'adresse 0x1000:
1 2 3 4 5 6 7 |
|
Nous utilisons ici l'arithmétique des pointeurs de type
uint32_t
, donc l'adresse avance en réalité deindex * 2 * 4
.Il faut faire attention à l'endianness !
Dans ce code, on peut noter l'utilisation de deux constantes, d'abord KERNEL_CS
qui est une constante prédéfini par le projet au segment mémoire utilisé pour le code kernel, ainsi que 0x8E00 qui représente les flags de la gate: P = 1 (present), PDL = 0 (niveau 0, privilège le plus élevé), 01110 pour le type de gate (interrupt gate 32 bits). On pourrait par exemple utiliser les flags 0x8F00 pour une trap gate.
Interrupt Flag (IF)
L'architecture défini un registre appelé EFLAGS (dont la définition de chaque bit peut être trouvée sur wikipedia). Ce registre contient différents bits correspondants à des états du processeurs, comme le Carry flag, Sign flag ou Zero flag qui sont définis par les instructions d'opérations et permettent les instructions conditionnelles.
Ce registre contient également un bit important pour les interruptions : le Interrupt flag, si ce bit est défini à 1, les interruptions "maskable" sont globalement activé sur le processeur. Ce bit peut être mis à 1 avec l'instruction sti
(Set Interrupt) ou mis à 0 avec cli
(Clear Interrupt).
Les interruptions "maskable" ne concernent que les interruptions externes provenant du PIC. Il n'est donc pas possible de désactiver les exceptions ou les software interrupts.
Les interruption externes sont généralement masquées dans le kernel.
Interrupt Service Routine (ISR)
Une ISR est une fonction qui va être appelée par le processeur quand une interruption est déclenchée (on part du postulat que l'on ne change pas de niveau de privilège). Lorsque que l'ISR est appelée, le processeur va pousser sur la stack en cours d'utilisation les registres EFLAGS, CS et EIP. Il réalise ensuite un saut sur l'adresse de notre ISR et commence son exécution. Certaines exceptions pousse un code d'erreur après EIP.
Cette gestion particulière de la stack requiert donc une instruction particulière pour return de notre fonction: iret
(instruction return). De plus, on va également vouloir sauvegarder/restorer les registres pour pouvoir les utiliser librement dans notre ISR.
Ces contraintes nous force généralement à implémenter les ISR en assembleur, afin de controller exactement les instructions prologue et épilogue de notre fonction.
Voici un exemple d'ISR en assembleur:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Avec le reste de l'ISR défini en C:
1 2 3 |
|
On peut alors utiliser notre fonction pour définir une interrupt gate vu précédemment pour déclarer notre ISR auprès du processeur (sur l'interruption 60 par exemple):
1 2 3 4 5 6 |
|
Attention! Si vous écrivez une ISR gérant une interruption générée par le PIC, voir la section sur la commande EOI.
Programmable Interrupt Controller (PIC)
Les PIC (8259A) sont des composants externes connectés à notre processeur, ils permettent de recevoir des IRQs (externes) d'un timer ou un clavier par exemple. Ils signalent alors au processeur l'arrivé d'une IRQ, quand le processeur accepte la requête, il déclenche l'interruption associée à ce numéro d'IRQ (voir IDT).
Un PIC peut gérer 8 numéros d'IRQs différents. Ce nombre étant trop faible, deux PIC (master et slave) sont utilisés en cascade:
On voit que lorsque le slave PIC reçoit une IRQ, il déclenche à son tour l'IRQ 2 sur le master PIC, qui envoi à son tour l'information au processeur. Nous avons donc 15 IRQ utilisable avec ce montage.
Le contrôle de ces PIC est possible via les ports de commande et de data:
- 0x20/0x21 pour le master
- 0xA0/0xA1 pour le slave
Dans les schémas ci-après concernant le PIC, le bit A0 indique quel port utiliser, A0 = 0 (commande) et A0 = 1 (data).
Masques du PIC
Un fois le PIC initialisé, on peut lire ou écrire le Operation Command Word (OCW) 1. Ce mot, comme on peut le voir sur le schéma ci-après, associe à chaque bit le masque de l'IRQ associée.
Pour que le PIC accepte une IRQ, on doit mettre à 0 (démasqué) le bit correspondant sur le port de data, on peut le remettre à 1 (masqué) pour désactiver l'IRQ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
End of Interrupt (EOI)
Pour toute ISR configuré pour des IRQ, il est important de signaler au PIC que l'ISR est terminé avant de retourner de l'ISR. Comme on peut le voir sur le schéma suivant, il faut envoyer le 6ème bit à 1 sur le port de data, donc 0x20.
Il est également spécifié qu'avant d'envoyer une EOI à un slave PIC, il faut d'abord l'envoyer au master PIC.
1 2 3 4 5 6 |
|
Initialisation du PIC (bonus)
L'initialisation des PIC est déjà fourni pour le projet système. Les PIC sont alors configurés pour utiliser les numéros d'interruption 32 à 39 pour les IRQ 0 à 7 et les numéros 40 à 47 pour les IRQ 8 à 15. Vous pouvez ignorer cette section si cela ne vous intéresse pas.
Pour chacun de nos PIC, on doit envoyer 4 Initialization Command Word (ICW). Le premier mot à envoyer: ICW1, démarre l'initialisation et doit être envoyé sur le port de commande. La valeur à envoyé est 0x11, IC4 = 1 (on prévoit d'envoyer un ICW4), SNGL = 0 (cascading mode, car on a slave/master).
1 2 |
|
On doit ensuite envoyer ICW2 (sur le port de data), qui contient l'offset du numéro d'interruption de notre PIC, dans notre cas on enverrait 0x20 pour le master (32-39) et 0x28 (40-47) pour le slave. On peut voir que les 3 bits de poids sont ignorés quand on est en mode 8086/8088 (notre cas).
1 2 |
|
Le mot ICW3 nous permet de donner aux master/slave les informations sur la topologie utilisé, on informe le master qu'il a un slave sur l'IRQ 2 et on informe le PIC slave de son identifiant.
1 2 |
|
Le dernier mot, ICW4 nous permet de spécifier que nous utiliserons le mode 8086/8088 correspondant à notre architecture, nous ne rentrerons pas dans les détails ici, mais tous les autres champs sont à 0.
1 2 |
|
Le code final est donc le suivant:
1 2 3 4 5 6 7 8 9 10 11 |
|
Sources
- https://wiki.osdev.org/Interrupts
- https://wiki.osdev.org/Exceptions
- https://wiki.osdev.org/Interrupt_Descriptor_Table
- https://en.wikipedia.org/wiki/FLAGS_register
- https://pdos.csail.mit.edu/6.828/2017/readings/hardware/8259A.pdf