Plug and Play WDM : cycle de vie complet d'un périphérique

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

Moderator: Rick

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

Plug and Play WDM : cycle de vie complet d'un périphérique

Post by Hydraxx »

Plug and Play WDM : cycle de vie complet d'un périphérique

Le Plug and Play (PnP) est le mécanisme par lequel Windows détecte les périphériques, construit leur pile de DEVICE_OBJECT, attribue leurs ressources, démarre les drivers, arrête les périphériques et détruit proprement leur représentation logicielle.

Dans un driver WDM, le PnP n'est pas un simple « événement de branchement ». Il impose un cycle de vie précis au périphérique et au DEVICE_OBJECT. Un driver doit accepter que son périphérique puisse être démarré, stoppé, redémarré, retiré proprement ou retiré brutalement pendant que des requêtes sont encore en cours.

Ce cours se concentre sur la mécanique PnP elle-même. Les ressources matérielles, les files d'IRP, les remove locks, les device interfaces et les notifications PnP sont traités dans des cours séparés.

1. Les acteurs du Plug and Play

Les principaux acteurs sont :
  • PnP Manager : composant du noyau qui orchestre l'énumération, le démarrage, l'arrêt et la suppression des périphériques.
  • Bus driver : détecte les enfants présents sur un bus et crée généralement leurs PDO.
  • Function driver : pilote principal du périphérique ; il crée typiquement un FDO et l'attache au PDO.
  • Filter driver : s'insère au-dessus ou au-dessous du function driver pour observer ou modifier certains comportements.
  • PDO : Physical Device Object, créé par le bus driver pour représenter une instance physique du périphérique.
  • FDO : Functional Device Object, créé par le function driver pour gérer la logique fonctionnelle du périphérique.
  • Filter DO : DEVICE_OBJECT ajouté par un filter driver.
Une pile typique ressemble à ceci :

Code: Select all

Application
    |
I/O Manager
    |
Upper Filter DO
    |
FDO                  <- function driver
    |
Lower Filter DO
    |
PDO                  <- bus driver
    |
Hardware / Bus
Le PnP Manager adresse ses requêtes à la device stack. Chaque driver doit donc savoir transmettre les IRP PnP au driver inférieur et ne traiter que les minor functions qui le concernent.

2. DriverEntry et AddDevice

Un driver WDM PnP enregistre normalement une routine AddDevice dans DriverEntry :

Code: Select all

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

    DriverObject->DriverExtension->AddDevice = AddDevice;
    DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnP;

    return STATUS_SUCCESS;
}
AddDevice est appelée par le PnP Manager lorsqu'une nouvelle instance de périphérique doit être associée au driver.

Sa responsabilité typique est :
  • Créer le FDO avec IoCreateDevice.
  • Initialiser la device extension.
  • Attacher le FDO à la pile existante avec IoAttachDeviceToDeviceStack.
  • Conserver le pointeur vers le DEVICE_OBJECT inférieur.
  • Initialiser les mécanismes de synchronisation, remove lock, queues, etc.
  • Configurer les flags du DEVICE_OBJECT.
  • Retirer DO_DEVICE_INITIALIZING lorsque l'objet est prêt.
Exemple de squelette :

Code: Select all

typedef struct _DEVICE_EXTENSION
{
    PDEVICE_OBJECT LowerDeviceObject;
    PDEVICE_OBJECT PhysicalDeviceObject;
    DEVICE_PNP_STATE PnpState;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT PhysicalDeviceObject)
{
    PDEVICE_OBJECT fdo = NULL;

    NTSTATUS status = IoCreateDevice(
        DriverObject,
        sizeof(DEVICE_EXTENSION),
        NULL,
        FILE_DEVICE_UNKNOWN,
        0,
        FALSE,
        &fdo
    );

    if (!NT_SUCCESS(status))
        return status;

    PDEVICE_EXTENSION ext = (PDEVICE_EXTENSION)fdo->DeviceExtension;
    RtlZeroMemory(ext, sizeof(*ext));

    ext->PhysicalDeviceObject = PhysicalDeviceObject;
    ext->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, PhysicalDeviceObject);

    if (ext->LowerDeviceObject == NULL)
    {
        IoDeleteDevice(fdo);
        return STATUS_NO_SUCH_DEVICE;
    }

    ext->PnpState = PnpStateNotStarted;

    fdo->Flags |= DO_POWER_PAGABLE;
    fdo->Flags &= ~DO_DEVICE_INITIALIZING;

    return STATUS_SUCCESS;
}
IoCreateDevice crée le DEVICE_OBJECT du driver.

IoAttachDeviceToDeviceStack attache ce DEVICE_OBJECT au sommet de la pile courante et retourne le DEVICE_OBJECT inférieur auquel les IRP devront être transmis.

