Writing Minidumps in C#

This is part of a related series of posts:

the first in what I intend to be a group of related posts, about exceptions and error handling in C#.

Minidumps are a mechanism for "post-mortem debugging" - debugging your application after it is "dead".  A minidump is a snapshot of the memory of your application, typically taken when it is has encountered a fatal error.  Various debuggers support loading minidumps and "debugging" with them, which really means just exploring them, since you can't do things like single-step or change the value of variables when the program isn't running any longer.  The .Net Framework 4.0 and Visual Studio 2010 finally bring easy minidump debugging to C# code.

In Windows, a minidump is created through the MiniDumpWriteDump API https://msdn.microsoft.com/en-us/library/ms680360(VS.85).aspx.  Since that is a native API, we need some interop code in order to write minidumps from C#.  Jochen Kalmbach has done the hard work in his short but excellent post https://blog.kalmbach-software.de/2008/12/13/writing-minidumps-in-c/.  I've taken his example and only made some minor changes, resulting in this...

 

 using System;

using System.Diagnostics;

using System.Runtime.InteropServices;





public static class MiniDump

{

    // Taken almost verbatim from https://blog.kalmbach-software.de/2008/12/13/writing-minidumps-in-c/

    [Flags]

    public enum Option : uint

    {

        // From dbghelp.h:

        Normal = 0x00000000,

        WithDataSegs = 0x00000001,

        WithFullMemory = 0x00000002,

        WithHandleData = 0x00000004,

        FilterMemory = 0x00000008,

        ScanMemory = 0x00000010,

        WithUnloadedModules = 0x00000020,

        WithIndirectlyReferencedMemory = 0x00000040,

        FilterModulePaths = 0x00000080,

        WithProcessThreadData = 0x00000100,

        WithPrivateReadWriteMemory = 0x00000200,

        WithoutOptionalData = 0x00000400,

        WithFullMemoryInfo = 0x00000800,

        WithThreadInfo = 0x00001000,

        WithCodeSegs = 0x00002000,

        WithoutAuxiliaryState = 0x00004000,

        WithFullAuxiliaryState = 0x00008000,

        WithPrivateWriteCopyMemory = 0x00010000,

        IgnoreInaccessibleMemory = 0x00020000,

        ValidTypeFlags = 0x0003ffff,

    };



    public enum ExceptionInfo

    {

        None,

        Present

    }



    //typedef struct _MINIDUMP_EXCEPTION_INFORMATION {

    //    DWORD ThreadId;

    //    PEXCEPTION_POINTERS ExceptionPointers;

    //    BOOL ClientPointers;

    //} MINIDUMP_EXCEPTION_INFORMATION, *PMINIDUMP_EXCEPTION_INFORMATION;

    [StructLayout(LayoutKind.Sequential, Pack = 4)]  // Pack=4 is important! So it works also for x64!

    public struct MiniDumpExceptionInformation

    {

        public uint ThreadId;

        public IntPtr ExceptionPointers;

        [MarshalAs(UnmanagedType.Bool)]

        public bool ClientPointers;

    }



    //BOOL

    //WINAPI

    //MiniDumpWriteDump(

    //    __in HANDLE hProcess,

    //    __in DWORD ProcessId,

    //    __in HANDLE hFile,

    //    __in MINIDUMP_TYPE DumpType,

    //    __in_opt PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,

    //    __in_opt PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,

    //    __in_opt PMINIDUMP_CALLBACK_INFORMATION CallbackParam

    //    );



    // Overload requiring MiniDumpExceptionInformation

    [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]

