Threads système en Kernel 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:

Threads système en Kernel Windows

Post by Hydraxx »

Threads système en Kernel Windows

Objectif du cours

Un driver Windows peut avoir besoin d’exécuter du travail en arrière-plan, indépendamment du thread qui a déclenché une requête I/O. Dans ce cas, il peut créer un thread système exécuté en mode noyau.

Ce cours présente la création, la gestion et l’arrêt propre de threads système dans un driver Windows. Les primitives de synchronisation comme les événements, sémaphores, mutex ou spinlocks ne sont pas réexpliquées en détail ici : elles sont supposées déjà connues et ne seront utilisées que lorsqu’elles sont nécessaires au fonctionnement du thread.

L’objectif est de comprendre :
  • pourquoi un driver crée un thread système ;
  • comment utiliser PsCreateSystemThread et IoCreateSystemThread ;
  • la différence entre un HANDLE et un PETHREAD ;
  • comment terminer proprement un thread ;
  • comment attendre réellement sa fin avant de décharger le driver ;
  • comment faire communiquer une routine de dispatch avec un thread de fond ;
  • quand préférer un thread système, un Work Item ou un DPC ;
  • quels pièges éviter concernant la durée de vie des objets et du driver.

1. Pourquoi créer un thread système ?

Une routine de dispatch est exécutée dans le contexte du thread qui a provoqué la requête. Elle ne doit pas nécessairement effectuer elle-même tout le travail demandé.

Certaines opérations peuvent :
  • durer longtemps ;
  • attendre un événement ;
  • attendre qu’une tâche soit disponible ;
  • effectuer plusieurs opérations successives ;
  • fonctionner continuellement pendant toute la durée de vie du driver ;
  • appeler d’autres composants du système ;
  • nécessiter une boucle de travail permanente.
Dans ces situations, il peut être intéressant de déléguer le travail à un thread système.

Exemple conceptuel :

Code: Select all

Application
    |
    | DeviceIoControl()
    v
DispatchDeviceControl
    |
    | ajoute une tâche dans une file
    v
Thread système du driver
    |
    | traite la tâche
    v
Résultat
Le thread système est donc souvent utilisé comme un worker permanent.

Important : créer un thread système n’est pas obligatoire pour chaque driver. Beaucoup de drivers n’en ont besoin d’aucun.


2. Thread user-mode et thread système

Un thread créé avec CreateThread appartient normalement à un processus user-mode.

Un thread créé avec PsCreateSystemThread est un thread système exécutant une routine kernel.

Le code exécuté par ce thread :
  • tourne en mode noyau ;
  • peut utiliser les APIs kernel autorisées ;
  • n’est pas destiné à exécuter du code user-mode ;
  • peut vivre indépendamment de la requête qui a provoqué sa création.
Le point essentiel est que le thread possède sa propre durée de vie.

Une routine de dispatch peut se terminer alors que le thread continue à fonctionner.


3. PsCreateSystemThread

L’API classique utilisée pour créer un thread système est :

Code: Select all

NTSTATUS PsCreateSystemThread(
    PHANDLE ThreadHandle,
    ULONG DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    HANDLE ProcessHandle,
    PCLIENT_ID ClientId,
    PKSTART_ROUTINE StartRoutine,
    PVOID StartContext
);
Exemple simple :

Code: Select all

HANDLE g_ThreadHandle = NULL;

VOID WorkerThread(PVOID Context)
{
    UNREFERENCED_PARAMETER(Context);

    KdPrint(("Worker thread started\n"));

    PsTerminateSystemThread(STATUS_SUCCESS);
}

NTSTATUS status = PsCreateSystemThread(
    &g_ThreadHandle,
    THREAD_ALL_ACCESS,
    NULL,
    NULL,
    NULL,
    WorkerThread,
    NULL
);
Si l’appel réussit, g_ThreadHandle contient un handle vers le nouveau thread.

Attention : ce handle n’est pas directement un PETHREAD.


4. Paramètres importants de PsCreateSystemThread

ThreadHandle

Reçoit le handle du thread nouvellement créé.

Code: Select all

