在日常工作中,本地c++代码发生崩溃时,编译器都可以帮我们捕捉到并且定位到具体的代码,这是因为编译器接收到了操作系统发送过来的程序异常通知并进行了处理。但是在使用我们软件的用户环境上,没有编译器帮我们处理这个异常,操作系统会使用它的异常处理机制:弹出程序异常对话框。因此我们需要将崩溃时产生的堆栈信息生成dump文件,传送到我们的服务器上,通过Windbg工具或者vs编译器进行崩溃分析。

系统的异常处理顺序

  1. 系统首先判断异常是否应发送给目标程序的异常处理模块,如果决定应该发送,并且目标程序正在被调试,则系统挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息。

  2. 如果目标程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否添加了线程相关的异常处理机制,如果有,系统就把异常发送给你的程序seh处理例程,交由其处理。

  3. 每个线程相关的异常处理例程可以处理或者不处理这个异常,如果不处理并且安装了多个线程相关的异常处理例程,,可交由链起来的其他例程处理.。

  4. 如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知调试器。

  5. 如果程序未处于被调试状态或者调试器没有能够处理,并且程序调用SetUnhandledExceptionFilter安装了异常捕捉的话,系统转向调用它的全局异常过滤函数。

  6. 在调用了SetUnhandledExceptionFilter后,UnhandledExceptionFilter依旧会首先检查当前应用程序是否在调试器的控制之下,如果是,它将返回EXCEPTION_CONTINUE_SEARCH,由调试器处理当前异常。

  7. 如果程序也没有调用SetUnhandledExceptionFilter,系统会调用默认的系统处理程序,通常显示一个对话框,“程序无响应”或者“程序中断”,在安装了开发环境的机器上,还会弹出附加调试的对话框,如果没有调试器能被附加于其上或者调试器也处理不了,系统 就调用ExitProcess终结程序.。

而我们程序中需要加入的崩溃捕捉模块也就是通过SetUnhandledExceptionFilter函数来实现的。

SetUnhandleExceptionFilter 函数

Windows操作系统提供了一个API函数可以在程序crash之前有机会处理这些异常,就是 SetUnhandleExceptionFilter函数。(C++也有一个类似函数set_terminate可以处理未被捕获的C++异常。)
SetUnhandleExceptionFilter函数声明如下:

1
2
3
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
[in] LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);

中 LPTOP_LEVEL_EXCEPTION_FILTER 定义如下:

1
2
3
4
typedef LONG (WINAPI *PTOP_LEVEL_EXCEPTION_FILTER)(
__in struct _EXCEPTION_POINTERS *ExceptionInfo
);
typedef PTOP_LEVEL_EXCEPTION_FILTER LPTOP_LEVEL_EXCEPTION_FILTER;

简单来说,SetUnhandleExceptionFilter允许我们设置一个自己的函数作为全局SEH过滤函数,当程序crash前会调用我们的函 数进行处理。我们可以利用的是 _EXCEPTION_POINTERS 结构类型的变量ExceptionInfo,它包含了对异常的描述以及发生异常的线程状态,过滤函数可以通过返回不同的值来让系统继续运行或退出应用程序。

Minidump

minidump(小存储器转储)可以理解为一个dump文件,里面记录了能够帮助调试crash的最小有用信息。实际上,如果你在 系统属性 -> 高级 -> 启动和故障恢复 -> 设置 -> 写入调试信息 中选择“小内存转储(64 KB)”的话,当系统意外停止时都会在C:\Windows\Minidump\路径下生成一个.dmp后缀的文件,这个文件就是minidump文件。

只不过这种方式生成的是内核态的minidump。我们要生成的是用户态的minidump,文件中包含了程序运行的模块信息、线程信息、堆栈调用信息等。而且为了符合其mini的特性,dump文件是压缩过的。

windows操作系统也给我们提供了一个API函数可以来写minidump信息,就是MiniDumpWriteDump函数,函数声明如下:

1
2
3
4
5
6
7
8
9
BOOL MiniDumpWriteDump(
[in] HANDLE hProcess,
[in] DWORD ProcessId,
[in] HANDLE hFile,
[in] MINIDUMP_TYPE DumpType,
[in] PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
[in] PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
[in] PMINIDUMP_CALLBACK_INFORMATION CallbackParam
);

ExceptionParam是最关键的参数,它是一个PMINIDUMP_EXCEPTION_INFORMATION 结构体,结构体内部有一个PEXCEPTION_POINTERS成员,也就是上面PTOP_LEVEL_EXCEPTION_FILTER函数指针的参数。

代码演示

