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.
Préambule
Comment utiliser WinDbg ?
Installation de WinDbg
Dans un environnement de développement
Lecture d’un “dump”
Quelques autres commandes utiles
Trouver l’origine d’une fuite mémoire
Dans un processus managé
Dans un processus natif
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
- Pour la version x86:
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
- Il faut exécuter WinDbg avec les privilèges Administrateur
- Lancer l’exécution de l’exécutable
NativeMemoryLeak.exe
- 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
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 la version x86:
Pour activer gflags.exe
sur un exécutable:
- Lancer une invite de commandes avec les droits administrateur
- Aller dans le répertoire de
NativeMemoryLeak.exe
- 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.
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
- Il faut exécuter WinDbg avec les privilèges Administrateur
- Lancer l’exécution de l’exécutable NativeMemoryLeak.exe
- 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:
- On lance WinDbg en mode Début ou avec un fichier de dump
- On charge l’extension SOS dans WinDbg:
.loadby sos clr
- On affiche les statistiques des objets instanciés dans le tas managé:
!dumpheap -stat
- Obtenir les adresses des instances d’un objet d’un type spécifié:
!dumpheap -type <type de l'objet>
- Obtenir les lignes ayant générées l’instance de l’objet
!gcroot <adresse de l'instance>
- Pour un processus natif:
- Activer la base de données permettant de stocker les traces d’appels
gflags.exe /i <fichier .exe> +ust
- Lancer WinDbg en mode debug
- Afficher les statistiques sur l’état des tas, cette instruction permet d’indiquer le tas nécessitant le plus de mémoire
!heap -s
- 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.
- 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
- Pour afficher la pile d’appels ayant permis de créer l’objet, on exécute:
!heap -p -a <adresse UsrPtr>
- 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>
- Activer la base de données permettant de stocker les traces d’appels
- GFlags Command Overview: https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/gflags-commands
- Download Debugging Tools for Windows: https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools
- Hunting .NET memory leaks with Windbg: https://snede.net/hunting-net-memory-leaks-with-windbg/
- WinDbg Cheat Sheet for .NET Developers: https://blog.stefangeiger.ch/2019/05/11/windbg-cheat-sheet.html
- !dumpheap –stat explained… (debugging .net leaks): https://blogs.msdn.microsoft.com/tess/2005/11/25/dumpheap-stat-explained-debugging-net-leaks/
- Inspecting Objects using WinDbg: http://www.dotnetspeak.com/debugging/inspecting-objects-using-windbg/
- SOS.dll (extension de débogage SOS): https://docs.microsoft.com/fr-fr/dotnet/framework/tools/sos-dll-sos-debugging-extension
- Investigating issues with Unmanaged Memory. First steps: http://kate-butenko.blogspot.com/2012/07/investigating-issues-with-unmanaged.html
- Debugging – Finding a native heap leak with WinDbg: http://www.debugthings.com/2015/01/09/debugging-heap-leaks/
- How to debug Windows services with Windbg: https://www.sysadmins.lv/retired-msft-blogs/alejacma/how-to-debug-windows-services-with-windbg.aspx
- Setting up managed code debugging (with SOS and SOSEX: https://docs.microsoft.com/fr-fr/archive/blogs/jankrivanek/setting-up-managed-code-debugging-with-sos-and-sosex
- d, da, db, dc, dd, dD, df, dp, dq, du, dw (Display Memory): https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/d–da–db–dc–dd–dd–df–dp–dq–du–dw–dw–dyb–dyd–display-memor
- dt (Display Type): https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/dt–display-type-
- MANAGED DEBUGGING with WINDBG. Managed Heap. Part 1: https://docs.microsoft.com/fr-fr/archive/blogs/alejacma/managed-debugging-with-windbg-managed-heap-part-1
- MANAGED DEBUGGING with WINDBG. Managed Heap. Part 2: https://docs.microsoft.com/fr-fr/archive/blogs/alejacma/managed-debugging-with-windbg-managed-heap-part-2
- StackOverflow: How to Enable User Mode Stack Trace Database for IMA Service to Detect Memory Leaks: https://support.citrix.com/article/CTX106970
- StackOverflow: Memory leak debuging with WinDbg for unmanaged code: https://stackoverflow.com/questions/51941880/memory-leak-debuging-with-windbg-for-unmanaged-code
- StackOverflow: What do the different columns in the “!heap -flt -s xxxx” windbg command represent: https://stackoverflow.com/questions/6687414/what-do-the-different-columns-in-the-heap-flt-s-xxxx-windbg-command-represe
- StackOverflow: How to find all instances of types that implement a given interface during debugging: https://stackoverflow.com/questions/1304882/how-to-find-all-instances-of-types-that-implement-a-given-interface-during-debugg
- StackOverflow: Write windbg command output to file, but not console: https://stackoverflow.com/questions/46182387/write-windbg-command-output-to-file-but-not-console
- What does fields in method table mean in this example?: https://stackoverflow.com/questions/4104099/what-does-fields-in-method-table-mean-in-this-example
- StackOverflow: How do I use a dump file to diagnose a memory leak?: https://stackoverflow.com/questions/9514401/how-do-i-use-a-dump-file-to-diagnose-a-memory-leak
- StackOverflow: windbg dds – unable to get source where memory allocated: https://stackoverflow.com/questions/51178758/windbg-dds-unable-to-get-source-where-memory-allocated
- StackOverflow: How can I set a stack trace with Windbg?: https://stackoverflow.com/questions/22152883/how-can-i-set-a-stack-trace-with-windbg
- StackOverflow: GFlags – command lines: https://stackoverflow.com/questions/35209951/gflags-command-lines
- StackOverflow: Why can’t WinDBG find the mscordacwks.dll?: https://stackoverflow.com/questions/9129852/why-cant-windbg-find-the-mscordacwks-dll
Commandes WinDbg:
- Common WinDbg Commands: http://windbg.info/doc/1-common-cmds.html
- WinDbg cheatsheet: https://github.com/hugsy/defcon_27_windbg_workshop/blob/master/windbg_cheatsheet.md
- windbg command summary: https://k1rha.tistory.com/entry/windbg-command-sheet-sheet
- SOS Cheat Sheet: https://blogs.msdn.microsoft.com/alejacma/2009/06/30/sos-cheat-sheet-net-2-03-03-5/
- WinDbg cheat sheet: https://theartofdev.com/windbg-cheat-sheet/
Précisions .NET:
- Type Loader Design: https://github.com/dotnet/coreclr/blob/master/Documentation/botr/type-loader.md#key-data-structures
- x86 Assembly Guide: https://www.cs.virginia.edu/~evans/cs216/guides/x86.html