IRQL, interruptions, ISR et DPC dans le kernel Windows

Ce cours dédiée au développement de drivers en kernel mode sans Framework

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 124
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

IRQL, interruptions, ISR et DPC dans le kernel Windows

Post by Hydraxx »

IRQL, interruptions, ISR et DPC dans le kernel Windows

Objectif

Ce cours présente les niveaux IRQL du noyau Windows, le traitement des interruptions matérielles, le rôle des ISR et des DPC, ainsi que les contraintes imposées au code lorsqu'il s'exécute à un niveau de priorité élevé.

L'idée centrale est la suivante :

Code: Select all

plus l'IRQL est élevé,
moins le système peut être interrompu par des niveaux inférieurs,
donc plus le code doit rester court et contrôlé.
L'IRQL ne représente pas une durée d'exécution. Il représente un niveau de priorité d'interruption/exécution utilisé par Windows pour ordonner certaines activités du noyau.

1. Qu'est-ce qu'un IRQL ?

IRQL signifie :

Code: Select all

Interrupt Request Level
Il s'agit d'un niveau de priorité utilisé par le noyau pour savoir quelles interruptions ou activités peuvent interrompre le code actuellement exécuté.

Le processeur exécute toujours du code à un certain IRQL logique géré par Windows.

Les niveaux importants à connaître sont :

Code: Select all

PASSIVE_LEVEL
APC_LEVEL
DISPATCH_LEVEL
DIRQL
On peut retenir mentalement :

Code: Select all

PASSIVE_LEVEL
    ↓
APC_LEVEL
    ↓
DISPATCH_LEVEL
    ↓
DIRQL
Plus on monte, plus les contraintes deviennent strictes.

2. PASSIVE_LEVEL

Code: Select all

PASSIVE_LEVEL
est le niveau normal d'exécution.

À ce niveau :
  • le thread peut être préempté ;
  • les attentes bloquantes sont possibles ;
  • la mémoire paginée peut être utilisée ;
  • les opérations pouvant provoquer une faute de page sont possibles ;
  • les API nécessitant un contexte passif peuvent être appelées.
Beaucoup de routines de driver classiques s'exécutent à ce niveau.

Exemples typiques :
  • initialisation ;
  • certaines routines de dispatch ;
  • accès fichier avec les routines Zw ;
  • opérations longues ;
  • travail exécuté via work item.
3. APC_LEVEL

Code: Select all

APC_LEVEL
est supérieur à

Code: Select all

PASSIVE_LEVEL
.

Les APC sont des mécanismes permettant d'exécuter du code dans le contexte d'un thread.

À ce niveau, certaines APC ne peuvent plus être délivrées normalement.

Ce niveau est moins courant dans les drivers simples, mais il est important pour comprendre certaines primitives de synchronisation et les régions critiques.

4. DISPATCH_LEVEL

Code: Select all

DISPATCH_LEVEL
est un niveau important dans les drivers Windows.

Les DPC s'exécutent à ce niveau.

Un spin lock classique acquis avec :

Code: Select all

KeAcquireSpinLock
élève aussi l'IRQL à

Code: Select all

DISPATCH_LEVEL
.

À ce niveau :
  • le code doit être très court ;
  • aucune attente bloquante classique ne doit être effectuée ;
  • la mémoire paginée ne doit pas être utilisée ;
  • une faute de page ne peut pas être traitée normalement ;
  • les routines appelées doivent être compatibles avec cet IRQL.
DISPATCH_LEVEL est donc un contexte de travail court, non bloquant et non pageable.

5. DIRQL

DIRQL désigne les niveaux utilisés par les interruptions matérielles.

Une ISR s'exécute généralement au DIRQL associé à son interruption.

Ce niveau est supérieur à

Code: Select all

DISPATCH_LEVEL
.

À ce niveau, les contraintes sont encore plus strictes.

Une ISR doit :
  • identifier rapidement si l'interruption lui appartient ;
  • effectuer le minimum de travail nécessaire ;
  • acquitter ou stabiliser le périphérique ;
  • reporter le travail plus long vers une DPC.
