Trouver l’origine d’une fuite mémoire avec WinDbg

Lors de l’exécution d’une application, des fuites mémoires peuvent subvenir y compris dans un cadre managé. Dans les pires cas, trouver l’origine de ces fuites peut s’avérer compliqué car elles peuvent se produire dans des circonstances qu’on a du mal à identifier ou reproduire. Par exemple, ces fuites peuvent se produire dans un environnement de production quand l’application est particulièrement sollicitée. Il n’est pas forcement facile de reproduire les mêmes conditions dans un environnement de développement.

D’autre part, si l’application comporte beaucoup de code, on peut être assez démuni pour trouver l’origine de la fuite simplement en regardant statiquement le code.

Dans l’article Performance Monitor en 10 min, on avait indiqué une méthode simple pour monitorer un processus et être capable de détecter une fuite mémoire dans un processus. Dans le cadre de .NET, il est généralement possible d’aller plus loin et d’analyser plus précisement la mémoire d’un processus pour mettre en évidence l’origine d’une fuite mémoire.

Le but de cet article est d’indiquer une méthode pour mettre en évidence une fuite en mémoire dans un processus .NET en utilisant WinDbg. Dans un 2e temps, on indiquera comment tenter de trouver une fuite mémoire dans un processus natif.

@danedeaner

Préambule

Avant d’expliciter les méthodes utilisées pour tenter de trouver l’origine de fuites mémoire, on va indiquer brièvement comment utiliser WinDbg. En effet, même si WinDbg est souvent très utile pour analyser un processus en cours d’exécution ou lors d’un crash, il est particulièrement peu ergonomique.

Comment utiliser WinDbg ?

Il y a 2 façons d’utiliser WinDbg: pendant l’exécution d’un processus en mode debug et en mode statique avec un fichier dump. Dans l’article Les “dumps” mémoire en 5 min, on avait montré comment utiliser WinDbg en mode statique en ouvrant un fichier dump.

L’utilisation de WinDbg avec un dump peut s’avérer particulièrement utile dans un environnement de production puisqu’on est capable de générer le dump pendant l’exécution du processus.

Installation de WinDbg

On peut se procurer WinDbg en téléchargeant les “Debugging Tools for Windows”. Après installation, WinDbg est disponible dans le menu Windows: “Windows Kits” ⇒ “Debugging Tools for Windows” (x86 ou x64). On peut y accéder directement dans les répertoires:

  • Sur un système 32 bits: C:\Program Files\Windows Kits\10\Debuggers\x86\windbg.exe
  • Sur un système 64 bits:
    • Pour la version x86: C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\windbg.exe
    • Pour la version x64: C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe

A l’époque où WinDbg était installé avec le SDK Windows, il pouvait se trouver dans un répertoire du type:
C:\Program Files\Microsoft SDKs\Windows\v7.1\Redist\Debugging Tools for Windows

Dans un environnement de développement

On peut lancer WinDbg de 2 façons pour analyser la mémoire d’un processus au cours de son exécution. Ces 2 méthodes sont plus adaptées dans un environnement de développement.

  • 1ère méthode: exécuter WinDbg à la ligne de commandes
    Il est possible d’exécuter WinDbg en mode debug en étant attaché à un processus.

    Pour lancer WinDbg dans ce mode, on peut exécuter la ligne suivante:

    windbg.exe <fichier exécutable>
    

    L’exécutable sera lancé mais son exécution sera directement stoppé.

    Pour continuer l’exécution, il faut utiliser la commande g, appuyer sur [F5] ou utiliser le menu “Debug” ⇒ “Go”.

    On peut lancer directement l’exécution en exécutant la ligne suivante:

    windbg.exe -g <fichier exécutable>
    

    D’autres options sont disponibles sur la page suivante: docs.microsoft.com/en-us/windows-hardware/drivers/debugger/windbg-command-line-options.

  • 2e méthode: s’attacher à un processus en cours d’exécution
    On peut directement s’attacher à un processus en cours d’exécution en appuyant sur [F6] ou en utilisant le menu “File” ⇒ “Attach to a process…”

    Après s’être attaché au processus, l’exécution est en mode debug.

Quelques commandes exécutables dans l’invite de commandes WinDbg utiles pendant le debug:

  • .attach <PID> pour s’attacher à un processus.
  • .detach pour stopper l’exécution en mode debug et stopper l’exécution du processus.
  • .restart pour relancer le processus en mode debug.
  • q permet d’arrêter le débuggage et quitte WinDbg.
  • g relance l’exécution si elle a été stoppée (équivalent à [F5]).
  • t permet d’exécuter une instruction en pas à pas détaillé (équivalent à [F11] ou [F8]).
  • p pour exécuter une instruction en pas à pas en restant au niveau principal (équivalent à [F10]).

