Les bases du 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 bases du kernel

Post by Hydraxx »

Structure de base d’un driver WDM

Salut l’équipe 8-)

Dans la suite de l’introduction au kernel, on va maintenant attaquer la structure concrète d’un driver WDM.

Jusqu’ici, on a vu la grande idée :
  • le système est en contrôle
  • les drivers sont passifs
  • les requêtes circulent sous forme d’IRP
  • plusieurs couches peuvent coopérer dans une pile de drivers
Maintenant, on va voir comment un driver existe réellement dans Windows.

Autrement dit :
  • comment il est représenté
  • comment il est chargé
  • comment il expose un device
  • comment Windows lui envoie des requêtes
  • comment une application user-mode peut finir par dialoguer avec lui
Ici, on est vraiment dans la charpente d’un driver.
Le but est d’installer les bons modèles mentaux.

1) Le bon modèle mental général

Le vrai modèle ressemble à ceci :

Code: Select all

Application user-mode
↓
CreateFile / DeviceIoControl / ReadFile / WriteFile
↓
I/O Manager
↓
IRP
↓
Device Object visé
↓
Driver stack
↓
Driver concerné
↓
Traitement / relais / complétion
Donc dès le départ, il faut bien comprendre une chose :

un driver n’est pas juste “du code qui tourne en kernel”.

C’est du code :
  • intégré dans un modèle système précis
  • représenté par des objets noyau
  • appelé par le kernel selon des événements déterminés
  • souvent inséré dans une pile de drivers déjà existante
2) Comment Windows découvre et charge les drivers

Le PnP Manager coordonne la détection des devices et le chargement des drivers associés.

Schéma mental :

Code: Select all

Hardware détecté
↓
PnP Manager
↓
Bus driver
↓
création d’un PDO
↓
recherche du driver adapté
↓
chargement du driver
↓
AddDevice
↓
création du FDO
↓
stack prête
Le bus driver connaît le bus sur lequel les devices sont attachés, par exemple PCI ou USB.

IRP PnP importants à reconnaître :
3) PDO, FDO et filter drivers

PDO — Physical Device Object

Le PDO est créé par le bus driver.
Il représente la présence du device du point de vue du bus.

FDO — Functional Device Object

Le FDO est généralement créé par le function driver.
C’est lui qui porte la logique principale de gestion du device.

Filter drivers

Un filter driver s’insère dans la stack pour :
  • observer
  • modifier
  • enrichir
  • bloquer
  • surveiller certaines requêtes
Modèle de pile :

Code: Select all

Upper filter
↓
Function driver (FDO)
↓
Lower filter
↓
Bus driver / PDO
↓
Hardware
4) Une requête traverse une pile, pas un driver isolé

Quand une application demande une opération, la requête ne va pas forcément dans “ton driver directement”.
Elle est routée vers un device object, puis traverse potentiellement plusieurs couches.

Donc un driver doit souvent être capable de :
  • traiter une requête
  • la relayer plus bas
  • éventuellement la modifier
  • compléter correctement l’opération
5) DRIVER_OBJECT et DEVICE_OBJECT

5.1) DRIVER_OBJECT

Le

Code: Select all

DRIVER_OBJECT
représente le driver globalement.

Il contient notamment :
  • les pointeurs de dispatch dans

    Code: Select all

    MajorFunction[]
  • Code: Select all

    DriverUnload
  • Code: Select all

    DriverExtension
  • d’autres informations globales sur le driver
5.2) DEVICE_OBJECT

Le

Code: Select all

DEVICE_OBJECT
représente une instance de device dans le système.

Donc :

Code: Select all

DRIVER_OBJECT = le driver global
DEVICE_OBJECT = une instance concrète de device
Un même driver peut gérer plusieurs devices.

6) DriverEntry — point d’entrée du driver

Prototype classique :

Code: Select all

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
Cette routine sert à faire l’initialisation globale du driver.

C’est ici qu’on configure notamment :
Exemple minimal :