Une ISR ne doit pas effectuer de traitement long.

6. Priorité de thread et IRQL

Il ne faut pas confondre :

Code: Select all

priorité de thread
et :

Code: Select all

IRQL
La priorité d'un thread sert au scheduler pour choisir quel thread exécuter.

L'IRQL contrôle la priorité des interruptions et de certaines activités noyau sur le processeur courant.

Exemple mental :

Code: Select all

Priorité thread
    -> qui le scheduler préfère exécuter

IRQL
    -> ce qui peut interrompre le code courant
Un thread très prioritaire peut toujours s'exécuter à

Code: Select all

PASSIVE_LEVEL
.

Inversement, du code exécuté à

Code: Select all

DISPATCH_LEVEL
n'est pas simplement un "thread très prioritaire".

7. Lire l'IRQL courant

La routine :

Code: Select all

KeGetCurrentIrql
permet de connaître le niveau IRQL courant.

Exemple :

Code: Select all

KIRQL irql = KeGetCurrentIrql();

KdPrint(("Current IRQL = %lu\n", irql));
Cela peut être utile pour du debug ou pour vérifier une hypothèse pendant le développement.

8. Élever l'IRQL

Windows fournit :

Code: Select all

KeRaiseIrql
pour élever l'IRQL du processeur courant.

Exemple conceptuel :

Code: Select all

KIRQL oldIrql;

KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);

// travail très court

KeLowerIrql(oldIrql);
Il faut conserver l'ancien niveau pour pouvoir le restaurer correctement.

Élever l'IRQL ne doit pas être utilisé arbitrairement pour "rendre le code plus prioritaire".

Le code doit avoir une raison technique précise de le faire.

9. Redescendre l'IRQL

La routine :

Code: Select all

KeLowerIrql
restaure un niveau inférieur.

Exemple :

Code: Select all

KeLowerIrql(oldIrql);
Il faut restaurer le niveau approprié.

Une mauvaise gestion de l'IRQL peut provoquer des comportements incohérents ou un bugcheck.

10. Pourquoi l'IRQL élevé bloque davantage le système

Lorsqu'un processeur exécute du code à un IRQL élevé, les activités de niveau inférieur ne peuvent pas le préempter.

Cela signifie qu'elles doivent attendre.

Exemple :

Code: Select all

CPU à DISPATCH_LEVEL
    |
    +-> travail PASSIVE_LEVEL retardé
    +-> APC normales retardées
    +-> certaines activités scheduler retardées
Plus le code reste longtemps à haut IRQL, plus il retarde le reste du système.

C'est pour cela que :

plus l'IRQL est élevé, plus le code doit être court.

Ce n'est donc pas "le code est long donc l'IRQL monte".

C'est l'inverse :

Code: Select all

le code est à IRQL élevé
    ↓
il doit rester court
11. Mémoire paginée et IRQL

À

Code: Select all

PASSIVE_LEVEL
, le système peut résoudre une faute de page.

À

Code: Select all

DISPATCH_LEVEL
ou au-dessus, ce n'est plus le cas.

Donc du code qui s'exécute à haut IRQL ne doit pas toucher des données paginées.

Exemple dangereux :

Code: Select all

#pragma alloc_text(PAGE, MyPagedFunction)

KeAcquireSpinLock(&Lock, &oldIrql);

// IRQL = DISPATCH_LEVEL

MyPagedFunction(); // mauvais contexte

KeReleaseSpinLock(&Lock, oldIrql);
Le code pageable doit rester dans des contextes où une faute de page peut être traitée.

12. Interruptions matérielles

Un périphérique matériel peut signaler au processeur qu'un événement vient de se produire.

Exemples :
  • fin d'une opération ;
  • réception de données ;
  • erreur matérielle ;
  • événement réseau ;
  • interruption de timer matériel.
Le processeur interrompt alors le flux normal et exécute une routine associée à cette interruption.

13. Priorités des interruptions

Toutes les interruptions n'ont pas la même priorité.

Une interruption de priorité plus élevée peut interrompre du code exécuté à un niveau inférieur.

