Les listes chaînées sont utilisées partout dans le noyau Windows pour organiser des objets, des requêtes, des processus, des éléments de file d'attente ou des structures internes.
Dans les drivers WDM, on rencontre principalement :
- pour les listes doublement chaînées ;
Code: Select all
LIST_ENTRY - pour les listes simplement chaînées ;
Code: Select all
SINGLE_LIST_ENTRY - pour retrouver la structure complète à partir du champ de liaison ;
Code: Select all
CONTAINING_RECORD - les variantes interlocked lorsque plusieurs contextes accèdent à la même liste ;
- les lookaside lists, qui sont des caches d'allocations et non de simples listes chaînées.
1. Principe d'une structure contenant un lien
Exemple :
Code: Select all
typedef struct _MY_ENTRY
{
LIST_ENTRY Link;
LONG Value;
} MY_ENTRY, *PMY_ENTRY;
Code: Select all
MY_ENTRY
Code: Select all
LIST_ENTRY Link;
Code: Select all
entry->Link
2. LIST_ENTRY
La structure :
Code: Select all
LIST_ENTRY
Conceptuellement, elle contient deux pointeurs :
Code: Select all
Flink
Blink
- : élément suivant ;
Code: Select all
Flink - : élément précédent.
Code: Select all
Blink
Code: Select all
A <-> B <-> C
3. Liste doublement chaînée circulaire
Une liste typique ressemble à :
Code: Select all
+-------------------------+
| |
v |
Head <-> A <-> B <-> C <--------+
Elle n'est pas nécessairement un véritable objet métier.
Sa fonction est de représenter le début et la fin de la liste.
4. Initialiser une liste
On utilise :
Code: Select all
InitializeListHead
Code: Select all
LIST_ENTRY Head;
InitializeListHead(&Head);
Code: Select all
Head.Flink == &Head
Head.Blink == &Head
5. Tester si une liste est vide
L'API prévue est :
Code: Select all
IsListEmpty
Code: Select all
if (IsListEmpty(&Head))
{
KdPrint(("Liste vide\n"));
}
Code: Select all
Head.Flink == &Head
6. Insérer en tête
Pour insérer un élément juste après la tête :
Code: Select all
InsertHeadList
Code: Select all
InsertHeadList(
&Head,
&entry->Link
);
Code: Select all
A -> B -> C
Code: Select all
X -> A -> B -> C
Pour insérer à la fin :
Code: Select all
InsertTailList
Code: Select all
InsertTailList(
&Head,
&entry->Link
);
Code: Select all
A -> B -> C
Code: Select all
A -> B -> C -> X
On utilise :
Code: Select all
RemoveHeadList
Code: Select all
PLIST_ENTRY first =
RemoveHeadList(&Head);
Il faut ensuite retrouver la structure contenant ce champ.
9. Retirer le dernier élément
On utilise :
Code: Select all
RemoveTailList
Code: Select all
PLIST_ENTRY last =
RemoveTailList(&Head);
10. Retirer un élément précis
Lorsqu'on connaît l'élément :
Code: Select all
RemoveEntryList(
&entry->Link
);
L'élément n'est pas libéré automatiquement.
Retirer une entrée de la liste et libérer sa mémoire sont deux opérations différentes.
11. CONTAINING_RECORD
Après avoir récupéré un :
Code: Select all
PLIST_ENTRY
Windows fournit :
Code: Select all
CONTAINING_RECORD
Code: Select all
PMY_ENTRY entry =
CONTAINING_RECORD(
current,
MY_ENTRY,
Link
);
Code: Select all
adresse du champ Link
↓
calcul de l'offset
↓
adresse de MY_ENTRY
Dans de nombreuses structures kernel, un objet contient directement un ou plusieurs `LIST_ENTRY`.
On ne stocke donc pas forcément :
Code: Select all
PMY_ENTRY Next;
Code: Select all
LIST_ENTRY Link;
13. Parcourir une liste vers l'avant
Le parcours standard est :
Code: Select all
PLIST_ENTRY current =
Head.Flink;
while (current != &Head)
{
PMY_ENTRY entry =
CONTAINING_RECORD(
current,
MY_ENTRY,
Link
);
KdPrint((
"Value = %ld\n",
entry->Value
));
current = current->Flink;
}
Code: Select all
current != &Head
Code: Select all
current != nullptr
14. Parcourir une liste vers l'arrière
On peut également commencer par :
Code: Select all
Head.Blink
Code: Select all
current = current->Blink;
Code: Select all
PLIST_ENTRY current =
Head.Blink;
while (current != &Head)
{
PMY_ENTRY entry =
CONTAINING_RECORD(
current,
MY_ENTRY,
Link
);
KdPrint((
"Value = %ld\n",
entry->Value
));
current = current->Blink;
}
Structure :
Code: Select all
typedef struct _MY_ENTRY
{
LIST_ENTRY Link;
LONG Value;
} MY_ENTRY, *PMY_ENTRY;
Code: Select all
LIST_ENTRY Head;
InitializeListHead(&Head);
Code: Select all
PMY_ENTRY entry =
(PMY_ENTRY)ExAllocatePool2(
POOL_FLAG_NON_PAGED,
sizeof(MY_ENTRY),
'rtnE'
);
if (entry == nullptr)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
Code: Select all
RtlZeroMemory(
entry,
sizeof(MY_ENTRY)
);
entry->Value = 42;
Code: Select all
InsertTailList(
&Head,
&entry->Link
);
Une structure peut appartenir à plusieurs listes différentes.
Exemple :
Code: Select all
typedef struct _MY_OBJECT
{
LIST_ENTRY GlobalLink;
LIST_ENTRY DeviceLink;
ULONG Id;
} MY_OBJECT, *PMY_OBJECT;
- une liste globale ;
- une liste spécifique à un périphérique.
Exemple :
Code: Select all
PMY_OBJECT obj =
CONTAINING_RECORD(
current,
MY_OBJECT,
DeviceLink
);
Le noyau Windows utilise énormément ce modèle.
Conceptuellement, une structure interne peut contenir :
Code: Select all
LIST_ENTRY SomeLinks;
Un exemple classique étudié dans les structures de processus est :
Code: Select all
ActiveProcessLinks
Cela explique pourquoi la maîtrise de `LIST_ENTRY` et `CONTAINING_RECORD` est importante pour comprendre les structures kernel.
18. SINGLE_LIST_ENTRY
Windows fournit également :
Code: Select all
SINGLE_LIST_ENTRY
Conceptuellement :
Code: Select all
Head -> A -> B -> C -> NULL
19. Structure avec SINGLE_LIST_ENTRY
Exemple :
Code: Select all
typedef struct _MY_SINGLE_ENTRY
{
SINGLE_LIST_ENTRY Link;
LONG Value;
} MY_SINGLE_ENTRY, *PMY_SINGLE_ENTRY;
20. Initialisation d'une singly-linked list
Une liste simple peut être initialisée avec :
Code: Select all
SINGLE_LIST_ENTRY Head = {};
Code: Select all
Head.Next = nullptr;
Code: Select all
NULL
21. PushEntryList
Pour ajouter en tête :
Code: Select all
PushEntryList(
&Head,
&entry->Link
);
Cette opération correspond naturellement à une structure de type pile.
22. PopEntryList
Pour retirer le premier élément :
Code: Select all
PSINGLE_LIST_ENTRY link =
PopEntryList(&Head);
Code: Select all
nullptr
Code: Select all
if (link != nullptr)
{
...
}
Exemple :
Code: Select all
PSINGLE_LIST_ENTRY current =
Head.Next;
while (current != nullptr)
{
PMY_SINGLE_ENTRY entry =
CONTAINING_RECORD(
current,
MY_SINGLE_ENTRY,
Link
);
KdPrint((
"Value = %ld\n",
entry->Value
));
current = current->Next;
}
`LIST_ENTRY` :
- doublement chaînée ;
- navigation avant et arrière ;
- suppression d'un élément plus flexible ;
- deux pointeurs par entrée ;
- généralement circulaire.
- simplement chaînée ;
- navigation uniquement vers l'avant ;
- moins de mémoire utilisée ;
- très adaptée aux piles simples ;
- fin avec `NULL`.
Les opérations classiques sur `LIST_ENTRY` ne rendent pas automatiquement une liste thread-safe.
Exemple problématique :
Code: Select all
Thread A:
InsertTailList(...)
Thread B:
RemoveHeadList(...)
Une liste partagée doit donc être protégée par une primitive appropriée.
26. Corruption de Flink et Blink
Une liste doublement chaînée dépend de la cohérence de :
Code: Select all
Flink
Blink
- boucle infinie ;
- accès mémoire invalide ;
- liste impossible à parcourir ;
- corruption d'autres structures ;
- bug check.
Windows fournit également des opérations interlocked pour certains scénarios.
On rencontre notamment :
Code: Select all
ExInterlockedInsertHeadList
ExInterlockedInsertTailList
ExInterlockedRemoveHeadList
Exemple conceptuel :
Code: Select all
KSPIN_LOCK Lock;
KeInitializeSpinLock(&Lock);
Code: Select all
ExInterlockedInsertTailList(
&Head,
&entry->Link,
&Lock
);
Si une liste est protégée par un spinlock explicite autour d'une série d'opérations :
Code: Select all
KeAcquireSpinLock(...);
// plusieurs opérations sur la liste
KeReleaseSpinLock(...);
Une liste doit avoir une politique de synchronisation claire et cohérente.
29. Listes et IRQL
`LIST_ENTRY` n'impose pas à lui seul un IRQL particulier.
Mais le contexte autour de la liste peut en imposer un.
Par exemple :
- une liste protégée par spinlock peut être manipulée à `DISPATCH_LEVEL` ;
- ses éléments doivent alors résider en mémoire nonpageable ;
- une liste en mémoire pageable ne doit pas être touchée à un IRQL incompatible.
Un pattern fréquent est :
Code: Select all
ExAllocatePool2
↓
initialiser l'objet
↓
InsertTailList
↓
utiliser l'objet
↓
RemoveEntryList
↓
ExFreePool
Elle ne gère pas automatiquement la durée de vie mémoire des objets.
31. Ownership d'un élément
Il faut toujours savoir :
- qui possède l'objet lorsqu'il est dans la liste ;
- qui peut le retirer ;
- qui le libère ;
- si un autre thread peut encore utiliser son adresse.
Code: Select all
RemoveEntryList(&entry->Link);
ExFreePool(entry);
32. Supprimer pendant un parcours
Une erreur fréquente est :
Code: Select all
RemoveEntryList(current);
ExFreePool(entry);
current = current->Flink;
Il ne faut donc plus utiliser :
Code: Select all
current->Flink
Il faut sauvegarder le prochain élément avant la suppression :
Code: Select all
PLIST_ENTRY current =
Head.Flink;
while (current != &Head)
{
PLIST_ENTRY next =
current->Flink;
PMY_ENTRY entry =
CONTAINING_RECORD(
current,
MY_ENTRY,
Link
);
RemoveEntryList(current);
ExFreePool(entry);
current = next;
}
Exemple :
Code: Select all
PMY_ENTRY FindValue(
PLIST_ENTRY Head,
LONG Value
)
{
PLIST_ENTRY current =
Head->Flink;
while (current != Head)
{
PMY_ENTRY entry =
CONTAINING_RECORD(
current,
MY_ENTRY,
Link
);
if (entry->Value == Value)
{
return entry;
}
current = current->Flink;
}
return nullptr;
}
Une `DEVICE_EXTENSION` peut contenir une tête de liste :
Code: Select all
typedef struct _DEVICE_EXTENSION
{
LIST_ENTRY Items;
KSPIN_LOCK Lock;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
Code: Select all
InitializeListHead(
&ext->Items
);
KeInitializeSpinLock(
&ext->Lock
);
36. Nettoyage avant IoDeleteDevice
Avant de supprimer le `DEVICE_OBJECT`, toutes les structures possédées par sa `DEVICE_EXTENSION` doivent être nettoyées.
Exemple conceptuel :
Code: Select all
while (!IsListEmpty(&ext->Items))
{
PLIST_ENTRY link =
RemoveHeadList(
&ext->Items
);
PMY_ENTRY entry =
CONTAINING_RECORD(
link,
MY_ENTRY,
Link
);
ExFreePool(entry);
}
IoDeleteDevice(DeviceObject);
Une erreur conceptuelle fréquente est de confondre :
Code: Select all
LIST_ENTRY
Code: Select all
LOOKASIDE_LIST_EX
organiser des objets.
Une lookaside list sert à :
réutiliser rapidement des blocs mémoire de taille fixe.
Les deux peuvent être utilisées ensemble.
38. Principe d'une lookaside list
Une lookaside list agit comme un petit cache.
Sans lookaside :
Code: Select all
besoin d'un objet
↓
allocation pool
↓
utilisation
↓
free pool
Code: Select all
besoin d'un objet
↓
bloc déjà disponible ?
/ \
oui non
| |
cache pool
\ /
utilisation
↓
retour au cache
39. Anciennes lookaside lists
Les anciens drivers utilisent :
Code: Select all
NPAGED_LOOKASIDE_LIST
PAGED_LOOKASIDE_LIST
Code: Select all
ExInitializeNPagedLookasideList
ExInitializePagedLookasideList
ExAllocateFromNPagedLookasideList
ExAllocateFromPagedLookasideList
ExFreeToNPagedLookasideList
ExFreeToPagedLookasideList
ExDeleteNPagedLookasideList
ExDeletePagedLookasideList
40. LOOKASIDE_LIST_EX
Les API plus modernes utilisent :
Code: Select all
LOOKASIDE_LIST_EX
Code: Select all
ExInitializeLookasideListEx
ExAllocateFromLookasideListEx
ExFreeToLookasideListEx
ExDeleteLookasideListEx
Exemple :
Code: Select all
LOOKASIDE_LIST_EX Lookaside;
NTSTATUS status =
ExInitializeLookasideListEx(
&Lookaside,
nullptr,
nullptr,
POOL_FLAG_NON_PAGED,
0,
sizeof(MY_ENTRY),
'rtnE',
0
);
Code: Select all
if (!NT_SUCCESS(status))
{
return status;
}
Code: Select all
PMY_ENTRY entry =
(PMY_ENTRY)
ExAllocateFromLookasideListEx(
&Lookaside
);
- réutiliser un bloc déjà disponible ;
- effectuer une nouvelle allocation si nécessaire.
Au lieu d'appeler directement `ExFreePool`, on utilise :
Code: Select all
ExFreeToLookasideListEx(
&Lookaside,
entry
);
44. Détruire une lookaside
Quand elle n'est plus nécessaire :
Code: Select all
ExDeleteLookasideListEx(
&Lookaside
);
Une lookaside ne doit pas simplement disparaître parce que la structure qui la contient est libérée.
45. Lookaside dans une DEVICE_EXTENSION
Exemple :
Code: Select all
typedef struct _DEVICE_EXTENSION
{
LOOKASIDE_LIST_EX Lookaside;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
Code: Select all
ExDeleteLookasideListEx(
&ext->Lookaside
);
IoDeleteDevice(
DeviceObject
);
46. Quand utiliser une lookaside
Elle est particulièrement adaptée lorsque :
- les objets ont une taille fixe ;
- les allocations sont nombreuses ;
- les objets sont souvent créés puis détruits ;
- le chemin de code est sensible aux performances.
47. Listes + lookaside
Un pattern fréquent est :
Code: Select all
ExAllocateFromLookasideListEx
↓
initialiser l'objet
↓
InsertTailList
↓
utiliser
↓
RemoveEntryList
↓
ExFreeToLookasideListEx
La lookaside fournit et recycle sa mémoire.
48. Exemple : structure PROCESS_ENTRY
Code: Select all
typedef struct _PROCESS_ENTRY
{
LIST_ENTRY Link;
HANDLE Pid;
CHAR Name[16];
} PROCESS_ENTRY, *PPROCESS_ENTRY;
Code: Select all
LIST_ENTRY ProcessList;
InitializeListHead(
&ProcessList
);
Code: Select all
PPROCESS_ENTRY entry =
(PPROCESS_ENTRY)
ExAllocatePool2(
POOL_FLAG_NON_PAGED,
sizeof(PROCESS_ENTRY),
'corP'
);
if (entry == nullptr)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
RtlZeroMemory(
entry,
sizeof(PROCESS_ENTRY)
);
entry->Pid = Pid;
InsertTailList(
&ProcessList,
&entry->Link
);
Code: Select all
PPROCESS_ENTRY FindProcess(
HANDLE Pid
)
{
PLIST_ENTRY current =
ProcessList.Flink;
while (current != &ProcessList)
{
PPROCESS_ENTRY entry =
CONTAINING_RECORD(
current,
PROCESS_ENTRY,
Link
);
if (entry->Pid == Pid)
{
return entry;
}
current = current->Flink;
}
return nullptr;
}
Code: Select all
PPROCESS_ENTRY entry =
FindProcess(Pid);
if (entry != nullptr)
{
RemoveEntryList(
&entry->Link
);
ExFreePool(entry);
}
52. Parcours inverse
Code: Select all
PLIST_ENTRY current =
ProcessList.Blink;
while (current != &ProcessList)
{
PPROCESS_ENTRY entry =
CONTAINING_RECORD(
current,
PROCESS_ENTRY,
Link
);
KdPrint((
"PID = %p\n",
entry->Pid
));
current = current->Blink;
}
- Oublier `InitializeListHead`.
- Tester `current != nullptr` sur une `LIST_ENTRY` circulaire.
- Utiliser le mauvais champ dans `CONTAINING_RECORD`.
- Faire `ExFreePool` avant `RemoveEntryList`.
- Lire `current->Flink` après avoir libéré l'objet correspondant.
- Modifier la liste depuis plusieurs threads sans synchronisation.
- Utiliser une structure pageable alors que la liste est manipulée à `DISPATCH_LEVEL`.
- Confondre une linked list avec une lookaside list.
- Oublier de détruire une lookaside.
- Appeler `IoDeleteDevice` avant de détruire une lookaside stockée dans la `DEVICE_EXTENSION`.
Pour les listes doublement chaînées :
Code: Select all
LIST_ENTRY
InitializeListHead
IsListEmpty
InsertHeadList
InsertTailList
RemoveHeadList
RemoveTailList
RemoveEntryList
CONTAINING_RECORD
Code: Select all
SINGLE_LIST_ENTRY
PushEntryList
PopEntryList
Code: Select all
ExInterlockedInsertHeadList
ExInterlockedInsertTailList
ExInterlockedRemoveHeadList
Code: Select all
LOOKASIDE_LIST_EX
ExInitializeLookasideListEx
ExAllocateFromLookasideListEx
ExFreeToLookasideListEx
ExDeleteLookasideListEx
Pour une `LIST_ENTRY` :
Code: Select all
structure métier
|
+--> LIST_ENTRY Link
|
v
liste circulaire
Code: Select all
PLIST_ENTRY
↓
CONTAINING_RECORD
↓
structure réelle
Code: Select all
taille fixe
↓
cache d'allocations
↓
allocation rapide
↓
réutilisation
Les listes chaînées kernel reposent surtout sur l'intégration d'un champ de liaison directement dans les structures.
Avec :
Code: Select all
LIST_ENTRY
Le parcours se termine lorsque :
Code: Select all
current == &Head
`CONTAINING_RECORD` permet de revenir du champ de liaison vers la structure complète.
Avec :
Code: Select all
SINGLE_LIST_ENTRY
Les listes ne gèrent pas automatiquement :
- la mémoire ;
- l'ownership ;
- la synchronisation ;
- la durée de vie des objets.
Les API modernes principales sont :
Code: Select all
ExInitializeLookasideListEx
ExAllocateFromLookasideListEx
ExFreeToLookasideListEx
ExDeleteLookasideListEx
Code: Select all
LIST_ENTRY
+
CONTAINING_RECORD
+
pool/lookaside
+
synchronisation
