c#偶发bug解决办法内存dump后还原调试现场

当使用C#开发的程序出现偶发的Bug时,怎么能调试该Bug?

  • 要调试c#程序,只需要三个因素源码(exe程序可反编译为源码)、pdb(调试信息)、当时的进程内存(内存dump)
  • 故只需要 exe、pdb、dmp三个文件在同一文件夹内,通过Virtual Studio打开dmp就可以直接进入出现bug的断点,同时可以看到上下文变量的值(其他是fullDump)

1、怎么dump内存?

第一种方式:自动dump,当进程出现异常时自动dump。此方式通过注册表实现。
  • Win + R 输入regedit打开注册表
  • 找到如下项:
计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps

image.png

  • 参数说明
   DumpCount: 文件数量      ( 值设为0即禁用此功能)
   DumpFolder:存储位置    (Dump文件名:(应用程序名称全称.进程号.dmp))
   DumpType:   dmp文件类型  (1:mini 2:full)
   mini:只包含某个线程和部分模块的信息。
   full: 包含了某个进程完整的地址空间数据,以及许多用于调试的信息 
第二种方式:程序自动控制(记得先把第一种方式禁用,否则会生成两份dmp文件)
  • 原理是使用win32 api :MiniDumpWriteDump实现
  • 封装了DumpHelper类
using System.Runtime.InteropServices;

namespace DumpTest
{
  public class DumpHelper
  {
      [Flags]
      public enum DumpType : uint
      {
          // From dbghelp.h:
          MiniDumpNormal = 0x00000000,                        //只包含调用栈相关信息
          MiniDumpWithDataSegs = 0x00000001,                  //包含已加载的模块的数据段信息,比如全局变量
          MiniDumpWithFullMemory = 0x00000002,                //包含全部可访问的内存
          MiniDumpWithHandleData = 0x00000004,                //包含句柄信息
          MiniDumpFilterMemory = 0x00000008,                  //过滤一些敏感信息,保护重建调用栈需要的信息
          MiniDumpScanMemory = 0x00000010,                    //扫描,以包含引用内存
          MiniDumpWithUnloadedModules = 0x00000020,           //包含最近被卸载的模块信息
          MiniDumpWithIndirectlyReferencedMemory = 0x00000040,//包含未直接引用的内存
          MiniDumpFilterModulePaths = 0x00000080,             //过滤某块的路径信息
          MiniDumpWithProcessThreadData = 0x00000100,         //包含完整的进程和线程信息
          MiniDumpWithPrivateReadWriteMemory = 0x00000200,    //包含页面属性为 PAGE_READWRITE 的页面
          MiniDumpWithoutOptionalData = 0x00000400,           //不包含可选数据
          MiniDumpWithFullMemoryInfo = 0x00000800,            //包含内存区信息
          MiniDumpWithThreadInfo = 0x00001000,                //包含线程状态信息
          MiniDumpWithCodeSegs = 0x00002000,                  //包含所有代码和有关的内存段
          MiniDumpWithoutAuxiliaryState = 0x00004000,         //关闭辅助内存收集
          MiniDumpWithFullAuxiliaryState = 0x00008000,        //使用所有的内存收集器
          MiniDumpWithPrivateWriteCopyMemory = 0x00010000,    //包含页面属性为 PAGE_WRITECOPY 的页面
          MiniDumpIgnoreInaccessibleMemory = 0x00020000,      //忽略不可访问的页面
          MiniDumpValidTypeFlags = 0x0003ffff,                //包含安全令牌相关信息
      };

      //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!
      struct MiniDumpExceptionInformation
      {
          public uint ThreadId;
          public IntPtr ExceptioonPointers;
          [MarshalAs(UnmanagedType.Bool)]
          public bool ClientPointers;
      }

      //BOOL
      //WINAPI
      //MiniDumpWriteDump(
      //    __in HANDLE hProcess,  :要转储的进程句柄。
      //    __in DWORD ProcessId,  :要转储的进程ID。
      //    __in HANDLE hFile,     :通过 CreateFile() 等 API 打开的,用来保存 dump 的文件句柄。
      //    __in MINIDUMP_TYPE DumpType,  :转储类型。此参数会直接影响转储文件的大小
      //    __in_opt PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,     :指向异常信息结构 MiniDumpExceptionInformation 的指针。如果本参数为 NULL,则转储文件中不会包含异常信息。
      //    __in_opt PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,  :指向用户自定义信息结构的指针
      //    __in_opt PMINIDUMP_CALLBACK_INFORMATION CallbackParam        :指向回调例程 MINIDUMP_CALLBACK_INFORMATION 的指针。如果此参数为NULL,转储过程中不会执行任何回调例程。
      //    );
      [DllImport("dbghelp.dll",EntryPoint = "MiniDumpWriteDump",CallingConvention = CallingConvention.StdCall,CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
      static extern bool MiniDumpWriteDump(
        IntPtr hProcess,
        uint processId,
        IntPtr hFile,
        uint dumpType,
        ref MiniDumpExceptionInformation expParam,
        IntPtr userStreamParam,
        IntPtr callbackParam);

      [DllImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)]
      static extern uint GetCurrentThreadId();

      [DllImport("kernel32.dll", EntryPoint = "GetCurrentProcess", ExactSpelling = true)]
      static extern IntPtr GetCurrentProcess();

      [DllImport("kernel32.dll", EntryPoint = "GetCurrentProcessId", ExactSpelling = true)]
      static extern uint GetCurrentProcessId();

      public static bool WriteDump(string fileName)
      {
          return Write(fileName, DumpType.MiniDumpNormal);
      }
      public static bool Write(string fileName, DumpType dumpType)
      {
          var path = Path.Combine(Environment.CurrentDirectory, fileName);
          using var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
          MiniDumpExceptionInformation exp;
          exp.ThreadId = GetCurrentThreadId();
          exp.ClientPointers = false;
          exp.ExceptioonPointers = Marshal.GetExceptionPointers();
          bool bRet = MiniDumpWriteDump(
            GetCurrentProcess(),
            GetCurrentProcessId(),
            fs.SafeFileHandle.DangerousGetHandle(),
            (uint)dumpType,
            ref exp,
            IntPtr.Zero,
            IntPtr.Zero);
          return bRet;
      }
  }
}
  • 调用:当某个系统异常未被捕获时,调用方法生成Dump文件
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(
  (obj, args) => DumpHelper.WriteDump($"Error_{DateTime.Now.ToString("yyyy_M_dd_HH_mm")}.dmp")
);
  • 结果:代码出现异常导致程序崩溃时,会在程序的工作目录下自动生成Dump文件,如下图:

  • 文件1:DumpType:MiniDumpWithFullMemory

  • 文件2:DumpType:MiniDumpNormal

  • 可以看出Dump文件保存的内容不同,文件大小差异巨大。

    image.png

2、用VS调试

  • 用Vs打开保存的dmp文件(exe、pdb、dmp三个文件同一文件夹)
    image.png
  • 使用托管内存打开或非托管内存打开都可以
    image.png
  • Vs直接跳转到bug处,同时可以监控变量的值
posted @ 2023-03-14 11:05  Bonker  阅读(661)  评论(0编辑  收藏  举报