Skip to content

Niveau 5 · Chapitre 5.5

Déroutements, interruptions et exceptions

Les trois façons dont le contrôle quitte un programme sans qu’il saute : les exceptions levées par une instruction (fautes, déroutements, abandons), les déroutements volontaires vers le noyau (appels système), et les interruptions des périphériques et des minuteries — ce que fait le CPU en matériel, les différences entre x86, ARM64 et RISC-V, et des mesures du coût des déroutements et des appels système sur de vraies machines.

Tous les chapitres précédents suivaient un programme qui saute et appelle dans son propre code. Mais le contrôle peut aussi quitter le programme sans qu’il contienne le moindre saut : une instruction divise par zéro, touche une page non projetée ou demande un service au système ; ou une carte réseau finit de recevoir un paquet. À chaque fois, le CPU s’arrête, sauvegarde juste de quoi revenir, passe en mode noyau et exécute un gestionnaire (handler) choisi dans une table. Ce mécanisme est ce qui rend un système d’exploitation possible : c’est la seule entrée dans le noyau, et le seul moyen pour le noyau de reprendre le CPU à un programme qui ne le rend jamais.

Tanenbaum sépare ces événements en deux :

  • les déroutements (traps), causés par le programme lui-même : synchrones et reproductibles — exécutez le même programme sur la même entrée, et ils surviennent à la même instruction à chaque fois ;
  • les interruptions, causées par quelque chose d’extérieur au programme, en général les entrées-sorties : asynchrones, elles arrivent entre deux instructions quelconques.

Chaque ISA ajoute son propre vocabulaire. Intel appelle exceptions les événements synchrones et les divise en fautes, déroutements et abandons ; ARM appelle tout exception, interruptions comprises ; RISC-V appelle tout trap, divisé en exceptions et interruptions. Les idées sont les mêmes.

Ce que fait le matériel

Quand une exception ou une interruption est prise, le CPU exécute en matériel une courte séquence fixe, sans aucune instruction du programme :

  1. Sauvegarder le point de retour : l’adresse de l’instruction interrompue ou fautive, et les indicateurs.
  2. Changer de privilège : passer du mode utilisateur au mode noyau et, sur x86, sur la pile propre au noyau.
  3. Trouver le gestionnaire : chercher le numéro de l’événement dans une table préparée à l’avance par le noyau.
  4. Sauter vers le gestionnaire.

Le gestionnaire sauvegarde les registres qu’il utilise, traite l’événement, les restaure, puis exécute une instruction de retour d’exception qui annule d’un coup les étapes 1 et 2. Si tout est bien fait, le programme interrompu ne s’aperçoit de rien ; Tanenbaum parle de transparence.

x86-64ARM64RISC-V
table des gestionnairesIDT : 256 entrées de 16 octets, repérée par le registre IDTRune table de 16 points d’entrée de 128 octets chacun, en VBAR_EL1un point d’entrée dans stvec (ou une table en mode vectorisé)
où trouver la causele numéro de vecteur (0–255), plus un code d’erreur sur la pilele registre de syndrome ESR_EL1le registre scause
adresse de retour rangée dansla pile du noyau (avec les indicateurs, le pointeur de pile, le segment de code)ELR_EL1 (indicateurs dans SPSR_EL1)sepc (état dans sstatus)
instruction de retouriretqeretsret

Le x86 empile l’état sauvegardé en mémoire ; ARM64 et RISC-V le placent dans des registres spéciaux et laissent au gestionnaire le soin de les sauvegarder s’il en a besoin. Le livre décrit la table du x86 comme des descripteurs de segment de 8 octets ; en mode 64 bits, chaque entrée fait 16 octets, pour contenir une adresse de gestionnaire sur 64 bits.

Les exceptions : fautes, déroutements et abandons