Lecture d’un “dump”

Une autre méthode pour utiliser WinDbg peut consister à lire un dump mémoire. Cette méthode est plus intéressante lorsqu’on veut analyser la mémoire d’un processus dans un environnement de production. Pour capturer un dump, quelques méthodes sont indiquées dans l’article Les “dumps” mémoire en 5 min.

On peut lire le fichier dump avec WinDbg en cliquant sur “File” ⇒ “Open Crash Dump”.

Quelques autres commandes utiles

D’autres commandes sont disponibles lorsque l’exécution est interrompue mais pas complêtement stoppée (ces commandes sont aussi disponibles si on lit un fichier dump):

r Permet d’afficher l’état des registres
lm Liste les modules
.lastevent Affiche des informations sur le debuggage
!analyze -v Affiche des informations détaillées sur le session de debug
k ou kp Affiche la pile d’appels (i.e. call stack) du thread courant
~ Affiche la liste des threads du processus
~* k Affiche la pile d’appels de tous les threads
db <adresse emplacement mémoire> Lire la mémoire sous forme d’octets simples et l’affiche sous forme de valeurs hexadécimales. Ces valeurs sont suivies de l’interprétation en caractères ASCII (quand c’est possible)
dc <adresse emplacement mémoire> Lire la mémoire sous forme de mot de 2 octets et l’affiche sous forme de valeurs hexadécimales.
dd <adresse emplacement mémoire> Lire la mémoire sous forme de double mot de 4 octets et l’affiche sous forme de valeurs hexadécimales.
dq <adresse emplacement mémoire> Lire la mémoire sous forme de quadri mot de 8 octets et l’affiche sous forme de valeurs hexadécimales.
dW <adresse emplacement mémoire> Lire la mémoire sous forme de mot de 2 octets et l’affiche sous forme de valeurs hexadécimales. Ces valeurs sont suivies de l’interprétation en caractères Unicode (quand c’est possible)
ds <adresse emplacement mémoire> Lire la mémoire sous forme d’une chaine de caractères ANSI
dS <adresse emplacement mémoire> Lire la mémoire sous forme d’une chaine de caractères Unicode
!dlls Afficher des informations sur les DLL chargées

Trouver l’origine d’une fuite mémoire

On va expliciter 2 méthodes pour tenter de trouver l’origine de fuites mémoires: dans un processus managé et dans un processus natif.

Le plus souvent des fuites mémoires proviennent d’objets instanciés et qui n’ont pas été libérés après utilisation. Plus les objets sont instanciés fréquemment et plus les fuites mémoires seront évidentes.

Pour détecter la fuite, dans le cadre des 2 méthodes, le but est dabord de détecter quel est le type d’objet le plus fréquent dans la mémoire occupée par le processus. Ensuite, on peut tenter de trouver le code qui a généré ce type. Dans le cadre d’une application réelle, ces 2 méthodes peuvent s’avérer plus complexes à mettre en œuvre car il peut y avoir beaucoup d’objets instanciés qui ne participent pas à la fuite mémoire menant ainsi à de fausses pistes.

Dans un processus managé

WinDbg peut aider à trouver l’origine de fuites mémoires dans un processus managé en s’aidant de quelques commandes. Ces commandes ont pour but de compter le nombre d’occurences des objets dans le tas. En identifiant judicieusement les objets les plus fréquents, puis en s’aidant de la pile d’appels ayant amené à la création de ces objets, on peut identifier l’origine de la fuite mémoire.

Pour montrer comment repérer l’origine d’une fuite mémoire, on se propose de créer une application .NET créant des instances d’objets de façon continue pour simuler une fuite mémoire. Cette application permettra de mettre en application une méthode pour trouver le type des objets instanciés et le code créant ces instances.

L’implémentation de l’application est:

class Program
{
    static void Main(string[] args)
    {
        var managedBigObjectGenerator = new ManagedBigObjectGenerator();
        managedBigObjectGenerator.CreateObjects();

        Console.WriteLine("Done");

        Console.ReadLine();
    }
}

Le détail de l’objet ManagedBigObjectGenerator est:

public class ManagedBigObjectGenerator
{
    private List<BigObject> objects = new List<BigObject>();

    public void CreateObjects()
    {
        for (int i = 0; i < 100000; i++)
        {
            this.objects.Add(new BigObject());

            if (i % 10 == 0)
            Thread.Sleep(10);
        }
    }
}

internal class BigObject
{
    private const string loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";

