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.
Code: Select all
Application
|
I/O Manager
|
Upper Filter DO
|
FDO <- function driver
|
Lower Filter DO
|
PDO <- bus driver
|
Hardware / Bus
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;
}
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.
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;
}
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
Code: Select all
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
UCHAR minor = stack->MinorFunction;
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);
}
}
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);
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);
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.
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;
}
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;
}
6. Les ressources de START_DEVICE
Dans l'IO_STACK_LOCATION :
Code: Select all
stack->Parameters.StartDevice.AllocatedResources
stack->Parameters.StartDevice.AllocatedResourcesTranslated
- AllocatedResources : ressources dites raw, exprimées dans l'espace du bus.
- AllocatedResourcesTranslated : ressources traduites dans l'espace système utilisable par le driver.
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;
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
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.
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.
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.
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.
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.
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.
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(...)
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.
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.
- 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.
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
- 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.
