Les "dumps" mémoire en 5 min

Lorsqu’un bug se produit en production, il n’est pas toujours facile de reproduire le problème sur une plateforme de dévelopement pour le corriger ensuite. Sur certaines applications, il peut être récurrent d’échouer à trouver le scénario exact qui permet de révéler le bug. Si ce problème se présente régulièrement, une possibilité est de générer un "dump" mémoire en cas de crash de façon à pouvoir l’analyser par la suite et identifier de façon plus précise l’origine du bug.

1. 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ébogueur 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écisemment la ligne de code qui a menée au crash.

A. 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. "callstack") de tous les "threads": on peut savoir précisemment 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éboguer.
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.

B. 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.

Le terme "mini dump" prête à confusion

"Mini dump" laisse penser que les "dumps" de ce type sont moins volumineux que les "full dumps". Un "mini dump" peut être plus volumineux et plus complet qu’un "full dump" car il donne la possibilité de choisir les éléments qui seront y stockés. Il est notamment de choisir des éléments plus volumineux que pour le “full dump”.

2. Capturer un "dump"

On peut capturer un "dump" de toute la mémoire ou d’un processus à un moment déterminé. Sachant qu’il est difficile de prévoir un crash, il ne sera pas aisé de trouver le moment où il faudra commencer la capture.

Ainsi les outils de capture de "dump" peuvent scruter un processus et effectue un "dump" quand certaines conditions sont remplies:
– si le processus provoque une activité trop élevée du processeur;
– si une fenêtre du processus reste bloquée pendant un certain temps;
– si le processus s’arrête etc…

Différents outils permettent de capturer des "dumps":

A. Gestionnaire de tâches (i.e. "task manager")

Le gestionnaire de tâches permet de capturer des "dumps" à la demande:
1. Ouvrir le gestionnaire de tâches: Ctrl + Majuscule + Echap.
2. Trouver le processus pour lequel on veut effectuer le "dump"
3. Clique droit puis sélectionner "Create dump file".
4. Le dump sera écrit dans un répertoire temporaire et le chemin sera indiqué dans une popup.

B. Visual Studio

A partir de Visual Studio 2010, on peut capturer un "dump" quand un processus est en cours de débug:
1. A partir du menu “Debug” en cliquant sur “Save Dump As…”.
2. Il est possible de sauvegarder le "dump" avec ou sans pile (“heap”).

Pour plus d’informations: Utiliser les fichiers de dump pour déboguer les pannes et les blocages d’application dans Visual Studio.

C. ProcDump

ProcDump appartient à la suite d’outils Windows Sysinternals.

Plus de détails sur ProcDump: https://technet.microsoft.com/en-us/sysinternals/dd996900.aspx.

Pour capturer un "dump" sans conditions:

procdump -ma [Name or PID]

Pour capturer un "dump" pour n’importe quelle exception (exception de plus bas niveau):

procdump -e 1 -ma [Name or PID]

Dans le cas d’une exception spécifique, dans l’exemple d’une exception de type System.NullReferenceException:

procdump -e 1 -f "System.NullReferenceException" -ma [Name or PID]

Dans le cas où le processus utilise plus de 500 Mo de mémoire:

procdump -m 500 -ma [Name or PID]

On peut déclencher la capture en fonction de la valeur d’un compteur de performances:
L’argument "-p \Process(Name_PID)\[counterName] [threshold]" permet d’indiquer un seuil pour une valeur spécifique du compteur de performance Windows (Windows Performance Counter).

Par exemple pour baser la valeur sur le nombre de "threads" du processus avec un seuil de déclenchement à 85 "threads", le processus ayant pour nom "w3mp" et pour PID "66666":

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.

D. API MiniDumpWriteDump

MiniDumpWriteDump est une fonction de la DLL DbgHelp.dll qui fait partie des "Debugging Tools For Windows". Le grand intérêt de cette fonction est de pouvoir être appelée par programmation:

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.IO;

