导读:本期聚焦于大象创作的《C++利用SEH能捕获Windows内核崩溃抛出的异常吗?【干货】》,敬请观看详情。SEH并不是C++标准异常处理的替代品,而是Windows线程在内核与用户态之间传递异常信息的一套分发机制。访问违规、除零、断点等系统级异常出现时,CPU先进入内核异常分发流程,再回到用户态查找当前线程的SEH处理链,标准try/catch在这一层几乎无能为力。本文围绕__try/__except的编译展开和过滤表达式写法,给出捕获访问违规的代码示例,并说明如何结合MiniDumpWriteDump生成崩溃转储。同时重点澄清边界:用户态SEH只能捕获当前进程内的异常,无法捕获内核崩溃或蓝屏,驱动级错误要借助WER、内核调试和转储分析。掌握这些细节后,程序闪退能留下关键定位信息,而不是只剩一个退出码。

在Windows平台上,C++程序崩溃时往往只看到一个十六进制的退出码,比如0xC0000005代表访问违规。标准C++的try/catch只能捕获显式抛出的C++异常,对空指针解引用、除零、栈溢出这类系统级异常几乎不起作用。要拿到这类异常的第一现场,需要用到Windows的结构化异常处理,也就是SEH。SEH不是编译器的语法糖,它背后是一套由系统内核参与分发的异常处理链,理解它的工作方式后,再结合崩溃转储,就能在程序闪退时留下足够的定位信息。

C++利用SEH能捕获Windows内核崩溃抛出的异常吗?【干货】

SEH的注册链与异常分发过程

SEH全称是Structured Exception Handling,它和C++标准里的异常处理走的是完全不同的两条路径。C++的try/catch由编译器和C++运行时负责,抛出的是C++对象;而SEH处理的是Windows系统定义的异常记录,核心数据结构是EXCEPTION_RECORD,里面包含异常码、异常地址、参数等信息。每个线程的线程信息块(TIB)里都保存了一个指向异常处理链表的指针,32位下通常通过fs:[0]访问,64位则使用GS基址对应的结构。链表节点类型是EXCEPTION_REGISTRATION_RECORD,每个节点包含前一个节点的指针和处理器回调地址。

当一条指令触发异常时,CPU会先陷入内核,内核的异常分发器根据异常类型决定是修复后返回用户态,还是把异常派发给当前线程。用户态的异常分发函数会从线程的SEH链头部开始,依次调用每个节点上的处理函数。处理函数可以先检查异常信息,再决定是处理该异常、继续搜索还是返回执行。编译器生成的__try/__except代码块,本质上就是构造一个这样的链表节点,并在进入__try之前把节点插入到当前线程的链表头部,离开时再摘除。理解这个链表结构,有助于判断异常过滤函数会在什么时机被调用,以及为什么未处理的异常会让整个进程崩溃。

常见的SEH异常码包括EXCEPTION_ACCESS_VIOLATION(0xC0000005)、EXCEPTION_INT_DIVIDE_BY_ZERO(0xC0000094)、EXCEPTION_BREAKPOINT(0x80000003)以及EXCEPTION_STACK_OVERFLOW(0xC00000FD)。其中访问违规在调试悬垂指针、野指针或者使用已释放内存时最常见。除零异常则多出现在整数除法中,而栈溢出异常本身比较特殊,因为此时栈空间已经不足以支持复杂的处理逻辑,后面会单独说明。

用__try/__except捕获访问违规并读取异常上下文

MSVC编译器为C和C++提供了__try与__except关键字,用法上看起来和标准try/catch类似,但过滤表达式的语义完全不同。__except后面的括号里可以写一个整数表达式,也可以调用一个过滤函数。系统根据这个表达式的返回值来决定后续动作:返回EXCEPTION_EXECUTE_HANDLER表示当前处理块接管异常,返回EXCEPTION_CONTINUE_SEARCH表示继续向链表上层搜索,返回EXCEPTION_CONTINUE_EXECUTION则表示回到异常发生处重新执行那条指令。实际项目中很少使用最后一种,因为如果异常原因没有消除,程序会在同一个位置反复崩溃。

下面这段代码演示了如何捕获一个空指针写入引发的访问违规。注意__try/__except不能和标准C++的try/catch写在同一个函数中混用,否则编译器会报错,因为两者的栈展开机制不同。

#include <windows.h>
#include <iostream>

int main()
{
    int* p = nullptr;
    __try
    {
        *p = 42;
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        DWORD code = GetExceptionCode();
        std::cout << "捕获到SEH异常,异常码: 0x"
                  << std::hex << code << std::endl;
    }
    return 0;
}

如果只判断异常码,可以直接在__except括号内写GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH。但更常见的做法是编写一个独立的过滤函数,在函数里调用GetExceptionInformation获取完整的异常指针。这个函数返回PEXCEPTION_POINTERS,里面既有ExceptionRecord,也有发生异常时的线程上下文ContextRecord。通过上下文可以拿到寄存器、栈指针和指令指针,对定位崩溃原因很有帮助。

