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.
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
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.
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
);
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
);
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
DesiredAccess
Spécifie les droits demandés sur l’objet thread.
Dans de nombreux exemples simples :
Code: Select all
THREAD_ALL_ACCESS
ObjectAttributes
Permet de fournir des attributs à l’objet.
Pour un thread système classique :
Code: Select all
NULL
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;
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
);
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);
}
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;
}
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
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);
}
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
);
Code: Select all
PsTerminateSystemThread(STATUS_SUCCESS);
9. Pourquoi ne pas simplement utiliser return ?
Une fonction classique peut se terminer avec :
Code: Select all
return;
L’utilisation de :
Code: Select all
PsTerminateSystemThread(...)
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, ...);
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
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
);
Code: Select all
g_ThreadObject
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);
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
);
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;
Code: Select all
PsCreateSystemThread(...)
|
v
HANDLE
|
+--> ObReferenceObjectByHandle(...)
| |
| v
| PETHREAD
|
+--> ZwClose(HANDLE)
- 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;
}
- 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é.
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(...);
}
Code: Select all
WorkerThread()
{
DeviceExtension->Counter++;
}
Le résultat peut être :
- use-after-free ;
- corruption mémoire ;
- accès invalide ;
- BSOD.
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(...);
}
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
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
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.
- 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
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.
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
22. Structure de tâche
Exemple :
Code: Select all
typedef struct _MY_TASK
{
LIST_ENTRY Entry;
ULONG Command;
ULONG Value;
} MY_TASK, *PMY_TASK;
Code: Select all
PMY_TASK task =
(PMY_TASK)ExAllocatePool2(
POOL_FLAG_NON_PAGED,
sizeof(MY_TASK),
'ksaT'
);
Code: Select all
task->Command = command;
task->Value = value;
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);
}
- 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.
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
- obtenir le DEVICE_OBJECT cible ;
- construire une requête ;
- appeler IoCallDriver ;
- attendre éventuellement la fin de l’opération ;
- traiter le résultat.
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
);
Code: Select all
NTSTATUS status = IoCallDriver(
TargetDevice,
irp
);
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 ?
Code: Select all
Dispatch()
{
TASK* task = AllocateTask();
QueueTask(task);
FreeTask(task); // ERREUR
}
Le bon principe est :
Code: Select all
Dispatch()
{
TASK* task = AllocateTask();
QueueTask(task);
// le worker devient propriétaire
}
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
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.
- généralement PASSIVE_LEVEL ;
- peut attendre ;
- peut rester vivant longtemps ;
- peut exécuter une boucle permanente ;
- adapté à un worker de fond.
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.
- 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
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(...)
Code: Select all
ObDereferenceObject(...)
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);
Il faut donc distinguer :
Code: Select all
ZwClose(handle);
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.
36. Piège : boucle active
Code incorrect :
Code: Select all
while (!Stop)
{
CheckQueue();
}
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();
Code: Select all
AcquireLock();
RemoveTask();
ReleaseLock();
ProcessTask();
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;
}
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
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
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.
- 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
Code: Select all
PsTerminateSystemThread
Code: Select all
ObReferenceObjectByHandle
ObDereferenceObject
PsThreadType
Code: Select all
ZwClose
Déjà étudiée dans la synchronisation, mais utilisée avec les threads :
Code: Select all
KeWaitForSingleObject
KeWaitForMultipleObjects
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.
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
