转储文件也就是我们常说的 dump 文件,可以把转储文件看成软件的某个时刻的一个快照,我们一般在软件出现问题时手动生成或者程序自动生成转储文件。
一、工具篇
下面我们介绍几种借助第三方工具生成转储文件的方法。
1.1 任务管理器
任务管理器可以说是最易获取的系统工具,同时它具有生成转储文件的功能。但要注意的是在 64 位操作系统上面,默认启动的是 64 位的任务管理器。使用任务管理器生成转储文件需要遵循一个原则:用 32 位任务管理器给 32 位进程(无论该进程是运行在 32 位还是 64 位系统上面)生成转储文件,用 64 位任务管理器给 64 位进程生成转储文件。
在 64 位系统上,32 位的任务管理器位于C:\Windows\SysWOW64\taskmgr.exe
。
生成方法:右键进程 –> 创建转储文件–>弹出对话框提示生成成功,以及 dmp 文件位置。
类似的工具还有:Process Explorer,PCHunter 等。
1.2 注册表
可以通过在注册表中进行配置,让操作系统在程序崩溃时自动生成 dmp 文件,并放到指定位置。
在注册表项 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps 下面根据进程名(含.exe)新建子项,并配置如下值:
名称:DumpCount,类型:REG_DWORD,最大保留 Dump 个数,默认为 10.
名称:DumpType,类型:REG_DWORD,Dump 类型(1-Mini dump, 2-Full dump),默认为 1.
名称:DumpFolder,类型:REG_EXPAND_SZ,Dump 文件保存的位置。
1.3 Windbg
生成方法:File 菜单–>Attach to Process–>输入命令.dump /ma /u d:\test.dmp
提示成功之后,可以在 D 盘看到生成 dmp 文件到 test_0bf0_2017-08-13_23-46-37-244_11cc.dmp 文件。
0bf0_2017-08-13_23-46-37-244_11cc 是/u 参数附加上去的,意思是 2017 年 08 月 13 日 23 时 46 分 37 秒 244 毫秒,进程 PID 位 11cc。
.dump 命令参数比较多,常用的组合就是/ma
,/m 表示生成 minidump,/a 表示 dmp 包含所有信息,/u 参数就是上面说的附加时间和 PID 信息到文件名。
1.4 Windbg -I
Windbg -I 可以将 Windbg 设置为及时调试器(开启了 UAC 的系统上面,需要以管理员权限运行),也就是我们常说的 JIT 调试器。设置成功之后,如遇到程序崩溃,Windbg 会自动运行并附加到崩溃进程。
设置成功之后会弹出对话框提示设置成功。如果不想弹出对话框,可以加上 S(slient 首字母)Windbg -IS.
也可以通过修改注册表项 AeDebug 来实现和 windbg -I 同样的功能。
根据 windbg 位数(32/64)和系统的位数(32/64)的不同,修改的注册表项的位置也不同:
- 32 位 windbg–32 位系统:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug
- 32 位 windbg–64 位系统:HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug
- 64 位 windbg–64 位系统:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug
AeDebug 项下面有 2 个值:
名称:Auto,类型:REG_SZ,0 表示出现崩溃弹出对话框,让用户选择关闭程序还是调试程序;1 表示自动弹出设置调试器。
名称:Debugger,类型:REG_SZ,调试器值。默认为”C:\WINDOWS\system32\vsjitdebugger.exe” -p %ld -e %ld,设置为 windbg 需更改为”C:\Debuggers\WinDbg\x86\windbg.exe” -p %ld -e %ld -g
看起来挺复杂,其实挺好理解的。
1.5 Adplus
adplus 工具位于 windbg 安装目录,最早叫 adplus.vbs,以 VBScript 脚本提供,最新版改成了 adplus.exe。adplus.exe 不仅可以在程序崩溃时手动运行来生成 dmp 文件,也可以在崩溃之前就运行它,当程序崩溃时它会自动生成 dmp 文件;甚至可以在程序没有运行之前就先运行 adplus,当程序崩溃时它会自动生成 dmp 文件。
如:adplus -pn powerpnt.exe -pn wincmd32.exe -hang -o c:\test
Adplus 用法:
1 2 3 4
| ADPlus <RunMode> -o <OutputDirectory> [Options] RunMode:-hang或-crash -hang 附加到进程,生成dmp,然后解除附加(detach)。多用于程序卡死的情况下。 -crash 附加到进程,直到程序崩溃或者其他事件发生,生成dmp文件,然后解除附加。
|
常用参数:
-o 目录
指定生成文件存储目录。
-p 进程 ID
指定进程 ID,可以同时使用多次-p 来指定多个进程。
-pn 进程名
指定进程名,支持通配符,也可以同时使用多次-pn 来指定多个进程,但进程名必须存在,不存在则失败。
-po 进程名
和-pn 类似,但-po 不要求进程名必须存在。可以在进程启动之前就先启动 Adplus.
-pmn 进程名
pmn 为 Process Monitor 缩写。顾名思义,可以监视进程列表,一旦指定进程运行,则附加上去。只适用于-crash 模式。
# 二、代码篇
使用代码生成dump文件的原理大致是:
- 捕获异常,如
SetUnhandledExceptionFilter
、__try...__except
。
- 使用MiniDumpWriteDump写入dump到文件。
为了便于使用,我封装了一个捕获异常并写入dump的辅助,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| #pragma once
#include <windows.h> #include <tchar.h> #include <strsafe.h>
namespace cpp4j { typedef struct _EXCEPTION_POINTERS EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
int __cdecl RecordExceptionInfo(PEXCEPTION_POINTERS pExceptPtrs, const TCHAR *szDumpNamePrefix); TCHAR *lstrrchr(LPCTSTR string, int ch); void DumpMiniDump(HANDLE hFile, PEXCEPTION_POINTERS excpInfo); }
#define WINMAIN_BEGIN(szDumpNamePrefix) \ int __96A9695E_RUN_WINMAIN_FUNC(HINSTANCE hInstance, LPTSTR lpCmdLine);\ LONG WINAPI __96A9695E_UnhandledExceptionHandler( _EXCEPTION_POINTERS *pExceptionInfo ) \ { \ OutputDebugString(TEXT("Create a dump file sine an exception occurred in sub-thread.\n")); \ int iRet = cpp4j::RecordExceptionInfo(pExceptionInfo, szDumpNamePrefix); \ return iRet; \ } \ int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) \ { \ UNREFERENCED_PARAMETER(hPrevInstance); \ UNREFERENCED_PARAMETER(nCmdShow); \ ::SetUnhandledExceptionFilter( __96A9695E_UnhandledExceptionHandler );\ int ret = 0;\ __try\ {\ ret = __96A9695E_RUN_WINMAIN_FUNC(hInstance, lpCmdLine);\ }\ __except(cpp4j::RecordExceptionInfo(GetExceptionInformation(), szDumpNamePrefix))\ {\ OutputDebugString(TEXT("Create a dump file sine an exception occurred in main-thread.\n")); \ }\ return ret;\ }\ int __96A9695E_RUN_WINMAIN_FUNC(HINSTANCE hInstance, LPTSTR lpCmdLine) \ {
#define WINMAIN_END }
#define MAIN_BEGIN(szDumpName) \ int __96A9695E_RUN_MAIN_FUNC(int argc, _TCHAR* argv[]);\ LONG WINAPI __96A9695E_UnhandledExceptionHandler( _EXCEPTION_POINTERS *pExceptionInfo ) \ { \ OutputDebugString(TEXT("Create a dump file since an exception occurred in sub-thread.\n")); \ int iRet = cpp4j::RecordExceptionInfo(pExceptionInfo, szDumpName); \ return iRet; \ } \ int _tmain(int argc, _TCHAR* argv[])\ { \ ::SetUnhandledExceptionFilter( __96A9695E_UnhandledExceptionHandler );\ int ret = 0;\ __try\ {\ ret = __96A9695E_RUN_MAIN_FUNC(argc, argv);\ }\ __except(cpp4j::RecordExceptionInfo(GetExceptionInformation(), szDumpName))\ {\ OutputDebugString(TEXT("Create a dump file since an exception occurred in main-thread.\n")); \ }\ return ret;\ }\ int __96A9695E_RUN_MAIN_FUNC(int argc, _TCHAR* argv[]) \ {
#define MAIN_END }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| #include "EasyDump.h" #include <DbgHelp.h> #pragma comment(lib, "Dbghelp.lib")
namespace cpp4j { TCHAR *lstrrchr(LPCTSTR string, int ch) { TCHAR *start = (TCHAR *)string;
while (*string++) ;
while (--string != start && *string != (TCHAR)ch) ;
if (*string == (TCHAR)ch) return (TCHAR *)string;
return NULL; }
inline void DumpMiniDump(HANDLE hFile, PEXCEPTION_POINTERS excpInfo) { if (!excpInfo) { static int iTimes = 0; if (iTimes++ > 1) return;
__try { RaiseException(EXCEPTION_BREAKPOINT, 0, 0, NULL); } __except (DumpMiniDump(hFile, GetExceptionInformation()), EXCEPTION_CONTINUE_EXECUTION) { } } else { MINIDUMP_EXCEPTION_INFORMATION eInfo; eInfo.ThreadId = GetCurrentThreadId(); eInfo.ExceptionPointers = excpInfo; eInfo.ClientPointers = FALSE;
MiniDumpWriteDump( GetCurrentProcess(), GetCurrentProcessId(), hFile, MiniDumpNormal, excpInfo ? &eInfo : NULL, NULL, NULL); } }
int __cdecl RecordExceptionInfo(PEXCEPTION_POINTERS pExceptPtrs, const TCHAR *szDumpNamePrefix) { static bool bFirstTime = true;
if (!bFirstTime) return EXCEPTION_CONTINUE_SEARCH;
bFirstTime = false;
TCHAR szLocalTime[50] = { 0 }; SYSTEMTIME st; GetLocalTime(&st); StringCchPrintf(szLocalTime, 50, TEXT("%04d%02d%02d.%02d.%02d.%02d.%04d"), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);
TCHAR szExeDir[MAX_PATH + 1] = { 0 };
GetModuleFileName(NULL, szExeDir, MAX_PATH);
if (TCHAR *p = lstrrchr(szExeDir, TEXT('\\'))) { *(p + 1) = 0; }
TCHAR szDumpFileName[MAX_PATH + 1] = { 0 }; _stprintf_s(szDumpFileName, MAX_PATH, TEXT("%s%s_%s.dmp"), szExeDir, szDumpNamePrefix, szLocalTime);
HANDLE hMiniDumpFile = CreateFile( szDumpFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH, NULL);
if (hMiniDumpFile != INVALID_HANDLE_VALUE) { DumpMiniDump(hMiniDumpFile, pExceptPtrs);
CloseHandle(hMiniDumpFile); hMiniDumpFile = NULL; }
return EXCEPTION_EXECUTE_HANDLER; } }
|
MAIN_BEGIN
和WINMAIN_BEGIN
中的参数为生成的 dump 文件的前缀,dump 文件命名方式: 前缀*年月日.时.分.秒.毫秒.dmp
。
使用方法也很简单,使用MAIN_BEGIN
替换main
,WINMAIN_BEGIN
替换WinMain
即可:
1 2 3 4 5 6 7 8 9 10 11 12
| #include "EasyDump.h"
MAIN_BEGIN(TEXT("Test"))
int i = 0; int *p = &i; p = NULL; *p = 5;
return 0;
MAIN_END
|
上面的代码会在程序的当前目录生成一个名为 Test_20171101.14.49.57.0264.dmp 的 dump 文件。