0%

Windows C++ 异常处理机制:VEH、SEH、UEF 与 MiniDump 实战

异常处理不是只有一个入口

很多 Windows C++ 开发者对异常处理的印象停留在 try/catch 和 “程序崩溃弹错误框”。但实际上,Windows 提供了一整套分层异常处理机制,从最先被调用的 VEH,到最后兜底的 UEF,每层职责不同、适用场景也不同。

理解这条链路,是做好崩溃捕获和 dump 分析的前提。

异常处理链路总览

当异常发生时,Windows 会按以下顺序寻找处理者:

1
2
3
4
5
6
7
异常发生
→ VEH(Vectored Exception Handling,向量异常处理)
→ SEH(Structured Exception Handling,结构化异常处理)
→ C++ EH(try/catch,仅处理 C++ throw 的异常)
→ VEC(Vectored Continue Execution)
→ UEF(Unhandled Exception Filter,未处理异常过滤器)
→ WER(Windows Error Reporting)

任何一层决定处理异常(返回 EXCEPTION_EXECUTE_HANDLEREXCEPTION_CONTINUE_EXECUTION),就不会再往后传递。

VEH:最早拿到异常通知

VEH 是全局注册的异常处理程序,不需要修改栈帧,调用顺序比 SEH 更早。它的典型用途是:在崩溃瞬间记录日志、生成 dump、做崩溃上报。

VEH 代码示例

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
#include <windows.h>
#include <cstdio.h>

LONG WINAPI VectoredHandler(PEXCEPTION_POINTERS pExceptionInfo) {
printf("[VEH] Exception caught!\n");
printf("[VEH] Exception code: 0x%08X\n",
pExceptionInfo->ExceptionRecord->ExceptionCode);
printf("[VEH] Exception address: 0x%p\n",
pExceptionInfo->ExceptionRecord->ExceptionAddress);

PCONTEXT ctx = pExceptionInfo->ContextRecord;
printf("[VEH] RIP/EIP = 0x%llX\n", ctx->Rip);

// 返回 EXCEPTION_CONTINUE_SEARCH,表示不处理,继续向后传递
return EXCEPTION_CONTINUE_SEARCH;
}

int main() {
PVOID vehHandle = AddVectoredExceptionHandler(1, VectoredHandler);
if (!vehHandle) {
printf("Failed to register VEH!\n");
return 1;
}
printf("VEH registered.\n");

int* p = nullptr;
*p = 42; // 触发 Access Violation (0xC0000005)

RemoveVectoredExceptionHandler(vehHandle);
return 0;
}

编译时需要链接 DbgHelp.lib 如果要做符号解析,但上面的示例只需要标准 Windows API。

重要细节:VEH 不能阻止崩溃

VEH 本身没有阻止崩溃的能力。它只是获得一次”抢先通知”的机会。如果你返回 EXCEPTION_CONTINUE_SEARCH,异常会继续往后传;如果你返回 EXCEPTION_CONTINUE_EXECUTION,则需要真正修复异常现场(例如修改 PC 指针),否则程序会原地再次触发同一个异常。

调试器会抢在 VEH 前面

这是最容易踩坑的地方。在 Visual Studio 里按 F5 调试运行时,调试器已经附加到进程,异常会先被调试器拦截,导致你自己的 VEH 回调不会执行

验证 VEH 行为的方法:

  1. Ctrl + F5 不调试运行。
  2. 或直接双击运行编译好的 .exe
  3. 或者在 VS 的”异常设置”中取消勾选 Win32 异常的”引发时中断”。

这个细节直接影响你如何在线下测试崩溃捕获逻辑。

SEH:基于栈帧的局部保护

SEH 是 Windows 原生的结构化异常处理机制,语法是 __try / __except。它能捕获系统级硬件异常(如空指针访问、除零、栈溢出),这是 C++ 的 try/catch 做不到的。

SEH 代码示例

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
#include <windows.h>
#include <cstdio.h>