    private List<string> strings = new List<string>();

    public void CreateObjects()
    {
        for (int i = 0; i < 100000; i++)
        {
            strings.Add(loremIpsum);
        }
    }
}

Le code complet de cette application se trouve dans le repo GitHub github.com/msoft/memory_leak_managed.

Ce code permet de créer plusieurs instances de l’objet BigObject qui seront placées dans une liste. Chaque instance de BigObject crée plusieurs instances d’une chaîne de caractères et place chacune d’elles dans une liste. Dans la boucle créant les objets BigObject, on ralentit l’exécution avec une instruction Thread.Sleep(10) de façon à ce que la consommation de mémoire ne soit pas trop rapide.

On compile cette application pour générer un exécutable nommé ManagedMemoryLeak.exe.

Si on lance l’exécutable, l’utilisation de la mémoire augmente régulièrement de façon à illustrer une fuite mémoire.

Pour trouver l’origine de la fuite mémoire, on effectue les étapes suivantes:

Lancer WinDbg en mode debug

  1. Il faut exécuter WinDbg avec les privilèges Administrateur
  2. Lancer l’exécution de l’exécutable NativeMemoryLeak.exe
  3. S’attacher un processus:
    • En cliquant sur “File” ⇒ “Attach to a process…” ⇒ Sélectionner le processus ⇒ cliquer sur “OK” ou
    • Appuyer sur [F6] et sélectionner le processus

Après ces étapes, l’exécution s’arrête et le mode debug est lancé.

On peut aussi lancer WinDbg et NativeMemoryLeak.exe directement en exécutant dans le répertoire de l’exécutable:

"C:\Program Files\Windows Kits\10\Debuggers\x86\windbg.exe" -g ManagedMemoryLeak.exe

Pour interrompre l’exécution sans la stopper il faut cliquer sur l’icone suivante sur la barre de tâche:

Dans WinDbg, pour que la fenêtre soit en plein écran, on peut cliquer sur “Window” ⇒ “Dock All”.

Charger l’extension SOS

Cette extension permet à WinDbg d’afficher davantage d’informations concernant la mémoire managée du processus.

Pour charger l’extension, il faut exécuter la commande suivante:

.loadby sos clr

Afficher les détails des objets du tas managé

En mode debug et après avoir interrompu l’exécution du processus, il est possible d’afficher quelques informations sur les objets se trouvant dans le tas managé. Par exemple, on peut compter les occurences des objets avec la commmande:

!dumpheap -stat

Dans notre cas, le résultat est:

0:004> !dumpheap -stat
Statistics:
      MT    Count    TotalSize Class Name