    static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, ref MiniDumpExceptionInformation expParam, IntPtr userStreamParam, IntPtr callbackParam);



    // Overload supporting MiniDumpExceptionInformation == NULL

    [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]

    static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, IntPtr expParam, IntPtr userStreamParam, IntPtr callbackParam);



    [DllImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)]

    static extern uint GetCurrentThreadId();



    public static bool Write(SafeHandle fileHandle, Option options, ExceptionInfo exceptionInfo)

    {

        Process currentProcess = Process.GetCurrentProcess();

        IntPtr currentProcessHandle = currentProcess.Handle;

        uint currentProcessId = (uint)currentProcess.Id;

        MiniDumpExceptionInformation exp;

        exp.ThreadId = GetCurrentThreadId();

        exp.ClientPointers = false;

        exp.ExceptionPointers = IntPtr.Zero;

        if (exceptionInfo == ExceptionInfo.Present)

        {

            exp.ExceptionPointers = System.Runtime.InteropServices.Marshal.GetExceptionPointers();

        }

        bool bRet = false;

        if (exp.ExceptionPointers == IntPtr.Zero)

        {

            bRet = MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint)options, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);

        }

        else

        {

            bRet = MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint)options, ref exp, IntPtr.Zero, IntPtr.Zero);

        }

        return bRet;

    }



    public static bool Write(SafeHandle fileHandle, Option dumpType)

    {

        return Write(fileHandle, dumpType, ExceptionInfo.None);

    }

}

 

 

The most significant change I made was to add an override for the MiniDumpWriteDump interop declaration, that supports passing NULL (aka IntPtr.Zero) for the expParam parameter.  The reason isn't applicable to this particular blog post since we aren't (yet) attempting to store "exception information" in the minidump, but what I found was that on my Windows 7 x64 machine, under particular circumstances the test program running as x86 under WOW64, the Marshall.GetExceptionPointers method would return IntPtr.Zero, while running the test program as x64 the method would return a well-formed exception pointer.  And when the ExceptionPointers field of expParam was IntPtr.Zero, the MiniDumpWriteDump call would have an Access Violation, the solution to which was to pass expParam as IntPtr.Zero.  So the two overloads of MiniDumpWriteDump support either a non-existent expParam (passed as IntPtr.Zero), or a well-formed expParam passed as a ref MiniDumpExceptionInformation with the ExceptionPointers field set to the return value from Marshall.GetExceptionPointers).

Of course we need an example of how to make the call to create a minidump...

         static void MainlineTest()

        {

            string fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "MiniDumpDemo_Mainline.mdmp");

            using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Write))

            {

                MiniDump.Write(fs.SafeFileHandle, MiniDump.Option.WithFullMemory);

            }

        }
 

The result is a file called minidump_mainline.dmp in your My Documents folder.  If you open that file in Visual Studio 2010, you'll get a page that displays information about the minidump.  On the right is a link to Debug With Mixed, which will start the debugger against the minidump.  At that point you can explore the call stack and inspect variables and fields.  The callstack will show execution as being in the middle of the call MiniDump.Write...

   ntdll.dll!NtGetContextThread()  + 0xa bytes 

    ntdll.dll!string "Dereferencing"()  

    [Managed to Native Transition]  

>    MiniDumpDemo.exe!MiniDump.Write(System.Runtime.InteropServices.SafeHandle fileHandle = {Microsoft.Win32.SafeHandles.SafeFileHandle}, MiniDump.Option options = WithFullMemory, MiniDump.ExceptionInfo exceptionInfo = None) Line 94 + 0x42 bytes    C#

    MiniDumpDemo.exe!MiniDump.Write(System.Runtime.InteropServices.SafeHandle fileHandle = {Microsoft.Win32.SafeHandles.SafeFileHandle}, MiniDump.Option dumpType = WithFullMemory) Line 105 + 0x11 bytes   C#

    MiniDumpDemo.exe!MiniDumpDemo.Program.MainlineTest() Line 73 + 0x28 bytes   C#

    MiniDumpDemo.exe!MiniDumpDemo.Program.Main(string[] args = {string[0]}) Line 81 + 0x5 bytes C#

    [Native to Managed Transition]  

    mscoreei.dll!_CorExeMain()  + 0x49 bytes    

    mscoree.dll!_CorExeMain_Exported()  + 0x69 bytes    

    kernel32.dll!BaseThreadInitThunk()  + 0xd bytes 

    ntdll.dll!RtlUserThreadStart()  + 0x21 bytes    

In the next post, writing minidumps from inside exception handlers to capture as much information as possible about why the application is failing.