const char* ExceptionCodeToString(DWORD code) {
switch (code) {
case EXCEPTION_ACCESS_VIOLATION: return "ACCESS_VIOLATION";
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "ARRAY_BOUNDS_EXCEEDED";
case EXCEPTION_FLT_DIVIDE_BY_ZERO: return "FLT_DIVIDE_BY_ZERO";
case EXCEPTION_INT_DIVIDE_BY_ZERO: return "INT_DIVIDE_BY_ZERO";
case EXCEPTION_STACK_OVERFLOW: return "STACK_OVERFLOW";
case EXCEPTION_ILLEGAL_INSTRUCTION: return "ILLEGAL_INSTRUCTION";
default: return "UNKNOWN";
}
}

void TestNullPointer() {
__try {
int* p = nullptr;
*p = 42;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
DWORD code = GetExceptionCode();
printf("[SEH] Caught: %s (0x%08X)\n", ExceptionCodeToString(code), code);
}
printf("[SEH] Program continues.\n");
}

void TestDivideByZero() {
__try {
int a = 10;
int b = 0;
int c = a / b;
(void)c;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
DWORD code = GetExceptionCode();
printf("[SEH] Caught: %s (0x%08X)\n", ExceptionCodeToString(code), code);
}
printf("[SEH] Program continues.\n");
}

int main() {
TestNullPointer();
TestDivideByZero();
return 0;
}

SEH 过滤函数

__except 后面可以跟一个过滤表达式或过滤函数,用来根据异常类型决定是处理还是继续搜索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LONG WINAPI ExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo) {
DWORD code = pExceptionInfo->ExceptionRecord->ExceptionCode;
if (code == EXCEPTION_ACCESS_VIOLATION) {
printf("[Filter] Access violation, handle it.\n");
return EXCEPTION_EXECUTE_HANDLER;
}
printf("[Filter] Not access violation, continue searching.\n");
return EXCEPTION_CONTINUE_SEARCH;
}

void TestFilter() {
__try {
int* p = nullptr;
*p = 1;
}
__except (ExceptionFilter(GetExceptionInformation())) {
printf("[SEH] Handled by filter.\n");
}
}

SEH 的局限

SEH 是基于栈帧的,只能保护 __try 块内的代码。它适合局部容错(例如保护某个可能崩溃的第三方调用),不适合做全局崩溃上报。

UEF + MiniDump:全局崩溃捕获的最后机会

UEF(Unhandled Exception Filter)是进程级的最后机会异常处理机制。当异常没有被任何 try/catch__except、线程级处理捕获时,Windows 会调用你注册的 UEF 回调。生产环境最常见的做法,就是在 UEF 中生成 MiniDump。

UEF + 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
#include <windows.h>
#include <dbghelp.h>
#include <cstdio.h>
#include <cstring>

#pragma comment(lib, "DbgHelp.lib")

LPTOP_LEVEL_EXCEPTION_FILTER g_oldFilter = nullptr;

bool GenerateMiniDump(EXCEPTION_POINTERS* pExceptionPointers,
const wchar_t* path) {
HANDLE hFile = CreateFileW(
path,
GENERIC_WRITE,
0,
nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr
);
if (hFile == INVALID_HANDLE_VALUE) {
printf("Failed to create dump file, error: %lu\n", GetLastError());
return false;
}

MINIDUMP_EXCEPTION_INFORMATION mei;
mei.ThreadId = GetCurrentThreadId();
mei.ExceptionPointers = pExceptionPointers;
mei.ClientPointers = FALSE;

// MiniDumpNormal 体积小,适合线上;MiniDumpWithFullMemory 信息更全
MINIDUMP_TYPE dumpType = MiniDumpNormal;

BOOL result = MiniDumpWriteDump(
GetCurrentProcess(),
GetCurrentProcessId(),
hFile,
dumpType,
&mei,
nullptr,
nullptr
);

CloseHandle(hFile);
return result == TRUE;
}