Code: Select all

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

    DriverObject->MajorFunction[IRP_MJ_CREATE] = MyCreate;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = MyClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyDeviceControl;
    DriverObject->DriverUnload = DriverUnload;

    return STATUS_SUCCESS;
}

Code: Select all

DriverEntry
n’est pas un

Code: Select all

main
classique.
C’est une routine d’initialisation qui enregistre des callbacks.

7) MajorFunction[] — le routage des IRP

Code: Select all

MajorFunction[]
est un tableau de pointeurs de fonctions.

Il permet de dire au système :
  • quelle routine appeler pour

    Code: Select all

    IRP_MJ_CREATE
  • laquelle appeler pour

    Code: Select all

    IRP_MJ_CLOSE
  • laquelle appeler pour

    Code: Select all

    IRP_MJ_DEVICE_CONTROL
Schéma mental :

Code: Select all

IRP reçu
↓
MajorFunction[type]
↓
ta routine
Exemples de correspondance :

Code: Select all

CreateFile(...)      -> IRP_MJ_CREATE
CloseHandle(...)     -> IRP_MJ_CLOSE
DeviceIoControl(...) -> IRP_MJ_DEVICE_CONTROL
8) Les dispatch routines

Exemple de prototype :

Code: Select all

NTSTATUS MyCreate(PDEVICE_OBJECT DeviceObject, PIRP Irp)
Elles reçoivent :
Leur rôle :
  • analyser la requête
  • décider quoi faire
  • remplir le statut
  • compléter l’IRP ou le relayer selon le cas
Pattern minimal de complétion :

Code: Select all

Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
Règle importante :

un IRP reçu doit être traité correctement, puis complété ou relayé selon le contrat.

9) DriverUnload

Prototype :

Code: Select all

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
Cette routine sert à :
  • supprimer les symbolic links créés
  • supprimer les device objects créés
  • libérer certaines ressources globales
  • faire le cleanup final
Vision correcte :

Code: Select all

AddDevice = setup du device
DriverUnload = cleanup global / final
10) AddDevice — création et intégration du device

Code: Select all

AddDevice
est enregistré via :

Code: Select all

DriverObject->DriverExtension->AddDevice = AddDevice;
Quand est-il appelé ?
  • quand le driver doit être associé à une nouvelle instance de device
Schéma mental :

Code: Select all

DriverEntry = init globale du driver
AddDevice = init d’une instance de device
Dans

Code: Select all

AddDevice
, on fait souvent :
  • Code: Select all

    IoCreateDevice
  • initialisation de la device extension
  • Code: Select all

    IoAttachDeviceToDeviceStack
  • initialisation de certains flags
  • éventuellement création d’un point d’accès user-mode
Son rôle réel :

intégrer ton driver au device et à la stack du noyau.

11) IoCreateDevice

Exemple de style :

Code: Select all

status = IoCreateDevice(
    DriverObject,
    sizeof(MY_DEVICE_EXTENSION),
    &DeviceName,
    FILE_DEVICE_UNKNOWN,
    0,
    FALSE,
    &DeviceObject
);
Cette API :
  • crée un objet device dans le noyau
  • réserve une zone pour la device extension
  • lie cet objet à ton driver
Elle ne crée pas un “fichier user-mode”.
Elle crée un objet noyau représentant ton device.

12) DeviceExtension — ta mémoire privée par device

Le

Code: Select all

DEVICE_OBJECT
contient un pointeur vers une zone mémoire qui t’appartient :

Code: Select all

DeviceObject->DeviceExtension
Cette zone sert à stocker l’état privé du device pour ton driver.

Exemple :

Code: Select all

typedef struct _MY_DEVICE_EXTENSION {
    PDEVICE_OBJECT LowerDeviceObject;
    UNICODE_STRING LinkName;
    BOOLEAN Started;
} MY_DEVICE_EXTENSION, *PMY_DEVICE_EXTENSION;
On y stocke typiquement :
  • des pointeurs importants
  • l’état du device
  • le pointeur vers le lower device
  • des locks
  • des noms
  • des buffers
  • des informations PnP / power