PHANDLE ThreadHandle
Ce handle peut ensuite servir à récupérer l’objet thread kernel ou être fermé avec ZwClose.


DesiredAccess

Spécifie les droits demandés sur l’objet thread.

Dans de nombreux exemples simples :

Code: Select all

THREAD_ALL_ACCESS
Dans du code de production, il est préférable de demander uniquement les droits nécessaires.


ObjectAttributes

Permet de fournir des attributs à l’objet.

Pour un thread système classique :

Code: Select all

NULL
est généralement suffisant.


ProcessHandle

Permet éventuellement d’associer le thread à un processus.

Lorsqu’il vaut NULL, le thread est créé comme thread système.


ClientId

Peut recevoir les identifiants du thread et du processus.

Il est souvent laissé à NULL.


StartRoutine

Adresse de la fonction que le thread exécutera.

Sa forme générale est :

Code: Select all

VOID ThreadRoutine(PVOID Context);

StartContext

Pointeur transmis à la fonction du thread.

Il permet de fournir au thread :
  • une structure de contexte ;
  • un pointeur vers une extension de device ;
  • une structure contenant ses données ;
  • un objet nécessaire à son fonctionnement.

5. Le contexte transmis au thread

Exemple :

Code: Select all

typedef struct _THREAD_CONTEXT
{
    LONG Id;
    PDEVICE_OBJECT DeviceObject;

} THREAD_CONTEXT, *PTHREAD_CONTEXT;
Puis :

Code: Select all

PTHREAD_CONTEXT context =
    (PTHREAD_CONTEXT)ExAllocatePool2(
        POOL_FLAG_NON_PAGED,
        sizeof(THREAD_CONTEXT),
        'CtxT'
    );

if (!context)
{
    return STATUS_INSUFFICIENT_RESOURCES;
}

context->Id = 1;
context->DeviceObject = DeviceObject;

NTSTATUS status = PsCreateSystemThread(
    &g_ThreadHandle,
    THREAD_ALL_ACCESS,
    NULL,
    NULL,
    NULL,
    WorkerThread,
    context
);
Dans le thread :

Code: Select all

VOID WorkerThread(PVOID Parameter)
{
    PTHREAD_CONTEXT context =
        (PTHREAD_CONTEXT)Parameter;

    KdPrint(("Thread id = %ld\n", context->Id));

    ExFreePool(context);

    PsTerminateSystemThread(STATUS_SUCCESS);
}
Piège important : les données passées au thread doivent rester valides suffisamment longtemps.

Il serait dangereux de transmettre l’adresse d’une variable locale qui disparaît dès que la routine appelante retourne.

Exemple incorrect :

Code: Select all

NTSTATUS SomeFunction()
{
    LONG value = 123;

    PsCreateSystemThread(
        &g_ThreadHandle,
        THREAD_ALL_ACCESS,
        NULL,
        NULL,
        NULL,
        WorkerThread,
        &value
    );

    return STATUS_SUCCESS;
}
Après le retour de SomeFunction, le pointeur reçu par le thread peut devenir invalide.


6. IoCreateSystemThread

Windows propose également :

IoCreateSystemThread

Cette API est similaire à PsCreateSystemThread, mais permet d’associer explicitement le thread à un objet I/O tel qu’un DRIVER_OBJECT ou un DEVICE_OBJECT.

Conceptuellement :

Code: Select all

Driver / Device
      |
      +------> Thread système
L’intérêt principal est lié à la durée de vie de l’objet.

Lorsque le thread est associé au driver ou au device, le système peut mieux garantir que l’objet ne disparaisse pas alors que le thread l’utilise encore.

C’est particulièrement intéressant dans un driver moderne lorsqu’un thread appartient clairement à un device donné.


7. Fonction principale du thread

Un thread permanent possède souvent une boucle de ce type :

Code: Select all

VOID WorkerThread(PVOID Context)
{
    UNREFERENCED_PARAMETER(Context);

    while (TRUE)
    {
        // Attendre une tâche

        // Vérifier si l'arrêt a été demandé

        // Traiter la tâche
    }

    PsTerminateSystemThread(STATUS_SUCCESS);
}
Dans un vrai driver, la boucle attend généralement un événement, un sémaphore ou plusieurs objets dispatcher.