LONG WINAPI MyUnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo) {
printf("\n[UEF] Unhandled exception occurred\n");
printf("[UEF] Exception code: 0x%08X\n",
pExceptionInfo->ExceptionRecord->ExceptionCode);
printf("[UEF] Exception address: 0x%p\n",
pExceptionInfo->ExceptionRecord->ExceptionAddress);

// dump 路径与 exe 同名,扩展名改为 .dmp
wchar_t dumpPath[MAX_PATH];
GetModuleFileNameW(nullptr, dumpPath, MAX_PATH);

wchar_t* ext = wcsrchr(dumpPath, L'.');
if (ext) {
wcscpy_s(ext, MAX_PATH - (ext - dumpPath), L".dmp");
} else {
wcscat_s(dumpPath, MAX_PATH, L".dmp");
}

printf("[UEF] Generating MiniDump...\n");
if (GenerateMiniDump(pExceptionInfo, dumpPath)) {
wprintf(L"[UEF] MiniDump saved to: %s\n", dumpPath);
} else {
printf("[UEF] Failed to generate MiniDump.\n");
}

// 调用旧的 UEF,保持链式行为
if (g_oldFilter != nullptr) {
return g_oldFilter(pExceptionInfo);
}

return EXCEPTION_EXECUTE_HANDLER;
}

int main() {
g_oldFilter = SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
printf("Unhandled exception filter registered.\n");

int* p = nullptr;
*p = 0xDEADBEEF; // 触发 Access Violation

return 0;
}

编译命令:

1
cl /EHsc /W4 /Zi /Fe:crash_dump.exe main.cpp DbgHelp.lib

为什么用 UEF 而不是 VEH 做崩溃上报

对比项 VEH UEF
调用时机 每次异常都调用 仅未处理异常才调用
噪音 大,会收到已捕获的异常 小,只处理真正崩溃
典型用途 抢先日志、调试注入 生成 dump、崩溃上报
全局性

所以如果只是想在”真正崩溃”时做处理,UEF 是更合适的入口。

MiniDump 类型的选择

类型 说明 适用场景
MiniDumpNormal 只包含基本信息,体积小 线上上报
MiniDumpWithFullMemory 包含完整内存,体积大 本地深度分析
MiniDumpWithIndirectlyReferencedMemory 包含间接引用内存 折中方案

线上通常用 MiniDumpNormal,必要时在回调里追加自定义信息(如日志片段、用户操作路径)来补充上下文。

常见异常代码速查

代码 含义 典型原因
0xC0000005 Access Violation 空指针、野指针、数组越界、use-after-free
0xC0000094 Integer Divide by Zero 整数除零
0xC00000FD Stack Overflow 无限递归、栈上分配过大内存
0xC000001D Illegal Instruction 执行了 CPU 不认识的指令
0x80000003 Breakpoint 调试断点
0xC0000409 Stack Buffer Overrun 栈缓冲区溢出
0xC0000374 Heap Corruption 堆损坏(如 double free)

各机制对比与工程选择

机制 调用顺序 作用 典型用途
VEH 1 抢先处理异常 全局日志、调试注入
SEH 2 栈帧级异常处理 局部容错、资源清理
C++ EH 3 处理 C++ throw 业务逻辑异常
VEC 4 SEH 后继续 很少单独使用
UEF 5 最后机会处理 生成 dump、崩溃上报
WER 6 系统级上报 系统备份 dump

工程建议:

  • 大多数客户端程序只需要注册 SetUnhandledExceptionFilter 生成 MiniDump。
  • 需要更灵活的抢先处理时,再加 VEH。
  • WER 保持默认开启,作为双重保险。
  • 复杂项目可以考虑 Google Crashpad 等成熟崩溃捕获库。

一个完整的崩溃捕获闭环

1
2
3
4
5
6
7
8
9
10
11
12
13
程序运行

异常发生

VEH 记录日志(可选)

SEH / C++ EH 尝试局部处理

UEF 生成 MiniDump

离线分析(WinDbg + PDB)

定位代码行,修复 bug

下一篇会进入具体崩溃类型的分析:空指针、堆破坏、栈溢出、未初始化、内存耗尽、纯虚调用,以及它们在 Debug / Release 下的行为差异。