67539664        1           12 System.AppDomainPauseManager
67531454        1           12 System.Security.HostSecurityManager
67530414        1           12 System.Collections.Generic.ObjectEqualityComparer`1[[System.Type, mscorlib]]
00234dd4        1           12 ManagedMemoryLeak.ManagedBigObjectGenerator
6753140c        1           16 System.Security.Policy.Evidence+EvidenceLockHolder
6752f54c        1           16 System.Char[]
6752de9c        1           16 System.Security.Policy.AssemblyEvidenceFactory
6752dde8        1           20 Microsoft.Win32.SafeHandles.SafePEFileHandle
6752eee0        2           24 System.Object
00234ea4        1           24 System.Collections.Generic.List`1[[ManagedMemoryLeak.BigObject, ManagedMemoryLeak]]
67531328        1           28 System.Reflection.RuntimeAssembly
6752ef7c        1           28 System.SharedStatics
6752de44        1           32 System.Security.Policy.PEFileEvidenceFactory
6752f944        1           36 System.Security.PermissionSet
6752f848        1           40 System.Security.Policy.Evidence
675312e0        1           44 System.Threading.ReaderWriterLock
6708a64c        1           48 System.Collections.Generic.Dictionary`2[[System.Type, mscorlib],[System.Security.Policy.EvidenceTypeDescriptor, mscorlib]]
67530460        1           52 System.Type[]
6752f3b0        1           68 System.AppDomainSetup
6752ee64        1           84 System.ExecutionEngineException
6752ee20        1           84 System.StackOverflowException
6752eddc        1           84 System.OutOfMemoryException
6752ec88        1           84 System.Exception
6752f698        3          108 System.String[]
002b3c88        9          110      Free
6752eff8        1          112 System.AppDomain
6752eea8        2          168 System.Threading.ThreadAbortException
67530958        4          444 System.Int32[]
67531100        3          468 System.Collections.Generic.Dictionary`2+Entry[[System.Type, mscorlib],[System.Security.Policy.EvidenceTypeDescriptor, mscorlib]][]
6752ff54       20          560 System.RuntimeType
6752eb40       38         2522 System.String
6752ef34        4        17604 System.Object[]
00235298       15       262308 ManagedMemoryLeak.BigObject[]
00234e54    24651       295812 ManagedMemoryLeak.BigObject
67091e00    24651       591624 System.Collections.Generic.List`1[[System.String, mscorlib]]
Total 49425 objects
Erreur possible

A la 1ère exécution, une erreur de ce type peut se produire:

0:004> !dumpheap -stat
c0000005 Exception in C:\Windows\Microsoft.NET\Framework\v4.0.30319\sos.dumpheap debugger extension.
      PC: 014cfa73  VA: 00000000  R/W: 0  Parameter: 0001003f

Il faut réexecuter la commande !dumpheap -stat pour que ça marche.

On remarque que les instances de type BigObject sont les plus fréquentes.

On peut ensuite afficher les différentes instances d’objets de type ManagedMemoryLeak.BigObject ou contenant des objets de ce type (dans le cas de listes) en exécutant:

!dumpheap -type ManagedMemoryLeak.BigObject

On obtient une liste de toutes les instances des objets de type ManagedMemoryLeak.BigObject et la dernière ligne indique probablement une liste contenant tous ces objets:

Address  MT             Size
01edaeb8 00234e54       12     
01edaedc 00234e54       12     
01edaf00 00234e54       12     
01edaf24 00234e54       12     
01edaf48 00234e54       12     
01edaf6c 00234e54       12     
01edaf90 00234e54       12     
01edafb4 00234e54       12     
01edafd8 00234e54       12
02de5530 00235298   131084

Pour voir les lignes qui ont généré ces objets, on choisit une adresse au hasard et on exécute:

0:004> !gcroot 02de5530
Thread 1918:
    001cf4b8 006f05af ManagedMemoryLeak.ManagedBigObjectGenerator.CreateObjects() [C:\ManagedMemoryLeak\BigObjectGenerator.cs @ 35]
        ebp+10: 001cf4c0
            ->  01de249c ManagedMemoryLeak.ManagedBigObjectGenerator
            ->  01de24a8 System.Collections.Generic.List`1[[ManagedMemoryLeak.BigObject, ManagedMemoryLeak]]
            ->  02de5530 ManagedMemoryLeak.BigObject[]

Found 1 unique roots (run '!GCRoot -all' to see all roots).

Dans ce cas il s’agit bien d’une liste de ManagedMemoryLeak.BigObject.

Si on exécute la commande suivante:

