C# アプリケーションでミニダンプを生成してみた

ずいぶん前に C# で minidump を生成する方法という記事を見つけていたのだけれど、今更引っ張り出して試してみた。
# 理由は自宅に帰れなくなったからなんだけど、そこは気にしない。

元ネタ

ここに記載されている MiniDump クラスを感謝しつつ、まるっともらいました。

http://blogs.msdn.com/b/dondu/archive/2010/10/24/writing-minidumps-in-c.aspx

minidump を生成する

MiniDump クラスはこんな感じ。
コードを見るとわかりますが、dbghelp.dll にある MiniDumpWriteDump 関数を P/Invoke で呼び出してダンプを生成してます。

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace MiniDumpSample
{
    public static class MiniDump
    {
        // Taken almost verbatim from http://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);
        }
    }
}

検証用コード

試しに例外が出たらダンプを生成するようにして、 try の中で 0 除算をしています。
というわけで、 DivideByZeroException 例外が出て、ダンプが生成されるはずです。

    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                int a = 100;
                int b = 0;
                int c = a/b;
                Console.WriteLine(c);
            }
            catch (Exception)
            {
                // create minidump

                string fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "MiniDumpDemo_Mainline.dmp");
                using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Write))
                {
                    MiniDump.Write(fs.SafeFileHandle, MiniDump.Option.WithFullMemory);
                }
            }
        }
    }

minidump を見る

実際に実行すると、ダンプファイルが生成されます。
Visual Studio でもいいですが、 windbg で開きます。

Windbg.exe -Q -z MiniDumpDemo_Mainline.dmp

開いたら、 sos をロードします。
(実験環境が .NET4 なのでモジュール名は clr です)

.loadby sos clr

ロードしたら、 !threads コマンドでスレッドの状況を確認。
結果はこんな感じに。

0:000> !threads
ThreadCount:      10
UnstartedThread:  0
BackgroundThread: 5
PendingThread:    0
DeadThread:       3
Hosted Runtime:   no
                                   PreEmptive   GC Alloc                Lock
       ID  OSID ThreadOBJ    State GC           Context       Domain   Count APT Exception
   0    1  1ce8 006307e8   201a220 Enabled  02272448:02273fe8 0062abb0     0 MTA
   2    2  2128 00671720      b220 Enabled  00000000:00000000 0062abb0     0 MTA (Finalizer)
XXXX    3       006b2588     10820 Enabled  00000000:00000000 0062abb0     0 Ukn
XXXX    5       006be310     19820 Enabled  00000000:00000000 0062abb0     0 MTA
XXXX    6       006df178     19820 Enabled  00000000:00000000 0062abb0     0 MTA
   5    4   b44 006eb1f8       220 Enabled  0227fdbc:0227ffe8 0062abb0     0 Ukn
   8    7  1de8 006e8470      b020 Enabled  0228a848:0228c798 0062abb0     0 MTA
   6    8   620 006f04a8       220 Enabled  00000000:00000000 0062abb0     0 Ukn
   9    9  1bfc 05085848      b220 Enabled  022843a0:02285fe8 0062abb0     0 MTA
  10    a  21e4 0507e860      b020 Enabled  022a775c:022a7fe8 0062abb0     0 MTA System.DivideByZeroException (0228e99c)

ID 10 (a) を確認すると、Exception に DivideByZeroException 例外が出ていることを確認。
詳細を見るために、スレッドを切り替えます。

~10s

切り替えたら、今度は例外情報を出力します。

!pe

すると次のように結果が表示されます。

0:010> !pe
Exception object: 0228e99c
Exception type:   System.DivideByZeroException
Message:          0 で除算しようとしました。
InnerException:   <none>
StackTrace (generated):
    SP       IP       Function
    06D0E9E8 003024BF MiniDumpSample!MiniDumpSample.Program.Main(System.String[])+0x7f

StackTraceString: <none>
HResult: 80020012

スタックトレースの結果から、 Program.Main メソッドで例外が発生した、ということが分かりました。
アセンブリでこの関数を追いかけてみます。

!U 003024BF

すると、次の実行結果が得られました。*1

0:010> !U 003024BF 
Normal JIT generated code
MiniDumpSample.Program.Main(System.String[])
Begin 00302440, size 14f
*** WARNING: Unable to verify checksum for MiniDumpSample.exe

(略)

C:\Users\xxxxx\Documents\Visual Studio 2010\Projects\MiniDumpSample\MiniDumpSample\Program.cs @ 19:
003024b6 33d2            xor     edx,edx
003024b8 8955bc          mov     dword ptr [ebp-44h],edx

C:\Users\xxxxx\Documents\Visual Studio 2010\Projects\MiniDumpSample\MiniDumpSample\Program.cs @ 20:
003024bb 8b45c0          mov     eax,dword ptr [ebp-40h]
003024be 99              cdq
>>> 003024bf f77dbc          idiv    eax,dword ptr [ebp-44h]
003024c2 8945b8          mov     dword ptr [ebp-48h],eax

(略)

例外が発生した該当のアドレスを見ると idiv 命令を実行しています。( >>> で強調されている行)
0 除算なので、この時の dword ptr [ebp-44h] が 0 になっているはずです。

直ぐ上を見ると「 xor edx edx 」の結果を代入していることが分かります。
お気づきの通り、ブール代数的には 同じ値でXOR(排他的論理和) をとると、 0 になります。
ということで、 0 除算している処理が Program.cs の 19 行目にあるみたいですよ。と言えそうです

今回はまさに自分でそういうコードを書いますから、これで無事解決。
実際のケースでは例外の内容を確認してからの調査方法はケース毎に異なるでしょうね。

お勉強はここまで。(夜が明け始めた!)

*1:私の名前の部分は xxxxx に置換してます。