Une interruption de priorité inférieure doit attendre si le processeur est déjà à un IRQL supérieur ou égal selon les règles du système.

Cela permet à Windows de prioriser les événements critiques.

14. Interruptions imbriquées

Une interruption plus prioritaire peut survenir pendant le traitement d'une interruption moins prioritaire.

On obtient alors un schéma du type :

Code: Select all

code normal
    ↓
interruption A
    ↓
ISR A
    ↓
interruption B plus prioritaire
    ↓
ISR B
    ↓
retour ISR A
    ↓
retour code normal
C'est une des raisons pour lesquelles les ISR doivent être très courtes.

15. ISR

ISR signifie :

Code: Select all

Interrupt Service Routine
C'est la routine appelée lorsqu'une interruption matérielle liée au driver se produit.

L'ISR doit généralement :
  • vérifier si le périphérique a réellement généré l'interruption ;
  • lire ou modifier le strict minimum de registres matériels nécessaires ;
  • acquitter l'interruption si nécessaire ;
  • enregistrer les informations indispensables ;
  • planifier une DPC pour le traitement plus long.
Elle retourne ensuite rapidement.

16. Pourquoi une ISR doit être courte

Une ISR s'exécute à DIRQL.

Pendant ce temps, les activités de priorité inférieure sont bloquées sur le processeur.

Si une ISR exécute une boucle longue, fait du traitement complexe ou attend une ressource, elle dégrade fortement le système.

Mauvaise idée :

Code: Select all

ISR
{
    // traitement lourd
    // parsing
    // grosses copies
    // attente
}
Meilleure architecture :

Code: Select all

ISR
{
    // minimum
    // sauvegarde état
    // queue DPC
}
Puis :

Code: Select all

DPC
{
    // traitement différé
}
17. DPC

DPC signifie :

Code: Select all

Deferred Procedure Call
Une DPC permet de différer une partie du travail d'une ISR vers un niveau inférieur :

Code: Select all

DISPATCH_LEVEL
Le schéma général est :

Code: Select all

Interruption matérielle
        ↓
ISR à DIRQL
        ↓
queue DPC
        ↓
retour rapide ISR
        ↓
DPC à DISPATCH_LEVEL
        ↓
traitement complémentaire
La DPC reste elle aussi exécutée à IRQL élevé.

Elle doit donc rester relativement courte.

18. ISR vers DPC

La logique classique d'un driver matériel est :
  • le périphérique interrompt ;
  • l'ISR capture l'état minimal ;
  • l'ISR programme une DPC ;
  • la DPC effectue le traitement différé ;
  • si un travail encore plus long ou bloquant est nécessaire, il est déplacé vers un contexte

    Code: Select all

    PASSIVE_LEVEL
    .
On obtient alors plusieurs niveaux de travail :

Code: Select all

ISR
    -> ultra court
    -> DIRQL

DPC
    -> court
    -> DISPATCH_LEVEL

Work item / thread
    -> plus long
    -> PASSIVE_LEVEL
19. KDPC

Une DPC est représentée par une structure :

Code: Select all

KDPC
Elle peut être initialisée avec :

Code: Select all

KeInitializeDpc
Exemple conceptuel :

Code: Select all

KDPC MyDpc;

KeInitializeDpc(
    &MyDpc,
    MyDpcRoutine,
    Context
);
Une DPC peut ensuite être insérée dans la file avec :

Code: Select all

KeInsertQueueDpc
Exemple :

Code: Select all

KeInsertQueueDpc(
    &MyDpc,
    NULL,
    NULL
);
20. Routine DPC

Une routine DPC reçoit plusieurs paramètres fournis par le noyau.

Exemple simplifié :

Code: Select all

VOID MyDpcRoutine(
    KDPC* Dpc,
    PVOID DeferredContext,
    PVOID SystemArgument1,
    PVOID SystemArgument2
)
{
    UNREFERENCED_PARAMETER(Dpc);
    UNREFERENCED_PARAMETER(DeferredContext);
    UNREFERENCED_PARAMETER(SystemArgument1);
    UNREFERENCED_PARAMETER(SystemArgument2);

    // travail différé court
}
La routine est exécutée à

