on continue avec un très gros morceau du développement Win32 : la mémoire.
C’est un sujet central, parce qu’en pratique presque tous les bugs sérieux finissent par ramener à elle. Quand un programme crashe avec un 0xC0000005, quand il devient instable au bout d’un certain temps, quand il fuit silencieusement, ou quand un bug parait “aléatoire”, la cause profonde est souvent une mauvaise compréhension du modèle mémoire.
Pourquoi la mémoire est un sujet fondamental
Comprendre la mémoire sous Windows, ce n’est pas seulement savoir appeler une fonction d’allocation. C’est comprendre comment un processus voit son espace d’adressage, comment les pages sont réservées et validées, comment les protections sont appliquées, comment les heaps sont organisés, et comment des fichiers peuvent etre projetés directement en mémoire.
Autrement dit, maitriser la mémoire permet de mieux comprendre :
- les violations d’accès
- les corruptions mémoire
- les leaks
- les comportements instables dans le temps
- les effets de bord en multithread
- les différences entre allocation haut niveau et allocation bas niveau
Mémoire virtuelle et mémoire physique
Sous Windows, un processus ne manipule jamais directement la RAM physique. Ce qu’il voit, c’est un espace d’adressage virtuel. Cet espace est propre au processus. Cela signifie qu’une meme adresse virtuelle peut exister dans deux processus différents sans désigner la meme zone physique.
Le processeur, via la MMU, et le systeme d’exploitation traduisent ensuite les adresses virtuelles vers :
- des pages physiques réelles
- ou éventuellement des pages backing store, par exemple dans le pagefile
Cette séparation est fondamentale, car elle permet :
- l’isolation entre processus
- la protection mémoire
- le chargement paresseux de pages
- le mapping de fichiers et d’images
Dans un processus classique, on retrouve plusieurs grandes zones logiques. Le découpage exact dépend du binaire, du chargeur et du runtime, mais le schéma général reste le meme.
On peut distinguer notamment :
- la section code, souvent associée au .text
- les données globales initialisées, souvent .data
- les données globales non initialisées, souvent .bss
- le ou les heaps du processus
- la pile de chaque thread
- les DLL chargées
- les zones mappées en mémoire
- les régions réservées ou validées via VirtualAlloc
Premièrement, chaque thread possède sa propre pile. Cela veut dire que les variables locales non statiques vivent normalement sur la stack du thread courant.
Deuxièmement, le heap, lui, est partagé au niveau du processus. Plusieurs threads peuvent donc allouer, réallouer ou libérer de la mémoire issue du meme heap, ce qui explique pourquoi des corruptions apparaissent parfois lorsqu’une structure est mal protégée.
Stack et heap : deux logiques très différentes
La stack est liée à l’exécution. Elle sert aux appels de fonctions, au stockage des variables locales, aux adresses de retour, et plus généralement à l’état transitoire de l’exécution d’un thread.
Ses propriétés générales sont les suivantes :
- elle est propre à chaque thread
- elle est rapide d’accès
- sa taille est limitée
- sa gestion est en grande partie automatique
Ses caractéristiques sont plutot :
- il est partagé au niveau du processus
- il est plus flexible
- il est généralement plus couteux que la stack
- il demande une gestion explicite
- double free
- use-after-free
- overflow de tampon
- sous-écriture ou sur-écriture hors limites
- corruption silencieuse de structures internes
- race condition sur des blocs partagés
Windows fournit un ou plusieurs heaps gérés par le systeme. Le plus courant est le heap principal du processus, récupérable via GetProcessHeap.
Les API de base à connaitre sont :
- GetProcessHeap
- HeapAlloc
- HeapReAlloc
- HeapFree
Code: Select all
#include <Windows.h>
#include <iostream>
int main()
{
HANDLE hHeap = GetProcessHeap();
if (!hHeap)
return 1;
int* p = (int*)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, sizeof(int));
if (!p)
return 1;
*p = 123;
std::cout << *p << std::endl;
if (!HeapFree(hHeap, 0, p))
return 1;
return 0;
}
HeapAlloc renvoie un bloc brut. Si tu veux qu’il soit initialisé à zéro, tu peux utiliser HEAP_ZERO_MEMORY.
HeapReAlloc permet de redimensionner un bloc existant :
Code: Select all
p = (int*)HeapReAlloc(hHeap, 0, p, 10 * sizeof(int));
Créer un heap dédié
Le heap du processus n’est pas la seule option. Windows permet aussi de créer son propre heap avec HeapCreate. Cela peut etre utile quand on veut isoler certaines allocations, compartimenter des sous-systèmes, ou détruire d’un coup tout un ensemble de blocs.
Les fonctions à connaitre ici sont :
- HeapCreate
- HeapDestroy
- HeapAlloc
- HeapFree
Code: Select all
HANDLE hHeap = HeapCreate(0, 0, 0);
if (!hHeap)
return 1;
void* p = HeapAlloc(hHeap, 0, 256);
if (!p)
{
HeapDestroy(hHeap);
return 1;
}
// ...
HeapFree(hHeap, 0, p);
HeapDestroy(hHeap);
Il existe aussi plusieurs fonctions utilitaires autour des heaps :
- GetProcessHeaps, pour lister les heaps du processus
- HeapSize, pour interroger la taille d’un bloc
- HeapValidate, utile en debug pour vérifier une cohérence
- HeapCompact, pour tenter une compaction
- HeapWalk, pour parcourir les entrées du heap
- HeapSetInformation, pour certaines options de comportement ou de sécurité
Quand on s’intéresse à l’inspection d’un heap, il faut au moins connaitre la structure PROCESS_HEAP_ENTRY, utilisée notamment avec HeapWalk.
Elle contient des informations sur une entrée du heap, par exemple :
- le pointeur vers le bloc
- la taille du bloc
- des flags décrivant le type d’entrée
VirtualAlloc : allocation bas niveau par pages
Quand on descend un niveau plus bas, on quitte la logique des blocs pour entrer dans celle des pages mémoire. C’est la que VirtualAlloc devient centrale.
Les API indispensables sont :
- VirtualAlloc
- VirtualFree
- VirtualProtect
- VirtualQuery
Code: Select all
LPVOID mem = VirtualAlloc(
NULL,
4096,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE
);
if (!mem)
return 1;
// utilisation...
if (!VirtualFree(mem, 0, MEM_RELEASE))
return 1;
Il faut bien distinguer deux notions :
- MEM_RESERVE : réserve une plage d’adresses virtuelles
- MEM_COMMIT : valide réellement des pages dans cette plage
VirtualFree, elle aussi, demande de comprendre ce qu’on veut faire :
- MEM_DECOMMIT : retire le commit des pages, mais garde la réservation
- MEM_RELEASE : libère la région réservée
Lorsque l’on inspecte une adresse avec VirtualQuery, la structure principale à connaitre est MEMORY_BASIC_INFORMATION.
Exemple :
Code: Select all
MEMORY_BASIC_INFORMATION mbi;
SIZE_T ret = VirtualQuery(mem, &mbi, sizeof(mbi));
if (ret == 0)
return 1;
- BaseAddress : début de la région décrite
- AllocationBase : base de l’allocation originale
- AllocationProtect : protection lors de l’allocation
- RegionSize : taille de la région
- State : état de la région
- Protect : protection actuelle
- Type : type de mémoire
- MEM_COMMIT
- MEM_RESERVE
- MEM_FREE
- MEM_PRIVATE
- MEM_MAPPED
- MEM_IMAGE
Protections mémoire et violations d’accès
Chaque page possède des droits. Ces droits déterminent si elle peut etre lue, écrite ou exécutée. Toute tentative incompatible avec ces protections peut provoquer une violation d’accès.
Parmi les protections fréquentes, on retrouve :
- PAGE_NOACCESS
- PAGE_READONLY
- PAGE_READWRITE
- PAGE_EXECUTE
- PAGE_EXECUTE_READ
- PAGE_EXECUTE_READWRITE
- PAGE_GUARD
Le crash classique 0xC0000005 apparait typiquement quand :
- on lit une adresse invalide
- on écrit dans une zone non accessible en écriture
- on exécute une page non exécutable
- on déréférence un pointeur NULL
- on utilise un pointeur déjà libéré
- on travaille avec une adresse corrompue
Changer la protection d’une région se fait avec VirtualProtect :
Code: Select all
DWORD oldProt = 0;
if (!VirtualProtect(mem, 4096, PAGE_READONLY, &oldProt))
return 1;
API utiles autour de la mémoire virtuelle
Autour de VirtualAlloc, plusieurs API valent la peine d’etre connues.
On peut citer notamment :
- VirtualAllocEx, pour allouer dans un autre processus
- VirtualFreeEx, pendant logique dans un autre processus
- VirtualProtectEx, pour changer les protections dans un autre processus
- VirtualQueryEx, pour inspecter l’espace mémoire d’un autre processus
- VirtualLock, pour verrouiller des pages en mémoire
- VirtualUnlock, pour les déverrouiller
- ReadProcessMemory, pour lire dans un autre processus
- WriteProcessMemory, pour écrire dans un autre processus
Mémoire mappée : fichiers, sections et vues
Un autre pilier du modèle mémoire Windows est la mémoire mappée. Ici, on ne parle plus simplement d’allouer des pages privées, mais de projeter un fichier ou une section dans l’espace d’adressage d’un processus.
C’est une mécanique extrêmement importante, notamment pour :
- le chargement des DLL et exécutables
- la lecture efficace de gros fichiers
- le partage mémoire entre processus
- certaines techniques IPC
- CreateFileMapping
- OpenFileMapping
- MapViewOfFile
- MapViewOfFileEx
- UnmapViewOfFile
- FlushViewOfFile
- CloseHandle
Code: Select all
HANDLE hFile = CreateFileW(
L"test.bin",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hFile == INVALID_HANDLE_VALUE)
return 1;
HANDLE hMap = CreateFileMappingW(
hFile,
NULL,
PAGE_READWRITE,
0,
0,
NULL
);
if (!hMap)
{
CloseHandle(hFile);
return 1;
}
LPVOID view = MapViewOfFile(
hMap,
FILE_MAP_ALL_ACCESS,
0,
0,
0
);
if (!view)
{
CloseHandle(hMap);
CloseHandle(hFile);
return 1;
}
// utilisation de la vue comme un pointeur mémoire
FlushViewOfFile(view, 0);
UnmapViewOfFile(view);
CloseHandle(hMap);
CloseHandle(hFile);
Cela change complètement la manière de raisonner sur l’I/O, surtout pour de gros volumes.
Structures et constantes utiles pour le mapping
Autour du mapping, il faut connaitre au minimum :
- les protections de CreateFileMapping, par exemple PAGE_READONLY ou PAGE_READWRITE
- les accès de MapViewOfFile, comme FILE_MAP_READ, FILE_MAP_WRITE, FILE_MAP_ALL_ACCESS
- la différence entre le handle de fichier et le handle de section
Ce modèle est très important à comprendre, car il apparait partout dans l’architecture Windows.
Informations système liées à la mémoire
Dès qu’on travaille au niveau pages, certaines informations système deviennent indispensables. En particulier, il faut connaitre la taille de page et la granularité d’allocation.
La structure de base ici est SYSTEM_INFO.
Exemple :
Code: Select all
SYSTEM_INFO si;
GetSystemInfo(&si);
// si.dwPageSize
// si.dwAllocationGranularity
- dwPageSize
- dwAllocationGranularity
- lpMinimumApplicationAddress
- lpMaximumApplicationAddress
Code: Select all
MEMORYSTATUSEX ms = { 0 };
ms.dwLength = sizeof(ms);
if (!GlobalMemoryStatusEx(&ms))
return 1;
- la quantité de mémoire physique disponible
- le niveau de charge mémoire
- la mémoire virtuelle disponible
- le commit total et le commit disponible
Autres API mémoire utiles à connaitre
Selon le niveau où tu travailles, il peut etre utile de connaitre aussi :
- GetNativeSystemInfo, si tu veux des infos non virtualisées selon l’architecture
- GetLargePageMinimum, pour connaitre la taille minimale des large pages
- SetProcessWorkingSetSize, dans certains cas spécifiques
- EmptyWorkingSet, surtout pour outils ou diagnostics
- QueryWorkingSetEx, pour l’inspection du working set
Les fonctions legacy
Dans du vieux code Win32, tu rencontreras parfois :
- GlobalAlloc
- GlobalFree
- LocalAlloc
- LocalFree
- HeapAlloc / HeapFree
- VirtualAlloc / VirtualFree
- new / delete
- malloc / free
Règles de base qui évitent beaucoup de problèmes
Une très grande partie des bugs mémoire peut etre évitée avec quelques règles simples, mais strictes :
- chaque allocation doit avoir une libération clairement identifiée
- il faut gérer proprement les chemins d’erreur
- il ne faut jamais mélanger les familles d’allocation et de libération
- après libération, le pointeur doit etre considéré comme invalide
- les très gros buffers ne devraient pas partir sur la stack sans réfléchir
- toute structure partagée entre threads doit etre protégée
- les valeurs de retour doivent etre vérifiées systématiquement
- GetLastError doit etre consulté si une API Win32 échoue et que le diagnostic compte
- new avec delete
- new[] avec delete[]
- malloc avec free
- HeapAlloc avec HeapFree
- VirtualAlloc avec VirtualFree
Résumé des structures qu’il faut connaitre
Si tu veux te faire une petite base solide, les structures suivantes valent vraiment le coup d’etre retenues :
- SYSTEM_INFO
- MEMORYSTATUSEX
- MEMORY_BASIC_INFORMATION
- PROCESS_HEAP_ENTRY
Résumé des API à connaitre absolument
Pour les allocations par blocs et les heaps :
- GetProcessHeap
- HeapAlloc
- HeapReAlloc
- HeapFree
- HeapCreate
- HeapDestroy
- GetProcessHeaps
- HeapSize
- HeapValidate
- HeapWalk
- HeapSetInformation
- VirtualAlloc
- VirtualFree
- VirtualProtect
- VirtualQuery
- VirtualAllocEx
- VirtualProtectEx
- VirtualQueryEx
- VirtualLock
- VirtualUnlock
- CreateFileMapping
- OpenFileMapping
- MapViewOfFile
- MapViewOfFileEx
- UnmapViewOfFile
- FlushViewOfFile
- GetSystemInfo
- GetNativeSystemInfo
- GlobalMemoryStatusEx
- GetLargePageMinimum
- ReadProcessMemory
- WriteProcessMemory
La mémoire sous Windows repose sur quelques idées simples en apparence, mais très riches dans leurs conséquences : un espace virtuel propre à chaque processus, une stack par thread, un ou plusieurs heaps pour les allocations dynamiques, des pages protégées par le systeme, et des objets de section permettant le mapping.
Si tu maitrises déjà correctement :
- HeapAlloc et HeapFree
- HeapCreate et HeapDestroy
- VirtualAlloc, VirtualFree, VirtualProtect, VirtualQuery
- CreateFileMapping, MapViewOfFile, UnmapViewOfFile
- les structures SYSTEM_INFO, MEMORYSTATUSEX et MEMORY_BASIC_INFORMATION
Et surtout, tu commences à voir la mémoire non plus comme un simple “pointeur qu’on alloue”, mais comme un ensemble de mécanismes précis, cohérents, et strictement encadrés par le systeme. C’est exactement ce changement de vision qui fait progresser en bas niveau.
A bientot pour le prochain cours
