Les IRP en WDM (Kernel)

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

Moderator: Rick

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

Les IRP en WDM (Kernel)

Post by Hydraxx »

les IRP dans le kernel Windows

Salut l'équipe 8-)

Aujourd'hui on attaque un gros morceau du développement driver Windows : les IRP, pour I/O Request Packet.

C'est un sujet central, parce qu'un driver Windows ne sert pas juste à exister en mémoire après son

Code: Select all

DriverEntry
. Une fois chargé, son vrai travail est de recevoir des requêtes, les comprendre, les traiter, puis les terminer proprement.

Ces requêtes sont encapsulées par Windows dans une structure semi-documentée appelée :

Code: Select all

IRP
Un IRP représente une demande d'entrée/sortie : ouvrir un périphérique, fermer un handle, lire, écrire, envoyer un IOCTL, démarrer un périphérique Plug & Play, gérer une requête power, etc.

Dans un driver WDM, presque tout finit par tourner autour de ça :
  • un programme user-mode appelle

    Code: Select all

    CreateFile
    ,

    Code: Select all

    ReadFile
    ,

    Code: Select all

    WriteFile
    ou

    Code: Select all

    DeviceIoControl
    ;
  • l'I/O Manager crée un IRP ;
  • l'IRP descend dans la pile de drivers ;
  • un driver traite la requête, ou la passe au driver du dessous ;
  • l'IRP remonte quand il est complété ;
  • l'I/O Manager rend le résultat au programme appelant.
Le but de ce cours est de comprendre le mécanisme proprement, sans rester au niveau "magique".

1. C'est quoi un IRP ?

Un IRP, c'est une structure allouée en kernel-mode, en général depuis du non-paged pool.

Pourquoi du non-paged pool ?

Parce qu'un IRP peut être manipulé dans des contextes où le paging n'est pas autorisé, ou par des composants noyau qui ne doivent pas dépendre d'une mémoire paginable. Un IRP doit rester accessible tant que la requête existe.

Un IRP peut être créé par plusieurs entités :
  • l'I/O Manager ;
  • le Plug & Play Manager ;
  • le Power Manager ;
  • parfois par un driver lui-même.
Mais dans le cas classique d'un driver logiciel simple, c'est souvent l'I/O Manager qui crée l'IRP suite à un appel user-mode.

Exemple :

Code: Select all

CreateFile(L"\\\\.\\Zero", ...);
ReadFile(hDevice, buffer, sizeof(buffer), &bytes, nullptr);
WriteFile(hDevice, buffer, sizeof(buffer), &bytes, nullptr);
DeviceIoControl(hDevice, IOCTL_XXX, ...);
Ces appels ne vont pas directement appeler ton driver comme une fonction normale. Ils passent par l'I/O Manager, qui fabrique une requête noyau : l'IRP.

2. L'IRP n'est jamais vraiment seul

Un IRP n'est pas juste une structure isolée.

Il est accompagné d'une ou plusieurs structures :

Code: Select all

IO_STACK_LOCATION
L'idée est simple :
  • l'IRP représente la requête globale ;
  • les

    Code: Select all

    IO_STACK_LOCATION
    représentent les informations spécifiques à chaque driver dans la pile.
On peut se représenter ça comme ceci :

Code: Select all

+------------------------------+
|              IRP             |
+------------------------------+
| IO_STACK_LOCATION pour drv 1  |
+------------------------------+
| IO_STACK_LOCATION pour drv 2  |
+------------------------------+
| IO_STACK_LOCATION pour drv 3  |
+------------------------------+
Quand l'IRP descend dans une stack de drivers, chaque driver a son propre emplacement de pile I/O.

Un driver ne doit pas lire n'importe quel emplacement au hasard. Il doit récupérer l'emplacement courant avec :

Code: Select all

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
Cette macro donne accès à la stack location qui correspond au driver actuellement appelé.

3. Pourquoi il y a plusieurs IO_STACK_LOCATION ?

Parce que les drivers Windows ne sont pas toujours seuls.

Un périphérique peut être représenté par une pile de drivers.

Exemple conceptuel :

Code: Select all

+----------------------+
| Filter Driver haut   |  FiDO
+----------------------+
| Function Driver      |  FDO
+----------------------+
| Filter Driver bas    |  FiDO
+----------------------+
| Bus Driver           |  PDO
+----------------------+
Un IRP peut traverser plusieurs drivers. Chaque couche doit avoir ses propres paramètres, son propre contexte, et éventuellement sa propre routine de completion.

Donc quand l'IRP est alloué, le système prévoit autant de

Code: Select all

IO_STACK_LOCATION
que nécessaire pour la pile.

4. Device node, PDO, FDO, FiDO

Windows est plutôt orienté device object que simplement "driver".

Un driver crée généralement un objet de périphérique avec :

Code: Select all

IoCreateDevice
ou parfois :

Code: Select all

IoCreateDeviceSecure
Ces objets forment une stack.

Dans les pages, on voit trois types de device objects :
  • PDO : Physical Device Object ;
  • FDO : Functional Device Object ;
  • FiDO : Filter Device Object.
4.1. PDO - Physical Device Object

Le nom est un peu trompeur. Un PDO n'est pas forcément "physique" dans le sens matériel brut.

Le PDO est généralement créé par le bus driver. Il représente le fait qu'un périphérique existe sur un bus : PCI, USB, etc.

Exemple :
  • le bus PCI détecte un périphérique ;
  • le bus driver crée un PDO ;
  • le Plug & Play Manager cherche quel driver doit gérer ce périphérique ;
  • un function driver est chargé au-dessus.
4.2. FDO - Functional Device Object

Le FDO est créé par le driver principal du périphérique, celui qui comprend réellement le matériel ou la logique du périphérique.

C'est souvent le "vrai" driver fonctionnel.

4.3. FiDO - Filter Device Object

Un FiDO est un device object créé par un filter driver.

Un filter driver peut intercepter, modifier, surveiller ou enrichir les requêtes qui passent dans la stack.

Il peut y avoir :
  • des filtres au-dessus du FDO ;
  • des filtres en-dessous du FDO.
Exemple conceptuel :

Code: Select all

Application
    |
I/O Manager
    |
Upper Filter
    |
Function Driver
    |
Lower Filter
    |
Bus Driver / PDO
5. Rôle du Plug & Play Manager

Le Plug & Play Manager est responsable de construire la stack de périphériques.

Il peut chercher dans le registre les drivers associés à un périphérique.

On retrouve des informations comme :

Code: Select all

HKLM\System\CurrentControlSet\Services\NomDuService
Le champ Service dans certaines clés registre permet d'indiquer indirectement quel driver doit être chargé.

Pour les filtres, Windows peut regarder des valeurs comme :

Code: Select all