Code: Select all

DISPATCH_LEVEL
.

Elle ne doit donc pas effectuer d'attente bloquante ni accéder à des données paginées.

21. DPC et spin locks

Comme une DPC s'exécute déjà à

Code: Select all

DISPATCH_LEVEL
, les variantes de spin lock adaptées peuvent être utilisées :

Code: Select all

KeAcquireSpinLockAtDpcLevel
KeReleaseSpinLockFromDpcLevel
Exemple :

Code: Select all

KeAcquireSpinLockAtDpcLevel(&Lock);

// accès partagé

KeReleaseSpinLockFromDpcLevel(&Lock);
Le code doit rester très court.

22. Timer + DPC

Un timer noyau peut être associé à une DPC.

Cela permet d'exécuter automatiquement une routine DPC lorsque le timer expire.

Structures :

Code: Select all

KTIMER Timer;
KDPC Dpc;
Initialisation :

Code: Select all

KeInitializeTimer(&Timer);

KeInitializeDpc(
    &Dpc,
    MyDpcRoutine,
    Context
);
Puis le timer peut être armé avec une DPC associée.

Ce mécanisme est utile pour :
  • polling périodique léger ;
  • timeouts noyau ;
  • maintenance périodique ;
  • traitement différé déclenché dans le temps.
23. DPC périodiques

Un timer périodique peut relancer régulièrement une DPC.

Mais il faut faire attention :
  • la DPC doit finir rapidement ;
  • la période ne doit pas être trop courte ;
  • une nouvelle échéance ne doit pas créer une accumulation excessive de travail ;
  • le code doit gérer correctement la concurrence.
24. Work items et retour à PASSIVE_LEVEL

Lorsqu'une DPC doit déclencher un traitement qui peut bloquer ou accéder à de la mémoire paginée, elle ne doit pas effectuer ce travail directement.

On peut utiliser un work item.

Le schéma devient :

Code: Select all

ISR
 ↓
DPC
 ↓
Work Item
 ↓
PASSIVE_LEVEL
C'est une architecture importante :
  • ISR : minimum absolu ;
  • DPC : traitement court ;
  • work item : traitement long ou bloquant.
25. APC

APC signifie :

Code: Select all

Asynchronous Procedure Call
Les APC permettent de programmer l'exécution d'une routine dans le contexte d'un thread.

Il existe plusieurs catégories d'APC.

Dans le cadre des IRQL, il faut surtout retenir que

Code: Select all

APC_LEVEL
influence la livraison de certaines APC.

Les APC sont distinctes des DPC :

Code: Select all

APC
    -> liée à un thread

DPC
    -> liée au processeur / scheduler kernel
26. Critical regions

Une critical region permet d'empêcher temporairement la livraison de certaines APC kernel normales au thread courant.

Exemple :

Code: Select all

KeEnterCriticalRegion();

// séquence sensible

KeLeaveCriticalRegion();
Cela ne protège pas une ressource contre les autres processeurs.

Ce n'est donc pas un remplacement de spin lock ou mutex.

C'est un mécanisme de contrôle du contexte APC.

27. Alertable waits

Certaines attentes peuvent être alertables.

Dans ce cas, elles peuvent être interrompues par certains mécanismes d'APC.

Cela modifie le comportement de fonctions telles que :

Code: Select all

KeWaitForSingleObject
selon le paramètre

Code: Select all

Alertable
.

Le détail complet des APC et des attentes alertables appartient davantage au domaine de la synchronisation et des threads, mais il est important de savoir qu'elles interagissent avec

Code: Select all

APC_LEVEL
.

28. Ce qu'on peut faire selon le niveau

Résumé conceptuel :

Code: Select all

PASSIVE_LEVEL
    attentes possibles
    mémoire paginée possible
    travail long possible

APC_LEVEL
    contraintes APC supplémentaires

DISPATCH_LEVEL
    pas d'attente bloquante classique
    pas de mémoire paginée
    DPC / spin locks
    travail court

DIRQL
    ISR
    contraintes maximales
    travail ultra court
29. Erreurs classiques