Le cours de synchronisation explique le fonctionnement de ces objets ; ici, le point important est architectural :

Code: Select all

while (driver actif)
{
    attendre;

    si arrêt demandé
        quitter;

    sinon
        traiter le travail;
}

8. PsTerminateSystemThread

Un thread système créé avec PsCreateSystemThread doit normalement se terminer avec :

PsTerminateSystemThread

Prototype :

Code: Select all

VOID PsTerminateSystemThread(
    NTSTATUS ExitStatus
);
Exemple :

Code: Select all

PsTerminateSystemThread(STATUS_SUCCESS);
Cette fonction termine le thread courant et lui attribue un code de sortie.


9. Pourquoi ne pas simplement utiliser return ?

Une fonction classique peut se terminer avec :

Code: Select all

return;
Mais une routine servant de point d’entrée à un thread système doit être pensée comme la routine principale d’un thread kernel.

L’utilisation de :

Code: Select all

PsTerminateSystemThread(...)
exprime explicitement que le thread kernel courant doit être terminé.

C’est la méthode standard pour terminer un thread créé avec PsCreateSystemThread.

Le code ne doit naturellement plus accéder aux données du thread après cet appel.


10. HANDLE contre PETHREAD

Lors de :

Code: Select all

PsCreateSystemThread(&ThreadHandle, ...);
le noyau retourne un :

HANDLE

Ce handle est une référence indirecte vers l’objet thread.

Mais certaines APIs kernel travaillent directement avec l’objet thread :

PETHREAD

Il faut donc bien distinguer :

Code: Select all

HANDLE
    |
    | référence via table de handles
    v
objet ETHREAD
    ^
    |
PETHREAD
Le HANDLE et le PETHREAD ne sont pas interchangeables.


11. Récupérer un PETHREAD depuis le HANDLE

On peut utiliser :

ObReferenceObjectByHandle

Exemple :

Code: Select all

PETHREAD g_ThreadObject = NULL;

NTSTATUS status = ObReferenceObjectByHandle(
    g_ThreadHandle,
    THREAD_ALL_ACCESS,
    *PsThreadType,
    KernelMode,
    (PVOID*)&g_ThreadObject,
    NULL
);
Après réussite :

Code: Select all

g_ThreadObject
pointe vers l’objet thread.


Très important :

ObReferenceObjectByHandle augmente le compteur de références de l’objet.

Il faudra donc plus tard appeler :

Code: Select all

ObDereferenceObject(g_ThreadObject);
Sinon l’objet conservera une référence inutile.


12. Pourquoi conserver un PETHREAD ?

Un cas très fréquent est l’unload du driver.

Le driver demande au thread de s’arrêter puis doit attendre sa terminaison.

Un objet thread est un objet dispatcher, ce qui signifie qu’il peut être attendu avec :

Code: Select all

KeWaitForSingleObject(
    g_ThreadObject,
    Executive,
    KernelMode,
    FALSE,
    NULL
);
Lorsque le thread termine, son objet devient signalé.

Le driver sait alors que le code du thread n’est plus en cours d’exécution.


13. Fermer le HANDLE du thread

Le handle obtenu avec PsCreateSystemThread doit être fermé lorsqu’il n’est plus nécessaire :

Code: Select all

ZwClose(g_ThreadHandle);
g_ThreadHandle = NULL;
Une architecture fréquente est :

Code: Select all

PsCreateSystemThread(...)
        |
        v
HANDLE
        |
        +--> ObReferenceObjectByHandle(...)
        |          |
        |          v
        |       PETHREAD
        |
        +--> ZwClose(HANDLE)
Le driver peut donc :
  • récupérer un PETHREAD ;
  • fermer rapidement le HANDLE ;
  • conserver le PETHREAD tant qu’il en a besoin ;
  • attendre le thread ;
  • appeler ensuite ObDereferenceObject.

14. Exemple complet de création et référencement

Code: Select all

HANDLE threadHandle = NULL;
PETHREAD threadObject = NULL;