dump捕捉分为进程内和进程外两种,一般都认为进程内进行dump捕捉是不安全的,因为程序已经异常了,在异常的堆或栈上进行操作是有风险的,会导致二次异常。因此在实际的项目中都会采用进程外捕捉dump,两个进行通过共享内存来共享PEXCEPTION_POINTERS异常指针,由异常捕捉进行来写dump文件,并上传到我们自己的服务器上。具体用哪种方式,我觉得视程序的使用场景来定,下面贴上封装好了的dump_catch.h文件的代码,只有一个文件,调用方式很简单,在程序的入口函数最上方调用::SetUnhandledExceptionFilter(UnhandledExceptionFilterEx);即可,默认在exe同级目录下的dump文件夹下生成minidump文件(日期命名).

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/*
created: 2018/12/25
filename: dump.h
author: libing
depend: dbghelp.lib,Dbghelp.h
build: vc(windows)s
purpose: 实现windows程序崩溃捕捉,落地成dump文件.
没有封装成类,是因为没有必要,都是几个简单的静态函数,写在一个.h文件中,也方便调用。
useway: 在main函数或者WinMain函数中,
程序初始化之前调用::SetUnhandledExceptionFilter(UnhandledExceptionFilterEx)即可。
*/

#pragma once
#include "Dbghelp.h"
#include <shlwapi.h>

#pragma comment(lib, "Dbghelp.lib")
#pragma comment(lib, "shlwapi.lib")

BOOL CALLBACK MiniDumpCallback(PVOID, const PMINIDUMP_CALLBACK_INPUT input, PMINIDUMP_CALLBACK_OUTPUT output)
{
if (input == NULL || output == NULL)
return FALSE;

BOOL ret = FALSE;
switch (input->CallbackType)
{
case IncludeModuleCallback:
case IncludeThreadCallback:
case ThreadCallback:
case ThreadExCallback:
ret = TRUE;
break;
case ModuleCallback:
{
if (!(output->ModuleWriteFlags & ModuleReferencedByMemory))
{
output->ModuleWriteFlags &= ~ModuleWriteModule;
}
ret = TRUE;
}
break;
default:
break;
}

return ret;
}

void WriteDump(EXCEPTION_POINTERS* exp, const std::string &path)
{
HANDLE h = ::CreateFile(path.c_str(), GENERIC_WRITE | GENERIC_READ, FILE_SHARE_WRITE | FILE_SHARE_READ,
NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if(h == InvalidHandle)
{
//AfxMessageBox("HANDLE h = ::CreateFile");
//这里可以加日志
return;
}

MINIDUMP_EXCEPTION_INFORMATION info;
info.ThreadId = ::GetCurrentThreadId();
info.ExceptionPointers = exp;
info.ClientPointers = NULL;

MINIDUMP_CALLBACK_INFORMATION mci;
mci.CallbackRoutine = (MINIDUMP_CALLBACK_ROUTINE)MiniDumpCallback;
mci.CallbackParam = 0;

MINIDUMP_TYPE mdt = (MINIDUMP_TYPE)(MiniDumpWithIndirectlyReferencedMemory | MiniDumpScanMemory);

MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), h, mdt, &info, NULL, &mci);
::CloseHandle(h);
}

void GetDirPath(std::string & strDir)
{
//dump文件存储路径,存储在exe同级目录下的Dump文件夹中
char szAppPath[MAX_PATH] = { 0 };
GetModuleFileName(NULL, szAppPath, sizeof(szAppPath) - 1);
(strrchr(szAppPath, '\\'))[0] = 0;
strDir = szAppPath;
strDir.append("\\Dump");

if(!PathFileExists(strDir.c_str()))
{
CreateDirectory(strDir.c_str(), NULL);
}

strDir.append("\\");
}

LONG WINAPI UnhandledExceptionFilterEx(EXCEPTION_POINTERS* exp)
{
SYSTEMTIME szSysDate;
GetLocalTime(&szSysDate);
char szFileName[MAX_PATH] = {0};
sprintf(szFileName, "%04d%02d%02d_%02d%02d%02d.dmp",
szSysDate.wYear, szSysDate.wMonth, szSysDate.wDay, szSysDate.wHour, szSysDate.wMinute, szSysDate.wSecond);

std::string dir;
GetDirPath(dir);
dir.append(szFileName);

WriteDump(exp, dir);

char szBuf[512] = {0};
sprintf(szBuf, "程序崩溃, dump文件为:%s", dir.c_str());
//可以做点别的

return EXCEPTION_EXECUTE_HANDLER;
}

Windbg 生成 ump

当我们通过windbg调试进程时(Attach Process),如果程序崩溃了,可以通过windbg的.dump命令,选择不同的参数来生成不同类型的dump文件。

  • .dump /m C:/dumps/myapp.dmp
    缺省选项,生成标准的minidump, 转储文件通常较小,便于在网络上通过邮件或其他方式传输。 这种文件的信息量较少,只包含系统信息、加载的模块(DLL)信息、 进程信息和线程信息。

  • .dump /ma C:/dumps/myapp.dmp
    带有尽量多选项的minidump(包括完整的内存内容、句柄、未加载的模块,等等),文件很大,但如果条件允许(本机调试,局域网环境), 推荐使用这种dump。

  • .dump /mFhutwd C:/dumps/myapp.dmp
    带有数据段、非共享的读/写内存页和其他有用的信息的minidump。包含了通过minidump能够得到的最多的信息。是一种折中方案。

参考: