在Windows平台上,C++程序可能因为空指针解引用、栈溢出或非法指令而直接终止。系统提供的结构化异常处理(SEH)允许我们在进程被系统强杀前接管这些异常,从而记录现场或做兜底逻辑。理解SEH与C++异常的差异,是写出稳定崩溃处理方案的前提。

SEH与C++异常的区别
很多开发者习惯用try-catch处理错误,但C++标准异常只能捕获通过throw抛出的对象。当程序触发的是操作系统级异常,例如访问了未提交内存页,CPU会产生陷阱,由Windows异常分发器抛出STATUS_ACCESS_VIOLATION,这类异常不会自动变成C++异常。SEH是Windows原生机制,通过编译器的__try和__except关键字暴露,能拦截包括除零、断点、页面错误在内的硬件与系统异常。
在Visual C++中,如果开启/EHa选项,C++异常和SEH会被统一成同一套帧处理,但语义仍不同。__except的过滤表达式在异常发生时立即求值,可以读取ExceptionRecord拿到错误码和地址;而catch块拿到的是已构造的异常对象,对系统异常通常只能看到std::exception的泛化信息。对于崩溃捕获,必须依赖SEH才能拿到原始上下文。
基础捕获代码框架
使用SEH需要包含windows.h,并在编译时允许编译器扩展。下面示例展示一个最小的异常保护块,当内部代码崩溃时进入过滤器,打印异常编号并选择继续执行或退出。
#include <windows.h>
#include <stdio.h>
// 异常过滤器:打印信息并返回处理策略
LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* pEp) {
printf("捕获到异常,代码: 0x%Xn", pEp->ExceptionRecord->ExceptionCode);
printf("异常地址: 0x%pn", pEp->ExceptionRecord->ExceptionAddress);
// EXCEPTION_EXECUTE_HANDLER表示已处理,不再向上抛
return EXCEPTION_EXECUTE_HANDLER;
}
void CrashFunc() {
int* p = nullptr;
*p = 10; // 触发访问违规
}
int main() {
__try {
CrashFunc();
}
__except(ExceptionFilter(GetExceptionInformation())) {
printf("异常已被处理,程序继续n");
}
return 0;
}
上面代码中,__except圆括号内调用了ExceptionFilter,并传入GetExceptionInformation()返回的指针。过滤器返回EXCEPTION_EXECUTE_HANDLER时,控制流进入__except块;若返回EXCEPTION_CONTINUE_SEARCH则交给外层;返回EXCEPTION_CONTINUE_EXECUTION会重试出错指令,通常仅在修复了上下文时使用。
需要注意,SEH块内不能包含C++对象析构依赖的复杂栈展开,因为__try块不是C++标准作用域。若块内定义了含析构函数的局部变量,在Visual C++下编译器会报警告并可能跳过析构,因此建议把资源操作放在普通函数里,SEH只包住纯C风格逻辑或调用点。
生成崩溃转储文件
仅仅打印异常码对事后排查远远不够。借助DbgHelp库的MiniDumpWriteDump,可以在异常过滤器里把进程内存、线程上下文写成minidump,用WinDbg或Visual Studio打开就能看调用栈。
#include <windows.h>
#include <dbghelp.h>
#include <stdio.h>
#pragma comment(lib, "dbghelp.lib")
LONG WINAPI DumpFilter(EXCEPTION_POINTERS* pEp) {
HANDLE hFile = CreateFileA("crash.dmp", GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION mei;
mei.ThreadId = GetCurrentThreadId();
mei.ExceptionPointers = pEp;
mei.ClientPointers = FALSE;
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
hFile, MiniDumpNormal, &mei, NULL, NULL);
CloseHandle(hFile);
}
return EXCEPTION_EXECUTE_HANDLER;
}
void BadCode() {
char* s = (char*)1;
s[0] = 'x'; // 非法写入
}
int main() {
__try {
BadCode();
}
__except(DumpFilter(GetExceptionInformation())) {
printf("已生成dump,程序退出n");
}
return 0;
}
这段程序在崩溃时创建crash.dmp,其中携带了异常指针和当前线程状态。生成dump时应尽量在过滤器中减少复杂调用,因为进程处于不稳定状态,过多API可能二次崩溃。MiniDumpNormal是最基础类型,若需要更多内存段可改用MiniDumpWithFullMemory,但文件会显著变大。
发布版本编译时务必保留PDB符号文件,否则dump里只有偏移地址。将PDB与exe版本对应保存,分析时指向符号路径即可还原函数名与行号。此外,若程序加载了多个模块,建议在过滤器中遍历模块列表一并记录版本信息,方便定位第三方库问题。
全局异常钩子与兼容性
除了局部__try块,还可以通过SetUnhandledExceptionFilter注册进程级兜底函数,捕获那些没有被任何__try包住的异常。这在main函数外层做最后保护非常实用。
#include <windows.h>
#include <stdio.h>
LONG WINAPI GlobalFilter(EXCEPTION_POINTERS* p) {
printf("全局过滤器触发,异常码 0x%Xn", p->ExceptionRecord->ExceptionCode);
return EXCEPTION_EXECUTE_HANDLER;
}
int main() {
SetUnhandledExceptionFilter(GlobalFilter);
// 此处崩溃若无局部SEH,会进入GlobalFilter
int* q = nullptr;
*q = 5;
return 0;
}
要注意的是,某些运行时库或第三方组件也会设置自己的未处理异常过滤器,后注册的会覆盖前一个,返回时会丢失原链。稳妥做法是保存旧过滤器地址,在新过滤器里根据情况调用。另外在64位程序上,SEH基于表驱动,不再使用栈链表,异常表必须随映像正确加载,因此不要手动修改PE节,否则会导致异常分发直接失败。
另一个常见坑是Qt或MFC等框架自带的消息循环可能内部吞掉异常。若发现崩溃没进过滤器,可检查是否编译选项混用了不同CRT,或是否在副线程中崩溃而主线程未等待。副线程异常同样走SEH,但进程终止行为取决于线程入口是否被包住,推荐在每个线程函数开头就套__try。
实践中的注意事项
SEH虽强,但不应作为普通错误控制流。频繁用异常做逻辑分支会带来巨大性能与维护成本。它只适合做最后一道防线:记录现场、上报、安全退出。在游戏或长期服务中,可结合心跳检测,当dump生成后重启自身,避免用户感知硬崩溃。
如果项目同时使用C++异常,请明确边界。在Visual C++中,__try块内不要throw C++异常,也不要在catch块里调用可能触发SEH的底层API而不加保护。多层混用容易让栈展开顺序错乱,引发更隐蔽的崩溃。保持SEH层在最外圈,C++异常在内部业务层,是较为清晰的结构。
C++SEHWindows_exception修改时间:2026-08-05 03:39:35