LowerFilters
UpperFilters
Ces valeurs contiennent des noms de services de drivers à charger comme filtres.

L'idée importante :
  • la stack de périphérique est construite couche par couche ;
  • les IRP vont ensuite pouvoir traverser cette stack ;
  • un driver peut traiter l'IRP, le passer, ou faire les deux.
6. Le chemin d'un IRP dans une stack

Un IRP peut être envoyé à un driver, puis éventuellement au driver du dessous.

Schéma :

Code: Select all

IRP
 |
 v
Driver A
 |
 v
Driver B
 |
 v
Driver C
 |
 v
Completion
 ^
 |
Remontée de l'IRP
Quand un driver reçoit un IRP, il a plusieurs possibilités.

6.1. Possibilité 1 : traiter l'IRP lui-même

Le driver peut décider que la requête le concerne directement.

Il remplit alors :

Code: Select all

Irp->IoStatus.Status
Irp->IoStatus.Information
Puis il appelle :

Code: Select all

IoCompleteRequest(Irp, IO_NO_INCREMENT);
Et il retourne un statut.

Exemple :

Code: Select all

Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
6.2. Possibilité 2 : passer l'IRP au driver inférieur

Si le driver n'est pas le dernier de la stack, il peut passer la requête plus bas avec :

Code: Select all

IoCallDriver(LowerDeviceObject, Irp);
Avant ça, il doit préparer la prochaine stack location.

Il peut utiliser :

Code: Select all

IoSkipCurrentIrpStackLocation(Irp);
ou :

Code: Select all

IoCopyCurrentIrpStackLocationToNext(Irp);
6.3. IoSkipCurrentIrpStackLocation

Code: Select all

IoSkipCurrentIrpStackLocation(Irp);
return IoCallDriver(LowerDeviceObject, Irp);
Cette macro avance simplement le pointeur de stack location.

Elle est utile quand le driver ne veut pas modifier les paramètres et veut simplement transmettre l'IRP au driver suivant.

C'est une optimisation : elle évite de copier toute la stack location.

6.4. IoCopyCurrentIrpStackLocationToNext

Code: Select all

IoCopyCurrentIrpStackLocationToNext(Irp);
return IoCallDriver(LowerDeviceObject, Irp);
Cette macro copie l'emplacement courant vers l'emplacement suivant.

Elle est utile si le driver veut transmettre les mêmes paramètres mais aussi ajouter une routine de completion ensuite.

Exemple :

Code: Select all

IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(
    Irp,
    MyCompletionRoutine,
    Context,
    TRUE,
    TRUE,
    TRUE
);

return IoCallDriver(LowerDeviceObject, Irp);
6.5. Possibilité 3 : traiter puis passer

Un driver peut lire/modifier des données de l'IRP, puis le passer.

Exemple :
  • un filtre inspecte un IOCTL ;
  • il loggue l'information ;
  • il laisse ensuite le driver inférieur gérer réellement la demande.
6.6. Possibilité 4 : passer puis être rappelé à la completion

Un driver peut enregistrer une routine de completion avant de passer l'IRP.

Quand le driver inférieur complète la requête, l'IRP remonte et la completion routine est appelée.

C'est très important pour les filtres.

6.7. Possibilité 5 : traitement asynchrone

Un driver peut aussi ne pas terminer l'IRP immédiatement.

Il peut :
  • marquer l'IRP comme pending ;
  • retourner

    Code: Select all

    STATUS_PENDING
    ;
  • compléter l'IRP plus tard.
Exemple conceptuel :

Code: Select all

IoMarkIrpPending(Irp);
QueueWorkItemOrStoreIrp(Irp);
return STATUS_PENDING;
Attention : si tu marques un IRP pending, tu dois vraiment le compléter plus tard. Sinon, le thread appelant peut rester bloqué, ou la requête peut ne jamais se terminer.

7. Les champs importants de IRP

La structure

Code: Select all

IRP
contient beaucoup de champs internes, mais certains reviennent tout le temps.

7.1. IoStatus

Le champ

Code: Select all

IoStatus
contient le résultat final de la requête.

Il contient notamment :

Code: Select all

Irp->IoStatus.Status
Irp->IoStatus.Information
Status indique le résultat NTSTATUS :

Code: Select all

STATUS_SUCCESS
STATUS_INVALID_PARAMETER
STATUS_BUFFER_TOO_SMALL
STATUS_INSUFFICIENT_RESOURCES
STATUS_INVALID_DEVICE_REQUEST
...
Information indique souvent le nombre d'octets réellement transférés ou retournés.

Exemple :

Code: Select all

Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = sizeof(MY_OUTPUT);
Si c'est une erreur,

Code: Select all

Information
est généralement mis à 0.

7.2. AssociatedIrp.SystemBuffer

Ce champ est très important en Buffered I/O.

Code: Select all

Irp->AssociatedIrp.SystemBuffer
Il pointe vers un buffer kernel alloué par l'I/O Manager.

Le driver peut lire/écrire dedans sans accéder directement à la mémoire user-mode.

7.3. MdlAddress

En Direct I/O, l'IRP contient un MDL :

Code: Select all

Irp->MdlAddress
Un MDL, pour Memory Descriptor List, décrit des pages mémoire verrouillées en RAM.

Le driver peut ensuite obtenir une adresse système avec :

Code: Select all

MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
7.4. CancelRoutine

Un IRP peut être annulé.

Le champ

Code: Select all

CancelRoutine
pointe vers une routine d'annulation éventuelle.

Dans beaucoup de drivers simples, on ne l'utilise pas au début. Mais dès qu'on garde un IRP en attente longtemps, l'annulation devient importante.

7.5. UserBuffer

Selon le type de buffering, il peut exister un pointeur vers le buffer utilisateur.

Mais attention : accéder directement à un pointeur user-mode depuis le kernel est dangereux si on ne maîtrise pas le contexte, l'IRQL et la validation.

8. Les champs importants de IO_STACK_LOCATION

Pour récupérer la stack location courante :

Code: Select all

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
Cette structure contient notamment :

Code: Select all

MajorFunction
MinorFunction
Parameters
FileObject
DeviceObject
CompletionRoutine
Context
8.1. MajorFunction

Code: Select all

stack->MajorFunction
C'est le type principal de requête.

Exemples :

Code: Select all

IRP_MJ_CREATE
IRP_MJ_CLOSE
IRP_MJ_READ
IRP_MJ_WRITE
IRP_MJ_DEVICE_CONTROL
IRP_MJ_INTERNAL_DEVICE_CONTROL
IRP_MJ_POWER
IRP_MJ_PNP
8.2. MinorFunction

Certaines requêtes ont aussi une fonction mineure.

C'est surtout important pour :
  • Plug & Play ;
  • Power Management ;
  • certains types de requêtes plus spécialisés.
Exemples conceptuels :

Code: Select all

IRP_MN_START_DEVICE
IRP_MN_QUERY_REMOVE_DEVICE
IRP_MN_SET_POWER
8.3. Parameters

Code: Select all

stack->Parameters
C'est une union. Son contenu dépend de

Code: Select all

MajorFunction
.

Pour une lecture :

Code: Select all

stack->Parameters.Read.Length
Pour une écriture :

Code: Select all

stack->Parameters.Write.Length
Pour un IOCTL :

Code: Select all

stack->Parameters.DeviceIoControl.IoControlCode
stack->Parameters.DeviceIoControl.InputBufferLength
stack->Parameters.DeviceIoControl.OutputBufferLength
Donc on ne lit pas les mêmes champs selon le type d'IRP.

8.4. FileObject

Code: Select all

stack->FileObject
Pointe vers le fichier ou device ouvert par l'utilisateur.

Dans un driver logiciel simple, on peut ne pas trop l'utiliser au début, mais c'est important dès qu'on veut gérer des états par handle.

8.5. DeviceObject

Code: Select all

stack->DeviceObject
Indique le device object concerné.

8.6. CompletionRoutine et Context

Un driver peut enregistrer une routine de completion pour être appelé quand l'IRP remonte.

Code: Select all

IoSetCompletionRoutine(
    Irp,
    MyCompletionRoutine,
    Context,
    TRUE,
    TRUE,
    TRUE
);
Le champ

Code: Select all

Context
permet de transmettre une donnée privée à la completion routine.

9. Les dispatch routines

Dans

Code: Select all

DriverEntry
, le driver configure les routines qui vont recevoir les IRP.

La table est dans :

Code: Select all

DriverObject->MajorFunction[]
Exemple :

Code: Select all

DriverObject->MajorFunction[IRP_MJ_CREATE] = MyCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = MyClose;
DriverObject->MajorFunction[IRP_MJ_READ] = MyRead;
DriverObject->MajorFunction[IRP_MJ_WRITE] = MyWrite;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyDeviceControl;
Le prototype typique :

Code: Select all

NTSTATUS DispatchRoutine(PDEVICE_OBJECT DeviceObject, PIRP Irp);
Exemple :

Code: Select all

NTSTATUS MyRead(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    UNREFERENCED_PARAMETER(DeviceObject);

    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    ULONG len = stack->Parameters.Read.Length;

    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = len;

    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}
10. IRP_MJ_CREATE

Code: Select all

IRP_MJ_CREATE
Cette requête arrive quand un handle est ouvert sur le device.

Depuis le user-mode, c'est souvent :

Code: Select all

CreateFile(L"\\\\.\\Zero", ...);
Même si le nom dit "Create", ça ne veut pas forcément dire créer un fichier. Pour un device, ça veut souvent dire "ouvrir un handle".

Exemple minimal :

Code: Select all

NTSTATUS ZeroCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    UNREFERENCED_PARAMETER(DeviceObject);

    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;

    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}
11. IRP_MJ_CLOSE

Code: Select all

IRP_MJ_CLOSE
Cette requête arrive quand le dernier handle associé à l'objet fichier est fermé.

Côté user-mode :

Code: Select all

CloseHandle(hDevice);
Dans beaucoup de drivers simples,

Code: Select all

CREATE
et

Code: Select all

CLOSE
peuvent pointer vers la même routine.

Code: Select all

DriverObject->MajorFunction[IRP_MJ_CREATE] = ZeroCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = ZeroCreateClose;
12. IRP_MJ_READ

Code: Select all

IRP_MJ_READ
Arrive après un appel user-mode :

Code: Select all

ReadFile(hDevice, buffer, sizeof(buffer), &bytes, nullptr);
Le driver peut récupérer la taille demandée :

Code: Select all

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
ULONG len = stack->Parameters.Read.Length;
Ensuite, selon le mode de buffering, l'accès au buffer change.

Dans les pages, le driver Zero utilise Direct I/O, donc il récupère l'adresse système avec :

Code: Select all

auto buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
Exemple :

Code: Select all

NTSTATUS ZeroRead(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    UNREFERENCED_PARAMETER(DeviceObject);

    auto stack = IoGetCurrentIrpStackLocation(Irp);
    auto len = stack->Parameters.Read.Length;

    if (len == 0) {
        return CompleteIrp(Irp, STATUS_INVALID_BUFFER_SIZE);
    }

    auto buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
    if (!buffer) {
        return CompleteIrp(Irp, STATUS_INSUFFICIENT_RESOURCES);
    }

    RtlZeroMemory(buffer, len);

    return CompleteIrp(Irp, STATUS_SUCCESS, len);
}
Le but du Zero Driver est simple : pour une lecture, il remplit le buffer avec des zéros.

13. IRP_MJ_WRITE

Code: Select all

IRP_MJ_WRITE
Arrive après :

Code: Select all

WriteFile(hDevice, buffer, sizeof(buffer), &bytes, nullptr);
Dans le Zero Driver, l'écriture ne fait rien de spécial : elle "consomme" le buffer.

Elle indique simplement que les octets ont été écrits.

Code: Select all

NTSTATUS ZeroWrite(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    UNREFERENCED_PARAMETER(DeviceObject);

    auto stack = IoGetCurrentIrpStackLocation(Irp);
    auto len = stack->Parameters.Write.Length;

    return CompleteIrp(Irp, STATUS_SUCCESS, len);
}
Pourquoi ne pas appeler

Code: Select all

MmGetSystemAddressForMdlSafe
ici ?

Parce qu'on n'a pas besoin de lire le contenu du buffer. Si on veut juste accepter l'écriture et retourner "ok, j'ai consommé X octets", on peut se contenter de la longueur.

14. IRP_MJ_DEVICE_CONTROL

Code: Select all

IRP_MJ_DEVICE_CONTROL
C'est une requête spéciale très utilisée dans les drivers.

Elle arrive quand le user-mode appelle :

Code: Select all

DeviceIoControl(...)
C'est ce qui permet d'envoyer des commandes personnalisées au driver.

Exemple user-mode :

Code: Select all

DeviceIoControl(
    hDevice,
    IOCTL_ZERO_GET_STATS,
    nullptr,
    0,
    &stats,
    sizeof(stats),
    &bytes,
    nullptr
);
Côté driver :

Code: Select all

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
ULONG inLen = stack->Parameters.DeviceIoControl.InputBufferLength;
ULONG outLen = stack->Parameters.DeviceIoControl.OutputBufferLength;
Ensuite on fait souvent un

Code: Select all

switch
:

Code: Select all

switch (code) {
case IOCTL_ZERO_GET_STATS:
    // retourner les stats
    break;

case IOCTL_ZERO_CLEAR_STATS:
    // reset les stats
    break;

default:
    status = STATUS_INVALID_DEVICE_REQUEST;
    break;
}
15. Compléter un IRP proprement