NTSTATUS status = PsCreateSystemThread(
    &threadHandle,
    THREAD_ALL_ACCESS,
    NULL,
    NULL,
    NULL,
    WorkerThread,
    NULL
);

if (!NT_SUCCESS(status))
{
    return status;
}

status = ObReferenceObjectByHandle(
    threadHandle,
    THREAD_ALL_ACCESS,
    *PsThreadType,
    KernelMode,
    (PVOID*)&threadObject,
    NULL
);

ZwClose(threadHandle);

if (!NT_SUCCESS(status))
{
    return status;
}
À ce moment :
  • le handle n’est plus nécessaire ;
  • le driver possède une référence sur l’objet thread ;
  • il pourra attendre sa terminaison ;
  • il devra appeler ObDereferenceObject.

15. Arrêter proprement un thread

Le driver ne doit pas essayer de tuer brutalement son thread.

La méthode propre est coopérative :
  • le driver modifie un état ou signale un événement d’arrêt ;
  • le thread se réveille ;
  • le thread détecte que l’arrêt est demandé ;
  • il termine son travail ;
  • il libère ce qu’il possède ;
  • il appelle PsTerminateSystemThread ;
  • DriverUnload attend que le thread soit réellement terminé.
Architecture :

Code: Select all

DriverUnload
    |
    | demande l'arrêt
    v
Thread
    |
    | nettoie
    | quitte
    v
Objet thread signalé
    |
    v
DriverUnload continue

16. Pourquoi attendre le thread pendant DriverUnload ?

Imaginez :

Code: Select all

DriverUnload()
{
    IoDeleteDevice(...);
}
alors que le thread continue :

Code: Select all

WorkerThread()
{
    DeviceExtension->Counter++;
}
Si le device ou l’extension est déjà supprimé, le thread peut accéder à une adresse mémoire invalide.

Le résultat peut être :
  • use-after-free ;
  • corruption mémoire ;
  • accès invalide ;
  • BSOD.
Règle importante :

Le driver ne doit pas détruire les ressources utilisées par le thread avant d’avoir la certitude que celui-ci est terminé.


17. Exemple de séquence d’unload

Exemple conceptuel :

Code: Select all

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    // 1. demander l'arrêt du thread
    KeSetEvent(&g_StopEvent, IO_NO_INCREMENT, FALSE);

    // 2. attendre sa terminaison
    if (g_ThreadObject)
    {
        KeWaitForSingleObject(
            g_ThreadObject,
            Executive,
            KernelMode,
            FALSE,
            NULL
        );

        // 3. supprimer la référence objet
        ObDereferenceObject(g_ThreadObject);
        g_ThreadObject = NULL;
    }

    // 4. seulement ensuite nettoyer le reste du driver
    IoDeleteSymbolicLink(...);
    IoDeleteDevice(...);
}
Le fonctionnement détaillé de KEVENT et de KeWaitForSingleObject est supposé connu via le cours de synchronisation.


18. Thread permanent ou thread temporaire ?

Deux architectures sont possibles.

Thread temporaire

Créé pour effectuer une tâche précise puis détruit.

Code: Select all

Créer
  |
  v
Travail
  |
  v
Terminer
Cela peut convenir à une opération relativement longue mais ponctuelle.


Thread permanent

Créé lors de l’initialisation du driver et conservé jusqu’au unload.

Code: Select all

DriverEntry
    |
    v
Créer thread
    |
    v
Boucle de travail
    |
    v
DriverUnload
    |
    v
Arrêter thread
Cette architecture est pratique lorsque le driver reçoit régulièrement des tâches.


19. Création dans DriverEntry

Une solution simple consiste à créer le thread pendant DriverEntry.

Avantages :
  • architecture simple ;
  • thread disponible immédiatement ;
  • pas de concurrence liée à sa création tardive.
Inconvénient :
  • le thread existe même si aucun client ne l’utilise.

20. Création à la demande

Le thread peut également être créé lorsqu’un client ouvre le device ou lors de la première requête.

Exemple conceptuel :

Code: Select all

IRP_MJ_CREATE
    |
    | thread existe ?
    |      |
    |      +-- oui --> continuer
    |
    +-- non --> créer
