客户端程序发布后在用户的计算机上运行,不可能时时刻刻的处于 WinDbg 的监控之下,恐怕没有一个用户会喜欢这样的程序的,所以我们要想办法把程序崩溃时的状态存储到一个文件中,然后通过该文件使用 WinDbg 来查看程序崩溃时的状态,来确定程序为什么发生了异常;
我们要实现的功能是:用户运行发行版的程序,该程序如果一旦发生异常,立刻收集异常时程序的内部信息,然后分门别类的输出到几个文件,此后出现一个界面询问用户是否发送错误报告给软件发行商,用户可以选择发送哪些文件或者不发送错误报告,如果用户点击“发送错误报告”,那么将用户愿意发送的错误输出文件打包到一个 ZIP 文件中,并通过 SMTP 发送到软件发行商的报修邮箱中;
首先我们要考虑的是,程序崩溃时已经不可能实现太多的功能,因为此时的程序已经处于异常状态了,所以在崩溃的程序内部仅仅实现输出错误信息,例如程序的状态、程序加载的模块(哪些动态库),这些动态库的版本和编译时间等,然后启动一个指定的程序,该程序实现后续的功能:提示用户发送错误报告、将错误输出文件打包为 ZIP 文件,然后发送到软件发行商的用户报修邮箱中;
从 Windows 2000 开始系统就提供了 DbgHelp.dll ,但是直到 WinXP 该动态库才算是真正提供了程序异常时完整的信息收集机制,所以如果你的程序需要在 WinXP 之前的计算机上运行,那么最好附带自己的 DbgHelp.dll 文件,该文件在 WinDebug 的安装目录中也有,可以直接复制使用;
另外 WinDebug 还提供了 DbgEng.dll ,该动态库就是 CDB.exe 和 WinDbg.exe 的调试引擎,内部提供仿 COM 接口的完整调试接口,可以自己实现特定的半自动化调试工具,此处不详细解说,有兴趣的可以自己参考相关的文章,另外 ACE 中的 Stack Trace 功能就是基于 DBGENG 实现的;
有了 DbgHelp.dll 文件,即可使用里面的函数来输出程序的状态,这里我们要使用的就是 MiniDumpWriteDump 函数,该函数可以导出程序内部的内存、堆栈、句柄、线程、模块等程序运行相关的信息,该函数的原型如下(具体细节参考 DbgHelp.h ):
BOOL WINAPI
MiniDumpWriteDump(
IN HANDLE hProcess,
IN DWORD ProcessId,
IN HANDLE hFile,
IN MINIDUMP_TYPE DumpType,
IN CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, OPTIONAL
IN CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, OPTIONAL
IN CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam OPTIONAL
);
因为程序运行时内存占用一般不会太小,尤其是大型程序动态申请的内存都有数百兆甚至更大,比如GIS程序一般都会占用将近 1GB 的内存,如果将内存全部记录下来,即使用户的磁盘空间够用,也不好从用户处发送回程序员处进行错误定义和分析,因此一般都是导出模块、线程和堆栈信息,这些信息通过详细的分析基本上可以定位绝大部分程序崩溃的原因,进而指引对程序的改进,所以 DumpType 参数指定为 MiniDumpNormal,这也是微软错误输出文件的设定值;
下面我们就要实现对程序崩溃的拦截,程序崩溃时Windows系统调用会发出异常通知,我们捕获该异常通知,然后使用上面的函数输出错误信息到指定的文件中,因为该套错误输出程序已经在公司的产品中运行了将近三年,考虑到软件的版权问题,下面我仅仅贴出最初实现的精简版的异常捕获代码:
#pragma once
// 异常处理函数句柄,该句柄根据情况返回对应的执行标志
LONG WINAPI ExceptionHandler(LPEXCEPTION_POINTERS lpExceptionInfo);
LONG WINAPI ExceptionInstall();
#define EXCEPTION_PROTECTED_BEGIN() _try {
#define EXCEPTION_PROTECTED_CATCH() } __except(ExceptionHandler(GetExceptionInformation())) {}
#include "StdAfx.h"
#include <time.h>
#include <DbgHelp.h>
#include "CrashOutput.h"
#pragma comment(lib, "shlwapi")
#pragma comment(lib, "dbghelp")
//==============================================================================
// 定义常用的宏
#define ERR_FAILURE 0xFFFFFFFF
#define ERR_SUCCESS 0x00000000
//==============================================================================
// 定义默认的输出文件名称
static const TCHAR szMiniDump[] = { TEXT("Crash.DMP") };
static const long scnReseve = 1024;
//==============================================================================
// 根据输入的标志、文件打开一个输出文件并返回句柄
HANDLE WINAPI GetDumpFile(LPCTSTR lpszTag, LPCTSTR lpszFile, DWORD dwAccess)
{
TCHAR szModule[MAX_PATH] = { TEXT("") };
GetModuleFileName(NULL,szModule,MAX_PATH);
LPTSTR lpszPath = PathFindExtension(szModule);
wsprintf(lpszPath, TEXT(" %s %s"), lpszTag, lpszFile);
DWORD dwAttr = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH;
return CreateFile(szModule,dwAccess,0,0,CREATE_ALWAYS,dwAttr,0);
}
LONG WINAPI DumpMini(HANDLE hFile, PEXCEPTION_POINTERS ExceptionInfo)
{
// 设置输出信息
MINIDUMP_EXCEPTION_INFORMATION eInfo;
eInfo.ThreadId = GetCurrentThreadId();
eInfo.ExceptionPointers = ExceptionInfo;
eInfo.ClientPointers = FALSE;
// 准备进程相关参数
HANDLE hProc = GetCurrentProcess();
DWORD nProc = GetCurrentProcessId();
PMINIDUMP_EXCEPTION_INFORMATION pMDI = ExceptionInfo ? &eInfo : NULL;
BOOL bDump = MiniDumpWriteDump(hProc,nProc,hFile,MiniDumpNormal,pMDI,NULL,NULL);
return bDump ? ERR_SUCCESS : ERR_FAILURE ;
}
//==================================================================================================
// 下面的函数为内部一级的函数,提供给外部接口调用
LONG WINAPI KeGenerateUniversalSymbol(LPTSTR lpszTag, UINT nCount)
{
SYSTEMTIME st; GetLocalTime(&st);
DWORD nThread = GetCurrentThreadId();
DWORD nProcess = GetCurrentProcessId();
DWORD tClock = (DWORD)time(NULL); srand( GetTickCount() );
DWORD nTime = st.wHour * 10000 + st.wMinute * 100 + st.wSecond;
DWORD nDate = (st.wYear % 100) * 10000 + st.wMonth * 100 + st.wDay;
static const TCHAR szFormat[] = { TEXT("{%08X-%04X-%04X-%04X-%06d%06d}") };
return wsprintf(lpszTag, szFormat, tClock, nProcess, nThread, rand(), nDate, nTime);
}
LONG WINAPI KeDumpDebugger(LPTSTR lpszTag, PEXCEPTION_POINTERS ExceptionInfo)
{
HANDLE hFile = GetDumpFile(lpszTag, szMiniDump, GENERIC_WRITE);
if(hFile == INVALID_HANDLE_VALUE) return 0;
DumpMini( hFile, ExceptionInfo );
CloseHandle( hFile );
return NO_ERROR;
}
LONG WINAPI KeExceptionHandler(LPCTSTR lpszTag /* != NULL */)
{
static const TCHAR scszWinCrash[] = { TEXT("WinCrash.exe") };
static const long scnBuffer = scnReseve * 2;
TCHAR szCommand[scnBuffer] = { TEXT("") };
lstrcpy(szCommand,scszWinCrash);
lstrcat(szCommand,TEXT(" \""));
// 加入模块名称作为第一个参数
LPTSTR lpCommand = szCommand + lstrlen(szCommand);
UINT nLength = scnBuffer + szCommand - lpCommand;
GetModuleFileName(NULL,lpCommand,nLength);
lstrcat(lpCommand,TEXT("\""));
if(lpszTag != NULL)
{
// 如果输入了标识,那么作为第二个参数
lpCommand = lpCommand + lstrlen(lpCommand);
wsprintf(lpCommand, TEXT(" \"/TAG:%s\""), lpszTag);
}
STARTUPINFO startInfo;
PROCESS_INFORMATION procInfo;
ZeroMemory(&procInfo, sizeof(procInfo));
ZeroMemory(&startInfo, sizeof(startInfo));
startInfo.cb = sizeof(startInfo);
startInfo.dwFlags = STARTF_USESHOWWINDOW;
startInfo.wShowWindow = SW_SHOW;
BOOL bCreate = CreateProcess(0,szCommand,0,0,0,0,0,0,&startInfo,&procInfo);
if( bCreate && procInfo.hProcess && procInfo.hThread )
{
CloseHandle(procInfo.hThread);
CloseHandle(procInfo.hProcess);
return EXCEPTION_EXECUTE_HANDLER;
}
return EXCEPTION_CONTINUE_SEARCH;
}
//==================================================================================================
// 该函数为真正的异常过滤器函数,如果使用全局未处理异常句柄,那么就是该函数
LONG WINAPI ExceptionFilter(LPEXCEPTION_POINTERS ExceptionInfo)
{
// 进程已经处于崩溃时错误信息输出中
TCHAR szTag[64] = { TEXT("") };
KeGenerateUniversalSymbol(szTag, 64);
KeDumpDebugger(szTag, ExceptionInfo);
return KeExceptionHandler(szTag);
}
LONG WINAPI ExceptionHandler(LPEXCEPTION_POINTERS lpExceptionInfo)
{
// 如果是在调试器下执行的,那么直接返回给调试器
if(IsDebuggerPresent())
{
return EXCEPTION_CONTINUE_SEARCH;
}
// 进行异常捕获并分析情况返回异常处理值
return ExceptionFilter(lpExceptionInfo);
}
LONG WINAPI ExceptionInstall()
{
SetUnhandledExceptionFilter(ExceptionFilter);
return HRESULT_FROM_WIN32(ERROR_SUCCESS);
}
static LONG snInstall = ExceptionInstall();
编译程序时将该CPP文件添加到主程序中即可,然后就是实现一个 WinCrash.exe 程序,该程序根据输入的参数找到对应程序的错误输出然后根据用户的选择将错误输出打包并发送到指定的邮箱中;
如果程序比较复杂,有较多的可执行模块构成,比如近百的动态库和较多的辅助工具,并且源代码是一个较大的团队在开发维护,那么就需要很多人来对自己相关的错误进行分析定位和改进程序代码,这样就带来了一个问题,如果搭建一个符号服务器给所有人使用而不是只能在特定的计算机上使用?
欲知如何处理请参阅《调试发行版程序 (三)》