软件在运行过程中会时常发生内存越界、内存访问为例、stack overflow线程栈溢出、空指针与野指针等异常崩溃,仅仅是依靠Debug和Release下的调试是远远不够的,因为有些崩溃不是必现的,或者是Debug下很难出现的。所以我们需要在软件中添加异常捕获的模块,在捕获到异常时生成包含异常上下文的dump文件,以供事后使用windbg进行分析。本文我们就来简单介绍一下如何设置异常回调函数,如何生成保存异常信息的dump文件。
Windows系统提供设置异常处理函数的API函数SetUnhandledExceptionFilter,其声明如下:
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
[in] LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);
该函数的参数就是设置的异常处理函数,该API返回的是系统默认的异常处理函数的地址。当软件发生异常时,会回调之前设置的异常处理函数,异常处理函数的原型如下:
LONG WINAPI SEHUnhandledExceptionFilter(PEXCEPTION_POINTERS pExInfo)
该函数的参数是存放异常信息的结构体EXCEPTION_POINTERS指针,如下:
//
// Typedef for pointer returned by exception_info()
//
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
其中,EXCEPTION_RECORD结构体定义如下:
//
// Exception record definition.
//
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
typedef EXCEPTION_RECORD *PEXCEPTION_RECORD;
上述结构体中的ExceptionCode字段对应异常类型的异常码,常见的异常码有:
#define STATUS_ACCESS_VIOLATION ((DWORD )0xC0000005L) // 内存访问违例
#define STATUS_IN_PAGE_ERROR ((DWORD )0xC0000006L)
#define STATUS_INVALID_HANDLE ((DWORD )0xC0000008L)
#define STATUS_INVALID_PARAMETER ((DWORD )0xC000000DL) // 无效参数
#define STATUS_NO_MEMORY ((DWORD )0xC0000017L)
#define STATUS_ILLEGAL_INSTRUCTION ((DWORD )0xC000001DL)
#define STATUS_NONCONTINUABLE_EXCEPTION ((DWORD )0xC0000025L)
#define STATUS_INVALID_DISPOSITION ((DWORD )0xC0000026L)
#define STATUS_ARRAY_BOUNDS_EXCEEDED ((DWORD )0xC000008CL) // 数组越界
#define STATUS_FLOAT_DENORMAL_OPERAND ((DWORD )0xC000008DL)
#define STATUS_FLOAT_DIVIDE_BY_ZERO ((DWORD )0xC000008EL)
#define STATUS_FLOAT_INEXACT_RESULT ((DWORD )0xC000008FL)
#define STATUS_FLOAT_INVALID_OPERATION ((DWORD )0xC0000090L)
#define STATUS_FLOAT_OVERFLOW ((DWORD )0xC0000091L)
#define STATUS_FLOAT_STACK_CHECK ((DWORD )0xC0000092L)
#define STATUS_FLOAT_UNDERFLOW ((DWORD )0xC0000093L)
#define STATUS_INTEGER_DIVIDE_BY_ZERO ((DWORD )0xC0000094L) // 除0崩溃
#define STATUS_INTEGER_OVERFLOW ((DWORD )0xC0000095L)
#define STATUS_PRIVILEGED_INSTRUCTION ((DWORD )0xC0000096L)
#define STATUS_STACK_OVERFLOW ((DWORD )0xC00000FDL) // 线程栈溢出
#define STATUS_DLL_NOT_FOUND ((DWORD )0xC0000135L)
#define STATUS_ORDINAL_NOT_FOUND ((DWORD )0xC0000138L)
#define STATUS_ENTRYPOINT_NOT_FOUND ((DWORD )0xC0000139L)
#define STATUS_CONTROL_C_EXIT ((DWORD )0xC000013AL)
#define STATUS_DLL_INIT_FAILED ((DWORD )0xC0000142L)
#define STATUS_FLOAT_MULTIPLE_FAULTS ((DWORD )0xC00002B4L)
#define STATUS_FLOAT_MULTIPLE_TRAPS ((DWORD )0xC00002B5L)
#define STATUS_REG_NAT_CONSUMPTION ((DWORD )0xC00002C9L)
#define STATUS_STACK_BUFFER_OVERRUN ((DWORD )0xC0000409L) // 栈内存buffer越界
#define STATUS_INVALID_CRUNTIME_PARAMETER ((DWORD )0xC0000417L)
此外,存放异常上下文信息的结构体Context的定义如下:
//
// Context Frame
//
// This frame has a several purposes: 1) it is used as an argument to
// NtContinue, 2) is is used to constuct a call frame for APC delivery,
// and 3) it is used in the user level thread creation routines.
//
// The layout of the record conforms to a standard call frame.
//
typedef struct _CONTEXT {
//
// The flags values within this flag control the contents of
// a CONTEXT record.
//
// If the context record is used as an input parameter, then
// for each portion of the context record controlled by a flag
// whose value is set, it is assumed that that portion of the
// context record contains valid context. If the context record
// is being used to modify a threads context, then only that
// portion of the threads context will be modified.
//
// If the context record is used as an IN OUT parameter to capture
// the context of a thread, then only those portions of the thread's
// context corresponding to set flags will be returned.
//
// The context record is never used as an OUT only parameter.
//
DWORD ContextFlags;
//
// This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
// set in ContextFlags. Note that CONTEXT_DEBUG_REGISTERS is NOT
// included in CONTEXT_FULL.
//
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
//
FLOATING_SAVE_AREA FloatSave;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_SEGMENTS.
//
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_INTEGER.
//
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_CONTROL.
//
DWORD Ebp;
DWORD Eip;
DWORD SegCs; // MUST BE SANITIZED
DWORD EFlags; // MUST BE SANITIZED
DWORD Esp;
DWORD SegSs;
//
// This section is specified/returned if the ContextFlags word
// contains the flag CONTEXT_EXTENDED_REGISTERS.
// The format and contexts are processor specific
//
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
该结构体中主要存放各个寄存器的信息。
设置异常处理函数的示例代码如下:
// 异常处理函数(回调函数)
LONG WINAPI SEHUnhandledExceptionFilter( PEXCEPTION_POINTERS pExInfo )
{
// 处理异常的代码
// ......
return EXCEPTION_EXECUTE_HANDLER;
}
// 调用SetUnhandledExceptionFilter设置异常处理函数
LPTOP_LEVEL_EXCEPTION_FILTER oldExceptionFilter = SetUnhandledExceptionFilter( SEHUnhandledExceptionFilter );
调用SetUnhandledExceptionFilter设置异常处理函数,只是给所在的线程设置异常处理函数。如果有多个线程,则需要给每个线程分别设置异常处理函数。推荐大家使用开源的CrashReport库去捕获异常。
生成dump文件的代码如下:
void CreateDump(struct _EXCEPTION_POINTERS *pExceptionPointers)
{
CString strDumpFile = _T("E:\\0328.dump");
HANDLE hDumpFile;
hDumpFile = CreateFile(strDumpFile, GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0);
MINIDUMP_EXCEPTION_INFORMATION ExpParam;
ExpParam.ThreadId = GetCurrentThreadId();
ExpParam.ExceptionPointers = pExceptionPointers;
ExpParam.ClientPointers = TRUE;
// 导出minidump,文件大小在几MB左右,如果要导出全dump,则文件会比较大
MINIDUMP_TYPE MiniDumpWithDataSegs = (MINIDUMP_TYPE)(MiniDumpNormal
| MiniDumpWithHandleData
| MiniDumpWithUnloadedModules
| MiniDumpWithIndirectlyReferencedMemory
| MiniDumpScanMemory
| MiniDumpWithProcessThreadData
| MiniDumpWithThreadInfo);
BOOL bMiniDumpSuccessful = FALSE;
bMiniDumpSuccessful = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
hDumpFile, MiniDumpWithDataSegs, &ExpParam, NULL, NULL);
return;
}
上述接口中我们设置的是导出mini dump文件,文件大小大概在几MB左右,如果要导出包含所有信息的全dump文件,则会比较大,大概在几百MB到上GB的大小。我们在自动生成dump文件时,文件不能太大,如果太大的话,会占用大量的磁盘空间。
mini dump文件和全dump文件的区别在于,全dump文件包含了所有的信息,而mini dump只包含了部分信息。在全dump文件中,使用windbg可以查看所有变量内存中的值,而mini dump中只能查看到部分变量在内存中的值。