Cette approche économise des ressources mais rend la gestion de concurrence plus délicate.

Deux threads user-mode pourraient ouvrir le device simultanément et essayer de créer le même worker.

La création elle-même doit alors être protégée.


21. Communication entre Dispatch Routine et thread

Un pattern extrêmement fréquent est :
  • la dispatch routine reçoit une requête ;
  • elle construit une structure représentant une tâche ;
  • elle ajoute cette tâche à une file ;
  • elle réveille le worker thread ;
  • le worker retire la tâche ;
  • il effectue le traitement.
Schéma :

Code: Select all

IRP_MJ_DEVICE_CONTROL
        |
        v
Créer WORK_ITEM interne
        |
        v
Ajouter à LIST_ENTRY
        |
        v
Réveiller worker
        |
        v
WorkerThread
        |
        v
Traiter
Les mécanismes de synchronisation de la liste ne sont pas détaillés ici puisqu’ils appartiennent au cours de synchronisation.


22. Structure de tâche

Exemple :

Code: Select all

typedef struct _MY_TASK
{
    LIST_ENTRY Entry;

    ULONG Command;
    ULONG Value;

} MY_TASK, *PMY_TASK;
Une dispatch routine pourrait allouer :

Code: Select all

PMY_TASK task =
    (PMY_TASK)ExAllocatePool2(
        POOL_FLAG_NON_PAGED,
        sizeof(MY_TASK),
        'ksaT'
    );
Puis :

Code: Select all

task->Command = command;
task->Value = value;
Le thread pourra ensuite récupérer l’élément et le traiter.

Important : il faut définir clairement qui possède l’allocation et qui doit la libérer.

Par exemple :
  • la dispatch routine alloue ;
  • le worker devient propriétaire après mise en file ;
  • le worker libère après traitement.

23. Thread et file de travail

Une boucle typique :

Code: Select all

VOID WorkerThread(PVOID Context)
{
    while (TRUE)
    {
        // attendre une tâche ou une demande d'arrêt

        if (StopRequested)
            break;

        // extraire une tâche de la file

        // traiter la tâche

        // libérer la tâche
    }

    PsTerminateSystemThread(STATUS_SUCCESS);
}
Cette architecture sépare très bien :
  • le chemin rapide de réception de la requête ;
  • le traitement de fond potentiellement plus long.

24. Effectuer des opérations bloquantes dans un thread

Un des intérêts du thread système est de pouvoir exécuter du code à PASSIVE_LEVEL dans une boucle de travail normale.

Cela rend possible de nombreuses opérations qui seraient inadaptées dans une DPC.

Par exemple :
  • attendre des dispatcher objects ;
  • appeler certaines APIs pouvant bloquer ;
  • effectuer du travail nécessitant PASSIVE_LEVEL ;
  • exécuter des opérations plus longues.
Cela ne signifie pas qu’un thread doit être bloqué inutilement : il doit généralement dormir tant qu’aucun travail n’est disponible.


25. Thread qui appelle un autre driver

Une architecture particulièrement intéressante consiste à faire effectuer les appels inter-drivers par un worker thread.

Schéma :

Code: Select all

Application
    |
    v
Driver A
    |
    | place la requête dans une file
    v
Worker Thread A
    |
    | construit / envoie un IRP
    v
Driver B
Le worker peut :
  • obtenir le DEVICE_OBJECT cible ;
  • construire une requête ;
  • appeler IoCallDriver ;
  • attendre éventuellement la fin de l’opération ;
  • traiter le résultat.
Cela permet de garder certaines dispatch routines plus simples.


26. IoBuildDeviceIoControlRequest depuis un thread

Pour certaines communications entre drivers :

Code: Select all

PIRP irp = IoBuildDeviceIoControlRequest(
    IoControlCode,
    TargetDevice,
    InputBuffer,
    InputBufferLength,
    OutputBuffer,
    OutputBufferLength,
    FALSE,
    &event,
    &ioStatus
);
Puis :

Code: Select all

NTSTATUS status = IoCallDriver(
    TargetDevice,
    irp
);
Si le statut vaut STATUS_PENDING, le thread peut attendre la fin de l’opération avec l’événement associé.