0:004> !gcroot 01edafd8
Thread 1918:
    001cf4b8 006f05af ManagedMemoryLeak.ManagedBigObjectGenerator.CreateObjects() [C:ManagedMemoryLeak\BigObjectGenerator.cs @ 35]
        ebp+10: 001cf4c0
            ->  01de249c ManagedMemoryLeak.ManagedBigObjectGenerator
            ->  01de24a8 System.Collections.Generic.List`1[[ManagedMemoryLeak.BigObject, ManagedMemoryLeak]]
            ->  02de5530 ManagedMemoryLeak.BigObject[]
            ->  01edafd8 ManagedMemoryLeak.BigObject

    001cf4b8 006f05af ManagedMemoryLeak.ManagedBigObjectGenerator.CreateObjects() [C:\ManagedMemoryLeak\BigObjectGenerator.cs @ 35]
        ebp+14: 001cf4bc
            ->  01edafd8 ManagedMemoryLeak.BigObject

Found 2 unique roots (run '!GCRoot -all' to see all roots).

Ainsi les différentes lignes indiquent d’où proviennent les objets ayant conduit à la fuite mémoire de façon à avoir une idée précise de son origine.

Dans un processus natif

Avoir une fuite mémoire dans un processus natif est courant et il est souvent difficile d’en trouver l’origine en particulier lorsqu’il y a beaucoup de code. WinDbg peut aider à trouver l’origine de la fuite même si le résultat n’est pas garanti à tous les coups.

Comme pour les fuites dans du code managé, la méthode consiste à lister les allocations en mémoire en considérant la taille des blocs. On peut, ainsi, repérer le ou les blocs qui se répètent le plus souvent en considérant la taille des blocs dont l’occurence se répète le plus fréquemment.

En listant tous les blocs dont la taille est la plus courante dans le processus, on peut en déduire l’objet le plus fréquemment alloué.

Pour montrer comment repérer les lignes de code à l’origine d’une fuite mémoire, on se propose de créer une application créant des instances d’objets de façon continue. On va ensuite essayer d’en trouver l’origine en utilisant la méthode décrite précédemment. L’application utilisée est très simple et n’est pas représentative de la complexité du code que l’on retrouve courammment dans des applications réelles. Le but est simplement de mettre en application une méthode d’analyse possible.

L’implémentation de l’application est:
Dans NativeMemoryLeak.cpp:

#include <windows.h>
#include <iostream>
#include "LeakingObject.h"

int main()
{
    int objectCount = 10000;

    LeakingObject **leakingObjects = new LeakingObject*[objectCount];
    for (int i = 0; i < objectCount; i++)
    {
        leakingObjects[i] = new LeakingObject(10);
    }

    for (int i = 0; i < objectCount; i++)
    {
        leakingObjects[i]->CreateObjects();

        if (i % 10 == 0)

        Sleep(100);
    }
}

L’objet LeakingObject est:

#include "LeakingObject.h"

LeakingObject::LeakingObject(int count)
{
    this->count = count;
}

LeakingObject::~LeakingObject()
{
}

void LeakingObject::CreateObjects()
{
    this->innerStrings = new std::string[this->count];

    for (int i = 0; i < this->count; i++)
    {
        this->innerStrings[i] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
    }
}

Le code complet de cette application se trouve dans le repo GitHub github.com/msoft/memory_leak_unmanaged.

Ce code permet de créer plusieurs instances de l’objet LeakingObject et les place dans un tableau. Chaque instance de LeakingObject crée plusieurs instances d’une chaîne de caractères et place chacune d’elles dans un tableau. Dans la boucle créant les objets LeakingObject, on ralentit l’exécution avec des instructions Sleep(100) de façon à ce que la consommation de mémoire ne soit pas trop rapide.

On compile cette application pour générer un exécutable nommé NativeMemoryLeak.exe.

Si on lance l’exécutable, l’utilisation de la mémoire augmente régulièrement de façon à illustrer une fuite mémoire.

Pour trouver l’origine de la fuite mémoire dans le processus natif, on effectue les étapes suivantes:

Activer gflags.exe

Dans un premier temps et avant de lancer l’exécutable NativeMemoryLeak.exe, on doit activer la création d’une base des informations des piles d’appels en utilisant l’utilitaire gflags.exe. Cet utilitaire est livré avec Windbg, il permet d’ajouter des informations qui sont stockées dans la base de registres et qui seront utiles lors du debug avec WinDbg. Par exemple, il est capable de monitorer les allocations sur le tas (i.e. heap) pour aider à trouver l’origine d’une fuite mémoire.

Après installation, gflags.exe se trouve dans le même répertoire que WinDbg:

  • Sur un système 32 bits: C:\Program Files\Windows Kits\10\Debuggers\x86\gflags.exe
  • Sur un système 64 bits:
    • Pour la version x86: C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\gflags.exe
    • Pour la version x64: C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\gflags.exe

Pour activer gflags.exe sur un exécutable:

  1. Lancer une invite de commandes avec les droits administrateur
  2. Aller dans le répertoire de NativeMemoryLeak.exe
  3. Exécuter la ligne suivante:
    gflags.exe /i <fichier .exe> +ust
    

Le détail des options est:

  • /i <fichier .exe> permet d’indiquer qu’on veut créer les traces pour un exécutable particulier,
  • +ust indique qu’on veut créer la base des traces d’appels en mode utilisateur (i.e. user mode stack trace).

On peut rajouter l’option +hpa pour activer la vérification des pages du tas (i.e. page heap) toutefois cette option ne semble pas compatible avec +ust. Lorsque l’option +hpa est activée, l’option +ust ne semble plus activée dans WinDbg.

Ne pas oublier de désactiver la base de traces

L’option +ust dégrade les performances lors de l’exécution, il ne faut pas oublier de la désactiver après utilisation en exécutant:

gflags.exe /i <fichier .exe> -ust

Dans notre cas, pour activer l’option +ust, on exécute la ligne suivante:

"C:\Program Files\Windows Kits\10\Debuggers\x86\gflags.exe" /i NativeMemoryLeak.exe +ust

Le résultat est:

Current Registry Settings for NativeMemoryLeak.exe executable are: 00001000
    ust - Create user mode stack trace database

Pour plus de détails sur gflags: docs.microsoft.com/en-us/windows-hardware/drivers/debugger/gflags-commands.

Lancer WinDbg en mode debug

  1. Il faut exécuter WinDbg avec les privilèges Administrateur
  2. Lancer l’exécution de l’exécutable NativeMemoryLeak.exe
  3. S’attacher un processus:
    • En cliquant sur “File” ⇒ “Attach to a process…” ⇒ Sélectionner le processus ⇒ cliquer sur “OK” ou
    • Appuyer sur [F6] et sélectionner le processus

Après ces étapes, l’exécution s’arrête et le mode debug est lancé.

On peut aussi lancer WinDbg et NativeMemoryLeak.exe directement en exécutant:

"C:\Program Files\Windows Kits\10\Debuggers\x86\windbg.exe" -g NativeMemoryLeak.exe

Pour interrompre sans stopper l’exécution il faut cliquer sur l’icone suivante sur la barre de tâche:

On peut vérifier le mode activé avec gflags.exe en exécutant la commande:

!gflag

Dans notre cas, le résultat est:

0:001> !gflag
Current NtGlobalFlag contents: 0x00001000
    ust - Create user mode stack trace database

Afficher l’état du tas

On peut afficher la mémoire occupée par les tas et ainsi vérifier la présence d’une fuite mémoire en exécutant la commande:

!heap -s

L’option -s permet d’afficher un résumé sur les différents tas.

Le résultat est:

0:001> !heap -s
NtGlobalFlag enables following debugging aids for new heaps:
    stack back traces
LFH Key                   : 0x0a17ba9d
Termination on corruption : ENABLED
  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
01590000 08000002   32576  17468  32576    135    13     6    0      0   LFH
00010000 08008000      64      4     64      2     1     1    0      0      
00020000 08008000      64     64     64     62     1     1    0      0      
-----------------------------------------------------------------------------

On peut aussi exécuter:

!heap -stat

-stat permet d’afficher des informations sur l’utilisation des tas.

Le résultat est dans notre cas:

0:001> !heap -stat
_HEAP 01590000
     Segments            00000006
         Reserved  bytes 01fd0000
         Committed bytes 0110f000
     VirtAllocBlocks     00000000
         VirtAlloc bytes 00000000
_HEAP 00020000
     Segments            00000001
         Reserved  bytes 00010000
         Committed bytes 00010000
     VirtAllocBlocks     00000000
         VirtAlloc bytes 00000000
_HEAP 00010000
     Segments            00000001
         Reserved  bytes 00010000
         Committed bytes 00001000
     VirtAllocBlocks     00000000
         VirtAlloc bytes 00000000

Pour davantage de détails sur la commande !heap: docs.microsoft.com/en-us/windows-hardware/drivers/debugger/-heap.

Dans le résumé présenté, on peut voir la quantité de mémoire allouée pour tous les tas. On peut supposer que le tas nécessitant le plus de mémoire est celui dans lequel se trouve les instances provoquant la fuite mémoire.

Pour s’en assurer, on peut continuer brièvement l’exécution en exécutant l’instruction suivante dans WinDbg:

g

Si on interrompt l’exécution après un instant et si exécute de nouveau !heap -s, on peut vérifier si l’utilisation de la mémoire a évolué:

0:001> !heap -s
NtGlobalFlag enables following debugging aids for new heaps:
    stack back traces
LFH Key                   : 0x0a17ba9d
Termination on corruption : ENABLED
  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
01590000 08000002   48768  32592  48768    151    14     7    0      0   LFH
00010000 08008000      64      4     64      2     1     1    0      0      
00020000 08008000      64     64     64     62     1     1    0      0      
-----------------------------------------------------------------------------

La quantité de mémoire utilisée par le tas situé à l’adresse virtuelle 3e0000 semble augmenter:

Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
01590000 08000002   32576  17468  32576    135    13     6    0      0   LFH

01590000 08000002   48768  32592  48768    151    14     7    0      0   LFH

Afficher les détails des tas

Après avoir répéré le tas où pourrait se trouver la fuite mémoire, on va afficher des détails sur l’utilisation des allocations mémoire par taille en exécutant la commande:

!heap -stat -h <adresse du tas>

Le résultat est:

0:001> !heap -stat -h 01590000
 heap @ 01590000
group-by: TOTSIZE max-display: 20
    size     #blocks     total     ( %) (percent of total busy bytes)
    1e4 c7a6 - 17975d8  (84.84)
    2c eed1 - 290bec  (9.23)
    140 13f7 - 18f4c0  (5.61)
    9c64 1 - 9c64  (0.14)
    39a6 1 - 39a6  (0.05)
    1806 1 - 1806  (0.02)
    c24 1 - c24  (0.01)
    20 61 - c20  (0.01)
    858 1 - 858  (0.01)
    824 1 - 824  (0.01)
    6ec 1 - 6ec  (0.01)
    78 e - 690  (0.01)
    224 3 - 66c  (0.01)
    244 2 - 488  (0.00)
    440 1 - 440  (0.00)
    400 1 - 400  (0.00)
    200 2 - 400  (0.00)
    36d 1 - 36d  (0.00)
    220 1 - 220  (0.00)
    208 1 - 208  (0.00

On affiche un détail des blocs mémoire ayant une taille particulière. On s’intéresse à l’allocation la plus fréquente obtenue précédemment. Ainsi is on regarde le pourcentage d’utilisation le plus élevé:

size     #blocks     total     ( %) (percent of total busy bytes)
1e4 c7a6 - 17975d8  (84.84)

Pour afficher tous les blocs, on exécute la commande:

!heap -flt s <taille>

Dans notre cas, on obtient:

0:001> !heap -flt s 1e4
_HEAP @ 3e0000
HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
0348d388 0041 0041  [00]   0348d3a0    001e4 - (busy)
0348d590 0041 0041  [00]   0348d5a8    001e4 - (busy)
0348d798 0041 0041  [00]   0348d7b0    001e4 - (busy)
0348d9a0 0041 0041  [00]   0348d9b8    001e4 - (busy)
0348dba8 0041 0041  [00]   0348dbc0    001e4 - (busy)
0348ddb0 0041 0041  [00]   0348ddc8    001e4 - (busy)
0348dfb8 0041 0041  [00]   0348dfd0    001e4 - (busy)
0348e1c0 0041 0041  [00]   0348e1d8    001e4 - (busy)
0348e3c8 0041 0041  [00]   0348e3e0    001e4 - (busy)
0348e5d0 0041 0041  [00]   0348e5e8    001e4 - (busy)
0348e7d8 0041 0041  [00]   0348e7f0    001e4 - (busy)
0348e9e0 0041 0041  [00]   0348e9f8    001e4 - (busy
...

Si on prends au hasard l’adresse d’un bloc, on peut accéder à la pile d’appel de l’entrée du tas:

!heap -p -a <adresse UsrPtr>

Dans notre cas, le résultat est:


0:001> !heap -p -a 0348d798
    address 0348d798 found in
    _HEAP @ 1590000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        0348d798 0041 0000  [00]   0348d7b0    001e4 - (busy)
        7731d78c ntdll!RtlAllocateHeap+0x00000274
        52d7b178 ucrtbased!heap_alloc_dbg_internal+0x00000198
        52d7af96 ucrtbased!heap_alloc_dbg+0x00000036
        52d7d72a ucrtbased!_malloc_dbg+0x0000001a
        52d7e054 ucrtbased!malloc+0x00000014
        39526d NativeMemoryLeak!operator new+0x0000000d
        393731 NativeMemoryLeak!std::_Default_allocate_traits::_Allocate+0x00000031
        391d1e NativeMemoryLeak!std::_Allocate<8,std::_Default_allocate_traits,0>+0x0000004e
        3944af NativeMemoryLeak!std::allocator<char>::allocate+0x0000003f
        3922bf NativeMemoryLeak!std::basic_string<char,std::char_traits<char>,std::allocator<char> >::_Reallocate_for<<lambda_9366063389c5f42a00a5088cf24e69de>,char const *>+0x0000008f
        39467d NativeMemoryLeak!std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign+0x000000ad
        39459f NativeMemoryLeak!std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign+0x0000004f
        3930b9 NativeMemoryLeak!std::basic_string<char,std::char_traits<char>,std::allocator<char> >::operator=+0x00000039
        393389 NativeMemoryLeak!LeakingObject::CreateObjects+0x000000f9
        394e2e NativeMemoryLeak!main+0x0000012e
        395a4e NativeMemoryLeak!invoke_main+0x0000001e
        3958b7 NativeMemoryLeak!__scrt_common_main_seh+0x00000157
        39574d NativeMemoryLeak!__scrt_common_main+0x0000000d
        395ac8 NativeMemoryLeak!mainCRTStartup+0x00000008
        755fef6c kernel32!BaseThreadInitThunk+0x0000000e
        77303618 ntdll!__RtlUserThreadStart+0x00000070
        773035eb ntdll!_RtlUserThreadStart+0x0000001b

Une ligne peut s’avérer intéressante pour trouver la ligne responsable de l’allocation:

393389 NativeMemoryLeak!LeakingObject::CreateObjects+0x000000f9

La commande uf permet de désassembler une fonction en mémoire. En utilisant l’adresse de l’entrée dans le tas, on peut voir le détail de la fonction et la correspondance dans le code:

0:001> uf 393389 
NativeMemoryLeak!LeakingObject::CreateObjects [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 17]:
   17 00393290 55              push    ebp
   17 00393291 8bec            mov     ebp,esp
   17 00393293 81ecf4000000    sub     esp,0F4h
   ...

NativeMemoryLeak!LeakingObject::CreateObjects+0x72 [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 18]:
   18 00393302 8b8d14ffffff    mov     ecx,dword ptr [ebp-0ECh]
   18 00393308 8b9520ffffff    mov     edx,dword ptr [ebp-0E0h]
   18 0039330e 8911            mov     dword ptr [ecx],edx
   ...

NativeMemoryLeak!LeakingObject::CreateObjects+0xb3 [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 18]:
   18 00393343 c7850cffffff00000000 mov dword ptr [ebp-0F4h],0

NativeMemoryLeak!LeakingObject::CreateObjects+0xbd [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 18]:
   18 0039334d 8b45f8          mov     eax,dword ptr [ebp-8]
   18 00393350 8b8d0cffffff    mov     ecx,dword ptr [ebp-0F4h]
   18 00393356 894804          mov     dword ptr [eax+4],ecx
   ...

NativeMemoryLeak!LeakingObject::CreateObjects+0xd2 [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 20]:
   20 00393362 8b45ec          mov     eax,dword ptr [ebp-14h]
   20 00393365 83c001          add     eax,1
   20 00393368 8945ec          mov     dword ptr [ebp-14h],eax

NativeMemoryLeak!LeakingObject::CreateObjects+0xdb [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 20]:
   20 0039336b 8b45f8          mov     eax,dword ptr [ebp-8]
   20 0039336e 8b4dec          mov     ecx,dword ptr [ebp-14h]
   20 00393371 3b08            cmp     ecx,dword ptr [eax]
   ...

NativeMemoryLeak!LeakingObject::CreateObjects+0xe5 [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 22]:
   22 00393375 6838cd3900      push    offset NativeMemoryLeak!`string' (0039cd38)
   22 0039337a 6b4dec1c        imul    ecx,dword ptr [ebp-14h],1Ch
   22 0039337e 8b45f8          mov     eax,dword ptr [ebp-8]
   ...