Intel classe les exceptions selon l’endroit où reprend l’exécution :

  • une faute est signalée avant que l’instruction se termine, et l’adresse sauvegardée désigne l’instruction fautive elle-même : le gestionnaire peut corriger le problème et la réexécuter. L’exemple clé est le défaut de page : le programme touche une page absente de la mémoire, le noyau la charge depuis le disque, et la même instruction se réexécute, avec succès. Le programme n’en sait rien. Ce redémarrage est ce qui rend la mémoire virtuelle possible.
  • un déroutement (trap) est signalé après l’instruction, et l’exécution reprend à la suivante. Les points d’arrêt (int3) et le pas à pas fonctionnent ainsi.
  • un abandon (abort) est irrécupérable, comme une machine check qui signale une erreur matérielle.

Redémarrer une instruction fautive exige des exceptions précises : quand le gestionnaire s’exécute, toutes les instructions précédentes doivent être terminées et aucune instruction suivante ne doit avoir rien modifié. C’est plus difficile qu’il n’y paraît dans un cœur à exécution dans le désordre, qui a des dizaines d’instructions en vol ; le tampon de réordonnancement existe en grande partie pour le garantir, en ne levant une exception que lorsque l’instruction fautive atteint sa tête.

Les exceptions x86 courantes, et ce qu’en fait Linux pour le programme :

VecteurNomLevée parSignal Linux
0#DE erreur de divisiondivision entière par zéro, ou quotient trop grandSIGFPE
1#DB débogagepas à pas, points d’arrêt matérielsSIGTRAP
3#BP point d’arrêtint3 (l’octet 0xCC)SIGTRAP
6#UD opcode invalideune instruction non définie, ou ud2 (0f 0b)SIGILL
13#GP protection généraleune instruction privilégiée en mode utilisateur, une adresse non canoniqueSIGSEGV
14#PF défaut de pageun accès à une page non projetée ou protégéetraité en silence, ou SIGSEGV
18#MC machine checkune erreur matérielleen général une panique du noyau

Le simulateur lève la même erreur de division :

Live · Diviser par zéro

À essayer : Appuyez sur Step pour exécuter une instruction, Run pour animer ou Continue pour aller au bout ; les boutons L2 à L7 changent de niveau, vers le bas ou le haut.

program— ▸ is the next instruction
  1. mov eax, 7
  2. xor edx, edx
  3. xor ecx, ecx
  4. div ecx ; edx:eax / ecx, with ecx = 0
  5. mov eax, 1 ; never reached
step 0
Loading emulator…
The instruction that just ran, as the bytes the CPU actually fetched and decoded.

En arrivant sur le div, la machine s’arrête avec #DE divide error. Sur un vrai système, le gestionnaire du vecteur 0 enverrait au processus le signal SIGFPE, et sans gestionnaire de signal, le processus meurt.

Toutes les ISA ne déroutent pas

Le livre cite le débordement et la division par zéro parmi les causes classiques de déroutement. C’est là que les ISA diffèrent le plus. Mesuré sur un Apple M2, avec le même programme C compilé pour ARM64 et pour x86-64 (exécuté sous Rosetta 2) :

volatile int zero = 0;ARM64x86-64
7 / zero (entier)renvoie 0, aucun déroutementSIGFPE
7.0 / 0.0 (virgule flottante)inf, aucun déroutementinf, aucun déroutement
instruction non définie (udf / ud2)SIGILLSIGILL
point d’arrêt (brk / int3)SIGTRAPSIGTRAP
lecture par un pointeur nulSIGSEGVSIGSEGV

L’instruction de division d’ARM64, sdiv, renvoie simplement 0 pour un diviseur nul, et celle de RISC-V renvoie −1 (tous les bits à 1) ; aucune des deux ne déroute. Les concepteurs de ces ISA ont choisi de garder un diviseur simple et de laisser le logiciel vérifier s’il le souhaite, ce que les compilateurs C ne font pas : la division par zéro est un comportement indéfini. La division flottante par zéro ne déroute nulle part par défaut. IEEE 754 lui définit un résultat (l’infini) et se contente de positionner un indicateur d’état ; les déroutements qu’elle autorise sont désactivés sauf si un programme les active.

