Capturer et lire un dump mémoire peut être très utile pour aider à comprendre l’origine d’un crash ou d’une erreur survenue dans un autre environnement qu’une machine de développement. Le but de cet article est de montrer comment on peut facilement capturer un dump mémoire et de le lire directement dans Visual Studio.
Qu’est-ce qu’un dump ?
Contenu d’un dump
Différents types de “dumps”
Capturer un dump
Gestionnaire de tâches (i.e. Task Manager)
Visual Studio
dotnet-dump
ProcDump
Lire un dump avec Visual Studio
Définir les chemin des fichiers de symboles
Passer en mode debug
Afficher le code source en C#
Partie “Analyse” en cas d’exception
Lorsqu’une exception ou une erreur survient dans une application .NET déployée sur une machine de production, il n’est pas toujours facile de comprendre l’origine du problème pour plusieurs raisons:
- L’environnement de production n’est pas tout à fait le même que la machine sur laquelle le développement a été effectué. Souvent la charge ou les données de l’application en production sont assez différentes pour qu’un cas de figure soit difficilement reproductible dans un environnement de développement ou de test.
- Le plus souvent, on observe les conséquences d’une erreur et il faut trouver la cause d’un problème à partir de ses conséquences. Par exemple, on constate qu’un comportement est différent de celui attendu, qu’un exécutable a crashé ou on lit les détails d’une exception dans des logs. Ces éléments permettent de donner des indices sur l’erreur survenue sans toutefois indiquer précisément les conditions ayant menées au problème.
- Il est généralement difficile, voir impossible, de débuguer dans l’environnement de production.
Ainsi une possibilité pour comprendre plus finement l’état d’un exécutable au moment d’un crash ou d’une exception, est de capturer un dump mémoire dans l’environnement de production et de le lire avec Visual Studio sur une machine de développement.
Qu’est-ce qu’un dump ?
Un dump mémoire d’un processus correspond à une copie du contenu de la mémoire virtuelle (pile, tas managé, pile d’appels des différents threads etc…). Un débugueur peut écrire le contenu de la mémoire virtuelle dans un fichier sur le disque de façon à pouvoir le lire plus tard. Avec les sources, on pourra ensuite lire le dump et voir une instance “gelée” du processus de façon à identifier plus précisement la ligne de code qui a menée au crash.
Contenu d’un dump
Un dump peut contenir:
- La pile en mémoire: contient les objets et variables créés par un processus.
- Pile d’exécution (i.e. call stack) de tous les threads: on peut savoir précisement les fonctions qui étaient exécutées au moment du dump.
- Blocs de l’environnement des threads: contient des informations sur les threads en cours d’exécution de façon à en connaître l’état et le thread ID.
- Code assembleur: dans le pire des cas, on peut avoir à lire le code assembleur. Cette solution est généralement trop fastidieuse et trop couteuse. Toutefois en rapprochant le dump des fichiers de symboles
.pdb
, on peut avoir les piles d’appels par rapport au code source, ce qui est plus facile pour débuguer. - Information sur les modules: le processus charge souvent plusieurs assemblies. Le dump permet d’avoir des informations sur les dépendances qui ont été chargées notamment la version des assemblies.
Différents types de “dumps”
Il existe des types différents de dumps suivant les informations qu’il contient:
- Full dump: les “full memory dumps” contiennent tout le contenu de la mémoire virtuelle. Ce type de dump est particulièrement utile lorsqu’on a aucune idée de l’origine du problème. L’inconvénient majeur de ce dump est qu’il faut du temps pour le collecter. Si le serveur à partir duquel on récupère le dump est saturé, la collecte pourrait encore ralentir l’exécution des processus.
- Mini dump: ce type de dump concerne un processus spécifique et est configurable de façon à choisir les informations qu’il contiendra.
Capturer un dump
Plusieurs outils permettent de capturer des dumps d’un processus:
- Le gestionnaire de tâches (i.e. Task Manager): assez pratique car il est présent sur tous les systèmes Windows.
- Visual Studio: après s’être attaché à un processus, on peut générer un dump.
- dotnet-dump: l’intérêt de cet outil est qu’il est possible de l’installer à la ligne de commande avec
dotnet
. Ce qui le rend disponible facilement sur toutes les plateformes. - ProcDump: c’est une espèce de debugger qu’on peut attacher à un processus. Cet outil peut monitorer un processus en scrutant certaines métriques. En cas de dépassement d’une métrique, il peut générer un dump automatiquement.
Gestionnaire de tâches (i.e. Task Manager)
Le gestionnaire de tâches permet de capturer des dumps à la demande. Le plus gros intérêt du gestionnaire de tâche est qu’il est présent directement sur tous les systèmes Windows, il n’est donc pas nécessaire de l’installer.
Pour capturer un dump, il faut:
- Ouvrir le gestionnaire de tâches: [Ctrl] + [Maj] + [Echap].
- Trouver le processus pour lequel on veut effectuer le dump
- Clique droit puis sélectionner “Create dump file”.
- Le dump sera écrit dans un répertoire temporaire et le chemin sera indiqué dans une popup.
Visual Studio
On peut capturer un dump avec Visual Studio en effectuant les étapes suivantes:
- S’attacher à un processus en cours d’exécution en cliquant sur “Debug” puis “Attach to process…”
- Cliquer sur “Pause” pour stopper l’exécution du processus ou cliquer sur “Debug” puis “Break All”:
- Cliquer sur “Debug” puis “Save Dump as…”
dotnet-dump
dotnet-dump peut être installé avec la commande dotnet
à partir d’un package Nuget:
dotnet tool install --global dotnet-dump
Pour capture un dump, il suffit d’exécuter:
dotnet-dump.exe collect -p <PID du processus>
Plus de précisions concernant dotnet-dump sur: learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-dump?WT.mc_id=DT-MVP-5003978#install
ProcDump
ProcDump est une espèce de débugger qui peut monitorer un processus et générer un dump lorsque certaines conditions sont réunies. Il faut avoir en tête que comme il s’agit d’un débugger, l’utiliser peut avoir certains inconvénients:
- Il ralentit un peu l’exécution du processus. Le plus souvent, on ne s’en rend pas compte mais pour des processus exigeants qui utilisent beaucoup des capacités de la machine, il peut y avoir un impact.
- Lorsque ProcDump monitore un processus, il n’est plus possible d’attacher ce processus à Visual Studio pour le débugguer.
On peut aussi utiliser ProcDump simplement pour générer un dump mémoire sans utiliser la fonctionnalité de monitoring.
ProcDump appartient à la suite d’outils Windows Sysinternals (plus de détails sur ProcDump sur: learn.microsoft.com/fr-fr/sysinternals/downloads/procdump).
Pour capturer un dump sans conditions:
procdump -ma <nom processus ou PID>
Pour capturer un dump pour n’importe quelle exception (exception de plus bas niveau):
procdump -e 1 -ma <nom processus ou PID>
Dans le cas d’une exception spécifique, par exemple d’une exception de type System.NullReferenceException
:
procdump -e 1 -f "System.NullReferenceException" -ma <nom processus ou PID>
Dans le cas où le processus utilise plus de 500 Mo de mémoire:
procdump -m 500 -ma <nom processus ou PID>
On peut déclencher la capture en fonction de la valeur d’un compteur de performances, par exemple l’argument -p \Process(Name_PID)\<counterName> <threshold>
permet d’indiquer un seuil pour une valeur spécifique d’un compteur de performance Windows (Windows Performance Counter).
Par exemple, pour baser le seuil de génération sur le nombre de threads d’un processus avec un seuil de déclenchement à 85 threads, le processus ayant pour nom "w3mp"
et pour PID "66666"
, on peut utiliser les arguments suivants:
procdump -p "\Process(w3wp_66666)\Thread Count" 85 -ma 66666
Il est recommandé d’utiliser le nom et le PID pour désigner le processus pour lequel on veut effectuer la capture. Dans le cas où 2 processus ont le même nom, l’utilisation seule du nom pour désigner le processus peut mener à la capture d’un autre processus.
Lire un dump avec Visual Studio
Visual Studio permet de facilement analyser et lire un dump. Un autre outil plus puissant permet de lire un dump et est plus puissant comme WinDbg mais il est beaucoup plus difficile à utiliser.
D’autres articles présentent quelques fonctionnalités de WinDbg:
- Lire un dump avec WinDbg
- Trouver l’origine d’une fuite mémoire avec WinDbg
On peut installer WinDbg en suivant: learn.microsoft.com/fr-fr/windows-hardware/drivers/debugger/.
Après installation, il se trouve dans C:\Users\<Windows user>\AppData\Local\Microsoft\WindowsApps\WinDbgX.exe
D’autres infos concernant l’interface sur: learn.microsoft.com/fr-fr/windows-hardware/drivers/debuggercmds/windbg-overview.
Pour illustrer comment on peut lire un fichier dump directement à partir de Visual Studio, on va:
- Générer un dump d’un exécutable,
- Ouvrir ce fichier dans Visual Studio
- Indiquer les manipulations nécessaires pour récupérer quelques informations du dump.
On considère le code suivant permettant d’incrémenter un entier dans une task et de récupérer la valeur de cet entier périodiquement:
internal class BasicCounter
{
private Task runningTask;
private long counter;
private CancellationToken cancellationToken;
private EventWaitHandle waitingHandle = new AutoResetEvent(false);
public BasicCounter(CancellationToken cancellationToken)
{
this.cancellationToken = cancellationToken;
this.counter = 0;
}
public void Launch()
{
this.runningTask = Task.Run(() => {
while (!this.cancellationToken.IsCancellationRequested)
{
Interlocked.Increment(ref counter);
waitingHandle.Set();
}
});
}
public long GetCounterValue()
{
this.waitingHandle.WaitOne();
return this.counter;
}
}
On affiche la valeur de cet entier dans le main
:
public static class Program
{
public static void Main()
{
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
var counter = new BasicCounter(cancellationTokenSource.Token);
counter.Launch();
while (true)
{
long counterValue = counter.GetCounterValue();
if (counterValue % 10000 == 0)
Console.WriteLine(counterValue);
}
}
}
Comme l’incrémentation du nombre est faite dans une task séparée, cela donnera la possibilité d’observer plusieurs piles d’appels (i.e. call stack).
On exécute ce code, puis on capture un fichier dump à partir du gestionnaire de tâches (i.e. Task Manager):
- On appuie simultanément sur les touches [Ctrl] + [Maj] + [Echap].
- Il faut chercher le processus par nom dans l’onglet “Details”, faire un clique droit sur “Create a dump file”.
- La popup indiquera l’emplacement du fichier dump.
Pour lire le contenu d’un dump avec Visual Studio, il suffit d’ouvrir directement le fichier avec Visual Studio. Une fois ouvert, Visual Studio affichera un onglet “Minidump file summary” contenant les caractéristiques du dump:
Cet onglet présente les caractéristiques du processus, les dépendances de l’exécutable et les actions possibles qu’il est possible d’effectuer pour analyser le dump (dans le coin en haut à droite):
Comme ce dump a été capturé au cours de l’exécution du processus, il n’y a pas d’informations concernant une éventuelle exception.
Définir les chemin des fichiers de symboles
La 1ère étape consiste à paramétrer les chemins des fichiers de symboles pour les assemblies ou DLL système ou du framework (fichiers .pdb
). Les fichiers de symboles contiennent des informations concernant le code des fichiers de dépendance comme le nom des objets, leur adresse dans le module, le type des variables, les signatures des fonctions etc… Ces informations permettent de débugguer le processus.
Il suffit de cliquer sur “Set symbol paths” dans l’onglet “Actions”:
Il faut ensuite cocher:
- Microsoft Symbol Servers: pour que le debugguer récupère les fichiers de symboles pour les assemblies ou DLL système ou du framework.
- NuGet.org Symbol Server: pour les fichiers des dépendances Nuget.
- Ajouter les chemins des fichiers de symboles pour le code de l’application.
Le chemin indiqué dans la partie “Cache symbols in this directory” sera le répertoire dans lequel sera téléchargé les fichiers de symboles.
A la lecture d’un dump mémoire, il faut que les fichiers de symboles soient de la même version que les fichiers dont sont issus le dump. Le risque si les fichiers ne correspondent pas est que les lignes indiquées lors de débuggage ne correspondent pas. L’idéal est d’utiliser les fichiers de symboles issus du même build, des fichiers provenant d’un build en debug ne sont pas les mêmes que des fichiers issues d’un build en release (les optimisations de code n’étant pas complêtement appliquées en mode debug).
Si les fichiers contenant le code source ne correspondent pas au processus dont le dump est issu, une erreur de ce type pourrait être affichée:
Passer en mode debug
Suivant le type de code exécuté par le processus, on peut débugger suivant des modes différents:
- “Debug with managed only” permet de limiter le debug au code managé uniquement lorsque cela est possible. Tous les dumps ne contiennent pas forcément les informations permettant de débugguer dans ce mode. Dans ce cas on peut avoir l’erreur suivante:
Dans ce mode, il est possible d’obtenir des informations sur les tasks exécutés et les piles d’appels avec le code source.
- “Debug with mixed”: ce mode correspond aux assemblies mixtes c’est-à-dire les assemblies contenant du code managé MSIL et du code natif. Dans le code managé, on peut voir les piles d’appels avec le code source et les tasks en cours d’exécution. Pour le code natif, on ne peut voir que le code assembleur où il est plus difficile d’analyser le code en cours d’exécution.
- “Debug with Native only”: ce mode limite le debug au code natif uniquement. Le code managé n’est pas traité. Lorsque le dump ne contient pas d’information sur le code managé, on ne peut utiliser que ce mode ou le mode mixed.
A ce stade, on peut afficher les threads ou les tasks en cours d’exécution (cliquer sur “Debug” ⇒ “Window” ⇒ “Threads” ou sur “Debug” ⇒ “Window” ⇒ “Tasks” pour afficher le panneau suivant):
On peut voir une vue différente des threads et tasks en affichant le panneau “Parallel Stacks” (cliquer sur “Debug” ⇒ “Window” ⇒ “Parallel Stacks”):
En sélectionnant “Threads” puis en cliquant sur le bon thread par exemple Program.Main
, on peut voir la ligne à partir de laquelle le dump a été générée.
Si le dump a été capturé sur une autre machine que la machine sur laquelle on effectue le debug, l’activation du debug dans un mode managé (Managed ou Mixed) n’affichera pas le code source en C#. On ne verra, à ce stade, que le code assembleur.
Afficher le code source en C#
Pour voir le code source en C#, il faut préciser l’emplacement du code source si ce n’est pas fait automatiquement:
- Afficher les panneaux “Solution Explorer”, “Threads“, “tasks” et “Call Stack“.
Pour les afficher:- “Solution Explorer”: cliquer sur View ⇒ Solution Explorer
- Threads, tasks et Call Stack: cliquer sur “Debug” ⇒ “Windows”
- Dans le panneau “Solution Explorer”, il faut sélectionner la solution en cliquant sur son nom puis cliquer sur la clé pour “Properties”:
Ensuite il faut préciser le chemin des fichiers du code source en cliquant sur “Debug Source Code” puis en indiquant tous les répertoires:
Ensuite si on affiche les panneaux “Parallel Stacks”, “Stacks” ou “Call Stack”, on peut ensuite cliquer sur la ligne du dernier appel de façon à voir le code source correspondant.
Partie “Analyse” en cas d’exception
Cette partie permet d’aider à l’analyse d’un dump mémoire en identifiant le thread dans lequel une exception pourrait s’être produite. Cette fonctionnalité permet d’indiquer le code ayant provoqué l’exception. Avoir la ligne où une exception a été lancée ne veut pas forcément dire qu’on va comprendre immédiatement ce qui a provoqué l’exception.
Si on considère le code suivant:
public static class Program
{
public static void Main()
{
Console.WriteLine("Waiting...");
Console.ReadLine();
throw new NullReferenceException();
}
}
Et si on lance ce code, l’exécution va s’interrompre à la ligne Console.ReadLine()
. On s’attache alors au processus avec ProcDump.exe
en lançant une ligne de ce type:
procdump.exe -e 1 -f "System.NullReferenceException" -ma SimpleCounter.exe
Comme expliqué plus haut, ProcDump s’attache au processus en attendant qu’une exception de type System.NullReferenceException
soit lancée:
ProcDump v11.0 - Sysinternals process dump utility Copyright (C) 2009-2022 Mark Russinovich and Andrew Richards Sysinternals - www.sysinternals.com Process: SimpleCounter.exe (26416) Process image: C:\MyStuff\Dev\MISC\SimpleCounterExample\bin\Debug\net8.0\SimpleCounter.exe CPU threshold: n/a Performance counter: n/a Commit threshold: n/a Threshold seconds: n/a Hung window check: Disabled Log debug strings: Disabled Exception monitor: First Chance+Unhandled Exception filter: [Includes] *System.NullReferenceException* [Excludes] Terminate monitor: Disabled Cloning type: Disabled Concurrent limit: n/a Avoid outage: n/a Number of dumps: 1 Dump folder: C:\MyStuff\Dev\MISC\SimpleCounterExample\bin\Debug\net8.0\ Dump filename/mask: PROCESSNAME_YYMMDD_HHMMSS Queue to WER: Disabled Kill after dump: Disabled
Si on poursuit l’exécution du processus en cliquant sur [Enter], l’exception est lancée et procdump génère le dump:
[12:53:59] Exception: E0434352.CLR [12:53:59] Unhandled: E0434352.CLR [12:53:59] Dump 1 initiated: C:\MyStuff\Dev\MISC\SimpleCounterExample\bin\Debug\net8.0\SimpleCounter.exe_250405_125359.dmp [12:53:59] Dump 1 writing: Estimated dump file size is 122 MB. [12:53:59] Dump 1 complete: 122 MB written in 0.8 seconds [12:54:00] Dump count reached.
Si on ouvre le dump avec Visual Studio, on peut analyser le dump directement:
- Ouvrir l’onglet “Diagnostic Analysis” en allant dans “Debug” ⇒ “Windows” ⇒ “Diagnostic Analysis”:
- En cliquant sur “Analyze”, VisualStudio va indiquer directement la ligne où l’exception a été lancée. Il faut ajouter le chemin correspondant aux fichiers du code source pour voir la ligne dans le fichiers
.cs
sinon c’est la code assembleur qui est présenté (voir Afficher le code source en C# pour ajouter les chemins correspondant aux fichiers du code source).
- Dump files in the Visual Studio debugger: https://learn.microsoft.com/en-us/visualstudio/debugger/using-dump-files
- ProcDump v11.0: https://learn.microsoft.com/fr-fr/sysinternals/downloads/procdump
- Creating process dumps with ProcDump: https://care.acronis.com/s/article/Creating-Process-Dumps-with-ProcDump?language=fr
- How Can I Create A Dump File of a Running Process in Linux?: https://superuser.com/questions/401182/how-can-i-create-a-dump-file-of-a-running-process-in-linux
- How to Capture a Memory Dump for a Specific Process using ProcDump and Debug with Visual Studio: https://gist.github.com/randyburden/3e0d6a10699ba1b43029eacc66bfd504
- Debugging dump files created on another machine: https://wallaceturner.azurewebsites.net/debugging-dump-files-created-on-another-machine
- How to generate a dump file of a .NET application: https://www.meziantou.net/how-to-generate-a-dump-file-of-a-dotnet-application.htm
- Debugging dump files in Visual Studio: https://stackoverflow.com/questions/4699285/debugging-dump-files-in-visual-studio
- SOS.dll (extension de débogage SOS): https://learn.microsoft.com/fr-fr/dotnet/framework/tools/sos-dll-sos-debugging-extension
- Télécharger le Kit de développement logiciel pour Windows (WDK): https://learn.microsoft.com/fr-fr/windows-hardware/drivers/download-the-wdk#arm64-support
- Qu’est-ce que WinDbg ?: https://learn.microsoft.com/fr-fr/windows-hardware/drivers/debuggercmds/windbg-overview
- Installer le débogueur Windows: https://learn.microsoft.com/fr-fr/windows-hardware/drivers/debugger/
- Dump collection and analysis utility (dotnet-dump): https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-dump?WT.mc_id=DT-MVP-5003978#install
- Using ProcDump to obtain the dump of a service?: https://stackoverflow.com/questions/9016490/using-procdump-to-obtain-the-dump-of-a-service
- Public and Private Symbols: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/public-and-private-symbols