NativeMemoryLeak!LeakingObject::CreateObjects+0xfb [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 24]:
   24 0039338b 5f              pop     edi
   24 0039338c 5e              pop     esi
   24 0039338d 5b              pop     ebx
   ...

Ces lignes correspondent au code:

void LeakingObject::CreateObjects()
{
    this->innerStrings = new std::string[this->count];

    for (int i = 0; i < this->count; i++)
    {
        this->innerStrings[i] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
    }
}

Pour quitter WinDbg et le mode debug, il suffit d’utliiser la commande:

q

De la même façon que pour le processus managé, on peut ainsi identifier les différentes lignes de code indiquant où les objets ayant conduit à la fuite mémoire ont été créés.

Pour résumer

Pour identifier l’origine d’une fuite mémoire:

  • Pour un processus managé, on exécute:
    1. On lance WinDbg en mode Début ou avec un fichier de dump
    2. On charge l’extension SOS dans WinDbg:
      .loadby sos clr
      
    3. On affiche les statistiques des objets instanciés dans le tas managé:
      !dumpheap -stat
      
    4. Obtenir les adresses des instances d’un objet d’un type spécifié:
      !dumpheap -type <type de l'objet>
      
    5. Obtenir les lignes ayant générées l’instance de l’objet
      !gcroot <adresse de l'instance>
      
  • Pour un processus natif:
    1. Activer la base de données permettant de stocker les traces d’appels
      gflags.exe /i <fichier .exe> +ust
      
    2. Lancer WinDbg en mode debug
    3. Afficher les statistiques sur l’état des tas, cette instruction permet d’indiquer le tas nécessitant le plus de mémoire
      !heap -s
      
    4. On sélectionne le tas nécessitant le plus de mémoire et on affiche des statistiques plus précises sur les objets se trouvant dans ce tas:
      !heap -stat -h <adresse du tas>
      

      A la suite de l’instruction précédente, on obtient une liste d’objets classés par taille.

    5. L’instruction suivante permet d’afficher les objets ayant une taille particulière:
      !heap -flt s <taille>
      

      On obtient une liste d’objets avec leur adresse

    6. Pour afficher la pile d’appels ayant permis de créer l’objet, on exécute:
      !heap -p -a <adresse UsrPtr>
      
    7. On affiche ensuite le détail de la fonction ayant permis de créer l’objet:
      uf <adresse de l'instruction ayant permis l'instanciation de l'objet>
      
Références

Commandes WinDbg:

Précisions .NET:

Leave a Reply