Le débordement entier, premier exemple du livre, ne déroute pas non plus sur les ISA courantes actuelles. Le x86 avait une instruction pour cela, INTO (l’octet 0xCE), qui déroutait si l’indicateur de débordement était positionné ; elle a été retirée du mode 64 bits — nasm la refuse avec instruction not supported in 64-bit mode. MIPS a un add qui déroute en cas de débordement, mais les compilateurs émettent addu, qui ne le fait pas. Les langages qui vérifient le débordement, comme Rust en mode débogage ou Swift, le font en logiciel, avec un saut conditionnel sur l’indicateur de débordement après chaque opération.

Les appels système : des déroutements volontaires

Un programme ne peut pas sauter dans le noyau : la mémoire du noyau n’est pas projetée pour lui, et y sauter provoquerait une faute. La seule entrée est un déroutement volontaire, une instruction dont le seul but est d’en provoquer un. Le noyau lit le numéro du service demandé et ses arguments dans des registres, fait le travail, et revient.

x86-64ARM64RISC-V
instructionsyscall (0f 05)svc #0ecall
numéro d’appel dansraxx8 (Linux)a7
arguments dansrdi, rsi, rdx, r10, r8, r9x0–x5a0–a5

Les programmes Linux 32 bits utilisaient int 0x80, une interruption logicielle passant par l’IDT. syscall est un chemin dédié, plus rapide, qui évite la consultation de la table. Le simulateur implémente les appels Linux write (numéro 1) et exit (numéro 60) :

Live · Deux appels système : write, puis exit

À essayer : Appuyez sur Step pour exécuter une instruction, Run pour animer ou Continue pour aller au bout ; les boutons L2 à L7 changent de niveau, vers le bas ou le haut.

program— ▸ is the next instruction
  1. .data
  2. msg: .asciz "hello\n"
  3. .text
  4. mov eax, 1 ; write(
  5. mov edi, 1 ; fd 1 = standard output,
  6. lea rsi, [rip+msg] ; buffer,
  7. mov edx, 6 ; 6 bytes)
  8. syscall
  9. mov eax, 60 ; exit(
  10. mov edi, 3 ; status 3)
  11. syscall
step 0
Loading emulator…
The instruction that just ran, as the bytes the CPU actually fetched and decoded.

Il affiche hello et se termine avec le code 3. Dans un vrai programme, les fonctions write et exit de la bibliothèque C sont de minces enveloppes autour de ces instructions ; le niveau du système d’exploitation regarde ce que fait le noyau de l’autre côté.

Entrer dans le noyau et en sortir coûte bien plus qu’un appel de fonction. Sur le M2 sous macOS, une boucle de getppid() — à peu près l’appel système le moins coûteux qui soit — a pris 92 ns par appel, contre 0,9 ns pour un appel de fonction ordinaire : un facteur d’environ 100. Dans une machine virtuelle Linux sur le même ordinateur, c’était 152 ns. L’instruction elle-même est rapide ; le coût vient du changement de pile, de la sauvegarde des registres et des protections de sécurité que les noyaux modernes appliquent à chaque entrée. C’est pourquoi on regroupe les appels système quand c’est possible, et pourquoi Linux répond à certains des plus fréquents, comme la lecture de l’horloge, depuis une page de données du noyau projetée dans chaque processus (le vDSO), sans aucun déroutement.

Les interruptions

Une interruption vient de l’extérieur du flot d’instructions : un disque qui termine un transfert, un paquet réseau qui arrive, une touche enfoncée, une minuterie qui expire. Le CPU vérifie entre deux instructions s’il y a des interruptions en attente, et quand l’une l’est et que les interruptions sont autorisées, il exécute la même séquence sauvegarde-bascule-saut que pour une exception, avec un numéro de vecteur fourni par le périphérique ou son contrôleur.

