18、异常与错误处理:结构化异常处理(SEH)、内核异常、BugCheck分析、Dump文件分析、错误恢复

大家好,我是老吴。今天我们来聊聊驱动开发里最让人头疼,也最绕不开的话题——异常与错误处理。

说实话,写用户态程序时,蓝屏了顶多骂一句“垃圾软件”。但写内核驱动,一个空指针解引用,系统直接给你来个 BugCheck 0x50(页面错误),然后整个机器 dump 掉。你想想看,这压力多大?

所以,这一章我打算把我在内核调试中踩过的坑、用过的招,都掏出来给你。咱们从结构化异常处理(SEH)讲起,再到内核异常、BugCheck 分析、Dump 文件解读,最后聊聊怎么优雅地做错误恢复。

18.1 结构化异常处理(SEH)——内核里的“try-catch”

很多从应用层转过来的朋友,第一反应是:内核里能不能用 __try/__except

答案是:可以,但有限制。

Windows 内核支持 SEH,但只限于在 相同线程 内捕获异常。你不能跨线程、不能跨 IRQL 级别乱搞。我个人习惯是在处理用户态传入的指针时,必须用 SEH 包一层。

核心原则: 所有来自用户态的指针访问,必须用 __try/__except 保护。否则,用户给你一个非法地址,你的驱动直接蓝屏。

来看一个典型的例子:

NTSTATUS ReadUserBuffer(PVOID UserBuffer, PVOID KernelBuffer, SIZE_T Size)
{
    __try
    {
        // 尝试从用户态地址读取
        RtlCopyMemory(KernelBuffer, UserBuffer, Size);
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        // 捕获异常,返回错误
        DbgPrint("访问用户缓冲区时发生异常\n");
        return STATUS_ACCESS_VIOLATION;
    }
    return STATUS_SUCCESS;
}

这里要注意,__except 后面的表达式决定了如何处理异常。我一般用 EXCEPTION_EXECUTE_HANDLER,意思是“我处理了,别往上抛”。

警告: 在 IRQL >= DISPATCH_LEVEL 时,SEH 可能无法正常工作。因为页面错误在高 IRQL 下会被视为致命错误。我曾经在 DPC 例程里用 SEH 保护一个内存访问,结果异常根本没被捕获,系统直接挂了。所以,高 IRQL 下,别指望 SEH 能救你。

18.2 内核异常——不只是“蓝屏”那么简单

内核异常分很多种。最常见的包括:

  • 访问违规(0x50):访问了无效内存地址。比如空指针、已释放的内存。
  • 除零错误(0x7E):整数除以零。内核里很少见,但一旦出现,基本是算法 bug。
  • 双重释放(0x19):释放了已经释放的内存池。这个我遇到过,查了两天才找到原因。
  • IRQL 不当(0x1E):在错误的 IRQL 级别上调用了需要更低 IRQL 的函数。

为什么会发生这些异常?说白了,就是驱动代码违反了内核的“交通规则”。

我记得有一次,一个同事写的驱动在卸载时总是蓝屏。我帮他看代码,发现他在 DriverUnload 里释放了一个全局指针,但那个指针在另一个线程里还在用。典型的“释放后使用”问题。

避坑指南: 我曾经在释放内存后忘记把指针置 NULL,结果后续代码又访问了它。虽然当时没蓝屏,但后来在客户机器上复现了。所以,释放后立即置 NULL,这是个好习惯。

18.3 BugCheck 分析——读懂蓝屏的“遗言”

当系统检测到不可恢复的内核错误时,它会调用 KeBugCheckEx,生成一个 BugCheck 代码,然后 dump 内存。

BugCheck 代码是诊断的第一线索。比如:

BugCheck 代码 含义 常见原因
0x50 PAGE_FAULT_IN_NONPAGED_AREA 访问了无效内存地址
0x7E SYSTEM_THREAD_EXCEPTION_NOT_HANDLED 系统线程中发生了未处理的异常
0x1E KMODE_EXCEPTION_NOT_HANDLED 内核模式异常未处理
0x19 BAD_POOL_HEADER 内存池头部损坏
0xD1 DRIVER_IRQL_NOT_LESS_OR_EQUAL 驱动在错误的 IRQL 上访问了分页内存

拿到 BugCheck 代码后,下一步就是看参数。每个 BugCheck 都有 4 个参数,它们提供了更详细的信息。比如 0x50 的参数 1 是出错的虚拟地址,参数 2 是访问类型(读/写)。

