转储文件也就是我们常说的 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文件的原理大致是:

  1. 捕获异常,如SetUnhandledExceptionFilter__try...__except
  2. 使用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
// EasyDump.h
#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
// EasyDump.cpp
#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;

// Dmp文件命名:前缀_年月日.时.分.秒.毫秒.dmp
//
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_BEGINWINMAIN_BEGIN中的参数为生成的 dump 文件的前缀,dump 文件命名方式: 前缀*年月日.时.分.秒.毫秒.dmp

使用方法也很简单,使用MAIN_BEGIN替换mainWINMAIN_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 文件。