Quand un driver décide de terminer une requête, il doit :
  • remplir

    Code: Select all

    Irp->IoStatus.Status
    ;
  • remplir

    Code: Select all

    Irp->IoStatus.Information
    ;
  • appeler

    Code: Select all

    IoCompleteRequest
    ;
  • retourner le même status que celui placé dans l'IRP.
Exemple :

Code: Select all

NTSTATUS CompleteIrp(PIRP Irp, NTSTATUS status = STATUS_SUCCESS, ULONG_PTR info = 0) {
    Irp->IoStatus.Status = status;
    Irp->IoStatus.Information = info;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return status;
}
C'est une bonne pratique de créer un helper comme ça dans les petits drivers.

16. Piège important : ne pas toucher l'IRP après IoCompleteRequest

Après :

Code: Select all

IoCompleteRequest(Irp, IO_NO_INCREMENT);
il ne faut plus toucher l'IRP.

Pourquoi ?

Parce que l'IRP peut déjà avoir été libéré ou réutilisé par le système.

Mauvais exemple :

Code: Select all

IoCompleteRequest(Irp, IO_NO_INCREMENT);
return Irp->IoStatus.Status; // dangereux
À la place :

Code: Select all

NTSTATUS status = STATUS_SUCCESS;

Irp->IoStatus.Status = status;
Irp->IoStatus.Information = 0;

IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
Ou mieux :

Code: Select all

return CompleteIrp(Irp, STATUS_SUCCESS);
17. Le deuxième argument de IoCompleteRequest

Prototype conceptuel :

Code: Select all

VOID IoCompleteRequest(
    PIRP Irp,
    CCHAR PriorityBoost
);
Le deuxième argument peut booster temporairement la priorité du thread qui attendait la requête.

Pour les drivers logiciels simples, on utilise souvent :

Code: Select all

IO_NO_INCREMENT
Exemple :

Code: Select all

IoCompleteRequest(Irp, IO_NO_INCREMENT);
Dans certains drivers, on peut utiliser d'autres valeurs selon le type d'opération, mais au début,

Code: Select all

IO_NO_INCREMENT
est très bien.

18. Pourquoi l'accès aux buffers user-mode est dangereux

Un piège classique : croire qu'on peut prendre un pointeur user-mode et l'utiliser tranquillement dans le kernel.

Ce n'est pas si simple.

Un pointeur user-mode peut être dangereux parce que :
  • il peut être invalide ;
  • il peut pointer vers une page non présente ;
  • la mémoire peut être pagée ;
  • le driver peut ne plus être dans le contexte du processus appelant ;
  • l'IRQL peut être trop élevé pour tolérer une page fault ;
  • l'utilisateur peut modifier le buffer pendant que le driver travaille ;
  • un pointeur mal contrôlé peut provoquer un crash kernel.
Même un

Code: Select all

try/except
ne rend pas tout magique. Si tu es dans le mauvais contexte processus, ou à un IRQL où les page faults ne sont pas acceptées, tu peux planter.

C'est pour ça que Windows propose des modèles de buffering :
  • Buffered I/O ;
  • Direct I/O ;
  • Neither I/O.
19. Buffered I/O

Le Buffered I/O est le modèle le plus simple.

On active ce mode pour Read/Write avec un flag dans le device object :

Code: Select all

DeviceObject->Flags |= DO_BUFFERED_IO;
Dans ce mode, l'I/O Manager alloue un buffer kernel en non-paged pool.

Le driver accède au buffer avec :

Code: Select all

Irp->AssociatedIrp.SystemBuffer
Schéma simplifié pour une lecture :

Code: Select all

User buffer
    |
    | copie par l'I/O Manager
    v
SystemBuffer kernel
    |
    | le driver écrit dans SystemBuffer
    v
copie retour vers User buffer
19.1. Étapes du Buffered I/O pour Read/Write

Pour une requête read/write :
  • l'I/O Manager alloue un buffer kernel de même taille que le buffer utilisateur ;
  • pour une écriture, il copie le buffer user vers le system buffer ;
  • le driver reçoit l'IRP ;
  • le driver lit/écrit dans

    Code: Select all

    Irp->AssociatedIrp.SystemBuffer
    ;
  • quand le driver complète l'IRP, l'I/O Manager recopie éventuellement vers le buffer user ;
  • l'I/O Manager libère le system buffer.
Exemple :

Code: Select all

auto buffer = Irp->AssociatedIrp.SystemBuffer;
19.2. Avantages du Buffered I/O
  • très simple ;
  • le driver n'accède pas directement au buffer user ;
  • le buffer est en kernel space ;
  • utile pour les petits buffers.
19.3. Inconvénient du Buffered I/O

Il y a une copie.

Pour des gros buffers, cette copie peut coûter cher.

C'est pour ça qu'on utilise parfois Direct I/O.

20. Direct I/O

Le Direct I/O est utilisé quand on veut éviter de copier de gros buffers.

On active ce mode avec :

Code: Select all

DeviceObject->Flags |= DO_DIRECT_IO;
Attention : ce flag concerne surtout les requêtes

Code: Select all

IRP_MJ_READ
et

Code: Select all

IRP_MJ_WRITE
.

Pour

Code: Select all

IRP_MJ_DEVICE_CONTROL
, c'est la méthode du

Code: Select all

CTL_CODE
qui décide du buffering.

20.1. Le principe du Direct I/O

L'I/O Manager :
  • valide le buffer user ;
  • verrouille ses pages en RAM ;
  • crée un MDL ;
  • place ce MDL dans

    Code: Select all

    Irp->MdlAddress
    .
Le driver peut ensuite obtenir une adresse kernel valide avec :

Code: Select all

PVOID buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
20.2. Pourquoi il y a un MDL ?

La mémoire virtuelle utilisateur peut être continue virtuellement, mais pas physiquement.

Un MDL décrit les pages physiques derrière le buffer.

Il permet au kernel de savoir quelles pages sont concernées, verrouillées, et comment les mapper.

20.3. MmGetSystemAddressForMdlSafe

Prototype simplifié :

Code: Select all

PVOID MmGetSystemAddressForMdlSafe(
    PMDL Mdl,
    ULONG Priority
);
Exemple :

Code: Select all

auto buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
if (!buffer) {
    return CompleteIrp(Irp, STATUS_INSUFFICIENT_RESOURCES);
}
Il faut vérifier le retour. Si la fonction retourne

Code: Select all

NULL
, le mapping a échoué.

20.4. Avantages du Direct I/O
  • moins de copies pour les gros buffers ;
  • les pages utilisateur sont verrouillées ;
  • le driver peut accéder à une adresse système stable ;
  • utile pour des transferts importants.
20.5. Inconvénients du Direct I/O
  • plus complexe que Buffered I/O ;
  • utilise des MDL ;
  • verrouiller des pages a un coût ;
  • il faut mapper avec

    Code: Select all

    MmGetSystemAddressForMdlSafe
    si on veut une adresse CPU côté driver.
21. Neither I/O

Le mode

Code: Select all

METHOD_NEITHER
ou Neither I/O signifie que l'I/O Manager aide très peu.

Il fournit les pointeurs user-mode, et le driver se débrouille.

C'est dangereux si mal utilisé.

Le driver doit vérifier, capturer et sécuriser les accès lui-même.

Dans un driver débutant ou pédagogique, il vaut mieux éviter ce mode sauf pour comprendre le mécanisme.

22. DeviceIoControl et les IOCTL

Un IOCTL permet d'envoyer une commande personnalisée à un driver.

Côté user-mode :

Code: Select all

BOOL DeviceIoControl(
    HANDLE hDevice,
    DWORD dwIoControlCode,
    LPVOID lpInBuffer,
    DWORD nInBufferSize,
    LPVOID lpOutBuffer,
    DWORD nOutBufferSize,
    LPDWORD lpBytesReturned,
    LPOVERLAPPED lpOverlapped
);
Paramètres importants :
23. La macro CTL_CODE

Un IOCTL se définit avec :

Code: Select all

CTL_CODE(DeviceType, Function, Method, Access)
Définition conceptuelle :

Code: Select all

#define CTL_CODE(DeviceType, Function, Method, Access) \
    (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))
Les paramètres :
  • Code: Select all

    DeviceType
    : type de device ;
  • Code: Select all

    Function
    : numéro de fonction personnalisé ;
  • Code: Select all

    Method
    : méthode de buffering ;
  • Code: Select all

    Access
    : droits nécessaires.
23.1. DeviceType

Pour un device custom, on utilise souvent une valeur à partir de

Code: Select all

0x8000
.

Exemple :

Code: Select all

#define DEVICE_ZERO 0x8002
23.2. Function

C'est un numéro de commande.

Pour les fonctions custom, on utilise souvent des valeurs à partir de

Code: Select all

0x800
.

Exemple :

Code: Select all

#define IOCTL_ZERO_GET_STATS \
    CTL_CODE(DEVICE_ZERO, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
23.3. Method

C'est la méthode utilisée pour accéder aux buffers.

Valeurs possibles :

Code: Select all

METHOD_BUFFERED
METHOD_IN_DIRECT
METHOD_OUT_DIRECT
METHOD_NEITHER
23.4. Access

Contrôle les droits nécessaires.

Exemples :

Code: Select all

FILE_ANY_ACCESS
FILE_READ_ACCESS
FILE_WRITE_ACCESS
24. Les méthodes de buffering pour IOCTL

Pour

Code: Select all

DeviceIoControl
, ce n'est pas

Code: Select all

DO_BUFFERED_IO
ou

Code: Select all

DO_DIRECT_IO
qui décide.

C'est le champ

Code: Select all

Method
dans le

Code: Select all

CTL_CODE
.

24.1. METHOD_BUFFERED

Avec

Code: Select all

METHOD_BUFFERED
, l'I/O Manager utilise :

Code: Select all

Irp->AssociatedIrp.SystemBuffer
Le même buffer sert souvent pour l'entrée et la sortie.

Exemple :

Code: Select all

auto buffer = Irp->AssociatedIrp.SystemBuffer;
Les tailles sont dans :

Code: Select all

stack->Parameters.DeviceIoControl.InputBufferLength
stack->Parameters.DeviceIoControl.OutputBufferLength
À la completion, le nombre d'octets retournés doit être placé dans :

Code: Select all

Irp->IoStatus.Information
24.2. METHOD_IN_DIRECT

Le nom peut être perturbant.

Avec

Code: Select all

METHOD_IN_DIRECT
:
  • le petit buffer d'entrée passe par

    Code: Select all

    SystemBuffer
    ;
  • le buffer principal de sortie est décrit par un MDL.
24.3. METHOD_OUT_DIRECT

Avec

Code: Select all

METHOD_OUT_DIRECT
:
  • le buffer d'entrée passe aussi par

    Code: Select all

    SystemBuffer
    ;
  • le buffer de sortie principal est accessible via

    Code: Select all

    Irp->MdlAddress
    .
En pratique,

Code: Select all

METHOD_IN_DIRECT
et

Code: Select all

METHOD_OUT_DIRECT
sont souvent source de confusion. Ce sont des modes directs avec un MDL pour le buffer secondaire.

24.4. METHOD_NEITHER

Avec

Code: Select all

METHOD_NEITHER
, l'I/O Manager ne copie pas et ne mappe pas automatiquement les buffers comme dans les autres modes.

Les pointeurs user-mode sont transmis dans la stack location.

C'est puissant, mais dangereux.

Il faut être très prudent.

25. Tableau mental des buffers IOCTL

Résumé pratique :

Code: Select all

METHOD_BUFFERED:
    entrée  -> Irp->AssociatedIrp.SystemBuffer
    sortie  -> Irp->AssociatedIrp.SystemBuffer

METHOD_IN_DIRECT:
    entrée  -> Irp->AssociatedIrp.SystemBuffer
    sortie  -> Irp->MdlAddress

METHOD_OUT_DIRECT:
    entrée  -> Irp->AssociatedIrp.SystemBuffer
    sortie  -> Irp->MdlAddress

METHOD_NEITHER:
    entrée/sortie -> pointeurs user-mode bruts
26. IRP_MJ_INTERNAL_DEVICE_CONTROL

Code: Select all

IRP_MJ_INTERNAL_DEVICE_CONTROL
Ressemble à

Code: Select all

IRP_MJ_DEVICE_CONTROL
, mais il est surtout utilisé pour des communications internes kernel-mode, entre drivers.

Un programme user-mode n'est pas censé l'appeler directement avec

Code: Select all

DeviceIoControl
.

27. WinDbg : voir les IRP

Quand on débogue le kernel, il existe des commandes utiles.

27.1. !irpfind

Code: Select all

!irpfind
Permet de chercher des IRP dans les pools mémoire.

On peut obtenir des informations comme :
  • adresse de l'IRP ;
  • thread associé ;
  • device object ;
  • stack count ;
  • driver concerné.
27.2. !irp

Pour afficher un IRP précis :

Code: Select all

!irp AdresseIrp
Avec plus de détails :

Code: Select all

!irp AdresseIrp 1
On peut voir :
  • la stack trace de l'IRP ;
  • les major functions ;
  • les device objects ;
  • les completion routines ;
  • le statut ;
  • si l'IRP est pending ;
  • les informations de Driver Verifier si disponibles.
27.3. Ce qu'il faut observer dans !irp

Quand tu regardes un IRP, fais attention à :
  • Code: Select all

    IRP_MJ_XXX
    affiché ;
  • l'état pending ou non ;
  • le driver actuellement concerné ;
  • les arguments ;
  • la completion routine ;
  • le device object ;
  • les flags.
C'est super utile pour comprendre pourquoi une requête est bloquée.

28. Le Zero Driver : idée générale

Le Zero Driver est un driver logiciel pédagogique.

Son comportement :
  • pour une lecture : il remplit le buffer avec des zéros ;
  • pour une écriture : il consomme les octets fournis ;
  • il maintient des statistiques : nombre total d'octets lus/écrits ;
  • il expose des IOCTL pour lire ou remettre à zéro ces stats.
Il utilise Direct I/O pour Read/Write afin d'éviter une copie supplémentaire.

29. Création du device et du symbolic link

Dans

Code: Select all

DriverEntry
, on crée un device kernel :

Code: Select all

UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\Zero");
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\Zero");
PDEVICE_OBJECT DeviceObject = nullptr;

status = IoCreateDevice(
    DriverObject,
    0,
    &devName,
    FILE_DEVICE_UNKNOWN,
    0,
    FALSE,
    &DeviceObject
);
Puis on crée un lien symbolique user-mode :

Code: Select all

status = IoCreateSymbolicLink(&symLink, &devName);
Grâce à ça, un programme user-mode peut ouvrir :

Code: Select all

\\\\.\\Zero
30. Activer Direct I/O

Dans l'exemple :

Code: Select all

DeviceObject->Flags |= DO_DIRECT_IO;
Cela signifie que les requêtes

Code: Select all

ReadFile
et

Code: Select all

WriteFile
utiliseront Direct I/O.

Pour lire le buffer dans

Code: Select all

IRP_MJ_READ
, on utilise donc :

Code: Select all

MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority)
31. DriverEntry complet simplifié

