Objectif du cours
Dans Windows, un driver peut avoir besoin de demander un service à un autre driver.
Exemples :
- un driver logiciel veut envoyer une commande à un autre driver ;
- un driver veut utiliser un device exposé par un autre composant ;
- un driver supérieur veut demander une opération à un driver inférieur ;
- un worker thread veut effectuer une requête vers un autre device ;
- un driver agit comme intermédiaire entre une application user-mode et un autre driver.
Ici, l’objectif est différent :
Comment un driver A retrouve volontairement un driver B, construit une requête pour lui, l’envoie avec IoCallDriver et récupère le résultat ?
1. Vue d’ensemble
Le scénario étudié est :
Code: Select all
Application user-mode
|
| DeviceIoControl
v
Driver A
|
| trouve le device de Driver B
|
| construit une requête
|
| IoCallDriver
v
Driver B
|
| traite
v
résultat
|
v
Driver A
Il peut aussi construire une nouvelle requête destinée à un autre driver.
2. DRIVER_OBJECT et DEVICE_OBJECT
Un driver Windows possède un DRIVER_OBJECT.
Un driver peut également créer un ou plusieurs DEVICE_OBJECT.
Exemple :
Code: Select all
DRIVER_OBJECT
|
+--> DEVICE_OBJECT 1
|
+--> DEVICE_OBJECT 2
Code: Select all
PDEVICE_OBJECT
C’est important, car IoCallDriver reçoit un DEVICE_OBJECT cible.
3. Nommer le device cible
Supposons que Driver B crée :
Code: Select all
\Device\Provider
On commence souvent avec :
Code: Select all
UNICODE_STRING deviceName;
RtlInitUnicodeString(
&deviceName,
L"\\Device\\Provider"
);
Il ne faut pas confondre :
Code: Select all
\Device\Provider
Code: Select all
\DosDevices\Provider
4. IoGetDeviceObjectPointer
L’API principale permettant de retrouver un device par son nom est :
IoGetDeviceObjectPointer
Prototype simplifié :
Code: Select all
NTSTATUS IoGetDeviceObjectPointer(
PUNICODE_STRING ObjectName,
ACCESS_MASK DesiredAccess,
PFILE_OBJECT* FileObject,
PDEVICE_OBJECT* DeviceObject
);
Code: Select all
PFILE_OBJECT fileObject = NULL;
PDEVICE_OBJECT deviceObject = NULL;
UNICODE_STRING name;
RtlInitUnicodeString(
&name,
L"\\Device\\Provider"
);
NTSTATUS status = IoGetDeviceObjectPointer(
&name,
FILE_READ_DATA | FILE_WRITE_DATA,
&fileObject,
&deviceObject
);
- fileObject reçoit un pointeur vers un FILE_OBJECT ;
- deviceObject reçoit un pointeur vers le DEVICE_OBJECT correspondant.
5. Pourquoi IoGetDeviceObjectPointer retourne aussi un FILE_OBJECT ?
Cela peut sembler étrange au début.
L’appel ne renvoie pas uniquement un DEVICE_OBJECT.
Il retourne aussi :
Code: Select all
PFILE_OBJECT
Le système maintient une référence sur les objets concernés tant que cette relation existe.
En pratique, le FILE_OBJECT sert aussi à maintenir correctement la durée de vie du device trouvé.
Lorsque Driver A n’a plus besoin de la relation, il devra libérer sa référence :
Code: Select all
ObDereferenceObject(fileObject);
Ne pas oublier cette référence, sinon le driver provoque une fuite d’objet kernel.
6. DEVICE_OBJECT retourné
Après :
Code: Select all
IoGetDeviceObjectPointer(...)
Code: Select all
PDEVICE_OBJECT deviceObject
C’est ce pointeur qui pourra être utilisé avec :
Code: Select all
IoCallDriver(deviceObject, Irp);
Selon l’architecture du système, des filtres peuvent être présents au-dessus ou en dessous.
Le cours sur les IRP et les stacks de drivers traite ces notions en détail.
7. Construire une nouvelle requête
Driver A a plusieurs façons de construire un IRP.
Pour envoyer facilement un IOCTL à un autre driver, Windows fournit :
IoBuildDeviceIoControlRequest
Cette fonction crée un IRP adapté à une requête de type :
Code: Select all
IRP_MJ_DEVICE_CONTROL
Code: Select all
IRP_MJ_INTERNAL_DEVICE_CONTROL
8. IoBuildDeviceIoControlRequest
Prototype :
Code: Select all
PIRP IoBuildDeviceIoControlRequest(
ULONG IoControlCode,
PDEVICE_OBJECT DeviceObject,
PVOID InputBuffer,
ULONG InputBufferLength,
PVOID OutputBuffer,
ULONG OutputBufferLength,
BOOLEAN InternalDeviceIoControl,
PKEVENT Event,
PIO_STATUS_BLOCK IoStatusBlock
);
- le code IOCTL ;
- le DEVICE_OBJECT cible ;
- le buffer d’entrée ;
- le buffer de sortie ;
- la taille des buffers ;
- le type d’IOCTL normal ou interne ;
- un KEVENT ;
- un IO_STATUS_BLOCK.
9. Exemple de structure échangée
Driver A et Driver B peuvent partager une structure commune.
Exemple :
Code: Select all
typedef struct _REQUEST_DATA
{
ULONG Value;
ULONG Result;
} REQUEST_DATA, *PREQUEST_DATA;
Code: Select all
Value = 10
Code: Select all
Result = 20
10. Définition d’un IOCTL partagé
Exemple :
Code: Select all
#define IOCTL_PROVIDER_CALCULATE \
CTL_CODE( \
FILE_DEVICE_UNKNOWN, \
0x800, \
METHOD_BUFFERED, \
FILE_ANY_ACCESS \
)
Dans un vrai projet, il est pratique de placer les définitions communes dans un header partagé.
11. KEVENT et IO_STATUS_BLOCK
Pour une requête synchrone construite avec IoBuildDeviceIoControlRequest, on utilise généralement :
Code: Select all
KEVENT event;
IO_STATUS_BLOCK ioStatus;
Code: Select all
KeInitializeEvent(
&event,
NotificationEvent,
FALSE
);
Code: Select all
RtlZeroMemory(
&ioStatus,
sizeof(ioStatus)
);
Ici, l’event sert simplement à être signalé lorsque la requête est terminée.
12. Construire l’IRP
Exemple :
Code: Select all
REQUEST_DATA data = { 0 };
data.Value = 10;
PIRP irp = IoBuildDeviceIoControlRequest(
IOCTL_PROVIDER_CALCULATE,
deviceObject,
&data,
sizeof(data),
&data,
sizeof(data),
FALSE,
&event,
&ioStatus
);
Code: Select all
FALSE
Code: Select all
IRP_MJ_DEVICE_CONTROL
Code: Select all
TRUE
Code: Select all
IRP_MJ_INTERNAL_DEVICE_CONTROL
13. Vérifier le retour de IoBuildDeviceIoControlRequest
La fonction retourne :
Code: Select all
PIRP
Code: Select all
NULL
Code: Select all
if (!irp)
{
ObDereferenceObject(fileObject);
return STATUS_INSUFFICIENT_RESOURCES;
}
14. IOCTL normal et IOCTL interne
Deux grandes possibilités existent.
IOCTL normal
Major function :
Code: Select all
IRP_MJ_DEVICE_CONTROL
Il peut être utilisé par des applications user-mode via :
Code: Select all
DeviceIoControl
IOCTL interne
Major function :
Code: Select all
IRP_MJ_INTERNAL_DEVICE_CONTROL
Il n’est pas directement exposé comme un IOCTL user-mode classique.
Le driver cible doit installer une routine de dispatch correspondante :
Code: Select all
DriverObject->MajorFunction[
IRP_MJ_INTERNAL_DEVICE_CONTROL
] = InternalDeviceControl;
15. Envoyer l’IRP avec IoCallDriver
Une fois l’IRP construit :
Code: Select all
NTSTATUS status = IoCallDriver(
deviceObject,
irp
);
C’est l’appel qui déclenche réellement le traitement de la requête par le driver cible.
16. Ce que retourne IoCallDriver
Le retour peut être :
- un statut final ;
- STATUS_PENDING.
Code: Select all
NTSTATUS status = IoCallDriver(
deviceObject,
irp
);
if (status == STATUS_PENDING)
{
// l'opération n'est pas encore terminée
}
Le statut retourné immédiatement par IoCallDriver n’est pas toujours le statut final de l’opération.
17. STATUS_PENDING
Si le driver cible retourne :
Code: Select all
STATUS_PENDING
Dans le cas d’une requête construite avec IoBuildDeviceIoControlRequest, Driver A peut attendre l’event associé :
Code: Select all
if (status == STATUS_PENDING)
{
KeWaitForSingleObject(
&event,
Executive,
KernelMode,
FALSE,
NULL
);
}
18. Récupérer le statut final
Après completion :
Code: Select all
status = ioStatus.Status;
Code: Select all
if (status == STATUS_PENDING)
{
KeWaitForSingleObject(
&event,
Executive,
KernelMode,
FALSE,
NULL
);
status = ioStatus.Status;
}
Code: Select all
ioStatus.Status
19. IO_STATUS_BLOCK.Information
Le champ :
Code: Select all
ioStatus.Information
Exemple :
Code: Select all
KdPrint((
"Bytes returned: %llu\n",
(ULONGLONG)ioStatus.Information
));
Code: Select all
Irp->IoStatus.Information
20. Driver B : réception de la requête
Driver B doit avoir enregistré la routine de dispatch adaptée.
Exemple :
Code: Select all
DriverObject->MajorFunction[
IRP_MJ_DEVICE_CONTROL
] = DeviceControl;
Code: Select all
NTSTATUS DeviceControl(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)
{
UNREFERENCED_PARAMETER(DeviceObject);
PIO_STACK_LOCATION stack =
IoGetCurrentIrpStackLocation(Irp);
NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
if (stack->Parameters.DeviceIoControl.IoControlCode ==
IOCTL_PROVIDER_CALCULATE)
{
// traitement
}
Irp->IoStatus.Status = status;
IoCompleteRequest(
Irp,
IO_NO_INCREMENT
);
return status;
}
21. Exemple de traitement côté Driver B
Pour un IOCTL METHOD_BUFFERED :
Code: Select all
PREQUEST_DATA data =
(PREQUEST_DATA)Irp->AssociatedIrp.SystemBuffer;
Code: Select all
if (data)
{
data->Result =
data->Value * 2;
status = STATUS_SUCCESS;
Irp->IoStatus.Information =
sizeof(REQUEST_DATA);
}
Code: Select all
data.Result
22. Exemple complet côté Driver A
Code: Select all
NTSTATUS SendRequestToProvider()
{
UNICODE_STRING name;
PFILE_OBJECT fileObject = NULL;
PDEVICE_OBJECT deviceObject = NULL;
RtlInitUnicodeString(
&name,
L"\\Device\\Provider"
);
NTSTATUS status =
IoGetDeviceObjectPointer(
&name,
FILE_READ_DATA | FILE_WRITE_DATA,
&fileObject,
&deviceObject
);
if (!NT_SUCCESS(status))
{
return status;
}
KEVENT event;
KeInitializeEvent(
&event,
NotificationEvent,
FALSE
);
IO_STATUS_BLOCK ioStatus = { 0 };
REQUEST_DATA data = { 0 };
data.Value = 10;
PIRP irp =
IoBuildDeviceIoControlRequest(
IOCTL_PROVIDER_CALCULATE,
deviceObject,
&data,
sizeof(data),
&data,
sizeof(data),
FALSE,
&event,
&ioStatus
);
if (!irp)
{
ObDereferenceObject(fileObject);
return STATUS_INSUFFICIENT_RESOURCES;
}
status = IoCallDriver(
deviceObject,
irp
);
if (status == STATUS_PENDING)
{
KeWaitForSingleObject(
&event,
Executive,
KernelMode,
FALSE,
NULL
);
status = ioStatus.Status;
}
if (NT_SUCCESS(status))
{
KdPrint((
"Result = %lu\n",
data.Result
));
}
ObDereferenceObject(fileObject);
return status;
}
23. Schéma complet
Code: Select all
Driver A
|
| RtlInitUnicodeString
v
Nom du device
|
| IoGetDeviceObjectPointer
v
PFILE_OBJECT + PDEVICE_OBJECT
|
| IoBuildDeviceIoControlRequest
v
PIRP
|
| IoCallDriver
v
Driver B
|
| traite IOCTL
|
| IoCompleteRequest
v
KEVENT signalé
|
| ioStatus.Status
| buffer de sortie
v
Driver A récupère le résultat
24. Ownership de l’IRP
La durée de vie d’un IRP est fondamentale.
Dans le cas de :
Code: Select all
IoBuildDeviceIoControlRequest
Le driver appelant ne doit pas arbitrairement appeler :
Code: Select all
IoFreeIrp
Le comportement dépend du mécanisme de construction utilisé.
Règle générale :
Toujours savoir :
- qui a créé l’IRP ;
- qui doit le compléter ;
- qui doit le libérer ;
- qui possède chaque buffer.
25. Ownership des buffers
Exemple :
Code: Select all
REQUEST_DATA data;
Dans le cas synchrone :
Code: Select all
IoCallDriver(...)
KeWaitForSingleObject(...)
La structure locale reste donc valide pendant l’opération.
En revanche, si Driver A lance la requête puis retourne immédiatement sans attendre, un pointeur vers une variable locale serait dangereux.
26. Lifetime du FILE_OBJECT
Après :
Code: Select all
IoGetDeviceObjectPointer
Code: Select all
PFILE_OBJECT
Code: Select all
ObDereferenceObject(fileObject);
Dans un design où le pointeur est conservé longtemps, le driver doit clairement décider à quel moment cette référence sera libérée.
27. Conserver le DEVICE_OBJECT longtemps
Une erreur possible consiste à retrouver le device une fois :
Code: Select all
g_TargetDevice = deviceObject;
Si le device peut disparaître à cause du PnP ou d’un unload, le pointeur peut devenir invalide.
Pour un simple driver logiciel chargé pendant toute la durée du test, le problème peut sembler invisible.
Dans du code robuste, il faut toujours réfléchir à la durée de vie réelle des objets.
28. Communication synchrone
Communication synchrone :
Code: Select all
Driver A
|
| IoCallDriver
v
attend
|
v
Driver B termine
|
v
Driver A continue
- simple à comprendre ;
- résultat directement disponible ;
- buffers faciles à gérer.
- le thread appelant est bloqué ;
- peut provoquer de la latence ;
- exige un IRQL compatible avec l’attente.
29. Communication asynchrone
Communication asynchrone :
Code: Select all
Driver A
|
| IoCallDriver
v
continue son travail
|
v
Driver B termine plus tard
- durée de vie des buffers ;
- durée de vie du contexte ;
- completion ;
- erreurs ;
- unload du driver.
30. IRQL et attente
Un driver ne peut pas attendre arbitrairement depuis n’importe quel IRQL.
Pour une communication synchrone qui utilise :
Code: Select all
KeWaitForSingleObject(...)
Dans la plupart des designs simples, on effectue ce travail à :
Code: Select all
PASSIVE_LEVEL
31. Communication depuis un worker thread
Architecture :
Code: Select all
IRP user-mode
|
v
Driver A
|
| met une tâche en file
v
Worker thread
|
| IoBuildDeviceIoControlRequest
| IoCallDriver
| attente
v
Driver B
- travail à PASSIVE_LEVEL ;
- attente possible ;
- dispatch routine plus courte ;
- possibilité de traiter une file de requêtes.
32. Exemple d’architecture Application → A → B
Supposons :
- une application envoie un IOCTL à Driver A ;
- Driver A doit demander le résultat à Driver B ;
- Driver B calcule une valeur ;
- Driver A renvoie la réponse à l’application.
Code: Select all
User-mode
|
| DeviceIoControl
v
Driver A
|
| nouvelle requête
v
Driver B
|
| résultat
v
Driver A
|
| complète l'IRP original
v
User-mode
33. Ne pas confondre forwarder et créer une nouvelle requête
Deux situations différentes existent.
Forwarding
Driver A reçoit un IRP et transmet ce même IRP.
Exemples déjà vus :
Code: Select all
IoSkipCurrentIrpStackLocation
IoCopyCurrentIrpStackLocationToNext
IoCallDriver
Nouvelle requête
Driver A crée un nouvel IRP indépendant :
Code: Select all
IoBuildDeviceIoControlRequest
Code: Select all
IoCallDriver
34. IoGetDeviceObjectPointer vs IoAttachDeviceToDeviceStack
Ces deux APIs ne servent pas au même objectif.
IoGetDeviceObjectPointer
Permet de récupérer un device existant pour lui adresser des requêtes.
IoAttachDeviceToDeviceStack
Permet à un driver de s’insérer dans une stack existante.
C’est plutôt utilisé dans les filter drivers.
Il ne faut pas les confondre.
35. Erreur classique : mauvais nom de device
Exemple :
Code: Select all
L"\\DosDevices\\Provider"
Code: Select all
L"\\Device\\Provider"
Code: Select all
STATUS_OBJECT_NAME_NOT_FOUND
Toujours vérifier le nom NT exact utilisé par le driver cible.
36. Erreur classique : oublier ObDereferenceObject
Après :
Code: Select all
IoGetDeviceObjectPointer(...)
Code: Select all
ObDereferenceObject(fileObject);
- référence conservée ;
- objet maintenu artificiellement ;
- fuite de ressource kernel.
37. Erreur classique : ignorer STATUS_PENDING
Code incorrect :
Code: Select all
status = IoCallDriver(
deviceObject,
irp
);
KdPrint(("Result = %lu\n", data.Result));
Code: Select all
status == STATUS_PENDING
Le buffer peut ne pas contenir le résultat final.
Il faut attendre ou utiliser un mécanisme asynchrone adapté.
38. Erreur classique : croire que IoCallDriver retourne toujours le statut final
Le statut immédiat de :
Code: Select all
IoCallDriver
Code: Select all
ioStatus.Status
Code: Select all
IoCallDriver -> STATUS_PENDING
Code: Select all
ioStatus.Status -> STATUS_SUCCESS
- statut du lancement de l’opération ;
- statut final de l’opération.
39. Erreur classique : buffer invalide
Exemple dangereux :
Code: Select all
PVOID buffer = SomeTemporaryPointer;
IoBuildDeviceIoControlRequest(
...,
buffer,
...
);
Toujours garantir le lifetime des buffers jusqu’à completion.
40. Erreur classique : attendre au mauvais IRQL
Une attente bloquante depuis un contexte où elle est interdite peut provoquer :
- bugcheck ;
- blocage ;
- comportement incorrect.
effectuer les appels synchrones depuis PASSIVE_LEVEL.
41. Erreur classique : utiliser un DEVICE_OBJECT devenu invalide
Un pointeur vers un device ne doit pas être considéré comme éternel.
Si le device peut être supprimé :
- le pointeur peut devenir invalide ;
- une requête ultérieure peut provoquer un crash.
Mais le principe de lifetime reste essentiel.
42. Projet pratique : Provider + Client
Créer deux drivers.
DriverProvider
Il crée :
Code: Select all
\Device\Provider
Code: Select all
IOCTL_PROVIDER_CALCULATE
Code: Select all
REQUEST_DATA.Value
Code: Select all
REQUEST_DATA.Result =
Value * 2;
DriverClient
Il doit :
- retrouver \Device\Provider ;
- récupérer PFILE_OBJECT et PDEVICE_OBJECT ;
- construire une requête ;
- envoyer l’IOCTL ;
- gérer STATUS_PENDING ;
- afficher le résultat ;
- libérer correctement la référence.
43. Étapes du projet
Étape 1
Créer DriverProvider.
Étape 2
Créer le DEVICE_OBJECT Provider.
Étape 3
Ajouter IRP_MJ_DEVICE_CONTROL.
Étape 4
Traiter IOCTL_PROVIDER_CALCULATE.
Étape 5
Créer DriverClient.
Étape 6
Utiliser IoGetDeviceObjectPointer.
Étape 7
Créer un KEVENT et un IO_STATUS_BLOCK.
Étape 8
Construire l’IRP avec IoBuildDeviceIoControlRequest.
Étape 9
Appeler IoCallDriver.
Étape 10
Gérer STATUS_PENDING.
Étape 11
Afficher le résultat.
Étape 12
ObDereferenceObject(fileObject).
44. Projet avancé : User → Client → Provider
Architecture :
Code: Select all
Application
|
| DeviceIoControl
v
DriverClient
|
| IoBuildDeviceIoControlRequest
| IoCallDriver
v
DriverProvider
|
| réponse
v
DriverClient
|
| complète l'IRP user-mode
v
Application
Ce projet permet de combiner :
- communication user/kernel ;
- communication driver/driver ;
- IRP ;
- IOCTL ;
- threads système éventuellement ;
- gestion correcte des lifetimes.
45. Variante avec worker thread
Une version plus avancée :
Code: Select all
Application
|
v
DriverClient Dispatch
|
| ajoute tâche
v
Worker Thread
|
| communication driver → driver
v
DriverProvider
Cela permet de ne pas bloquer directement la routine de dispatch.
46. APIs principales à retenir
Localiser le device
Code: Select all
RtlInitUnicodeString
IoGetDeviceObjectPointer
Code: Select all
IoBuildDeviceIoControlRequest
Code: Select all
IoCallDriver
Code: Select all
KeInitializeEvent
KeWaitForSingleObject
Code: Select all
IO_STATUS_BLOCK
```
[color=#FF0000][b]Libérer la référence[/b][/color]
[code]
ObDereferenceObject
47. Concepts à retenir
- Un driver peut construire volontairement une nouvelle requête destinée à un autre driver.
- La cible d’un IoCallDriver est un DEVICE_OBJECT.
- IoGetDeviceObjectPointer permet de retrouver un device par son nom.
- IoGetDeviceObjectPointer retourne aussi un FILE_OBJECT dont la référence doit être libérée.
- IoBuildDeviceIoControlRequest permet de construire facilement un IRP de type DEVICE_CONTROL ou INTERNAL_DEVICE_CONTROL.
- IoCallDriver transmet réellement l’IRP au driver cible.
- STATUS_PENDING signifie que le traitement n’est pas terminé.
- Le statut final peut être récupéré dans IO_STATUS_BLOCK.
- La durée de vie des buffers doit être garantie jusqu’à completion.
- Il faut distinguer forwarding d’un IRP existant et création d’un nouvel IRP.
- Une communication synchrone doit être effectuée depuis un contexte où l’attente est autorisée.
- Un worker thread est souvent pratique pour ce type de communication.
Conclusion
La communication entre drivers repose sur les mêmes mécanismes fondamentaux que le reste de l’I/O Manager : DEVICE_OBJECT, IRP et IoCallDriver.
Le pattern principal à retenir est :
Code: Select all
Trouver le device
|
v
IoGetDeviceObjectPointer
|
v
Construire la requête
|
v
IoBuildDeviceIoControlRequest
|
v
IoCallDriver
|
v
Attendre si STATUS_PENDING
|
v
Lire IO_STATUS_BLOCK
|
v
ObDereferenceObject
La partie réellement importante est de comprendre :
- quel device on appelle ;
- quel objet doit rester vivant ;
- qui possède l’IRP ;
- qui possède les buffers ;
- quand l’opération est réellement terminée ;
- à quel IRQL l’appel est effectué.
