La gestion mémoire dans un driver Windows est plus contraignante qu'en user mode. Un driver travaille directement dans l'espace noyau, peut s'exécuter à différents niveaux d'IRQL et doit respecter des règles strictes concernant les pages mémoire, les allocations, les buffers utilisateur et la durée de vie des objets.
Une erreur mémoire dans un processus user mode peut généralement faire planter uniquement le processus. Une erreur mémoire dans un driver peut provoquer une corruption globale du système ou un bug check.
Ce cours regroupe les concepts fondamentaux du WDM classique et les API modernes du noyau Windows.
1. Espace d'adressage user mode et kernel mode
Windows utilise la mémoire virtuelle. Un processus manipule des adresses virtuelles qui sont traduites par le système vers des pages physiques.
L'espace d'adressage virtuel contient notamment :
- une partie accessible au user mode ;
- une partie réservée au kernel mode.
Une adresse user mode n'a donc pas les mêmes propriétés qu'une adresse kernel.
Un driver ne doit jamais supposer qu'un pointeur user mode est valide simplement parce qu'il contient une adresse non nulle.
2. Mémoire virtuelle et mémoire physique
Une adresse virtuelle n'est pas directement une adresse physique.
Le processeur et le Memory Manager utilisent des tables de pages pour traduire :
Code: Select all
Adresse virtuelle
↓
Table(s) de pages
↓
Page physique
Dans un driver, il faut donc distinguer clairement :
- adresse virtuelle ;
- adresse physique ;
- mémoire pageable ;
- mémoire nonpageable.
Windows organise la mémoire en pages.
Le WDK expose notamment :
Code: Select all
PAGE_SIZE
PAGE_SHIFT
Quelques macros utiles :
Code: Select all
ROUND_TO_PAGES(x)
BYTES_TO_PAGES(x)
BYTE_OFFSET(x)
- arrondir une taille à une frontière de page ;
- calculer combien de pages couvrent une taille donnée ;
- obtenir le déplacement d'une adresse dans sa page.
Une différence fondamentale en kernel est celle entre mémoire pageable et mémoire nonpageable.
La mémoire pageable peut être retirée de la RAM physique lorsque Windows manque de mémoire et être restaurée plus tard.
La mémoire nonpageable doit rester accessible physiquement tant qu'elle est allouée.
On retrouve historiquement :
Code: Select all
PagedPool
NonPagedPool
La règle générale est :
- mémoire pageable : utilisable lorsque le code peut tolérer une faute de page ;
- mémoire nonpageable : obligatoire lorsque le code peut s'exécuter à un IRQL où les fautes de page sont interdites.
À un IRQL élevé, Windows ne peut pas toujours traiter une faute de page.
Par exemple, du code exécuté à :
Code: Select all
DISPATCH_LEVEL
Sinon, si la page n'est pas actuellement résidente, le système ne pourra pas nécessairement la restaurer et un bug check peut se produire.
On retient donc :
- PASSIVE_LEVEL : accès possible à de la mémoire pageable ;
- APC_LEVEL : restrictions plus fortes ;
- DISPATCH_LEVEL : utiliser uniquement des données et du code résidents/nonpageables pour les accès concernés.
6. Placement du code dans des sections
Un driver peut placer certaines fonctions dans des sections spécifiques.
Exemple classique :
Code: Select all
#pragma alloc_text(INIT, DriverEntry)
#pragma alloc_text(PAGE, MyPagedFunction)
Code: Select all
INIT
Une fois l'initialisation terminée, cette mémoire peut être libérée par le système.
La section :
Code: Select all
PAGE
Une routine pageable doit être appelée uniquement dans un contexte compatible avec la pagination.
7. PAGED_CODE()
La macro :
Code: Select all
PAGED_CODE();
Exemple :
Code: Select all
#pragma alloc_text(PAGE, MyFunction)
void MyFunction()
{
PAGED_CODE();
// traitement
}
Elle ne transforme pas magiquement une fonction en fonction pageable : le placement de section reste distinct.
8. Sections INIT
Les fonctions uniquement nécessaires au chargement du driver peuvent être placées dans une section d'initialisation.
Exemple :
Code: Select all
#pragma alloc_text(INIT, DriverEntry)
Il faut évidemment éviter d'y placer une fonction encore utilisée après le chargement.
9. Verrouillage dynamique de code pageable
Windows fournit des mécanismes permettant de verrouiller temporairement une section pageable en mémoire.
On rencontre notamment :
Code: Select all
MmLockPagableCodeSection()
MmUnlockPagableImageSection()
Code: Select all
PVOID handle =
MmLockPagableCodeSection(MyPagedFunction);
// la section reste résidente
MmUnlockPagableImageSection(handle);
Il ne faut pas l'utiliser sans raison, car cela réduit la quantité de mémoire que le système peut paginer.
10. Anciennes API d'allocation de pool
Les anciens drivers utilisent souvent :
Code: Select all
ExAllocatePool()
ExAllocatePoolWithTag()
ExAllocatePoolWithTagPriority()
Code: Select all
PVOID p = ExAllocatePoolWithTag(
NonPagedPool,
1024,
'gaTM'
);
Cependant, les API modernes sont préférables pour du nouveau code.
11. ExAllocatePool2
Pour du code moderne, l'une des API principales est :
Code: Select all
ExAllocatePool2
Code: Select all
PVOID buffer = ExAllocatePool2(
POOL_FLAG_NON_PAGED,
4096,
'gaTM'
);
Code: Select all
if (buffer == nullptr)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
Code: Select all
PVOID buffer = ExAllocatePool2(
POOL_FLAG_PAGED,
4096,
'gaTM'
);
La fonction :
Code: Select all
ExAllocatePool3
Elle est utile lorsqu'un simple choix paged/nonpaged + taille + tag ne suffit pas.
Elle peut notamment accepter des paramètres supplémentaires liés à certaines propriétés d'allocation.
Dans un driver classique, `ExAllocatePool2` suffit souvent.
13. POOL_FLAGS
Les API modernes utilisent des flags.
Exemples importants :
Code: Select all
POOL_FLAG_PAGED
POOL_FLAG_NON_PAGED
POOL_FLAG_UNINITIALIZED
POOL_FLAG_CACHE_ALIGNED
- l'IRQL d'utilisation ;
- la durée de vie de la mémoire ;
- les contraintes d'alignement ;
- les besoins de sécurité ;
- la nécessité éventuelle d'exécution de code.
Selon l'API et les flags utilisés, une allocation peut être initialisée à zéro ou laissée non initialisée.
Une mémoire non initialisée peut contenir des données précédemment présentes dans cette zone.
Lorsqu'une allocation est destinée à contenir des données qui seront exposées au user mode, il faut éviter de divulguer du contenu ancien.
Exemple :
Code: Select all
RtlZeroMemory(buffer, size);
15. Pool tags
Les pool tags servent à identifier l'origine des allocations kernel.
Exemple :
Code: Select all
'gaTM'
Exemple :
Code: Select all
#define DRIVER_TAG 'gaTM'
Code: Select all
PVOID p = ExAllocatePool2(
POOL_FLAG_NON_PAGED,
512,
DRIVER_TAG
);
- identifier les allocations d'un driver ;
- rechercher des fuites ;
- analyser le pool ;
- utiliser Driver Verifier ;
- utiliser certains outils de debugging mémoire.
Sur une machine little-endian, l'ordre visuel d'un tag peut sembler inversé dans certains outils.
Par exemple :
Code: Select all
'gaTM'
Cela ne signifie pas que le tag est incorrect.
17. Choisir de bons tags
Un bon tag doit être :
- reconnaissable ;
- stable ;
- distinct pour différents types d'allocations importantes ;
- éviter les tags trop génériques.
Code: Select all
#define TAG_DEVICE 'veDM'
#define TAG_BUFFER 'ffuB'
#define TAG_ENTRY 'rtnE'
18. Libération avec ExFreePool
Une allocation obtenue depuis le pool doit être libérée lorsqu'elle n'est plus utilisée.
Exemple :
Code: Select all
PVOID p = ExAllocatePool2(
POOL_FLAG_NON_PAGED,
1024,
DRIVER_TAG
);
if (p == nullptr)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
// utilisation
ExFreePool(p);
Code: Select all
ExFreePoolWithTag()
Une règle fondamentale est de savoir qui possède la mémoire.
Pour chaque allocation, il faut pouvoir répondre à :
- qui l'alloue ?
- qui la libère ?
- quand doit-elle être libérée ?
- peut-elle être partagée entre plusieurs threads ?
- peut-elle survivre jusqu'à DriverUnload ?
- fuites ;
- double free ;
- use-after-free ;
- corruptions.
Exemple incorrect :
Code: Select all
ExFreePool(buffer);
ExFreePool(buffer);
Cela peut entraîner une corruption du pool et un bug check.
Une bonne habitude après une libération, lorsque le pointeur reste accessible, est :
Code: Select all
ExFreePool(buffer);
buffer = nullptr;
21. Use-after-free
Exemple :
Code: Select all
ExFreePool(data);
data->Value = 10;
Le pointeur ne doit plus être déréférencé.
Les use-after-free kernel sont particulièrement dangereux.
22. Fuites mémoire
Une fuite se produit lorsqu'une allocation n'est jamais libérée.
Exemple :
Code: Select all
PVOID p = ExAllocatePool2(
POOL_FLAG_NON_PAGED,
4096,
DRIVER_TAG
);
// aucun ExFreePool()
Le système peut finir par manquer de ressources kernel.
23. Gestion d'un échec d'allocation
Une allocation peut échouer.
Il faut donc tester :
Code: Select all
if (buffer == nullptr)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
Code: Select all
PVOID buffer = ExAllocatePool2(...);
RtlZeroMemory(buffer, size);
Sinon une allocation échouée conduit immédiatement à un accès invalide.
24. Anciennes allocations qui lèvent une exception
Certaines anciennes variantes ou certains flags historiques pouvaient demander un comportement dans lequel un échec d'allocation lève une exception au lieu de retourner `NULL`.
Cette approche existe surtout dans du code ancien.
Pour du code moderne, un comportement explicite avec contrôle du résultat est généralement plus facile à maintenir.
25. Allocation et IRQL
Le choix du type de pool dépend du contexte.
À haut IRQL, une allocation pageable est interdite.
De manière générale :
Code: Select all
PASSIVE_LEVEL
-> paged ou nonpaged selon besoin
DISPATCH_LEVEL
-> nonpaged uniquement pour les données accessibles
26. Pool NX et mémoire exécutable
Une allocation de données n'a généralement aucune raison d'être exécutable.
Les versions modernes de Windows favorisent des pools non exécutables.
Le principe de sécurité est simple :
si une zone contient des données, elle ne doit pas être exécutable sans nécessité précise.
Cela réduit les possibilités d'exploitation en cas de corruption mémoire.
27. Alignement mémoire
Certaines structures ou certains périphériques exigent un alignement particulier.
On rencontre notamment des besoins :
- d'alignement naturel ;
- d'alignement cache ;
- d'alignement matériel ;
- d'alignement DMA.
Il ne faut pas ajouter arbitrairement des casts pour contourner un problème d'alignement.
28. Secure Pools
Les versions modernes de Windows introduisent également des mécanismes de secure pools dans certains contextes.
Ils permettent de renforcer la gestion d'allocations sensibles et peuvent être associés à des environnements protégés comme VBS.
Le principe général est :
- création d'un pool sécurisé ;
- allocation depuis ce pool ;
- validation ou mise à jour contrôlée ;
- destruction lorsque le pool n'est plus utilisé.
29. Pool priorities
Des API historiques comme :
Code: Select all
ExAllocatePoolWithTagPriority
On rencontre par exemple :
Code: Select all
LowPoolPriority
NormalPoolPriority
HighPoolPriority
Dans un driver moderne, les nouvelles API de pool et une bonne gestion des erreurs sont généralement prioritaires.
30. Surcharge de operator new en kernel
Un driver C++ peut définir son propre `operator new` pour rediriger les allocations vers le pool kernel.
Exemple conceptuel :
Code: Select all
void* operator new(
size_t size,
ULONG tag
)
{
return ExAllocatePool2(
POOL_FLAG_NON_PAGED,
size,
tag
);
}
Code: Select all
MyObject* obj =
new(DRIVER_TAG) MyObject();
31. Placement new
Le placement new construit un objet à une adresse déjà allouée.
Exemple :
Code: Select all
PVOID buffer = ExAllocatePool2(
POOL_FLAG_NON_PAGED,
sizeof(MyObject),
DRIVER_TAG
);
if (buffer == nullptr)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
MyObject* obj =
new(buffer) MyObject();
Il construit simplement l'objet dans le bloc existant.
32. Lookaside lists
Lorsqu'un driver alloue et libère très fréquemment des objets de taille fixe, le coût des allocations de pool peut devenir important.
Les lookaside lists servent de cache d'objets.
Principe :
Code: Select all
allocation demandée
|
v
objet disponible dans la lookaside ?
|
oui|non
|
+----> retour direct
ou allocation pool
33. Anciennes lookaside lists
On rencontre dans du code ancien :
Code: Select all
NPAGED_LOOKASIDE_LIST
PAGED_LOOKASIDE_LIST
Ces API restent importantes à connaître pour lire d'anciens drivers.
34. LOOKASIDE_LIST_EX
Une API plus moderne utilise :
Code: Select all
LOOKASIDE_LIST_EX
Code: Select all
ExInitializeLookasideListEx()
ExAllocateFromLookasideListEx()
ExFreeToLookasideListEx()
ExDeleteLookasideListEx()
35. Exemple conceptuel de lookaside moderne
Code: Select all
LOOKASIDE_LIST_EX Lookaside;
NTSTATUS status =
ExInitializeLookasideListEx(
&Lookaside,
nullptr,
nullptr,
POOL_FLAG_NON_PAGED,
0,
sizeof(MY_ENTRY),
DRIVER_TAG,
0
);
Code: Select all
PMY_ENTRY entry =
(PMY_ENTRY)ExAllocateFromLookasideListEx(
&Lookaside
);
Code: Select all
ExFreeToLookasideListEx(
&Lookaside,
entry
);
Code: Select all
ExDeleteLookasideListEx(
&Lookaside
);
Une lookaside list est adaptée lorsque :
- les objets ont une taille fixe ;
- les allocations sont fréquentes ;
- les objets sont souvent libérés puis recréés ;
- la réduction du coût d'allocation est utile.
- des allocations rares ;
- des tailles variables ;
- des objets très gros ;
- des objets dont la durée de vie est très longue.
Le kernel Windows utilise beaucoup les listes chaînées.
La structure classique est :
Code: Select all
LIST_ENTRY
Code: Select all
typedef struct _MY_ENTRY
{
LIST_ENTRY Link;
LONG Value;
} MY_ENTRY, *PMY_ENTRY;
38. Liste doublement chaînée circulaire
Une liste `LIST_ENTRY` est généralement circulaire.
Une tête de liste vide référence elle-même :
Code: Select all
Head.Flink == &Head
Head.Blink == &Head
Code: Select all
LIST_ENTRY Head;
InitializeListHead(&Head);
Code: Select all
InsertTailList(
&Head,
&entry->Link
);
Lorsqu'on possède un pointeur vers le champ `LIST_ENTRY`, on peut récupérer la structure contenant ce champ avec :
Code: Select all
CONTAINING_RECORD
Code: Select all
PMY_ENTRY entry =
CONTAINING_RECORD(
current,
MY_ENTRY,
Link
);
40. SINGLE_LIST_ENTRY
Pour certaines structures plus simples, Windows fournit également des listes simplement chaînées.
On rencontre :
Code: Select all
SINGLE_LIST_ENTRY
Une liste doublement chaînée permet une navigation et des suppressions plus flexibles, au prix de deux pointeurs par élément.
41. Durée de vie d'un objet kernel
Une structure allouée dans un driver doit avoir une durée de vie clairement définie.
Exemple :
Code: Select all
typedef struct _DEVICE_EXTENSION
{
PVOID Buffer;
ULONG BufferSize;
} DEVICE_EXTENSION;
Code: Select all
extension->Buffer =
ExAllocatePool2(
POOL_FLAG_NON_PAGED,
4096,
DRIVER_TAG
);
42. Exemple de nettoyage dans DriverUnload
Code: Select all
void DriverUnload(
PDRIVER_OBJECT DriverObject
)
{
PDEVICE_OBJECT device =
DriverObject->DeviceObject;
while (device != nullptr)
{
PDEVICE_EXTENSION ext =
(PDEVICE_EXTENSION)
device->DeviceExtension;
if (ext->Buffer != nullptr)
{
ExFreePool(ext->Buffer);
ext->Buffer = nullptr;
}
PDEVICE_OBJECT next =
device->NextDevice;
IoDeleteDevice(device);
device = next;
}
}
43. Mémoire et concurrence
Une allocation peut être valide mais quand même utilisée de manière incorrecte si plusieurs threads y accèdent simultanément.
Exemple :
Code: Select all
struct DATA
{
LONG Counter;
};
Les solutions possibles comprennent selon le contexte :
- interlocked operations ;
- spinlocks ;
- mutex ;
- ERESOURCE ;
- autres primitives de synchronisation.
Un pointeur fourni par une application doit être considéré comme non fiable.
Il peut :
- être invalide ;
- changer entre deux accès ;
- pointer vers une page retirée ;
- être mal aligné ;
- pointer vers une région dont les permissions changent.
45. ProbeForRead et ProbeForWrite
Pour certains scénarios utilisant des pointeurs user mode directs, on rencontre :
Code: Select all
ProbeForRead()
ProbeForWrite()
Elles doivent être utilisées dans un bloc SEH lorsqu'elles sont nécessaires :
Code: Select all
__try
{
ProbeForRead(
UserBuffer,
Length,
1
);
// accès
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return GetExceptionCode();
}
46. TOCTOU
TOCTOU signifie :
Code: Select all
Time Of Check To Time Of Use
Code: Select all
vérifier le buffer
↓
le contexte change
↓
utiliser le buffer
Il faut donc concevoir le code pour réduire cette fenêtre et utiliser les mécanismes d'I/O adaptés.
47. Buffered I/O
Avec le buffered I/O, l'I/O Manager fournit typiquement un buffer système.
Le driver travaille avec :
Code: Select all
Irp->AssociatedIrp.SystemBuffer
L'I/O Manager effectue les copies nécessaires entre l'application et le buffer kernel selon le type de requête.
48. Direct I/O
Avec le direct I/O, Windows utilise notamment une MDL pour décrire les pages du buffer.
On retrouve :
Code: Select all
Irp->MdlAddress
Le direct I/O évite certaines copies pour de gros buffers, mais demande une bonne compréhension des MDL.
49. METHOD_NEITHER
Avec `METHOD_NEITHER`, l'I/O Manager fournit moins de protection automatique.
Le driver peut recevoir directement des adresses user mode.
C'est donc un mode beaucoup plus délicat.
Il faut tenir compte de :
- validation ;
- SEH ;
- contexte processus ;
- durée de vie du buffer ;
- synchronisation ;
- TOCTOU.
Une MDL décrit un ensemble de pages mémoire.
Elle permet notamment de représenter les pages physiques correspondant à un buffer virtuel.
Les MDL sont essentielles pour :
- Direct I/O ;
- verrouillage de pages ;
- mapping kernel ;
- certaines opérations DMA ;
- accès sûr à des buffers.
Lorsqu'un buffer doit rester accessible pendant une opération asynchrone, les pages correspondantes peuvent devoir être verrouillées.
Le verrouillage empêche leur disparition physique pendant l'opération.
Une MDL peut être utilisée pour décrire ces pages.
Il faut toujours libérer ou déverrouiller les ressources correspondantes lorsqu'elles ne sont plus nécessaires.
52. Mapping d'une MDL
Une MDL peut être mappée dans l'espace d'adressage système.
Dans les scénarios adaptés, on peut ensuite travailler avec une adresse virtuelle kernel représentant les pages décrites.
Il faut respecter les API documentées et les protections attendues.
53. Mémoire physiquement contiguë
Certaines opérations matérielles peuvent nécessiter une mémoire physiquement contiguë.
Ce besoin est beaucoup plus rare que l'utilisation d'une mémoire virtuelle classique.
On l'associe notamment à :
- certains périphériques ;
- DMA ;
- contraintes matérielles spécifiques.
54. Adresse virtuelle != adresse physique
Exemple :
Code: Select all
PVOID buffer = ...
Il ne faut pas la transmettre à un périphérique qui attend une adresse physique sans utiliser les mécanismes appropriés.
Les opérations DMA modernes passent généralement par l'abstraction DMA du système.
55. Pool tracking
Windows fournit plusieurs outils pour analyser les allocations kernel.
Les pool tags permettent de regrouper les allocations par origine.
Pendant le debugging, on peut rechercher :
- nombre d'allocations ;
- nombre de libérations ;
- octets actuellement utilisés ;
- croissance anormale d'un tag.
56. Driver Verifier
Driver Verifier peut surveiller de nombreux comportements dangereux d'un driver.
Pour la mémoire, il peut notamment aider à détecter :
- mauvaises allocations ;
- mauvaises libérations ;
- utilisation incorrecte du pool ;
- certaines violations d'IRQL ;
- corruptions mémoire ;
- fuites selon le contexte.
57. Special Pool
Le Special Pool est particulièrement utile pour détecter des dépassements de buffer.
Principe simplifié :
Code: Select all
page accessible
buffer
frontière de page
page protégée
Cela aide à trouver :
- buffer overflow ;
- buffer underrun ;
- écritures hors limites ;
- certaines corruptions du pool.
Des outils de diagnostic Windows peuvent aider à activer ou analyser certains comportements de pool.
Le but est de transformer des corruptions silencieuses en erreurs détectées au plus près de leur origine.
Plus une corruption est détectée tard, plus il est difficile de retrouver l'instruction responsable.
59. Erreurs classiques de mémoire kernel
Les erreurs suivantes doivent être considérées comme critiques :
- accéder à de la mémoire pageable à un IRQL incompatible ;
- oublier de tester une allocation ;
- double free ;
- use-after-free ;
- fuite mémoire ;
- mauvaise taille d'allocation ;
- mauvais calcul d'offset ;
- dépasser un buffer ;
- utiliser un pointeur user non validé ;
- garder trop longtemps une adresse user ;
- libérer une allocation encore utilisée par un autre thread ;
- détruire une lookaside alors qu'elle est encore utilisée ;
- mauvaise durée de vie d'une MDL ;
- exécuter du code placé dans une section pageable à un IRQL trop élevé.
Code: Select all
#define DRIVER_TAG 'gaTM'
typedef struct _DEVICE_EXTENSION
{
PVOID Buffer;
SIZE_T BufferSize;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
Code: Select all
PDEVICE_EXTENSION ext =
(PDEVICE_EXTENSION)
DeviceObject->DeviceExtension;
ext->BufferSize = 4096;
ext->Buffer = ExAllocatePool2(
POOL_FLAG_NON_PAGED,
ext->BufferSize,
DRIVER_TAG
);
if (ext->Buffer == nullptr)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
RtlZeroMemory(
ext->Buffer,
ext->BufferSize
);
Code: Select all
if (ext->Buffer != nullptr)
{
ExFreePool(ext->Buffer);
ext->Buffer = nullptr;
ext->BufferSize = 0;
}
Code: Select all
typedef struct _MY_ENTRY
{
LIST_ENTRY Link;
ULONG Id;
LONG Value;
} MY_ENTRY, *PMY_ENTRY;
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->Id = 1;
entry->Value = 42;
Code: Select all
InsertTailList(
&Head,
&entry->Link
);
Code: Select all
RemoveEntryList(
&entry->Link
);
ExFreePool(entry);
Dans une vraie liste partagée, une primitive de synchronisation est généralement nécessaire.
63. Exemple avec une lookaside list
Pour des objets de taille fixe souvent créés :
Code: Select all
typedef struct _MY_PACKET
{
LIST_ENTRY Link;
ULONG Type;
UCHAR Data[128];
} MY_PACKET, *PMY_PACKET;
Cela évite de demander une allocation générale au pool à chaque création.
64. Bonnes pratiques modernes
Pour du nouveau code :
- préférer `ExAllocatePool2` ou `ExAllocatePool3` ;
- utiliser un pool non exécutable pour les données ;
- choisir des tags explicites ;
- vérifier systématiquement les retours d'allocation ;
- définir clairement l'ownership ;
- tenir compte de l'IRQL futur d'utilisation ;
- utiliser les lookaside lists seulement quand elles ont un intérêt réel ;
- utiliser Driver Verifier pendant les tests ;
- utiliser Special Pool pour traquer les corruptions ;
- éviter les API obsolètes dans du nouveau code tout en sachant les reconnaître.
Un ancien driver peut contenir :
Code: Select all
ExAllocatePoolWithTag(
NonPagedPool,
size,
tag
);
Code: Select all
ExAllocatePool2(
POOL_FLAG_NON_PAGED,
size,
tag
);
- ancienne forme : lecture de code historique ;
- nouvelle forme : développement actuel.
La gestion mémoire kernel repose sur plusieurs notions fondamentales :
- mémoire virtuelle ;
- pages ;
- paged / nonpaged ;
- IRQL ;
- pool kernel ;
- pool tags ;
- durée de vie des allocations ;
- lookaside lists ;
- listes chaînées ;
- buffers user ;
- MDL ;
- Driver Verifier ;
- Special Pool.
Il faut aussi connaître :
- son propriétaire ;
- sa durée de vie ;
- son contexte d'adressage ;
- son type de mémoire ;
- l'IRQL auquel il sera utilisé ;
- les threads susceptibles d'y accéder.
Code: Select all
ExAllocatePool2()
ExAllocatePool3()
Code: Select all
ExAllocatePoolWithTag()
Enfin, le debugging mémoire doit faire partie du développement normal d'un driver :
Code: Select all
Pool Tags
Driver Verifier
Special Pool
WinDbg