IoDetachDevice détache le driver de la pile lors de la suppression.

IoDeleteDevice détruit le DEVICE_OBJECT. Cette opération ne doit être faite qu'une fois toutes les opérations utilisant l'objet terminées.

3. IRP_MJ_PNP

Toutes les opérations PnP passent par le major code :

Code: Select all

IRP_MJ_PNP
Le type exact de l'opération se trouve dans :

Code: Select all

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
UCHAR minor = stack->MinorFunction;
Le squelette classique est :

Code: Select all

NTSTATUS DispatchPnP(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    PDEVICE_EXTENSION ext = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);

    switch (stack->MinorFunction)
    {
        case IRP_MN_START_DEVICE:
            return HandleStartDevice(DeviceObject, Irp);

        case IRP_MN_QUERY_STOP_DEVICE:
            return HandleQueryStop(DeviceObject, Irp);

        case IRP_MN_CANCEL_STOP_DEVICE:
            return HandleCancelStop(DeviceObject, Irp);

        case IRP_MN_STOP_DEVICE:
            return HandleStopDevice(DeviceObject, Irp);

        case IRP_MN_QUERY_REMOVE_DEVICE:
            return HandleQueryRemove(DeviceObject, Irp);

        case IRP_MN_CANCEL_REMOVE_DEVICE:
            return HandleCancelRemove(DeviceObject, Irp);

        case IRP_MN_SURPRISE_REMOVAL:
            return HandleSurpriseRemoval(DeviceObject, Irp);

        case IRP_MN_REMOVE_DEVICE:
            return HandleRemoveDevice(DeviceObject, Irp);

        default:
            IoSkipCurrentIrpStackLocation(Irp);
            return IoCallDriver(ext->LowerDeviceObject, Irp);
    }
}
Règle importante : un driver ne doit pas inventer le comportement des minor functions qu'il ne gère pas. Il doit généralement les transmettre au driver inférieur.

4. Transmission simple d'une requête PnP

Lorsqu'un driver n'a rien à ajouter à une IRP PnP :

Code: Select all

IoSkipCurrentIrpStackLocation(Irp);
return IoCallDriver(ext->LowerDeviceObject, Irp);
IoSkipCurrentIrpStackLocation réutilise l'emplacement de pile courant pour le driver suivant.

IoCallDriver transmet l'IRP au prochain DEVICE_OBJECT.

Si le driver doit installer une completion routine ou modifier des paramètres pour l'étage suivant, il utilise plutôt :

Code: Select all

IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(Irp, CompletionRoutine, Context, TRUE, TRUE, TRUE);
return IoCallDriver(ext->LowerDeviceObject, Irp);
5. IRP_MN_START_DEVICE

IRP_MN_START_DEVICE signifie que le PnP Manager a attribué les ressources nécessaires au périphérique et autorise la stack à le démarrer.

Pour un function driver, l'ordre est important :
  • Transmettre START_DEVICE au driver inférieur.
  • Attendre que les drivers inférieurs aient terminé avec succès.
  • Lire les ressources attribuées.
  • Mapper/configurer le matériel.
  • Connecter les interruptions si nécessaire.
  • Activer les interfaces du périphérique.
  • Passer l'état interne à Started/Working.
  • Autoriser les nouvelles I/O.
Pourquoi attendre le driver inférieur ? Parce que le bus driver doit avoir démarré le matériel et préparé le bus avant que le function driver commence à l'utiliser.

Une fonction helper classique pour transmettre une IRP de manière synchrone peut utiliser un KEVENT et une completion routine :

Code: Select all

NTSTATUS SyncCompletion(PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context)
{
    UNREFERENCED_PARAMETER(DeviceObject);

    PKEVENT event = (PKEVENT)Context;
    KeSetEvent(event, IO_NO_INCREMENT, FALSE);

    return STATUS_MORE_PROCESSING_REQUIRED;
}
Puis :

Code: Select all

KEVENT event;
KeInitializeEvent(&event, NotificationEvent, FALSE);

IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(Irp, SyncCompletion, &event, TRUE, TRUE, TRUE);

NTSTATUS status = IoCallDriver(ext->LowerDeviceObject, Irp);

if (status == STATUS_PENDING)
{
    KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);
    status = Irp->IoStatus.Status;
}
Après seulement, le function driver configure son hardware.

6. Les ressources de START_DEVICE

Dans l'IO_STACK_LOCATION :

Code: Select all

stack->Parameters.StartDevice.AllocatedResources
stack->Parameters.StartDevice.AllocatedResourcesTranslated
Les deux champs pointent vers des CM_RESOURCE_LIST.
  • AllocatedResources : ressources dites raw, exprimées dans l'espace du bus.
  • AllocatedResourcesTranslated : ressources traduites dans l'espace système utilisable par le driver.