Code: Select all

extern "C"
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    UNREFERENCED_PARAMETER(RegistryPath);

    DriverObject->DriverUnload = ZeroUnload;

    DriverObject->MajorFunction[IRP_MJ_CREATE] = ZeroCreateClose;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = ZeroCreateClose;
    DriverObject->MajorFunction[IRP_MJ_READ] = ZeroRead;
    DriverObject->MajorFunction[IRP_MJ_WRITE] = ZeroWrite;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ZeroDeviceControl;

    UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\Zero");
    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\Zero");

    PDEVICE_OBJECT DeviceObject = nullptr;

    NTSTATUS status = IoCreateDevice(
        DriverObject,
        0,
        &devName,
        FILE_DEVICE_UNKNOWN,
        0,
        FALSE,
        &DeviceObject
    );

    if (!NT_SUCCESS(status)) {
        return status;
    }

    DeviceObject->Flags |= DO_DIRECT_IO;

    status = IoCreateSymbolicLink(&symLink, &devName);
    if (!NT_SUCCESS(status)) {
        IoDeleteDevice(DeviceObject);
        return status;
    }

    return STATUS_SUCCESS;
}
32. DriverUnload

Quand le driver se décharge, il faut supprimer le lien symbolique et le device.

Code: Select all

void ZeroUnload(PDRIVER_OBJECT DriverObject) {
    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\Zero");

    IoDeleteSymbolicLink(&symLink);
    IoDeleteDevice(DriverObject->DeviceObject);
}
33. Helper CompleteIrp

Code: Select all

NTSTATUS CompleteIrp(PIRP Irp, NTSTATUS status = STATUS_SUCCESS, ULONG_PTR info = 0) {
    Irp->IoStatus.Status = status;
    Irp->IoStatus.Information = info;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return status;
}
Ce helper évite de répéter toujours le même code.

34. Create/Close

Code: Select all

NTSTATUS ZeroCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    UNREFERENCED_PARAMETER(DeviceObject);
    return CompleteIrp(Irp);
}
35. Read : remplir le buffer avec des zéros

Code: Select all

NTSTATUS ZeroRead(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    UNREFERENCED_PARAMETER(DeviceObject);

    auto stack = IoGetCurrentIrpStackLocation(Irp);
    auto len = stack->Parameters.Read.Length;

    if (len == 0) {
        return CompleteIrp(Irp, STATUS_INVALID_BUFFER_SIZE);
    }

    auto buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
    if (!buffer) {
        return CompleteIrp(Irp, STATUS_INSUFFICIENT_RESOURCES);
    }

    RtlZeroMemory(buffer, len);

    return CompleteIrp(Irp, STATUS_SUCCESS, len);
}
Point important :

Code: Select all

IoStatus.Information = len;
indique à l'appelant combien d'octets ont été lus.

36. Write : consommer les octets

Code: Select all

NTSTATUS ZeroWrite(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    UNREFERENCED_PARAMETER(DeviceObject);

    auto stack = IoGetCurrentIrpStackLocation(Irp);
    auto len = stack->Parameters.Write.Length;

    return CompleteIrp(Irp, STATUS_SUCCESS, len);
}
Ici, le driver ne lit même pas le buffer. Il dit simplement : "j'ai accepté X octets".

37. Ajouter des statistiques

Le driver peut compter les octets lus et écrits :

Code: Select all

long long g_TotalRead;
long long g_TotalWritten;
Structure exposée au client :

Code: Select all

struct ZeroStats {
    long long TotalRead;
    long long TotalWritten;
};
Quand on lit :

Code: Select all

InterlockedAdd64(&g_TotalRead, len);
Quand on écrit :

Code: Select all

InterlockedAdd64(&g_TotalWritten, len);
Pourquoi

Code: Select all

InterlockedAdd64
?

Parce que plusieurs threads peuvent appeler le driver en même temps. Une simple addition globale peut créer une data race.

38. Définir des IOCTL pour les stats

Exemple :

Code: Select all

#define DEVICE_ZERO 0x8002

