在日常工作中,本地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函数声明如下:

LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(
__in LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);


 

其中 LPTOP_LEVEL_EXCEPTION_FILTER 定义如下:

 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文件。

(Windbg调试一)minidump崩溃捕捉_2d

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

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

BOOL
WINAPI
MiniDumpWriteDump(
IN HANDLE hProcess, //当前进程句柄
IN DWORD ProcessId, //当前进程ID
IN HANDLE hFile, //文件句柄
IN MINIDUMP_TYPE DumpType, //MINIDUMP类型
IN CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, OPTIONAL //异常信息(最重要)
IN CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, OPTIONAL //用户数据流(一般不需要)
IN CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam OPTIONAL //回调(一般不需要)
);



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

四,代码演示

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

/*
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;
}