La lecture détaillée des CM_RESOURCE_LIST, ports, MMIO et interruptions est traitée dans le cours « Ressources matérielles des drivers WDM ».

7. Machine à états PnP

Un driver doit conserver son propre état logique. Un modèle simple peut être :

Code: Select all

typedef enum _DEVICE_PNP_STATE
{
    PnpStateNotStarted,
    PnpStateStarted,
    PnpStateStopPending,
    PnpStateStopped,
    PnpStateRemovePending,
    PnpStateSurpriseRemoved,
    PnpStateDeleted
} DEVICE_PNP_STATE;
Transitions simplifiées :

Code: Select all

NotStarted
    |
START_DEVICE
    v
Started
  |   \
  |    \ QUERY_REMOVE
  |     v
  |  RemovePending
  |     |
  |  CANCEL_REMOVE -> Started
  |
QUERY_STOP
  v
StopPending
  |
  +-- CANCEL_STOP -> Started
  |
STOP_DEVICE
  v
Stopped
  |
START_DEVICE
  v
Started

A n'importe quel moment pertinent :
SURPRISE_REMOVAL -> SurpriseRemoved -> REMOVE_DEVICE -> Deleted
8. IRP_MN_QUERY_STOP_DEVICE

Cette requête demande : « le périphérique peut-il être stoppé ? »

Le driver doit vérifier s'il peut mettre le périphérique en pause sans casser une opération non interrompable.

Un driver qui accepte la requête doit généralement :
  • Empêcher le démarrage de nouvelles I/O nécessitant le hardware.
  • Attendre ou préparer la fin des I/O déjà en cours selon son modèle.
  • Passer dans un état StopPending.
  • Transmettre l'IRP dans la pile.
Si le périphérique ne peut réellement pas être stoppé à cet instant, la requête peut être échouée avec un NTSTATUS approprié.

9. IRP_MN_CANCEL_STOP_DEVICE

Le PnP Manager peut annuler un QUERY_STOP précédemment accepté.

Le driver doit alors restaurer son état précédent :
  • Repasser de StopPending à Started.
  • Réautoriser les nouvelles requêtes.
  • Relancer la file d'I/O si elle avait été stallée.
  • Transmettre l'IRP.
10. IRP_MN_STOP_DEVICE

Cette requête demande l'arrêt effectif du périphérique.

Le driver doit :
  • Stopper les opérations matérielles.
  • Désactiver les interruptions si nécessaire.
  • Relâcher les ressources qui devront être réattribuées au prochain START_DEVICE.
  • Conserver cependant les structures logicielles nécessaires à un redémarrage futur.
  • Passer à l'état Stopped.
STOP_DEVICE ne signifie pas DELETE_DEVICE.

Le DEVICE_OBJECT reste vivant. Le périphérique peut recevoir plus tard un nouveau START_DEVICE.

11. IRP_MN_QUERY_REMOVE_DEVICE

QUERY_REMOVE demande si Windows peut retirer le périphérique.

Cette étape permet au driver de refuser temporairement la suppression si une opération critique l'empêche réellement.

S'il accepte :
  • Il bloque normalement les nouvelles I/O dépendantes du périphérique.
  • Il prépare la fermeture de ressources applicatives ou matérielles.
  • Il passe en RemovePending.
  • Il transmet la requête.
12. IRP_MN_CANCEL_REMOVE_DEVICE

Si la suppression prévue est annulée, le driver remet le périphérique dans son état antérieur.

Typiquement :
  • RemovePending -> Started.
  • Réouverture de la queue.
  • Reprise des requêtes.
13. IRP_MN_SURPRISE_REMOVAL

SURPRISE_REMOVAL signifie que le périphérique n'est plus disponible pour les I/O. Exemple typique : périphérique USB arraché sans procédure d'éjection.

Cette IRP doit être considérée comme une notification irréversible concernant l'accès au hardware.

Le driver doit notamment :
  • Arrêter immédiatement de toucher le hardware disparu.
  • Refuser les nouvelles I/O matérielles.
  • Annuler/purger les opérations qui ne peuvent plus aboutir.
  • Désactiver les device interfaces exposées.
  • Libérer ce qui peut être libéré sans attendre REMOVE_DEVICE.
  • Passer à l'état SurpriseRemoved.
  • Transmettre l'IRP au driver inférieur.
Important : SURPRISE_REMOVAL ne détruit généralement pas encore le DEVICE_OBJECT. Le PnP Manager enverra ensuite IRP_MN_REMOVE_DEVICE pour la destruction finale de la représentation logicielle.

14. IRP_MN_REMOVE_DEVICE

REMOVE_DEVICE est l'étape finale.

Après cette requête, le driver ne doit plus accepter de nouvelles opérations sur ce DEVICE_OBJECT.