#define IOCTL_ZERO_GET_STATS \
    CTL_CODE(DEVICE_ZERO, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_ZERO_CLEAR_STATS \
    CTL_CODE(DEVICE_ZERO, 0x801, METHOD_NEITHER, FILE_ANY_ACCESS)
Ici :
  • Code: Select all

    IOCTL_ZERO_GET_STATS
    retourne une structure ;
  • Code: Select all

    IOCTL_ZERO_CLEAR_STATS
    remet les compteurs à zéro.
39. Traiter IRP_MJ_DEVICE_CONTROL

Exemple simplifié :

Code: Select all

NTSTATUS ZeroDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    UNREFERENCED_PARAMETER(DeviceObject);

    auto stack = IoGetCurrentIrpStackLocation(Irp);
    auto& dic = stack->Parameters.DeviceIoControl;

    NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
    ULONG_PTR len = 0;

    switch (dic.IoControlCode) {
    case IOCTL_ZERO_GET_STATS:
    {
        if (dic.OutputBufferLength < sizeof(ZeroStats)) {
            status = STATUS_BUFFER_TOO_SMALL;
            break;
        }

        auto stats = (ZeroStats*)Irp->AssociatedIrp.SystemBuffer;
        if (!stats) {
            status = STATUS_INVALID_PARAMETER;
            break;
        }

        stats->TotalRead = g_TotalRead;
        stats->TotalWritten = g_TotalWritten;

        len = sizeof(ZeroStats);
        status = STATUS_SUCCESS;
        break;
    }

    case IOCTL_ZERO_CLEAR_STATS:
        g_TotalRead = 0;
        g_TotalWritten = 0;
        status = STATUS_SUCCESS;
        break;
    }

    return CompleteIrp(Irp, status, len);
}
40. Attention à la synchronisation des stats

Dans l'exemple, pour ajouter des valeurs on utilise :

Code: Select all

InterlockedAdd64
Mais pour lire ou remettre à zéro proprement les deux compteurs ensemble, ce n'est pas totalement parfait si plusieurs threads écrivent en même temps.

On pourrait utiliser :
  • un fast mutex ;
  • un push lock ;
  • un spin lock selon le contexte ;
  • ou accepter une approximation si c'est juste pédagogique.
Exemple avec fast mutex conceptuel :

Code: Select all

FAST_MUTEX g_Mutex;

ExInitializeFastMutex(&g_Mutex);

ExAcquireFastMutex(&g_Mutex);
g_TotalRead = 0;
g_TotalWritten = 0;
ExReleaseFastMutex(&g_Mutex);
Mais attention : chaque primitive a ses règles d'IRQL.

41. Application user-mode de test

Le programme user-mode ouvre le driver :

Code: Select all

HANDLE hDevice = CreateFile(
    L"\\\\.\\Zero",
    GENERIC_READ | GENERIC_WRITE,
    0,
    nullptr,
    OPEN_EXISTING,
    0,
    nullptr
);
Tester une lecture :

Code: Select all

BYTE buffer[64];

DWORD bytes = 0;
BOOL ok = ReadFile(hDevice, buffer, sizeof(buffer), &bytes, nullptr);

if (!ok) {
    printf("ReadFile failed: %u\n", GetLastError());
}
Tester une écriture :

Code: Select all

for (int i = 0; i < sizeof(buffer); i++) {
    buffer[i] = i + 1;
}

ok = WriteFile(hDevice, buffer, sizeof(buffer), &bytes, nullptr);

if (!ok) {
    printf("WriteFile failed: %u\n", GetLastError());
}
Lire les stats :

Code: Select all

ZeroStats stats;
DWORD bytes = 0;

BOOL ok = DeviceIoControl(
    hDevice,
    IOCTL_ZERO_GET_STATS,
    nullptr,
    0,
    &stats,
    sizeof(stats),
    &bytes,
    nullptr
);

printf("Total Read: %lld, Total Written: %lld\n",
    stats.TotalRead,
    stats.TotalWritten);
42. Les erreurs classiques avec les IRP

42.1. Oublier IoCompleteRequest

Si le driver traite l'IRP mais ne le complète jamais, le client peut rester bloqué.

Mauvais :

Code: Select all

return STATUS_SUCCESS;
si tu n'as pas complété l'IRP.

Correct :

Code: Select all

return CompleteIrp(Irp, STATUS_SUCCESS);
42.2. Compléter deux fois le même IRP

Très dangereux.

Un IRP doit être complété une seule fois par le chemin responsable.

Double completion = crash possible.

42.3. Mauvais IoStatus.Information

Si tu retournes une structure de 16 octets, mais que tu mets :

Code: Select all

Irp->IoStatus.Information = 0;
le programme user-mode peut croire qu'aucune donnée n'a été retournée.

Correct :

Code: Select all

Irp->IoStatus.Information = sizeof(ZeroStats);
42.4. Lire le mauvais champ Parameters

Pour un read :

Code: Select all

stack->Parameters.Read.Length
Pour un write :

Code: Select all

stack->Parameters.Write.Length
Pour un IOCTL :

Code: Select all

stack->Parameters.DeviceIoControl.OutputBufferLength
Il ne faut pas lire au hasard.

42.5. Accéder directement à un buffer user sans précaution

C'est une erreur dangereuse.

Préférer :
  • Code: Select all

    SystemBuffer
    pour Buffered I/O ;
  • Code: Select all

    MmGetSystemAddressForMdlSafe
    pour Direct I/O ;
  • éviter

    Code: Select all

    METHOD_NEITHER
    au début.
42.6. Retourner un status différent de l'IoStatus.Status

Mauvais :

Code: Select all

Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_INVALID_PARAMETER;
Ça crée une incohérence.

Correct :

Code: Select all

return CompleteIrp(Irp, STATUS_SUCCESS);
43. Résumé mental du trajet ReadFile

Quand un programme fait :

Code: Select all

ReadFile(hDevice, buffer, size, &bytes, nullptr);
Le chemin ressemble à ça :

Code: Select all

User-mode
  |
  | ReadFile
  v
Kernel transition
  |
  v
I/O Manager
  |
  | crée IRP_MJ_READ
  v
Driver dispatch read
  |
  | récupère IO_STACK_LOCATION
  | récupère longueur
  | accède au buffer selon le mode I/O
  | écrit les données
  | remplit IoStatus
  | IoCompleteRequest
  v
I/O Manager
  |
  | met à jour bytes
  v
Retour user-mode
44. Résumé mental du trajet DeviceIoControl

Code: Select all

User-mode
  |
  | DeviceIoControl(IOCTL_XXX)
  v
I/O Manager
  |
  | crée IRP_MJ_DEVICE_CONTROL
  | prépare les buffers selon METHOD_XXX
  v
Driver
  |
  | stack->Parameters.DeviceIoControl.IoControlCode
  | switch(IOCTL)
  | lit/écrit les buffers
  | CompleteIrp
  v
Retour user-mode
45. Différence entre driver simple et driver en stack

Dans un driver logiciel simple comme Zero :
  • le driver est souvent seul ;
  • il traite les IRP directement ;
  • il complète les requêtes lui-même ;
  • il n'a pas besoin de

    Code: Select all

    IoCallDriver
    .
Dans un vrai driver PnP/filter :
  • le driver fait partie d'une stack ;
  • il peut passer les IRP au driver inférieur ;
  • il peut installer des completion routines ;
  • il doit gérer des requêtes PnP et power ;
  • il doit respecter l'ordre de propagation et de completion.
46. Le driver du dessous

