Share via


Writing Minidumps from Exceptions in C#

 

This is part of a related series of posts:

Minidumps are about debugging, and when you write in C# the vast majority of times that you need to debug, it will be because of some exception that was thrown.  So let’s see what is involved in capturing information about Exceptions in a minidump.

First a simple test method that creates a vaguely interesting callstack and throws an exception…

    1: public static void CreateStackWithException(int level = 1)
    2: {
    3:     bool cleanedUp = false;
    4:     try
    5:     {
    6:         if (level == 6)
    7:             throw new Exception();
    8:         CreateStackWithException(level + 1);
    9:     }
   10:     finally
   11:     {
   12:         cleanedUp = true;
   13:     }
   14: }

The CreateStackWithException method calls itself recursively until the level is 6, at which point is throws an Exception object.  (The coding guidelines say that you shouldn’t throw plain Exception objects but of course this is just for exploring behavior so it doesn’t matter in this case.)  The method also has a finally block which cleans up some (very simple) state, to illustrate how that interacts with exception handling and  minidumps.

Now a simple method to invoke CreateStackWithException and catch the exception and write a minidump.

    1: static void CatchTest()
    2: {
    3:     try
    4:     {
    5:         CreateStackWithException();
    6:     }
    7:     catch (Exception ex)
    8:     {
    9:         string stackTrace = ex.StackTrace;
   10:         string fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "MiniDumpDemo_Catch.mdmp");
   11:         using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Write))
   12:         {
   13:             MiniDump.Write(fs.SafeFileHandle, MiniDump.Option.WithFullMemory, MiniDump.ExceptionInfo.Present);
   14:         }
   15:     }
   16: }

The CatchTest method calls CreateStackWithException.  It catches the exception, grabs a local copy of the Exception.StackTrace property (you’ll see why in a bit), and then writes a minidump to a file called MiniDumpDemo_Catch.mdmp in your My Documents folder.

Once we execute that and load the minidump into Visual Studio 2010 and Debug with Mixed, we see this popup…

ExceptionDialog

We get that pop-up because we included exception pointers in the minidump.

Except…  I am running Windows 7 x64, and when I run this test as a 64-bit process, I get the exception pop-up.  But when I run it as a 32-bit process I don’t get the pop-up.  I investigated, and the call to System.Runtime.InteropServices.Marshal.GetExceptionPointers() returns null in the catch block, when running as a 32-bit process.  The low-level mechanisms for raising exceptions are very different between 32-bit and 64-bit processes, so that is probably the cause.

Let’s look at the callstack as displayed in the debugger.

    1: ntdll.dll!NtGetContextThread()  + 0xa bytes    
    2: ntdll.dll!string "Dereferencing"()     
    3: [Managed to Native Transition]    
    4: MiniDumpDemo.exe!MiniDump.Write(System.Runtime.InteropServices.SafeHandle fileHandle = {Microsoft.Win32.SafeHandles.SafeFileHandle}, MiniDump.Option options = WithFullMemory, MiniDump.ExceptionInfo exceptionInfo = Present) Line 98 + 0x40 bytes    C#
    5: MiniDumpDemo.exe!MiniDumpDemo.Program.CatchTest() Line 63 + 0x2e bytes    C#
    6: MiniDumpDemo.exe!MiniDumpDemo.Program.Main(string[] args = {string[0]}) Line 80 + 0x5 bytes    C#
    7: [Native to Managed Transition]    
    8: mscoreei.dll!_CorExeMain()  + 0x49 bytes    
    9: mscoree.dll!_CorExeMain_Exported()  + 0x69 bytes    
   10: kernel32.dll!BaseThreadInitThunk()  + 0xd bytes    
   11: ntdll.dll!RtlUserThreadStart()  + 0x21 bytes    

That’s not actually a very good result:  The exception happened within several levels of recursion inside the CreateStackWithException method, but that method doesn’t appear on the callstack captured in the debugger at all.  All of that context has been lost, making it much harder for us to use the minidump to determine the cause of the exception.

But at least we have the actual Exception object in the minidump, and we can inspect that.  We’ll just use the minidump to view the Exception.StackTrace property…

    1: -        $exception    {"Exception of type 'System.Exception' was thrown."}    System.Exception
    2: -        StackTrace    '$exception.StackTrace' threw an exception of type 'System.NotSupportedException'    string {System.NotSupportedException}
    3: +        base    {"Exception stack trace information is not available while minidump debugging."}    System.SystemException {System.NotSupportedException}

Whoa!  It’s a conspiracy!  The Exception.StackTrace property doesn’t work inside of minidumps, and there is an explicit error message generated.

And that’s why we snapped a copy of the Exception.StackTrace property before we wrote the minidump.  We can use the Call Stack window to change the context to the CatchTest method on the stack, and then we can inspect the local strackTrace variable in that call frame.

    1: at MiniDumpDemo.Program.CreateStackWithException(Int32 level) in MiniDumpDemo\Program.cs:line 20
    2: at MiniDumpDemo.Program.CreateStackWithException(Int32 level) in MiniDumpDemo\Program.cs:line 20
    3: at MiniDumpDemo.Program.CreateStackWithException(Int32 level) in MiniDumpDemo\Program.cs:line 20
    4: at MiniDumpDemo.Program.CreateStackWithException(Int32 level) in MiniDumpDemo\Program.cs:line 20
    5: at MiniDumpDemo.Program.CreateStackWithException(Int32 level) in MiniDumpDemo\Program.cs:line 20
    6: at MiniDumpDemo.Program.CreateStackWithException(Int32 level) in MiniDumpDemo\Program.cs:line 20
    7: at MiniDumpDemo.Program.CatchTest() in MiniDumpDemo\Program.cs:line 55