Erreur 1 : appeler une fonction pageable à DISPATCH_LEVEL

Une faute de page ne peut pas être traitée normalement.

Erreur 2 : attendre dans une DPC

Une DPC ne doit pas effectuer une attente bloquante.

Erreur 3 : effectuer trop de travail dans l'ISR

Cela bloque les niveaux inférieurs trop longtemps.

Erreur 4 : oublier de restaurer l'IRQL

Après une élévation manuelle, il faut restaurer le niveau précédent.

Erreur 5 : utiliser KeAcquireSpinLockAtDpcLevel alors qu'on n'est pas à DISPATCH_LEVEL

La variante doit correspondre au contexte réel.

Erreur 6 : croire que l'IRQL mesure le temps d'exécution

L'IRQL est un niveau de priorité d'interruption, pas une durée.

30. Workflow mental ISR / DPC

Lorsqu'une interruption arrive :

Code: Select all

1. Le matériel déclenche une interruption
2. Windows exécute l'ISR au DIRQL approprié
3. L'ISR vérifie la source
4. L'ISR capture l'état minimal
5. L'ISR programme une DPC
6. L'ISR retourne
7. La DPC s'exécute à DISPATCH_LEVEL
8. La DPC fait le traitement court restant
9. Si nécessaire, elle délègue le travail long à PASSIVE_LEVEL
31. APIs et structures à retenir

IRQL :

Code: Select all

KeGetCurrentIrql
KeRaiseIrql
KeLowerIrql
DPC :

Code: Select all

KDPC
KeInitializeDpc
KeInsertQueueDpc
Spin locks dans ce contexte :

Code: Select all

KeAcquireSpinLock
KeReleaseSpinLock
KeAcquireSpinLockAtDpcLevel
KeReleaseSpinLockFromDpcLevel
Timers :

Code: Select all

KTIMER
KeInitializeTimer
APC / régions critiques :

Code: Select all

KeEnterCriticalRegion
KeLeaveCriticalRegion
32. Concepts à retenir
  • IRQL ne représente pas la durée d'une fonction.
  • IRQL représente un niveau de priorité d'interruption/exécution du noyau.
  • Plus l'IRQL est élevé, plus le code bloque les niveaux inférieurs.
  • Le code à IRQL élevé doit donc être court.
  • PASSIVE_LEVEL permet le plus de choses.
  • DISPATCH_LEVEL interdit les attentes bloquantes classiques et la mémoire paginée.
  • DIRQL correspond au traitement des interruptions matérielles.
  • Une ISR doit faire le minimum.
  • Une DPC permet de reporter le travail d'une ISR.
  • Une DPC s'exécute à DISPATCH_LEVEL.
  • Un travail encore plus long doit être déplacé vers PASSIVE_LEVEL.
33. Schéma mental global

Code: Select all

PASSIVE_LEVEL
    |
    | code normal / waits / pageable memory
    |
APC_LEVEL
    |
    | restrictions APC
    |
DISPATCH_LEVEL
    |
    | DPC / spin locks / code court
    |
DIRQL
    |
    | ISR / interruption matérielle / code ultra court
Et pour le traitement d'une interruption :

Code: Select all

Périphérique
    ↓
Interruption
    ↓
ISR
DIRQL
    ↓
DPC
DISPATCH_LEVEL
    ↓
Work Item / Thread
PASSIVE_LEVEL
Conclusion

IRQL, interruptions, ISR et DPC forment une seule chaîne logique dans le fonctionnement du kernel Windows.

L'IRQL indique quelles activités peuvent interrompre le code courant. Une interruption matérielle déclenche une ISR à DIRQL. L'ISR réalise uniquement le travail indispensable puis reporte le traitement plus long dans une DPC. La DPC s'exécute à DISPATCH_LEVEL et doit rester courte. Si un traitement bloquant ou paginé est nécessaire, il doit être déplacé vers un contexte PASSIVE_LEVEL.

La règle centrale est simple : plus le niveau IRQL est élevé, moins le code peut faire de choses et plus il doit finir rapidement.

Who is online

Users browsing this forum: No registered users and 0 guests