Séquence conceptuelle :

Code: Select all

IRP_MN_REMOVE_DEVICE
        |
        +--> arrêter / purger les I/O
        +--> transmettre l'IRP au driver inférieur
        +--> attendre la fin des références en cours
        +--> désactiver/libérer les interfaces
        +--> libérer les ressources logicielles
        +--> IoDetachDevice(...)
        +--> IoDeleteDevice(...)
La synchronisation exacte avec les opérations encore en cours est traitée dans le cours sur IO_REMOVE_LOCK.

15. Les autres minor functions PnP utiles

Un function driver peut rencontrer d'autres requêtes PnP :
  • IRP_MN_QUERY_CAPABILITIES : interroger/compléter les capacités du périphérique.
  • IRP_MN_QUERY_DEVICE_RELATIONS : demander des relations avec d'autres objets de périphérique.
  • IRP_MN_QUERY_INTERFACE : demander une interface kernel exposée par un autre driver.
  • IRP_MN_QUERY_ID : identifiants PnP ; surtout important pour les bus drivers.
  • IRP_MN_QUERY_DEVICE_TEXT : texte descriptif du périphérique ; surtout bus/function selon la stack.
  • IRP_MN_QUERY_RESOURCES : ressources actuellement utilisées ; principalement bus drivers.
  • IRP_MN_QUERY_RESOURCE_REQUIREMENTS : ressources possibles/requises ; principalement bus drivers.
  • IRP_MN_FILTER_RESOURCE_REQUIREMENTS : permet au function driver d'ajuster les exigences en ressources.
  • IRP_MN_DEVICE_USAGE_NOTIFICATION : indique certains usages système particuliers du périphérique, par exemple paging/hibernation/dump selon la classe de device.
  • IRP_MN_QUERY_PNP_DEVICE_STATE : permet de signaler certains états PnP au système.
Un driver ne doit gérer que ce dont il a réellement besoin et transmettre proprement le reste.

16. API essentielles du cours
  • IoCreateDevice : crée un DEVICE_OBJECT.
  • IoDeleteDevice : détruit un DEVICE_OBJECT.
  • IoAttachDeviceToDeviceStack : attache le DEVICE_OBJECT du driver à la stack PnP.
  • IoDetachDevice : détache le driver de la stack.
  • IoGetCurrentIrpStackLocation : récupère l'IO_STACK_LOCATION courant.
  • IoSkipCurrentIrpStackLocation : prépare une transmission simple sans modifier la stack location.
  • IoCopyCurrentIrpStackLocationToNext : copie la stack location vers l'étage suivant, utile avec une completion routine.
  • IoSetCompletionRoutine : installe une completion routine.
  • IoCallDriver : transmet l'IRP au driver inférieur.
  • IoCompleteRequest : complète une IRP lorsque le driver en est responsable.
  • KeInitializeEvent / KeSetEvent / KeWaitForSingleObject : utiles pour attendre synchroniquement une opération inférieure lorsque cela est requis.
17. Erreurs fréquentes
  • Traiter STOP_DEVICE comme une suppression définitive.
  • Toucher au hardware après SURPRISE_REMOVAL.
  • Détruire le DEVICE_OBJECT dès SURPRISE_REMOVAL au lieu d'attendre REMOVE_DEVICE.
  • Oublier de transmettre les minor functions non gérées.
  • Configurer le hardware avant que START_DEVICE ait réussi dans les drivers inférieurs.
  • Accepter de nouvelles I/O alors que l'état est StopPending, RemovePending ou SurpriseRemoved.
  • Appeler IoDeleteDevice alors que des IRP, timers, DPC ou callbacks détiennent encore une référence logique au périphérique.
  • Copier des modèles Windows 98/Me/XP sans vérifier les règles actuelles de Windows 10/11.
18. Résumé

Le PnP impose un véritable automate au driver WDM :

Code: Select all

AddDevice
   |
NotStarted
   |
START_DEVICE
   v
Started
   |
   +--> QUERY_STOP --> STOP --> éventuellement START
   |
   +--> QUERY_REMOVE --> REMOVE
   |
   +--> SURPRISE_REMOVAL --> REMOVE
Les points à retenir sont :
  • Un périphérique PnP peut changer d'état à tout moment autorisé par le modèle.
  • IRP_MJ_PNP contient une MinorFunction qui indique l'opération précise.
  • Les IRP non gérées doivent être transmises correctement.
  • START_DEVICE autorise réellement l'utilisation du hardware.
  • SURPRISE_REMOVAL interdit immédiatement les accès au hardware.
  • REMOVE_DEVICE détruit finalement la représentation logicielle.
  • La suppression sûre exige une coordination avec les IRP et autres callbacks encore actifs.

Who is online

Users browsing this forum: No registered users and 1 guest