Tanenbaum décrit la conception classique : le périphérique active une ligne d’interruption sur le bus, le CPU accuse réception, et le périphérique place son numéro de vecteur sur le bus de données. Les PC de l’époque utilisaient un contrôleur d’interruptions 8259A pour arbitrer les priorités entre périphériques. Les machines modernes fonctionnent autrement :

  • Chaque cœur a un APIC local (sur ARM, un redistributeur du GIC) qui reçoit les interruptions, les classe par priorité et les livre à son cœur.
  • Les périphériques PCIe n’ont plus du tout de fils d’interruption. Ils signalent une interruption par un MSI (message-signaled interrupt) : une écriture mémoire ordinaire à une adresse spéciale, dont la donnée est le numéro de vecteur. Une carte réseau à plusieurs files peut viser un vecteur différent — et un cœur différent — pour chacune.
  • Les cœurs s’interrompent mutuellement par des interruptions inter-processeurs (IPI), par exemple pour faire vider le TLB d’un autre cœur ou le faire changer de tâche.
  • Une interruption de minuterie sur chaque cœur permet au noyau de reprendre le CPU à un programme qui boucle indéfiniment, et de passer à un autre : c’est le battement de cœur du multitâche préemptif.
  • Les interruptions non masquables (NMI) existent toujours, comme le dit le livre, pour les urgences — et pour les profileurs et les chiens de garde qui doivent fonctionner même quand les interruptions sont désactivées.

Le schéma de priorités du livre survit lui aussi. Le gestionnaire d’une interruption prioritaire peut lui-même être interrompu par une interruption plus prioritaire encore, mais les moins prioritaires attendent. L’APIC local l’implémente en classant les interruptions par numéro de vecteur.

Quelle est l’activité réelle ? Sur une machine virtuelle Linux inactive à 24 cœurs, /proc/stat a compté environ 1 500 interruptions et 2 200 changements de contexte par seconde, sans aucun programme en cours. Chacun était un déroutement vers le noyau suivi d’un retour transparent — invisible pour tous les programmes interrompus.

À retenir

  • Les exceptions (les déroutements de Tanenbaum) sont synchrones : causées par une instruction, au même endroit à chaque exécution. Les interruptions sont asynchrones : causées par les périphériques et les minuteries, entre deux instructions quelconques.
  • Le matériel sauvegarde l’adresse de retour et les indicateurs, passe en mode noyau et saute à travers une table (l’IDT du x86, VBAR_EL1 sur ARM64, stvec sur RISC-V) ; iretq, eret et sret reviennent.
  • Une faute réexécute l’instruction fautive (les défauts de page rendent la mémoire virtuelle possible) ; un déroutement reprend après elle ; un abandon ne peut pas reprendre. Le redémarrage exige des exceptions précises.
  • Ce qui déroute dépend de l’ISA : le x86 déroute sur une division entière par zéro, ARM64 renvoie 0 et RISC-V −1. Les erreurs flottantes et le débordement entier ne déroutent nulle part par défaut ; le x86 a retiré INTO en mode 64 bits.
  • Les appels système sont des déroutements volontaires (syscall, svc, ecall), environ 100 fois plus coûteux qu’un appel de fonction — 92 ns mesurées sur un M2.
  • Les interruptions modernes sont livrées par des APIC propres à chaque cœur, sous forme d’écritures mémoire MSI venant des périphériques PCIe, et d’IPI entre cœurs ; une interruption de minuterie fait tourner l’ordonnanceur.

Dans ce niveau

  1. 5.1Ce que garantit l’ISA : modèle mémoire, alignement et ordre
  2. 5.2Formats et encodage des instructions
  3. 5.3Modes d’adressage
  4. 5.4x86, ARM et RISC-V côte à côte
  5. 5.5Déroutements, interruptions et exceptions
  6. 5.6Les types que comprend le matériel
  7. 5.7VLIW, EPIC et l’Itanium