Communication entre drivers Windows

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:

Communication entre drivers Windows

Post by Hydraxx »

Communication entre drivers Windows

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.
Ce cours ne revient pas sur les bases générales des IRP déjà étudiées ailleurs : forwarding, completion routines, cancellation, queues, IRP asynchrones, etc.

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
Driver A n’est pas obligé de forwarder un IRP qu’il a reçu.

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
Lorsqu’un driver veut envoyer une requête I/O à un autre composant, il travaille généralement avec un :

Code: Select all

PDEVICE_OBJECT
et non directement avec le PDRIVER_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
Driver A peut utiliser ce nom pour retrouver le device.

On commence souvent avec :

Code: Select all

UNICODE_STRING deviceName;

RtlInitUnicodeString(
    &deviceName,
    L"\\Device\\Provider"
);
Le nom utilisé ici appartient au namespace NT.

Il ne faut pas confondre :

Code: Select all

\Device\Provider
avec un lien symbolique du type :

Code: Select all

\DosDevices\Provider
Dans une communication kernel → kernel, on travaille généralement directement avec le nom NT du device.


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
);
Exemple :

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
);
Si l’appel réussit :
  • 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 FILE_OBJECT représente l’ouverture logique du device.

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);
Important :

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(...)
Driver A possède un :

Code: Select all

PDEVICE_OBJECT deviceObject
Ce pointeur représente le device auquel la requête sera envoyée.

C’est ce pointeur qui pourra être utilisé avec :

Code: Select all

IoCallDriver(deviceObject, Irp);
Il faut garder en tête qu’un device peut appartenir à une stack.

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
ou :

Code: Select all

IRP_MJ_INTERNAL_DEVICE_CONTROL
selon le paramètre utilisé.


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
);
Les paramètres importants sont :
  • 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;
Driver A envoie :

Code: Select all

Value = 10
Driver B peut répondre :

Code: Select all

Result = 20
Cela permet d’avoir une communication simple et structurée.


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          \
    )
Les deux drivers doivent connaître le même code IOCTL.

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;
Initialisation :

Code: Select all

KeInitializeEvent(
    &event,
    NotificationEvent,
    FALSE
);
Puis :

Code: Select all

RtlZeroMemory(
    &ioStatus,
    sizeof(ioStatus)
);
Le détail du fonctionnement des événements appartient au cours de synchronisation.

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
);
La valeur :

Code: Select all

FALSE
indique ici qu’on veut construire une requête de type :

Code: Select all

IRP_MJ_DEVICE_CONTROL
Si l’on passe :

Code: Select all

TRUE
l’IRP utilisera :

Code: Select all

IRP_MJ_INTERNAL_DEVICE_CONTROL

13. Vérifier le retour de IoBuildDeviceIoControlRequest

La fonction retourne :

Code: Select all

PIRP
Si elle échoue :

Code: Select all

NULL
Exemple :

Code: Select all

if (!irp)
{
    ObDereferenceObject(fileObject);

    return STATUS_INSUFFICIENT_RESOURCES;
}
Ne jamais appeler IoCallDriver avec un IRP NULL.


14. IOCTL normal et IOCTL interne

Deux grandes possibilités existent.

IOCTL normal

Major function :

Code: Select all

IRP_MJ_DEVICE_CONTROL
Il correspond aux IOCTL classiques.

Il peut être utilisé par des applications user-mode via :

Code: Select all

DeviceIoControl
mais aussi être construit par un autre driver.


IOCTL interne

Major function :

Code: Select all

IRP_MJ_INTERNAL_DEVICE_CONTROL
Il est destiné à la communication interne entre drivers.

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
);
IoCallDriver transmet l’IRP au driver suivant associé au DEVICE_OBJECT fourni.

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.
Exemple :

Code: Select all

NTSTATUS status = IoCallDriver(
    deviceObject,
    irp
);

if (status == STATUS_PENDING)
{
    // l'opération n'est pas encore terminée
}
Attention :

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
cela signifie que la requête sera terminée plus tard.

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
    );
}
Une fois l’attente terminée, l’IO_STATUS_BLOCK contient le résultat final.


18. Récupérer le statut final

Après completion :

Code: Select all

status = ioStatus.Status;
Exemple :

Code: Select all

if (status == STATUS_PENDING)
{
    KeWaitForSingleObject(
        &event,
        Executive,
        KernelMode,
        FALSE,
        NULL
    );

    status = ioStatus.Status;
}
Le vrai statut final se trouve donc dans :

Code: Select all

ioStatus.Status

19. IO_STATUS_BLOCK.Information

Le champ :

Code: Select all

ioStatus.Information
peut contenir le nombre d’octets transférés ou une information spécifique à l’opération.

Exemple :

Code: Select all

KdPrint((
    "Bytes returned: %llu\n",
    (ULONGLONG)ioStatus.Information
));
Dans Driver B, cette valeur est généralement définie via :

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;
Puis :

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;
}
Le fonctionnement général de l’IRP est supposé déjà connu.