Quand on parle du driver du dessous, on parle du driver situé juste en-dessous dans la stack de périphériques.

Exemple :

Code: Select all

Upper Filter
    |
Function Driver   <-- si on est ici
    |
Lower Filter      <-- driver du dessous
    |
PDO / Bus Driver
Pour lui passer un IRP :

Code: Select all

IoSkipCurrentIrpStackLocation(Irp);
return IoCallDriver(LowerDeviceObject, Irp);
Mais il faut avoir conservé un pointeur vers le device object inférieur, souvent obtenu avec :

Code: Select all

IoAttachDeviceToDeviceStack
dans les drivers qui s'attachent à une stack.

47. IRP pending

Un IRP peut être complété plus tard.

Exemple :

Code: Select all

IoMarkIrpPending(Irp);
return STATUS_PENDING;
Mais ça implique que le driver garde l'IRP quelque part et le complète plus tard.

Exemple conceptuel :

Code: Select all

IoMarkIrpPending(Irp);
InsertTailList(&g_PendingIrps, &Irp->Tail.Overlay.ListEntry);
return STATUS_PENDING;
Puis plus tard :

Code: Select all

CompleteIrp(Irp, STATUS_SUCCESS, bytes);
C'est puissant mais dangereux si on gère mal :
  • annulation ;
  • synchronisation ;
  • durée de vie de l'IRP ;
  • déchargement du driver ;
  • double completion.
48. Les IRQL et les IRP

Toutes les opérations ne sont pas autorisées à tous les IRQL.

Quelques rappels :
  • accéder à une mémoire paginable peut provoquer une page fault ;
  • à DISPATCH_LEVEL, les page faults ne sont pas autorisées ;
  • certaines fonctions ne peuvent être appelées qu'à PASSIVE_LEVEL ;
  • les buffers user-mode sont particulièrement sensibles à ces règles.
Dans les dispatch routines classiques appelées depuis un thread user-mode, on est souvent à PASSIVE_LEVEL, mais il ne faut pas construire toute sa compréhension sur cette supposition.

49. Ce qu'il faut retenir absolument
  • Un IRP est la requête noyau utilisée par Windows pour communiquer avec les drivers.
  • Chaque IRP possède une ou plusieurs

    Code: Select all

    IO_STACK_LOCATION
    .
  • Le driver récupère sa stack location avec

    Code: Select all

    IoGetCurrentIrpStackLocation
    .
  • Le type de requête est dans

    Code: Select all

    MajorFunction
    .
  • Les paramètres dépendent du type de requête.
  • Un driver peut traiter, passer, ou traiter puis passer un IRP.
  • Pour passer un IRP :

    Code: Select all

    IoCallDriver
    .
  • Pour compléter un IRP :

    Code: Select all

    IoCompleteRequest
    .
  • Après

    Code: Select all

    IoCompleteRequest
    , on ne touche plus l'IRP.
  • Code: Select all

    IoStatus.Status
    contient le résultat.
  • Code: Select all

    IoStatus.Information
    contient souvent le nombre d'octets transférés.
  • Buffered I/O utilise

    Code: Select all

    Irp->AssociatedIrp.SystemBuffer
    .
  • Direct I/O utilise

    Code: Select all

    Irp->MdlAddress
    et

    Code: Select all

    MmGetSystemAddressForMdlSafe
    .
  • Les IOCTL sont définis avec

    Code: Select all

    CTL_CODE
    .
  • Pour

    Code: Select all

    DeviceIoControl
    , la méthode de buffering vient du

    Code: Select all

    Method
    dans

    Code: Select all

    CTL_CODE
    .
  • Code: Select all

    METHOD_NEITHER
    est dangereux si mal utilisé.
  • WinDbg permet d'inspecter les IRP avec

    Code: Select all

    !irpfind
    et

    Code: Select all

    !irp
    .
50. Mini fiche API

IoGetCurrentIrpStackLocation

Code: Select all

PIO_STACK_LOCATION IoGetCurrentIrpStackLocation(PIRP Irp);
Permet de récupérer les paramètres de la requête pour le driver courant.

IoCompleteRequest

Code: Select all

VOID IoCompleteRequest(PIRP Irp, CCHAR PriorityBoost);
Termine une requête. À appeler seulement quand le driver est responsable de compléter l'IRP.

IoCallDriver

Code: Select all

NTSTATUS IoCallDriver(PDEVICE_OBJECT DeviceObject, PIRP Irp);
Envoie l'IRP au driver du dessous.

IoSkipCurrentIrpStackLocation

Code: Select all

IoSkipCurrentIrpStackLocation(Irp);
Prépare rapidement l'IRP pour le driver suivant sans copier la stack location.

IoCopyCurrentIrpStackLocationToNext

Code: Select all

IoCopyCurrentIrpStackLocationToNext(Irp);
Copie les paramètres courants vers la prochaine stack location.

IoSetCompletionRoutine

Code: Select all

IoSetCompletionRoutine(Irp, Routine, Context, TRUE, TRUE, TRUE);
Enregistre une routine appelée quand l'IRP remonte après completion.

MmGetSystemAddressForMdlSafe

Code: Select all

PVOID MmGetSystemAddressForMdlSafe(PMDL Mdl, ULONG Priority);
Mappe un MDL vers une adresse système utilisable par le driver.

IoCreateDevice

Code: Select all

IoCreateDevice(...)
Crée un device object kernel.

IoCreateSymbolicLink

Code: Select all

IoCreateSymbolicLink(...)
Crée un lien permettant au user-mode d'ouvrir le device.

CTL_CODE

Code: Select all

CTL_CODE(DeviceType, Function, Method, Access)
Construit un code de contrôle pour

Code: Select all

DeviceIoControl
.

Conclusion

Les IRP sont la base du modèle I/O de Windows.

Quand tu comprends les IRP, tu comprends beaucoup mieux ce qu'un driver fait réellement :
  • il ne reçoit pas juste des appels de fonction classiques ;
  • il reçoit des paquets de requête ;
  • chaque requête transporte son type, ses paramètres, ses buffers et son état ;
  • le driver doit respecter le protocole : lire la stack location, traiter, remplir le status, compléter ou passer l'IRP.
C'est aussi ce qui permet à Windows d'avoir des piles de drivers puissantes : filtres, bus drivers, function drivers, drivers logiciels, drivers matériels, etc.

Le point vraiment important à retenir :

Un IRP est une responsabilité.

Si ton driver décide de le gérer, il doit le compléter proprement. S'il ne le gère pas, il doit le passer correctement. Et s'il le garde pour plus tard, il doit gérer sa durée de vie, l'annulation, la synchronisation et la completion.

C'est exactement là qu'on commence à passer du simple "code driver" au vrai raisonnement kernel.

Who is online

Users browsing this forum: No registered users and 1 guest