Cette partie est liée au cours sur les IRP et à celui sur la communication entre drivers ; elle n’est ici qu’un exemple d’utilisation pratique d’un worker thread.


27. Threads et mémoire

Le thread doit avoir une politique claire concernant la mémoire qu’il utilise.

Questions à toujours se poser :
  • qui alloue ?
  • qui devient propriétaire ?
  • qui libère ?
  • la mémoire reste-t-elle valide jusqu’à la fin du traitement ?
  • le thread peut-il encore utiliser cette mémoire pendant DriverUnload ?
Exemple de bug :

Code: Select all

Dispatch()
{
    TASK* task = AllocateTask();

    QueueTask(task);

    FreeTask(task); // ERREUR
}
Le thread récupérera plus tard un pointeur vers une zone déjà libérée.

Le bon principe est :

Code: Select all

Dispatch()
{
    TASK* task = AllocateTask();

    QueueTask(task);

    // le worker devient propriétaire
}
Puis :

Code: Select all

Worker()
{
    TASK* task = GetTask();

    ProcessTask(task);

    FreeTask(task);
}

28. Lookaside Lists et worker threads

Si le driver alloue et libère très fréquemment des structures de même taille, une lookaside list peut être utilisée.

Exemple conceptuel :

Code: Select all

Dispatch
    |
    | alloue TASK
    v
Lookaside
    |
    v
Queue
    |
    v
Worker
    |
    | traite
    v
retour dans Lookaside
Le thread lui-même n’exige pas l’utilisation d’une lookaside list.

C’est simplement une optimisation particulièrement adaptée lorsque les mêmes objets sont recyclés en permanence.


29. Thread système vs DPC

Un thread système et une DPC n’ont pas le même rôle.

DPC
  • exécution courte ;
  • IRQL élevé ;
  • ne doit pas bloquer ;
  • ne doit pas effectuer de longues attentes ;
  • adaptée au travail différé très court.
Thread système
  • généralement PASSIVE_LEVEL ;
  • peut attendre ;
  • peut rester vivant longtemps ;
  • peut exécuter une boucle permanente ;
  • adapté à un worker de fond.
Un thread n’est donc pas une « grosse DPC ».

Ce sont deux mécanismes différents.


30. Thread système vs Work Item

Un Work Item permet d’exécuter du travail différé à PASSIVE_LEVEL sans créer un thread dédié au driver.

Le système utilise ses propres worker threads.

Le choix dépend du besoin.

Work Item
  • travail ponctuel ;
  • relativement court ;
  • pas besoin de garder un thread personnel ;
  • très pratique pour reporter du travail vers PASSIVE_LEVEL.
Thread système
  • worker permanent ;
  • besoin d’attendre plusieurs événements ;
  • boucle de travail dédiée ;
  • besoin de conserver un état propre au thread ;
  • tâches longues ou continues.

31. Résumé du choix

Code: Select all

Travail très court à DISPATCH_LEVEL
        -> DPC

Travail différé ponctuel à PASSIVE_LEVEL
        -> Work Item

Worker permanent / boucle / attentes
        -> System Thread

32. Piège : décharger le driver trop tôt

C’est probablement l’erreur la plus importante à éviter.

Un thread exécute du code qui appartient à l’image du driver.

Si le driver est déchargé alors que le thread continue :

Code: Select all

RIP ---> code du driver
mais cette image peut ne plus être présente.

Le résultat est catastrophique.

Le thread doit être complètement terminé avant que DriverUnload retourne.


33. Piège : fuite de référence PETHREAD

Après :

Code: Select all

ObReferenceObjectByHandle(...)
il faut :

Code: Select all

ObDereferenceObject(...)
Sinon une référence reste attachée à l’objet thread.

C’est une fuite d’objet kernel.


34. Piège : oublier de fermer le HANDLE

Le handle obtenu avec PsCreateSystemThread doit être fermé :

Code: Select all

ZwClose(ThreadHandle);
La référence objet et le handle sont deux choses différentes.

Il faut donc distinguer :

Code: Select all

ZwClose(handle);
et :

Code: Select all

ObDereferenceObject(threadObject);

35. Piège : utiliser des données temporaires

Ne jamais transmettre au thread un pointeur vers :
  • une variable locale déjà détruite ;
  • un buffer user-mode non verrouillé conservé après le retour de la requête ;
  • une structure libérée par une autre routine ;
  • une extension de device pouvant être détruite avant le thread.
La durée de vie des données est une partie essentielle du design.


36. Piège : boucle active

Code incorrect :

Code: Select all

while (!Stop)
{
    CheckQueue();
}
Cette boucle consomme inutilement du CPU.

Le worker doit normalement attendre un événement, un sémaphore ou un autre objet dispatcher lorsqu’il n’a rien à faire.

Conceptuellement :

Code: Select all

while (!Stop)
{
    Wait();

    ProcessWork();
}

37. Piège : conserver un lock pendant une opération longue

Même si la synchronisation est traitée dans un autre cours, un pattern mérite d’être rappelé.

Éviter :

Code: Select all

AcquireLock();

RemoveTask();

ProcessTask(); // très long

ReleaseLock();
Préférer :

Code: Select all

AcquireLock();

RemoveTask();

ReleaseLock();

ProcessTask();
Le verrou protège la structure partagée, pas nécessairement tout le traitement.


38. Architecture recommandée d’un worker thread

Une architecture robuste ressemble à :

Code: Select all

DriverEntry
    |
    +--> initialiser ressources
    |
    +--> créer worker thread
            |
            v
      attente travail/arrêt
            |
      +-----+------+
      |            |
    travail       arrêt
      |            |
      v            v
   traiter      nettoyage
      |            |
      +-----<------+
                   |
                   v
         PsTerminateSystemThread


DriverUnload
    |
    +--> signaler arrêt
    |
    +--> attendre PETHREAD
    |
    +--> ObDereferenceObject
    |
    +--> détruire ressources

39. Exemple minimal complet

Code: Select all

#include <ntddk.h>

HANDLE g_ThreadHandle = NULL;
PETHREAD g_ThreadObject = NULL;
KEVENT g_StopEvent;

VOID WorkerThread(PVOID Context)
{
    UNREFERENCED_PARAMETER(Context);

    KdPrint(("Worker started\n"));

    KeWaitForSingleObject(
        &g_StopEvent,
        Executive,
        KernelMode,
        FALSE,
        NULL
    );

    KdPrint(("Worker stopping\n"));

    PsTerminateSystemThread(STATUS_SUCCESS);
}

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

    KeSetEvent(
        &g_StopEvent,
        IO_NO_INCREMENT,
        FALSE
    );

    if (g_ThreadObject)
    {
        KeWaitForSingleObject(
            g_ThreadObject,
            Executive,
            KernelMode,
            FALSE,
            NULL
        );

        ObDereferenceObject(g_ThreadObject);
        g_ThreadObject = NULL;
    }

    KdPrint(("Driver unloaded\n"));
}

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

    DriverObject->DriverUnload = DriverUnload;

    KeInitializeEvent(
        &g_StopEvent,
        NotificationEvent,
        FALSE
    );

    NTSTATUS status = PsCreateSystemThread(
        &g_ThreadHandle,
        THREAD_ALL_ACCESS,
        NULL,
        NULL,
        NULL,
        WorkerThread,
        NULL
    );

    if (!NT_SUCCESS(status))
    {
        return status;
    }

    status = ObReferenceObjectByHandle(
        g_ThreadHandle,
        THREAD_ALL_ACCESS,
        *PsThreadType,
        KernelMode,
        (PVOID*)&g_ThreadObject,
        NULL
    );

    ZwClose(g_ThreadHandle);
    g_ThreadHandle = NULL;

    if (!NT_SUCCESS(status))
    {
        return status;
    }

    return STATUS_SUCCESS;
}
Cet exemple est volontairement minimal.

Il montre uniquement :
  • la création ;
  • le passage du thread en attente ;
  • la demande d’arrêt ;
  • l’attente de la terminaison ;
  • la gestion du PETHREAD ;
  • la fermeture du handle ;
  • la suppression de la référence objet.