13) IoAttachDeviceToDeviceStack

Cette API sert à attacher ton

Code: Select all

DEVICE_OBJECT
à la pile de devices déjà existante.

Appel classique :

Code: Select all

ext->LowerDeviceObject = IoAttachDeviceToDeviceStack(DeviceObject, PhysicalDeviceObject);
Pourquoi c’est important ?
  • parce qu’un driver WDM vit rarement seul
  • il est souvent inséré dans une driver stack
Ensuite, certaines requêtes devront parfois être relayées vers la couche inférieure.

14) Nommer le device et l’exposer

Créer un

Code: Select all

DEVICE_OBJECT
dans le kernel ne suffit pas à le rendre automatiquement accessible depuis user-mode.

Il faut distinguer :
  • le nom noyau du device
  • l’accès user-mode pratique
Nom noyau typique :

Code: Select all

\Device\MonDriver
Mais pour le user-mode, on passe souvent par un symbolic link.

15) UNICODE_STRING

Code: Select all

UNICODE_STRING
est la structure standard du noyau pour représenter une chaîne Unicode.

Prototype simplifié :

Code: Select all

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
API clé :

Code: Select all

RtlInitUnicodeString(&u, L"\Device\MonDriver");
16) IoCreateSymbolicLink — pont user ↔ kernel

Exemple :

Code: Select all

UNICODE_STRING DeviceName;
UNICODE_STRING LinkName;

RtlInitUnicodeString(&DeviceName, L"\Device\MonDriver");
RtlInitUnicodeString(&LinkName, L"\DosDevices\MonDriver");

IoCreateSymbolicLink(&LinkName, &DeviceName);
Vue mentale :

Code: Select all

\Device\MonDriver
↓
\DosDevices\MonDriver
↓
\\.\MonDriver côté user-mode
Sans ce pont logique, ton driver peut exister dans le noyau mais rester pratiquement invisible pour une application user-mode classique.

17) Comment une application ouvre le driver

Code: Select all

HANDLE h = CreateFile(
    L"\\.\MonDriver",
    GENERIC_READ | GENERIC_WRITE,
    0,
    nullptr,
    OPEN_EXISTING,
    0,
    nullptr
);
Schéma mental :

Code: Select all

CreateFile("\\.\MonDriver")
↓
I/O Manager
↓
IRP_MJ_CREATE
↓
MajorFunction[IRP_MJ_CREATE]
↓
MyCreate
Donc

Code: Select all

CreateFile
sur un driver ouvre un handle sur le device exposé par le driver.

18) DeviceIoControl — envoyer une commande à un driver

Schéma :

Code: Select all

Application
↓
DeviceIoControl(...)
↓
I/O Manager
↓
IRP_MJ_DEVICE_CONTROL
↓
MajorFunction[IRP_MJ_DEVICE_CONTROL]
↓
MyDeviceControl
Cela sert à envoyer un ordre explicite au driver :
  • demander un état
  • configurer un comportement
  • envoyer un petit buffer
  • récupérer une réponse
Le user-mode ne “call” pas directement une fonction kernel.
Il demande au système d’envoyer cette commande à ce device.

19) IoGetCurrentIrpStackLocation

Dans une routine comme

Code: Select all

IRP_MJ_DEVICE_CONTROL
, on a souvent besoin de récupérer les paramètres associés à l’IRP courant.

Code: Select all

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
Exemple :

Code: Select all

ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
Le

Code: Select all

IRP
transporte la requête, mais les paramètres spécifiques sont récupérés via la stack location courante.

20) Les flags du DEVICE_OBJECT

Trois flags importants au début :

Code: Select all

DO_BUFFERED_IO
:

Code: Select all

DeviceObject->Flags |= DO_BUFFERED_IO;
C’est souvent le choix le plus simple pour commencer.

Code: Select all

DO_DIRECT_IO
:
plus avancé, plus proche du chemin direct mémoire.

Code: Select all

DO_DEVICE_INITIALIZING
:
il faut l’enlever une fois l’initialisation terminée.