namespace MiniDumpUtility
{
    public static class MiniDump
    {
        public static class MINIDUMP_TYPE
        {
            public const int MiniDumpNormal = 0x00000000;
            public const int MiniDumpWithDataSegs = 0x00000001;
            public const int MiniDumpWithFullMemory = 0x00000002;
            public const int MiniDumpWithHandleData = 0x00000004;
            public const int MiniDumpFilterMemory = 0x00000008;
            public const int MiniDumpScanMemory = 0x00000010;
            public const int MiniDumpWithUnloadedModules = 0x00000020;
            public const int MiniDumpWithIndirectlyReferencedMemory = 0x00000040;
            public const int MiniDumpFilterModulePaths = 0x00000080;
            public const int MiniDumpWithProcessThreadData = 0x00000100;
            public const int MiniDumpWithPrivateReadWriteMemory = 0x00000200;
            public const int MiniDumpWithoutOptionalData = 0x00000400;
            public const int MiniDumpWithFullMemoryInfo = 0x00000800;
            public const int MiniDumpWithThreadInfo = 0x00001000;
            public const int MiniDumpWithCodeSegs = 0x00002000;
        }

        [DllImport("dbghelp.dll")]
        public static extern bool MiniDumpWriteDump(IntPtr hProcess,
            Int32 ProcessId,
            IntPtr hFile,
            int DumpType,
            IntPtr ExceptionParam,
            IntPtr UserStreamParam,
            IntPtr CallackParam);

        public static void CreateMiniDump()
        {
            using(FileStream fs = new FileStream("dump.dmp", FileMode.Create))
            {
                using(System.Diagnostics.Process process = 
                  System.Diagnostics.Process.GetCurrentProcess())
                {
                    MiniDumpWriteDump(process.Handle,
                        process.Id,
                        fs.SafeFileHandle.DangerousGetHandle(),
                        MINIDUMP_TYPE.MiniDumpNormal,
                        IntPtr.Zero,
                        IntPtr.Zero,
                        IntPtr.Zero);
                }
            }
        }
    }
}

On peut appeler MiniDump.CreateMiniDump() dans le cas où des exceptions surviennent. Toutefois il est préférable de lancer la capture du "dump" à partir d’un processus séparé car la création du "dump" peut elle-même causer un crash si la situation est très critique.

Le détail des valeurs de l’enum MINIDUMP_TYPE se trouve sur: MINIDUMP_TYPE enumeration.
Les options les plus intéressantes sont: MiniDumpWithFullMemory, MiniDumpWithFullMemoryInfo, MiniDumpWithUnloadedModules et MiniDumpWithThreadInfo.

Déclenchement de la création du "dump" au moment d’une exception

On peut gérer différent type d’exceptions au travers de clauses try...catch:

try
{
  ...
}
catch (NotImplementedException e)
{
  ...
}
catch (NullReferenceException e)
{
  ...
}

Toutes les exceptions ne sont pas forcément capturées par cette clause try...catch, elles seront remontées dans l’AppDomain courant (AppDomain.CurrentDomain) jusqu’elles soient interceptées par le CLR qui stoppera l’application. Pour capturer un "dump" au moment de ces exceptions, on peut s’abonner à certains évènements:

L’évènement AppDomain.CurrentDomain.FirstChanceException qui est déclenché en première position avant que le runtime ne cherche dans la pile d’appels, du code qui pourrait intercepter l’exception:

AppDomain.CurrentDomain.FirstChanceException += 
	(sender, eventArgs) => MiniDump.CreateMiniDump();

Plus de détails sur AppDomain.CurrentDomain.FirstChanceException sur MSDN.

– Les exceptions non gérées survenant dans l’AppDomain:

AppDomain.CurrentDomain.UnhandledException += 
	(sender, eventArgs) => MiniDump.CreateMiniDump();

Plus de détails sur MSDN.

3. Comment lire un dump ?

A. Avec Visual Studio

A partir de Visual Studio 2010 et à condition que le Windows Driver Kit (WDK) soit installé, il est possible d’ouvrir un fichier de "dump" en faisant:
1. "Open | Crash Dump"
2. Sélectionner le fichier du "dump"
3. Cliquer sur "Open".

Cette solution permet de voir les piles d’appels, le code assembleur qui était en cours d’exécution au moment de la capture et des informations sur le "dump".

Toutefois pour rapprocher la pile d’appels du code source, et pouvoir plus efficacement savoir les lignes du code source qui étaient en cours d’exécution, il faut indiquer le chemin des fichiers de symboles (fichiers ".pdb") liés aux assemblies à déboguer. Il faut impérativement que les versions des ".pdb" soient les mêmes que celles des assemblies. Il n’est pas nécessaire de rajouter les fichiers ".pdb" pour toutes les assemblies, seuls ceux des assemblies à déboguer sont nécessaires.

Pour indiquer le chemin des fichiers de symboles, il faut cliquer sur "Find symbol (.pdb) files" puis rajouter les chemins des répertoires contenant les fichiers ".pdb".