int FilterAccessViolation(PEXCEPTION_POINTERS ep)
{
    if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
    {
        return EXCEPTION_EXECUTE_HANDLER;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

void Test()
{
    char* p = reinterpret_cast<char*>(0x1000);
    __try
    {
        *p = 0;
    }
    __except(FilterAccessViolation(GetExceptionInformation()))
    {
        // 可以在这里记录RIP或EIP以及异常地址
    }
}

在这个过滤函数中,GetExceptionInformation只能在__except的过滤表达式内调用,不能在处理块里调用,这是编译器的限制。过滤函数返回EXCEPTION_EXECUTE_HANDLER后,异常处理块才会执行,此时可以用GetExceptionCode再次读取异常码。需要特别提醒的是,如果捕获的是EXCEPTION_STACK_OVERFLOW,不要试图在当前线程里分配内存或调用复杂的函数,因为栈已经耗尽,应当尽可能简单地记录信息并结束进程,否则可能引发二次异常导致无法生成有效转储。

结合MiniDumpWriteDump与全局异常过滤器生成崩溃转储

单独用__try/__except只能覆盖已知的风险代码段,实际工程里很难把每一段可能崩溃的代码都包起来。更实用的方案是设置一个全局的未处理异常过滤器,当某个线程的SEH链上没有任何处理函数接管异常时,系统会调用这个过滤器。通过SetUnhandledExceptionFilter可以把自定义函数挂到进程的顶层,从而在程序崩溃前生成minidump文件,方便事后用WinDbg或Visual Studio分析。

下面是一个生成minidump的示例。转储函数MiniDumpWriteDump由dbghelp.dll导出,使用时需要链接DbgHelp.lib。转储文件路径这里写入了C:\CrashDumps\app.dmp,实际项目中可以根据当前时间或进程名动态拼接。

#include <windows.h>
#include <dbghelp.h>
#include <ctime>

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

LONG WINAPI MyUnhandledExceptionFilter(_EXCEPTION_POINTERS* ExceptionInfo)
{
    CreateDirectory(L"C:\\CrashDumps", nullptr);

    SYSTEMTIME st;
    GetLocalTime(&st);

    wchar_t filePath[MAX_PATH];
    swprintf_s(filePath, MAX_PATH,
        L"C:\\CrashDumps\\crash_%04d%02d%02d_%02d%02d%02d.dmp",
        st.wYear, st.wMonth, st.wDay,
        st.wHour, st.wMinute, st.wSecond);

    HANDLE hFile = CreateFile(filePath, GENERIC_WRITE, 0,
        nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);

    if (hFile != INVALID_HANDLE_VALUE)
    {
        MINIDUMP_EXCEPTION_INFORMATION mei;
        mei.ThreadId = GetCurrentThreadId();
        mei.ExceptionPointers = ExceptionInfo;
        mei.ClientPointers = FALSE;

        MiniDumpWriteDump(
            GetCurrentProcess(),
            GetCurrentProcessId(),
            hFile,
            MiniDumpNormal,
            &mei,
            nullptr,
            nullptr);

        CloseHandle(hFile);
    }

    return EXCEPTION_EXECUTE_HANDLER;
}

int main()
{
    SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
    // 其他初始化逻辑...
    return 0;
}

除了在代码里调用SetUnhandledExceptionFilter,Windows还提供注册表级的调试器自动启动机制。键位HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug下的Debugger值可以配置为WinDbg或cdb的命令行,系统在进程崩溃时会自动启动该调试器。不过这个配置会影响整台机器,一般只在测试环境使用。生产环境更推荐依赖WER(Windows Error Reporting)服务,它会自动把崩溃信息写入C:\ProgramData\Microsoft\Windows\WER\ReportArchive等目录,并可按策略上报。

SEH的边界:为什么它无法捕获内核崩溃

标题里提到“内核崩溃”,这里必须澄清一个关键概念。用户态进程通过SEH能捕获的异常,全部都是当前进程上下文中的异常,它们的异常记录由用户态分发器处理。当内核自身发生严重错误时,系统会触发bugcheck,也就是通常说的蓝屏。内核崩溃由KeBugCheckEx等内核函数主动引发,随后系统停止所有处理,把物理内存转储到C:\Windows\MEMORY.DMP或迷你转储到C:\Windows\Minidump目录。这个过程完全在内核态完成,用户态的SEH根本不会收到任何异常记录。

换句话说,如果你的驱动程序或内核模块导致蓝屏,应用程序里写得再完善的__try/__except也拦不住,因为在系统停止响应之前,根本不会回到用户态去执行异常处理链。此时需要的是内核调试,例如在双机调试环境下用WinDbg连接,执行!analyze -v分析转储文件,或者通过WER收集蓝屏后生成的dump。对于驱动开发,WDK提供了一套不同的结构化异常处理包装,但应用程序开发者通常接触不到这一层。

还有一种容易混淆的情况:用户态代码访问了非法地址,CPU产生访问违规,内核在处理这个异常时发现它来自用户态,于是把异常信息返回给用户态分发器。这时SEH能捕获,但它捕获的并不是内核崩溃,而是内核转发回用户态的一次普通异常。因此,当有人说“用SEH捕获Windows内核崩溃”时,更准确的描述应当是“捕获由内核参与分发的用户态崩溃异常”。区分这一点,能避免在驱动蓝屏排查时走错方向。

综合来看,SEH给C++开发者提供了一条在系统级异常发生时紧急抢险的通道。通过__try/__except可以精确捕获并检查访问违规等致命错误,通过SetUnhandledExceptionFilter与MiniDumpWriteDump可以做到崩溃不白崩,事后有dump可查。理解线程异常链的插入顺序、过滤表达式的返回值以及栈溢出等特殊异常的限制,是避免二次崩溃的关键。同时也要把用户态SEH与内核态bugcheck的边界划清楚,不要让一个技术名词误导了问题排查方向。掌握这些内容后,再把方案落到实际项目中,就能显著减少程序闪退后无从下手的情况。

SEH结构化异常处理Windows异常捕获修改时间:2026-09-24 15:09:56

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0924/61357.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。