40. Amélioration : worker avec file de tâches

Une évolution logique consiste à combiner :

Code: Select all

LIST_ENTRY
KEVENT / KSEMAPHORE
PETHREAD
PsCreateSystemThread
Architecture :

Code: Select all

IOCTL
  |
  v
Créer tâche
  |
  v
Queue
  |
  v
Signaler worker
  |
  v
WorkerThread
  |
  v
Retirer tâche
  |
  v
Traiter
  |
  v
Libérer tâche
Cette architecture est très fréquente dans les drivers logiciels.


41. Exercice proposé

Créer un driver nommé :

KernelWorker

Le driver doit :
  • créer un thread système au chargement ;
  • conserver un PETHREAD ;
  • fermer le HANDLE après obtention du PETHREAD ;
  • recevoir des valeurs via un IOCTL ;
  • placer chaque valeur dans une LIST_ENTRY ;
  • réveiller le worker ;
  • faire afficher les valeurs par le thread ;
  • utiliser les primitives de synchronisation déjà vues dans le cours précédent ;
  • permettre l’arrêt propre du thread ;
  • attendre la fin du thread dans DriverUnload ;
  • libérer toutes les tâches restantes.
Bonus :
  • utiliser une lookaside list pour les tâches ;
  • faire appeler un second driver par le worker ;
  • remplacer PsCreateSystemThread par IoCreateSystemThread et comparer l’architecture.

42. APIs principales à retenir

Création

Code: Select all

PsCreateSystemThread
IoCreateSystemThread
Terminaison

Code: Select all

PsTerminateSystemThread
Objet thread

Code: Select all

ObReferenceObjectByHandle
ObDereferenceObject
PsThreadType
Handle

Code: Select all

ZwClose
Attente

Déjà étudiée dans la synchronisation, mais utilisée avec les threads :

Code: Select all

KeWaitForSingleObject
KeWaitForMultipleObjects
Temporisation éventuelle

Code: Select all

KeDelayExecutionThread

43. Concepts à retenir
  • Un thread système est un worker kernel indépendant de la routine qui l’a créé.
  • PsCreateSystemThread retourne un HANDLE, pas directement un PETHREAD.
  • ObReferenceObjectByHandle permet d’obtenir une référence sur l’objet thread.
  • Toute référence obtenue doit être compensée avec ObDereferenceObject.
  • Le HANDLE doit être fermé avec ZwClose lorsqu’il n’est plus nécessaire.
  • Le thread doit terminer proprement avec PsTerminateSystemThread.
  • DriverUnload doit attendre que le thread soit réellement terminé.
  • Un thread ne doit jamais continuer à exécuter du code après le déchargement du driver.
  • Le contexte transmis au thread doit rester valide assez longtemps.
  • Un worker thread est particulièrement adapté à une file de tâches ou à du travail permanent.
  • Une DPC sert au travail court à haut IRQL.
  • Un Work Item sert au travail différé ponctuel à PASSIVE_LEVEL.
  • Un System Thread est adapté aux boucles, attentes et workers permanents.

Conclusion

Les threads système permettent à un driver Windows d’exécuter du travail kernel de façon autonome et durable.

Leur utilisation n’est pas complexe en elle-même : la difficulté principale vient surtout de leur durée de vie.

Un driver doit toujours savoir :
  • quand le thread est créé ;
  • quelles données il utilise ;
  • qui possède ces données ;
  • comment demander son arrêt ;
  • comment savoir qu’il est réellement terminé ;
  • quelles ressources peuvent être détruites ensuite.
Le pattern le plus important à retenir est donc :

Code: Select all

Créer
  ->
Conserver une référence
  ->
Faire travailler
  ->
Demander l'arrêt
  ->
Attendre la terminaison
  ->
Supprimer la référence
  ->
Nettoyer le driver
Une fois cette architecture maîtrisée, les threads système deviennent un outil très puissant pour construire des drivers logiciels robustes, capables de traiter des files de tâches, d’effectuer du travail de fond et de communiquer avec d’autres composants du noyau.

Who is online

Users browsing this forum: No registered users and 1 guest