Pour plus d’informations: Specify Symbol (.pdb) and Source Files in the Visual Studio Debugger.

B. Avec WinDbg

WinDbg est un outil très puissant pour obtenir des informations à partir d’un fichier de "dump". Il est difficile à utiliser cependant on arrive à obtenir plus d’informations qu’avec Visual Studio notamment pour les DLL non managés. Parfois, il peut s’avérer plus utile que Visual Studio à condition de connaître quelques commandes.

WinDbg fait partie de la suite d’outils Windows Development Toolkit (SDK). Pour obtenir WinDbg, il suffit de télécharger le SDK: https://msdn.microsoft.com/fr-FR/windows/hardware/dn913721.aspx#windbg-symbols.

Après installation, WinDbg se trouve à partir du menu Windows dans:
Windows Kits => Debugging Tools for Windows (x86 ou x64).
Il faut choisir la version correspondant à l’architecture sur laquelle le processus a été exécutée.

Charger le "dump"

Pour charger le "dump", il faut cliquer sur:
1. Cliquer sur File
2. Puis "Open Crash Dump"

Ajouter les fichiers de symboles

Pour ajouter les fichiers de symboles de façon à voir la pile d’exécution, il faut:
1. Cliquer sur File
2. Puis sur "Symbol File Path"
3. Tous les fichiers ne doivent pas être nécessairement dans le même répertoire. Ils peuvent être dans des répertoires différents dont on indique le chemin séparé par le caractère ";".
Par exemple:

C:\Repertoire1;D:\Repertoire2

4. Il faut rajouter les fichiers de symboles des assemblies du framework:

C:\Windows\Microsoft.NET\Framework\v4.0.30319

5. Il faut ensuite rajouter le chemin permettant d’accéder aux fichiers des assemblies du système. Ces fichiers seront téléchargés automatiquement par WinDbg au besoin. Le chemin correspond au répertoire dans lequel les fichiers seront copiés et l’adresse du serveur à partir duquel les fichiers seront téléchargés:

srv*c:\symbols*http://msdl.microsoft.com/download/symbols
Répertoire C:\symbols

Il faut créer ce répertoire à la main. Si il n’existe pas, le téléchargement des fichiers va échouer. Même si on indique un autre répertoire, il faut que "C:\symbols" soit créé et qu’il soit accessible en écriture.

Pour résumer, dans le cas notre exemple, les chemins seront:

C:\Repertoire1;D:\Repertoire2;srv*c:\symbols*http://msdl.microsoft.com/download/symbols;C:\Windows\Microsoft.NET\Framework\v4.0.30319

Charger les fichiers de symboles

Généralement c’est durant cette étape qu’on peut voir s’il manque des fichiers de symboles, si leur version n’est pas correcte ou si une autre erreur liée au chargement, se produit.

Pour augmenter le niveau de log pour voir tous les feedbacks liés au chargement des "symbols":

!sym noisy

Pour revenir à un niveau de log normal:

!sym quiet

Pour charger ou recharger les fichiers de symboles:

.reload /i /f [nom de l'assembly]

Commandes principales

Quelques commandes utiles pour explorer le "dump".

S’il y a plusieurs "threads", on peut voir la pile d’exécution pour tous les "threads" en tapant:

∼*k

Pour sélectionner un "thread" particulier:

∼[numéro du thread]s

Pour voir la pile d’exécution du "thread" sélectionné:

kb

Pour effacer l’écran:

.cls

Pour effectuer une analyse de l’exception:

!analyze -v -f

Avoir des informations sur les assemblies connexes

Pour choisir les bons fichiers de symboles, il peut être utile de vérifier les informations sur les assemblies chargés dans le processus au moment du dump.

Ainsi pour avoir des infos générales:

!peb

Pour avoir les versions des assemblies:

lm -v

Pour avoir le détail de toutes les commandes Windbg:
windbg.info/doc/1-common-cmds.html

Utiliser Sosex

Sosex est une extension à WinDbg qui permet d’obtenir, en particulier, des piles d’exécution plus complètes. Il est possible de télécharger cette extension suivant l’architecture sur: http://www.stevestechspot.com.

Dans le fichier readme.txt, on peut voir les commandes spécifiques à Sosex.

Pour charger Sosex:

.load [chemin de la DLL]

Pour voir la pile d’exécution du thread sélectionné avec Sosex:

!mk

Pour voir les piles d’exécution pour tous les threads:

∼*e!mk
One response... add one

Leave a Reply