Code: Select all

DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
Sinon le device peut rester dans un état où il n’est pas considéré comme prêt.

21) APIs clés à connaître à ce stade
22) Flow complet à retenir

Code: Select all

Le système charge le driver
↓
DriverEntry configure le driver
↓
Le système associe le driver à un device
↓
AddDevice crée / initialise le DEVICE_OBJECT
↓
Le driver s’attache à la stack
↓
Le driver expose éventuellement un symbolic link
↓
L’application ouvre le driver avec CreateFile
↓
Le système envoie IRP_MJ_CREATE
↓
L’application envoie un DeviceIoControl
↓
Le système envoie IRP_MJ_DEVICE_CONTROL
↓
Le driver traite et complète l’IRP
23) Exemple mental minimal de code

Code: Select all

#include <ntddk.h>

NTSTATUS MyCreate(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;
}

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);
}

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

    DriverObject->MajorFunction[IRP_MJ_CREATE] = MyCreate;
    DriverObject->DriverUnload = DriverUnload;

    return STATUS_SUCCESS;
}
Cela montre déjà :
  • Code: Select all

    DriverEntry
    initialise
  • Code: Select all

    MajorFunction[]
    route les IRP
  • une dispatch routine doit compléter l’IRP
24) Ce qu’il faut absolument retenir
  • un driver existe dans Windows via des objets noyau
  • les deux plus importants au début sont

    Code: Select all

    DRIVER_OBJECT
    et

    Code: Select all

    DEVICE_OBJECT
  • Code: Select all

    DriverEntry
    n’est pas une boucle principale, il initialise
  • Code: Select all

    MajorFunction[]
    est le routeur principal des IRP
  • Code: Select all

    AddDevice
    sert à créer et intégrer une instance de device
  • Code: Select all

    DeviceExtension
    est ta mémoire privée par device
  • le user-mode passe généralement par un symbolic link
  • Code: Select all

    CreateFile
    et

    Code: Select all

    DeviceIoControl
    deviennent des IRP
  • un IRP simple doit être correctement complété
Pattern classique :

Code: Select all

Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
Conclusion

Tu vois maintenant que :
  • un driver est représenté par un

    Code: Select all

    DRIVER_OBJECT
  • un device est représenté par un

    Code: Select all

    DEVICE_OBJECT
  • le driver s’initialise via

    Code: Select all

    DriverEntry
  • les requêtes sont routées via

    Code: Select all

    MajorFunction[]
  • les devices sont créés et intégrés via

    Code: Select all

    AddDevice
  • l’état privé vit dans la

    Code: Select all

    DeviceExtension
  • l’accès user-mode passe souvent par un symbolic link
  • Code: Select all

    CreateFile
    et

    Code: Select all

    DeviceIoControl
    deviennent des IRP côté noyau
  • une requête doit être correctement terminée
Si tu comprends vraiment ça, tu as déjà posé une base très solide pour la suite.

Résumé ultra condensé

WDM repose sur des objets noyau et des callbacks.

Code: Select all

DRIVER_OBJECT
représente le driver global.

Code: Select all

DEVICE_OBJECT
représente une instance de device.

Code: Select all

DriverEntry
initialise le driver.

Code: Select all

MajorFunction[]
associe les types d’IRP aux bonnes routines.

Code: Select all

AddDevice
crée et intègre une instance de device.
La

Code: Select all

DeviceExtension
contient l’état privé du device.

Code: Select all

IoCreateDevice
crée le device object.

Code: Select all

IoAttachDeviceToDeviceStack
l’insère dans la pile.

Code: Select all

UNICODE_STRING
sert à manipuler les noms noyau.

Code: Select all

IoCreateSymbolicLink
permet l’accès user-mode.

Code: Select all

CreateFile
et

Code: Select all

DeviceIoControl
deviennent des IRP routés vers le driver.
Un IRP simple doit être correctement complété avec

Code: Select all

IoStatus
et

Code: Select all

IoCompleteRequest
.

Who is online

Users browsing this forum: No registered users and 0 guests