Cours sur la gestion de la mémoire en win32

Développement système natif en c/c++ avec win32 ...

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 46
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Cours sur la gestion de la mémoire en win32

Post by Hydraxx »

Salut, :D

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
Dans ce cours, l’objectif est de poser une vision propre et exploitable, puis de passer en revue les API Win32 qu’il faut réellement connaitre.

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
Pour le développeur user-mode, le point essentiel est le suivant : on travaille toujours avec des adresses virtuelles. La mémoire physique reste une réalité sous-jacente, mais elle n’est pas directement visible.

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
Vue générale de la mémoire d’un processus

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
Deux remarques sont particulièrement importantes.

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
Le heap répond à une logique différente. Il sert aux allocations dynamiques, c’est-à-dire à la mémoire dont la durée de vie n’est pas strictement liée à la portée d’une fonction.

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
Le heap est aussi la source d’une grande quantité de bugs classiques :
  • 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
Les heaps Windows : allocation par blocs

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
Exemple simple :

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;
}
Quelques remarques utiles ici.

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));
Après un HeapFree, le pointeur ne doit plus etre utilisé. A partir de ce moment, il est mort. Continuer à lire ou écrire dedans revient à travailler sur une zone qui n’appartient plus logiquement au programme.

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
Exemple :

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);
L’intérêt d’un heap privé est architectural autant que technique. On peut par exemple décider qu’un module donné possède son propre heap, ce qui simplifie parfois les nettoyages, les diagnostics, et l’isolement de certaines corruptions.

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é
Structures utiles autour du heap

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
Ce n’est pas une structure qu’on manipule dans tous les programmes applicatifs, mais elle est très utile en debug, dans des outils, ou pour mieux comprendre ce que contient un heap.

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
Exemple :

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;
Ici, on demande une région d’une page, à la fois réservée et validée, avec des droits lecture/écriture.

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
On peut réserver d’abord, puis valider plus tard. Cette séparation est importante dans du code bas niveau ou lorsqu’on veut gérer précisément l’occupation d’un espace virtuel.

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
Structures essentielles pour VirtualQuery

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;
Cette structure contient notamment :
  • 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
Les valeurs les plus importantes à connaitre sont souvent :
  • MEM_COMMIT
  • MEM_RESERVE
  • MEM_FREE
  • MEM_PRIVATE
  • MEM_MAPPED
  • MEM_IMAGE
Avec cette seule structure, on peut déjà faire beaucoup d’analyse mémoire.

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
PAGE_EXECUTE_READWRITE existe, mais il faut s’en méfier. Dans du code moderne, on évite en général de rendre une page à la fois modifiable et exécutable, sauf cas très particulier. Cela a des implications de sécurité évidentes.

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
Dans énormément de cas, cette erreur n’a rien d’aléatoire. Elle obéit à une logique mémoire stricte.

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;
Le paramètre oldProt permet de récupérer l’ancienne protection, ce qui est souvent utile pour la restaurer ensuite.

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
Toutes ces fonctions sont importantes dès qu’on touche aux outils système, au debug, à l’inspection mémoire ou à l’injection de code.

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
Les API clés sont :
  • CreateFileMapping
  • OpenFileMapping
  • MapViewOfFile
  • MapViewOfFileEx
  • UnmapViewOfFile
  • FlushViewOfFile
  • CloseHandle
Exemple classique :

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);
Le point fondamental à comprendre est que MapViewOfFile ne “copie” pas simplement le fichier dans un buffer classique. Il crée une vue mappée dans l’espace virtuel du processus. Le fichier devient donc accessible comme de la mémoire.

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
Le handle retourné par CreateFileMapping ne représente pas directement le fichier ouvert, mais l’objet de section créé par le noyau. Ensuite, MapViewOfFile crée une vue de cet objet de section dans l’espace du processus.

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
Les champs particulièrement utiles sont :
  • dwPageSize
  • dwAllocationGranularity
  • lpMinimumApplicationAddress
  • lpMaximumApplicationAddress
Pour obtenir une vue plus globale de la mémoire disponible et du commit, on utilise MEMORYSTATUSEX avec GlobalMemoryStatusEx :

Code: Select all

MEMORYSTATUSEX ms = { 0 };
ms.dwLength = sizeof(ms);

if (!GlobalMemoryStatusEx(&ms))
    return 1;
Cette structure donne notamment accès à :
  • 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
Dans du code plus orienté architecture ou diagnostic, ces informations sont précieuses.

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
Tout cela ne sert pas dans chaque programme applicatif, mais ce sont des briques qu’on rencontre vite en dev système, debug ou outillage.

Les fonctions legacy

Dans du vieux code Win32, tu rencontreras parfois :
  • GlobalAlloc
  • GlobalFree
  • LocalAlloc
  • LocalFree
Historiquement, ces fonctions avaient un sens plus marqué. Aujourd’hui, pour du code moderne, elles sont surtout là pour compatibilité ou pour certaines API anciennes. En pratique, on privilégie plutot :
  • HeapAlloc / HeapFree
  • VirtualAlloc / VirtualFree
  • new / delete
  • malloc / free
L’essentiel n’est pas seulement le choix de la famille d’API, mais la cohérence. Il ne faut jamais mélanger les couples de gestion mémoire.

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
En pratique, cela veut dire par exemple :
  • new avec delete
  • new[] avec delete[]
  • malloc avec free
  • HeapAlloc avec HeapFree
  • VirtualAlloc avec VirtualFree
Le non-respect de cette simple cohérence suffit à produire des corruptions parfois très discrètes.

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
Ce ne sont pas les seules structures du domaine mémoire, mais ce sont clairement parmi les plus utiles à connaitre au début pour lire du code Win32 bas niveau et écrire ses propres outils.

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
Pour la mémoire virtuelle par pages :
  • VirtualAlloc
  • VirtualFree
  • VirtualProtect
  • VirtualQuery
  • VirtualAllocEx
  • VirtualProtectEx
  • VirtualQueryEx
  • VirtualLock
  • VirtualUnlock
Pour les vues mappées et les sections :
  • CreateFileMapping
  • OpenFileMapping
  • MapViewOfFile
  • MapViewOfFileEx
  • UnmapViewOfFile
  • FlushViewOfFile
Pour les informations système :
  • GetSystemInfo
  • GetNativeSystemInfo
  • GlobalMemoryStatusEx
  • GetLargePageMinimum
Pour le travail inter-processus ou l’outillage :
  • ReadProcessMemory
  • WriteProcessMemory
Conclusion

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
alors tu possèdes une base déjà sérieuse en développement système Windows.

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 :D

Who is online

Users browsing this forum: No registered users and 0 guests