21. Exemple de traitement côté Driver B

Pour un IOCTL METHOD_BUFFERED :

Code: Select all

PREQUEST_DATA data =
    (PREQUEST_DATA)Irp->AssociatedIrp.SystemBuffer;
Puis :

Code: Select all

if (data)
{
    data->Result =
        data->Value * 2;

    status = STATUS_SUCCESS;

    Irp->IoStatus.Information =
        sizeof(REQUEST_DATA);
}
Driver A peut ensuite lire :

Code: Select all

data.Result
après completion.


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
l’I/O Manager construit l’IRP pour le driver appelant.

Le driver appelant ne doit pas arbitrairement appeler :

Code: Select all

IoFreeIrp
sur cet IRP comme s’il avait lui-même alloué un IRP brut avec une autre méthode.

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;
Si cette structure est locale à une fonction, elle doit rester valide jusqu’à ce que l’IRP soit complètement terminé.

Dans le cas synchrone :

Code: Select all

IoCallDriver(...)
KeWaitForSingleObject(...)
la fonction ne retourne qu’après completion.

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
Driver A possède une référence sur :

Code: Select all

PFILE_OBJECT
Cette référence doit être libérée :

Code: Select all

ObDereferenceObject(fileObject);
Tant que Driver A a besoin du device associé, il ne faut pas libérer cette référence trop tôt.

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;
puis à conserver ce pointeur indéfiniment sans penser à la durée de vie du device.

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
Avantages :
  • simple à comprendre ;
  • résultat directement disponible ;
  • buffers faciles à gérer.
Inconvénients :
  • 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
Dans ce cas, la gestion devient plus complexe :
  • durée de vie des buffers ;
  • durée de vie du contexte ;
  • completion ;
  • erreurs ;
  • unload du driver.
Ces mécanismes ont déjà été abordés dans les cours sur les IRP asynchrones et les completion routines.


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(...)
il faut être dans un contexte compatible avec une attente.

Dans la plupart des designs simples, on effectue ce travail à :

Code: Select all

PASSIVE_LEVEL
C’est une des raisons pour lesquelles un worker thread est pratique.


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
Avantages :
  • travail à PASSIVE_LEVEL ;
  • attente possible ;
  • dispatch routine plus courte ;
  • possibilité de traiter une file de requêtes.
Cela fait directement le lien avec le cours sur les threads système.


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.
Schéma :

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
Driver A joue ici le rôle d’intermédiaire.


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
puis :

Code: Select all

IoCallDriver
Le deuxième cas est le cœur de ce cours.


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"
alors que le driver cible expose réellement :

Code: Select all

L"\\Device\\Provider"
Le résultat peut être :

Code: Select all

STATUS_OBJECT_NAME_NOT_FOUND
ou une erreur similaire.

Toujours vérifier le nom NT exact utilisé par le driver cible.


36. Erreur classique : oublier ObDereferenceObject

Après :

Code: Select all

IoGetDeviceObjectPointer(...)
on oublie :

Code: Select all

ObDereferenceObject(fileObject);
Résultat :
  • 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));
Si :

Code: Select all

status == STATUS_PENDING
Driver B n’a peut-être pas encore terminé.

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
n’est pas nécessairement :

Code: Select all

ioStatus.Status
Si l’opération est pending :

Code: Select all

IoCallDriver -> STATUS_PENDING
puis plus tard :

Code: Select all

ioStatus.Status -> STATUS_SUCCESS
Il faut donc distinguer :
  • 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,
    ...
);
Si le buffer disparaît avant la fin de l’IRP, le driver cible peut accéder à une zone invalide.

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.
Pour un design simple :

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.
Dans les drivers logiciels simples non-PnP, ce problème est souvent moins visible.

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
et expose :

Code: Select all

IOCTL_PROVIDER_CALCULATE
Il reçoit :

Code: Select all

REQUEST_DATA.Value
et renvoie :

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
Le but est d’utiliser DriverClient comme intermédiaire.

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
Le worker effectue l’appel à PASSIVE_LEVEL.

Cela permet de ne pas bloquer directement la routine de dispatch.


46. APIs principales à retenir

Localiser le device

Code: Select all

RtlInitUnicodeString
IoGetDeviceObjectPointer
Construire la requête

Code: Select all

IoBuildDeviceIoControlRequest
Envoyer la requête

Code: Select all

IoCallDriver
Attendre

Code: Select all

KeInitializeEvent
KeWaitForSingleObject
Récupérer le résultat

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 difficulté n’est pas simplement d’appeler IoCallDriver.

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é.
Une fois ces règles maîtrisées, un driver peut communiquer proprement avec un autre driver sans dépendre d’une application user-mode intermédiaire.

Who is online

Users browsing this forum: No registered users and 1 guest