That’s much better.  I’ve personally had many many instances where I’ve been given only a callstack like that and the bug is usually painfully obvious.  Occasionally it takes a few minutes of thought and reading the code to figure out the bug.  Just having the text of the callstack is useful so much of the time that the server software we develop just writes the callstack to a log file and we inspect the log file later.

But it’s not actually ideal.  Ideal would be to have the full context of the location of the exception in the minidump.  The reason that we don’t have that is we are writing the minidump from within a catch handler, and the way exceptions work, the stack has been unwound and all of the finally blocks have run before the catch handler starts running.  (I’m not going to attempt to describe it in any detail here.  Here’s a post that provides some information:  https://blogs.msdn.com/b/clrteam/archive/2009/02/05/catch-rethrow-and-filters-why-you-should-care.aspx)

One solution would be to write the minidump from inside of CreateStackWithException just before the exception is thrown.  However that would require that you push a lot of the policy related to minidumps (the filename of the minidump, whether to write the minidump or not) down into code that is less likely to have information about those policies.  You could solve those problems with dependency injection https://en.wikipedia.org/wiki/Dependency_Injection but what if the location where the exception is being thrown is in a module that you can’t rebuild?

This post has grown fairly long, so I am going to close.  But first here is a taste of a solution…

    1: static void FilterTest()
    2: {
    3:     try
    4:     {
    5:         ExceptionFilter.Invoke(
    6:             () => {
    7:                 CreateStackWithException();
    8:             },
    9:             (ex) => {
   10:                 string stackTrace = ex.StackTrace;
   11:                 string fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "MiniDumpDemo_Filter.mdmp");
   12:                 using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Write))
   13:                 {
   14:                     MiniDump.Write(fs.SafeFileHandle, MiniDump.Option.WithFullMemory, MiniDump.ExceptionInfo.Present);
   15:                 }
   16:             });
   17:     }
   18:     catch (Exception)
   19:     {
   20:         /* ignore */
   21:     }
   22: }
    1: ntdll.dll!NtGetContextThread()  + 0xa bytes    
    2: [Managed to Native Transition]    
    3: MiniDumpDemo.exe!MiniDump.Write(System.Runtime.InteropServices.SafeHandle fileHandle = {Microsoft.Win32.SafeHandles.SafeFileHandle}, MiniDump.Option options = WithFullMemory, MiniDump.ExceptionInfo exceptionInfo = Present) Line 98 + 0x40 bytes    C#
    4: MiniDumpDemo.exe!MiniDumpDemo.Program.FilterTest.AnonymousMethod__1(System.Exception ex = {"Exception of type 'System.Exception' was thrown."}) Line 41 + 0x2e bytes    C#
    5: ExceptionFilter.dll!ExceptionFilter.Invoke(System.Action body = {Method = Could not evaluate expression}, System.Action<System.Exception> filter = {Method = Could not evaluate expression}) Line 89    Unknown
    6: [Native to Managed Transition]    
    7: ntdll.dll!RtlpExecuteHandlerForException()  + 0xd bytes    
    8: ntdll.dll!RtlDispatchException()  + 0x38c bytes    
    9: ntdll.dll!KiUserExceptionDispatcher()  + 0x2e bytes    
   10: KERNELBASE.dll!RaiseException()  + 0x3d bytes    
   11: [Managed to Native Transition]    
   12: MiniDumpDemo.exe!MiniDumpDemo.Program.CreateStackWithException(int level = 6) Line 20    C#
   13: MiniDumpDemo.exe!MiniDumpDemo.Program.CreateStackWithException(int level = 5) Line 20 + 0xa bytes    C#
   14: MiniDumpDemo.exe!MiniDumpDemo.Program.CreateStackWithException(int level = 4) Line 20 + 0xa bytes    C#
   15: MiniDumpDemo.exe!MiniDumpDemo.Program.CreateStackWithException(int level = 3) Line 20 + 0xa bytes    C#
   16: MiniDumpDemo.exe!MiniDumpDemo.Program.CreateStackWithException(int level = 2) Line 20 + 0xa bytes    C#
   17: MiniDumpDemo.exe!MiniDumpDemo.Program.CreateStackWithException(int level = 1) Line 20 + 0xa bytes    C#
   18: MiniDumpDemo.exe!MiniDumpDemo.Program.FilterTest.AnonymousMethod__0() Line 34 + 0xa bytes    C#
   19: ExceptionFilter.dll!ExceptionFilter.Invoke(System.Action body = {Method = Could not evaluate expression}, System.Action<System.Exception> filter = {Method = Could not evaluate expression}) Line 74    Unknown
   20: MiniDumpDemo.exe!MiniDumpDemo.Program.FilterTest() Line 32 + 0xe9 bytes    C#
   21: MiniDumpDemo.exe!MiniDumpDemo.Program.Main(string[] args = {string[0]}) Line 79 + 0x5 bytes    C#
   22: [Native to Managed Transition]    
   23: mscoreei.dll!_CorExeMain()  + 0x49 bytes    
   24: mscoree.dll!_CorExeMain_Exported()  + 0x69 bytes    
   25: kernel32.dll!BaseThreadInitThunk()  + 0xd bytes    
   26: ntdll.dll!RtlUserThreadStart()  + 0x21 bytes    

Using an exception filter, you can write the minidump before the stack is unwound.  That gives you the full callstack from the point where the exception is thrown, without any finally blocks having run, so you have the full context in the minidump.

The ExceptionFilter.Invoke method is code I wrote in IL.  The C# language doesn’t provide support for writing exception filters, so you have to resort to one of several mechanisms to author the code.  In my next post, I will cover in detail my ExceptionFilter.Invoke method.