我建议你学会用 WinDbg 的 !analyze -v 命令。它会自动解析 BugCheck 参数,并给出可能的根因。这个命令救过我无数次。

18.4 Dump 文件分析——从“尸体”中找线索

Dump 文件是系统崩溃时的内存快照。分析它,就像法医解剖尸体。

Dump 文件有三种类型:

  • 完全内存转储:包含所有物理内存。文件很大,但信息最全。
  • 内核内存转储:只包含内核使用的物理内存。我一般用这个。
  • 小内存转储(64KB):只包含关键信息。适合快速定位。

分析步骤大致如下:

  1. 用 WinDbg 打开 Dump 文件。
  2. 运行 !analyze -v 获取初步分析。
  3. 查看 STACK_TEXT,找到崩溃时的调用栈。
  4. 检查 PROCESS_NAMEMODULE_NAME,确定是哪个驱动出了问题。
  5. kb 命令查看栈回溯,定位到具体的函数和偏移。

我记得有一次,客户反馈系统频繁蓝屏,Dump 分析显示是 nt!MmAccessFault 里出的问题。但调用栈里没有我们的驱动。后来我用 !thread 命令查看线程信息,发现是另一个第三方驱动在卸载时没清理干净,导致我们的驱动访问了已释放的内存。嗯,这锅背得冤。

关键技巧: 分析 Dump 时,别只看当前线程。用 !process 0 0 列出所有进程,用 !vm 查看虚拟内存状态,用 !poolused 2 查看内存池使用情况。有时候,问题不在崩溃的线程里,而在其他线程留下的“烂摊子”。

18.5 错误恢复——让系统“软着陆”

不是所有错误都能避免。当错误发生时,我们要做的是让系统“软着陆”,而不是直接“硬摔”。

错误恢复的策略有几种:

  • 优雅降级:如果某个功能失败,关闭它,但保持驱动其他部分运行。比如,一个网卡驱动如果某个队列出问题,可以只禁用那个队列,而不是整个网卡。
  • 状态回滚:在操作前保存状态,失败时恢复到之前的状态。我习惯在修改硬件寄存器前,先读回原始值,如果修改失败就写回去。
  • 资源清理:确保在错误路径上释放所有已分配的资源。这个说起来简单,做起来难。我见过太多驱动在错误处理时忘了释放自旋锁,导致死锁。

来看一个简单的错误恢复示例:

NTSTATUS SafeHardwareOperation()
{
    NTSTATUS status;
    KIRQL oldIrql;
    PVOID buffer = NULL;

    // 分配资源
    buffer = ExAllocatePoolWithTag(NonPagedPool, 1024, 'TAG1');
    if (buffer == NULL) {
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    // 提升 IRQL
    KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);

    // 执行硬件操作
    status = WriteToHardware(buffer);
    if (!NT_SUCCESS(status)) {
        // 失败时回滚
        KeLowerIrql(oldIrql);
        ExFreePoolWithTag(buffer, 'TAG1');
        return status;
    }

    // 正常路径
    KeLowerIrql(oldIrql);
    ExFreePoolWithTag(buffer, 'TAG1');
    return STATUS_SUCCESS;
}

这个例子里,我用了“资源分配 → 操作 → 清理”的模式。注意,在错误路径上,我同样调用了 KeLowerIrqlExFreePoolWithTag。这是关键——错误路径和正常路径的清理必须一致。

避坑指南: 我曾经在错误处理里忘了释放一个事件对象,结果导致后续所有等待该事件的线程都挂死了。后来我用 !locks 命令查看锁状态,才发现那个事件一直处于 signaled 状态,但没人清理。所以,我建议你在写错误处理代码时,用“goto cleanup”模式,把清理代码集中到一处,避免遗漏。

18.6 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

异常与错误处理知识体系 异常与错误处理 结构化异常处理(SEH) 内核异常类型 BugCheck分析 Dump文件分析 错误恢复策略 调试工具(WinDbg) __try/__except 用户指针保护 0x50, 0x7E, 0x1E IRQL不当等 BugCheck代码 4个参数解析 !analyze -v 栈回溯分析 优雅降级 状态回滚 !thread !poolused

这张图把本章的核心内容串起来了。从 SEH 保护,到识别内核异常,再到用 BugCheck 和 Dump 分析定位问题,最后用错误恢复策略让系统稳定。每一步都环环相扣。

好了,关于异常与错误处理,我就讲这么多。记住,写驱动不是写应用,每一个错误都可能让整个系统陪葬。所以,多花点时间在错误处理上,绝对值得。


公众号:蓝海资料掘金营,微信deep3321