Reactos 第 8 章 结构化异常处理 — 8.1 结构化异常处理的程序框架

第 8 章 结构化异常处理 --- 8.1 结构化异常处理的程序框架

本节深入剖析 ReactOS 中结构化异常处理(Structured Exception Handling, SEH)的程序框架与运行原理。 从操作系统的视角看,SEH 不仅是一套语言级别的 try/except 语法糖,更是一套横跨 硬件中断层、内核态分派层、用户态分派层 的多层协作机制。理解这套框架的关键,是把握数据在不同层之间的流向:硬件 trap 把信息填入 KTRAP_FRAME,内核再把 trap frame 转换为 CONTEXT,用户态的 RtlDispatchException 沿着 EXCEPTION_REGISTRATION_RECORD 链表查找并调用异常处理函数,handler 返回 EXCEPTION_DISPOSITION 后由 RtlUnwind 决定是恢复还是继续搜索。


概述

结构化异常处理(SEH)是 Windows NT 内核为用户态与内核态代码提供的 同步异常处理机制。它在 ReactOS 中由三个相互协作的子系统构成:

  1. 编译器/语言运行时层 :MSVC 的 __try / __except / __finally、GCC 借助 PSEH2 宏的 _SEH2_TRY / _SEH2_EXCEPT
  2. 操作系统运行时层(RTL)RtlDispatchExceptionRtlUnwindRtlRaiseException 等一组函数;
  3. 硬件/内核异常分派层 :x86 IDT、trap handler、KiDispatchException

三个层次的数据通过 EXCEPTION_RECORDCONTEXTEXCEPTION_REGISTRATION_RECORD 三个结构贯通。理解 SEH 的关键,就在于看清这三个结构在不同层之间如何相互转化与传递。

本节内容概览

  • 8.1.0 框架图:三层 SEH 体系总览
  • 8.1.1 SEH 的本质与设计目标
  • 8.1.2 三层 SEH 体系结构
  • 8.1.3 关键数据结构:EXCEPTION_RECORD
  • 8.1.4 关键数据结构:EXCEPTION_REGISTRATION_RECORD
  • 8.1.5 关键数据结构:EXCEPTION_DISPOSITIONEXCEPTION_POINTERS
  • 8.1.6 关键数据结构:CONTEXT(处理器上下文)
  • 8.1.7 异常标志位(EXCEPTION_*)的语义
  • 8.1.8 异常处理流程总览(用户态/内核态双向流动)
  • 8.1.9 关键 API 索引

学习目标

  • 能够画出 SEH 在三层之间的数据流图
  • 能够口述 EXCEPTION_RECORDEXCEPTION_REGISTRATION_RECORDEXCEPTION_DISPOSITION 三个核心结构的作用
  • 理解硬件异常如何跨越内核边界进入用户态 handler
  • 区分 硬件异常(hardware exception)软异常(software exception)

涉及的内核子系统

子系统 头文件 核心作用
HAL / 内核 trap ntoskrnl/ke/i386/exp.c IDT → KiDispatchException
平台无关 SEH ntoskrnl/ke/except.c KiContinueKiRaiseExceptionNtRaiseException
用户态 RTL SEH sdk/lib/rtl/i386/except.c RtlDispatchExceptionRtlUnwind
RTL 公共 SEH sdk/lib/rtl/exception.c RtlRaiseExceptionRtlUnhandledExceptionFilter
向量化异常(VEH) sdk/lib/rtl/vectoreh.c RtlCallVectoredExceptionHandlers
编译期 SEH 宏 sdk/include/vcruntime/pseh/pseh2.h _SEH2_TRY
异常结构定义 sdk/include/xdk/rtltypes.h EXCEPTION_RECORD
异常结构定义(Wine) sdk/include/wine/winnt.h EXCEPTION_REGISTRATION_RECORD
异常分发枚举 sdk/include/vcruntime/excpt.h EXCEPTION_DISPOSITION

8.1.0 框架图

复制代码
                       用户态 (Ring 3)                        用户态/内核态边界
  +-----------------------------------------------------+    +------------------+
  |                                                     |    |                  |
  |  编译器层 (MSVC __try/__except, GCC PSEH2)          |    |   ntdll.dll      |
  |  +-----------------+    +-----------------------+  |    |                  |
  |  | try { ... }     |    | __except(filter) { }  |  |    | KeUserException- |
  |  +-----------------+    +-----------------------+  |    |   Dispatcher     |
  |              |                       ^              |    |   (用户态入口)     |
  |              v                       |              |    |                  |
  |  +-------------------+    +-----------+------+      |    |   RtlDispatch-   |
  |  | EXCEPTION_REGIS-  |    |  _SEH2_TRY |filter|      |    |     Exception    |
  |  | TRATION_RECORD    |<---+-------------+------+      |    |                  |
  |  | .prev ->  .next   |    |  (FS:[0]    |      |      |    |   RtlUnwind      |
  |  | .handler          |    |   Exception |      |      |    |                  |
  |  +-------------------+    |   List)     |      |      |    |   RtlRaise-      |
  |            |              +-------------+------+     |    |     Exception    |
  |            |                        ^                |    |                  |
  |            |                        |                |    |   RtlUnhandled-  |
  |            v                        |                |    |   ExceptionFilter|
  |  +-------------------+    +---------+----------+     |    |                  |
  |  | EXCEPTION_RECORD  |    |  EXCEPTION_        |     |    |   VEH (vectored) |
  |  | CONTEXT           |    |  DISPOSITION       |     |    |   handlers chain |
  |  | EXCEPTION_POINTERS|    |  (handler 返回值)   |     |    |                  |
  |  +-------------------+    +--------------------+     |    +------------------+
  |                                                     |            |
  +-----------------------------------------------------+            |
                                                                      | 系统调用
                                                                      v
  +-----------------------------------------------------+   +--------------------+
  |              内核态 (Ring 0)                         |   |   ntoskrnl.exe     |
  |                                                     |   |                    |
  |  硬件 trap entry:   KiTrap00 .. KiTrap13            |   | NtRaiseException   |
  |  KiDebugHandler (int 1)                             |   | NtContinue         |
  |  KiEnterTrap(构造 KTRAP_FRAME)                       |   |                    |
  |                     |                               |   | KiDispatch-        |
  |                     v                               |   |   Exception        |
  |  KiDispatchException0Args/1Args/2Args               |<--+                    |
  |                     |                               |                        |
  |                     v                               |   | KiDispatch-        |
  |  KeTrapFrameToContext -> CONTEXT                    |   |   ExceptionFrom-   |
  |  KiDispatchException(,FirstChance=)                 |   |   TrapFrame        |
  |      |  PreviousMode == KernelMode:                 |   | (软异常统一入口)    |
  |      |     FirstChance -> KiDebugRoutine ->         |   |                    |
  |      |                  RtlDispatchException        |   | KiContinue         |
  |      |     SecondChance -> KiDebugRoutine           |   |                    |
  |      |     Three-Strike -> KeBugCheckEx             |   |                    |
  |      |  PreviousMode == UserMode:                   |   |                    |
  |      |     FirstChance -> KiDebugRoutine ->         |   |                    |
  |      |                  DbgkForwardException ->     |   |                    |
  |      |                  构造 user-mode 调用栈 ->    |   |                    |
  |      |                  EIP = KeUserException-       |   |                    |
  |      |                        Dispatcher            |   |                    |
  |      v                                               |   |                    |
  |  (异常未被处理) -> KeBugCheckEx                      |   |                    |
  |                                                     |   |                    |
  +-----------------------------------------------------+   +--------------------+
                                                                      ^
                                                                      |
                                                                  硬件 (CPU)
                                                              INT n / #DE ...

8.1.1 SEH 的本质与设计目标

SEH 的本质 :SEH 是一套"穿越调用栈的协作式控制流反转"机制。当硬件 trap、错误指令、API 调用、调试器断点等事件发生时,CPU 控制流被强制转入一个统一的入口点(内核 trap handler),由入口点沿着调用栈逐层询问每个函数"是否要处理这个异常",任何一层都可以选择 处理(恢复执行)拒绝(继续向栈顶搜索)

与 C++ 异常(throw/catch)的区别在于:

维度 SEH C++ 异常
触发源 硬件 trap + 软件 API 只能由 throw 主动抛出
匹配规则 沿调用栈线性搜索 沿调用栈做类型匹配
栈展开 显式 RtlUnwind 编译器自动生成清理代码
处理函数形态 EXCEPTION_DISPOSITION 返回值 catch 块
平台绑定 NT 内核原生 编译器(MSVC、Itanium ABI)

设计目标

  1. 统一处理硬件异常与软件异常 (除零、访问违例、RtlRaiseException 都走同一套框架)
  2. 支持跨语言/跨模块(C、C++、MASM、__try/except/__finally 全部统一)
  3. 支持嵌套异常与栈展开 (析构函数、__finally 块的执行)
  4. 支持多级机会(FirstChance → SecondChance → 蓝屏)
  5. 支持调试器接入(VEH、用户态调试器、内核 KD)

8.1.2 三层 SEH 体系结构

ReactOS 的 SEH 体系可以清晰划分为三层,每层都定义了自己的接口与数据结构:

第一层:编译器/语言运行时层

MSVC 通过 __try / __except / __finally 关键字直接生成 SEH 框架。GCC 用户态下也能通过 _try1 / __try1 宏直接插入 FS:0 链表(仅限 x86),但 ReactOS 跨平台代码倾向使用 PSEH2(Portable SEH 2) 宏,它把 SEH 包装成可移植的宏展开。

PSEH2 的核心宏在 pseh2.h(file:///d:/reactos/sdk/include/vcruntime/pseh/pseh2.h) 中,关键形式为:

c 复制代码
_SEH2_TRY
{
    /* 受保护代码 */
}
_SEH2_EXCEPT(filter_expression)
{
    /* 异常处理 */
}
_SEH2_END;

宏展开后,会在栈上构造一个 _SEH2_TRY_CONTEXT,并把它链入 NtCurrentTib()->ExceptionList。内核态(ntoskrnl.exe)的代码也使用同样的 PSEH2 宏,这也是为什么在内核中可以看到 _SEH2_TRY 调用。

第二层:操作系统 RTL 层

RtlDispatchException(file:///d:/reactos/sdk/lib/rtl/i386/except.c#L65-L223) 是 RTL 异常分派入口,被 KiDispatchException(file:///d:/reactos/ntoskrnl/ke/i386/exp.c#L795-L1050) 在内核态异常分派中调用一次(内核模式RtlDispatchException 复用同一份链表代码)。它负责:

  1. 优先调用 VEH(向量化异常处理器)
  2. 沿 EXCEPTION_REGISTRATION_RECORD 链表逐层调用 handler
  3. 根据 EXCEPTION_DISPOSITION 决定 恢复执行继续搜索转换为新异常

RtlUnwind(file:///d:/reactos/sdk/lib/rtl/i386/except.c#L230-L402) 是栈展开入口,被 __finally 块或 EXCEPTION_EXECUTE_HANDLER 触发时调用。它沿着链表反向调用每个 handler 的"展开回调"。

RtlRaiseException(file:///d:/reactos/sdk/lib/rtl/exception.c#L32-L69) / RtlRaiseStatus(file:///d:/reactos/sdk/lib/rtl/exception.c#L85-L120) 是用户态主动抛出异常的接口。

第三层:硬件/内核异常分派层

硬件 trap 通过 IDT 入口进入 `ntoskrnl/ke/i386/trap.s`(file:///d:/reactos/ntoskrnl/ke/i386/trap.s) 中的 KiTrap00 ~ KiTrap13 桩。每个 trap 桩都通过 TRAP_ENTRY 宏展开为:

asm 复制代码
TRAP_ENTRY KiTrap00, KI_PUSH_FAKE_ERROR_CODE

其内部调用 KiEnterTrap 把硬件上下文保存为 KTRAP_FRAME,然后跳到 `traphdlr.c`(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) 中的 KiTrap00Handler ~ KiTrap13Handler。后者调用 KiDispatchException0Args / 1Args / 2Args 包装函数,最终汇入 KiDispatchException

KiDispatchException 是内核异常分派的 核心枢纽 ,根据 PreviousMode 决定是内核态处理还是用户态处理。


8.1.3 关键数据结构:EXCEPTION_RECORD

EXCEPTION_RECORD 是描述"发生了什么异常"的核心结构,定义于 rtltypes.h(file:///d:/reactos/sdk/include/xdk/rtltypes.h#L171-L178):

c 复制代码
typedef struct _EXCEPTION_RECORD {
    NTSTATUS ExceptionCode;        // 异常码 (STATUS_*)
    ULONG ExceptionFlags;          // 异常标志位
    struct _EXCEPTION_RECORD *ExceptionRecord;  // 嵌套异常的"上一条"
    PVOID ExceptionAddress;        // 异常发生地址
    ULONG NumberParameters;        // 有效参数个数
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; // 参数数组
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;

要点:

  • ExceptionCodeNTSTATUS 类型,常见的 STATUS_ACCESS_VIOLATION(0xC0000005)、STATUS_BREAKPOINT(0x80000003)、STATUS_INTEGER_DIVIDE_BY_ZERO(0xC0000094)等。
  • ExceptionFlags 是位标志组合(见 8.1.7 节)。
  • ExceptionRecord 形成一个 嵌套链 :当 handler 内部又抛出异常时,新异常的 ExceptionRecord 指向旧的。
  • ExceptionInformation 是变长数组,最多 15 个 ULONG_PTR 参数;STATUS_ACCESS_VIOLATION 用前两个参数分别表示"读/写"和"出错地址"。

EXCEPTION_MAXIMUM_PARAMETERS 定义于 winnt_old.h(file:///d:/reactos/sdk/include/xdk/winnt_old.h#L2579):

c 复制代码
#define EXCEPTION_MAXIMUM_PARAMETERS 15

8.1.4 关键数据结构:EXCEPTION_REGISTRATION_RECORD

EXCEPTION_REGISTRATION_RECORD异常处理链表 的节点,定义于 wine/winnt.h(file:///d:/reactos/sdk/include/wine/winnt.h#L173-L177):

c 复制代码
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
  struct _EXCEPTION_REGISTRATION_RECORD *Prev; // This is 'Next' in MS' headers
  PEXCEPTION_HANDLER       Handler;
} EXCEPTION_REGISTRATION_RECORD;

注意:

  • 字段名 Prev 实际上指向 栈深方向的下一个 (即"指向下一层"),这是 Wine 风格命名(与 Windows DDK 的 Next 等价)。
  • Handler 的类型是 PEXCEPTION_HANDLER(在 Wine 中定义为 4 参数 DOWRD 返回值函数,在 vcruntime/excpt.h(file:///d:/reactos/sdk/include/vcruntime/excpt.h#L21-L27) 中则被包装为 EXCEPTION_DISPOSITION 4 选 1 枚举)。

链表通过线程环境块(TEB)中的 NtTib.ExceptionList(x86 用户态下为 FS:[0])访问,形成 单链表

复制代码
   高地址(栈顶)
   +----------------------------+
   |  当前函数栈帧              |
   |  +----------------------+  |
   |  | EXCEPTION_REGISTRATION|  |
   |  | .Prev -> 上一个        |  |
   |  | .Handler = MyHandler  |  |
   |  +----------------------+  |
   |       ...                  |
   |  +----------------------+  |
   |  | EXCEPTION_REGISTRATION|  |
   |  | .Prev -> (FS:[0])    |  |
   |  | .Handler = BaseHandler|  |
   |  +----------------------+  |
   +----------------------------+
   TEB->NtTib.ExceptionList (FS:[0])
   低地址(栈底)

EXCEPTION_CHAIN_END(值为 0xFFFFFFFF-1)是链表末尾哨兵。


8.1.5 关键数据结构:EXCEPTION_DISPOSITIONEXCEPTION_POINTERS

EXCEPTION_DISPOSITIONhandler 函数的返回值枚举,定义于 vcruntime/excpt.h(file:///d:/reactos/sdk/include/vcruntime/excpt.h#L21-L27):

c 复制代码
typedef enum _EXCEPTION_DISPOSITION
{
    ExceptionContinueExecution,  // 0 - 已处理,从 CONTEXT.Eip 处继续执行
    ExceptionContinueSearch,     // 1 - 不处理,继续向栈顶搜索
    ExceptionNestedException,    // 2 - handler 内部又抛出了异常
    ExceptionCollidedUnwind,     // 3 - 展开过程中遇到冲突(与目标 frame 不一致)
} EXCEPTION_DISPOSITION;

这四个值是整个 SEH 框架的 决策点RtlDispatchException 根据 handler 返回的 Disposition 决定下一步动作(见 8.3.2 节)。

EXCEPTION_POINTERShandler 收到的参数打包,定义于 rtltypes.h(file:///d:/reactos/sdk/include/xdk/rtltypes.h#L199-L202):

c 复制代码
typedef struct _EXCEPTION_POINTERS {
    PEXCEPTION_RECORD ExceptionRecord;  // 异常记录
    PCONTEXT ContextRecord;             // 现场上下文
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

注意:用户态 C 语言 handler 收到的是 4 个独立参数(异常记录、注册 frame、上下文、dispatcher context),而 EXCEPTION_POINTERSC++/高层 API 使用的便捷打包。GetExceptionInformation() 宏正是返回当前异常对应的 EXCEPTION_POINTERS*


8.1.6 关键数据结构:CONTEXT(处理器上下文)

CONTEXT 描述异常发生瞬间的 完整 CPU 状态。在 x86 平台上它定义于 NT 头文件中,包含:

  • 通用寄存器EaxEbxEcxEdxEsiEdiEbpEsp
  • 指令指针Eip
  • 段寄存器SegCsSegDsSegEsSegFsSegGsSegSs
  • 标志寄存器EFlags
  • 浮点/FXSAVE 区FloatSaveExtendedRegisters
  • 调试寄存器Dr0 ~ Dr7

CONTEXT.ContextFlags 字段决定 哪些字段是有效的。常见的标志:

  • CONTEXT_INTEGER(0x00001):通用寄存器
  • CONTEXT_CONTROL(0x00001 | 0x00004 等组合):Eip/Esp/Ebp/SegCs/SegSs/EFlags
  • CONTEXT_SEGMENTS(0x00004 | 0x00008):段寄存器
  • CONTEXT_FLOATING_POINT(0x00008 | 0x00010):x87 FPU
  • CONTEXT_EXTENDED_REGISTERS(0x00020):FXSAVE
  • CONTEXT_DEBUG_REGISTERS(0x00010):Dr0-Dr3、Dr6、Dr7
  • CONTEXT_FULL = CONTEXT_INTEGER | CONTEXT_CONTROL | CONTEXT_SEGMENTS

CONTEXTKTRAP_FRAME 之间的转换由 exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c) 中的 KeTrapFrameToContextKeContextToTrapFrame 完成(8.2 节详解)。


8.1.7 异常标志位(EXCEPTION_*)的语义

定义于 winnt_old.h(file:///d:/reactos/sdk/include/xdk/winnt_old.h#L2579-L2585):

c 复制代码
#define EXCEPTION_MAXIMUM_PARAMETERS 15
#define EXCEPTION_NONCONTINUABLE     0x01
#define EXCEPTION_UNWINDING          0x02
#define EXCEPTION_EXIT_UNWIND        0x04
#define EXCEPTION_STACK_INVALID      0x08
#define EXCEPTION_NESTED_CALL        0x10
#define EXCEPTION_TARGET_UNWIND      0x20
#define EXCEPTION_COLLIDED_UNWIND    0x40
#define EXCEPTION_UNWIND             (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)
标志 含义
EXCEPTION_NONCONTINUABLE 异常不可继续执行(RtlRaiseStatus 设置)
EXCEPTION_UNWINDING 当前正在执行栈展开(RtlUnwind 设置)
EXCEPTION_EXIT_UNWIND 展开到链表末尾(无 TargetFrameRtlUnwind
EXCEPTION_STACK_INVALID 链表节点不在合法栈范围内(RtlDispatchException 检测到)
EXCEPTION_NESTED_CALL handler 内部又抛出了新异常
EXCEPTION_TARGET_UNWIND 当前是 TargetFrame 内部引发的展开
EXCEPTION_COLLIDED_UNWIND 展开中遇到不同线程的 frame

EXCEPTION_DISPATCH 等附加标志位在内核 trap 层有不同含义(KI_EXCEPTION_INTERNAL、KI_EXCEPTION_ACCESS_VIOLATION 等),这些在 8.2 节讨论。


8.1.8 异常处理流程总览

下图展示了 硬件异常软异常 从产生到被处理(或被放弃)的完整路径:

复制代码
                  异常源                                          异常处理路径
                  --------                                        -----------
                  (1) 硬件 trap (int 0..int 13)                   KiTrap0XHandler
                          |                                      KiDispatchException0Args
                          v                                              |
                  (2) 软件 API (RtlRaiseException)                  KiDispatchException
                          |                                              |
                          v                                              v
                  (3) 软件抛 NtRaiseException 系统调用         <----- 决策点
                          |                                     FirstChance?
                          v                                              |
                  (4) 内核态 KiDispatchException 入口  <------------+    |
                          |                                            |  v
                          |                                  +----------+----------+
                          |                                  |  PreviousMode?     |
                          v                                  +-----+--------------+
                  (5) KeTrapFrameToContext -> CONTEXT            |
                          |                                       |
                          v                                       v
                  (6) 提取异常码,设置 Eip/Context        内核模式   用户模式
                          |                                       |        |
                          v                                       |        v
                  (7) 调整异常码 (KI_EXCEPTION_* -> STATUS_*)      |     KiDebugRoutine
                          |                                       |     DbgkForwardException
                          v                                       |     复制 CONTEXT/ExceptionRecord 到用户栈
                  (8) <--- 根据 PreviousMode 分派 --->             |     设置 EIP = KeUserExceptionDispatcher
                          |                                       |        |
                          |                                       v        v
                          |                                内核 handler  用户态 RtlDispatchException
                          |                                链 RtlDispatch      |
                          |                                Exception            v
                          |                                       |        (9) VEH 链
                          |                                       |             v
                          |                                       |        (10) 遍历注册链
                          |                                       |             v
                          |                                       |        (11) handler 返回 Disposition
                          |                                       |             |
                          |                                       v             v
                          |                              RtlUnwind 或  KiContinue /
                          |                              ZwContinue    ZwRaiseException
                          |                                       |             |
                          v                                       v             v
                  (12) 失败则 KeBugCheckEx                    蓝屏      进程继续运行 / 终止

关键观察:

  1. 硬件 trap 与软异常在进入 KiDispatchException 后路径合流。
  2. 内核模式异常处理只有"机会"机制(FirstChance/SecondChance),无链表遍历(虽然仍可调用 RtlDispatchException)。
  3. 用户模式异常处理会回到 KeUserExceptionDispatcher(由 ntdll 实现),由其调用 RtlDispatchException 遍历链表。
  4. handler 返回 ExceptionContinueExecution 时,内核调用 KiContinue 把修改后的 CONTEXT 写回 KTRAP_FRAME 并 iret 回原指令。

8.1.9 关键 API 索引

内核态(ntoskrnl)

函数 文件 作用
KiDispatchException exp.c:795(file:///d:/reactos/ntoskrnl/ke/i386/exp.c#L795-L1050) 内核异常分派核心
KiDispatchExceptionFromTrapFrame exp.c:1055(file:///d:/reactos/ntoskrnl/ke/i386/exp.c#L1052-L1100) 软异常统一入口
KiContinue except.c:42(file:///d:/reactos/ntoskrnl/ke/except.c#L42-L86) CONTEXT → TrapFrame 转换
KiRaiseException except.c:90(file:///d:/reactos/ntoskrnl/ke/except.c#L88-L167) 平台无关的异常抛出
NtRaiseException except.c:172(file:///d:/reactos/ntoskrnl/ke/except.c#L171-L212) Nt 系统调用版本
NtContinue except.c:216(file:///d:/reactos/ntoskrnl/ke/except.c#L214-L251) Nt 系统调用版本
KeContextToTrapFrame exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c) CONTEXT → KTRAP_FRAME
KeTrapFrameToContext exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c) KTRAP_FRAME → CONTEXT
KiDebugRoutine traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) 内核调试器回调
DbgkForwardException ps/win32k 调用 向用户态调试器转发

用户态(ntdll + rtl)

函数 文件 作用
RtlDispatchException i386/except.c:65(file:///d:/reactos/sdk/lib/rtl/i386/except.c#L65-L223) 用户态分派入口
RtlUnwind i386/except.c:230(file:///d:/reactos/sdk/lib/rtl/i386/except.c#L230-L402) 用户态栈展开
RtlRaiseException exception.c:32(file:///d:/reactos/sdk/lib/rtl/exception.c#L32-L69) 主动抛出(带 CONTEXT)
RtlRaiseStatus exception.c:85(file:///d:/reactos/sdk/lib/rtl/exception.c#L85-L120) 主动抛出(仅状态码)
RtlUnhandledExceptionFilter exception.c:313(file:///d:/reactos/sdk/lib/rtl/exception.c#L311-L317) 最后未处理过滤器
RtlSetUnhandledExceptionFilter exception.c:344(file:///d:/reactos/sdk/lib/rtl/exception.c#L342-L348) 注册顶层过滤器
RtlCallVectoredExceptionHandlers vectoreh.c(file:///d:/reactos/sdk/lib/rtl/vectoreh.c) VEH 链调用
RtlpCaptureContext i386/except_asm.s(file:///d:/reactos/sdk/lib/rtl/i386/except_asm.s) 捕获当前 CONTEXT
RtlpExecuteHandlerForException i386/except_asm.s(file:///d:/reactos/sdk/lib/rtl/i386/except_asm.s) 调用 handler(含 frame 包装)
RtlpExecuteHandlerForUnwind i386/except_asm.s(file:///d:/reactos/sdk/lib/rtl/i386/except_asm.s) 调用 handler(unwind 模式)
KeUserExceptionDispatcher ntdll 用户态分派入口(与 RtlDispatchException 配合)

8.1.10 PSEH2 宏的编译器实现细节

PSEH2(Portable SEH 2)是 ReactOS 为 GCC 编译器实现的可移植 SEH 框架,它通过宏展开模拟 MSVC 的 __try/__except/__finally 语义。理解 PSEH2 的实现对于编写跨平台的内核代码至关重要。

PSEH2 的编译器适配策略

PSEH2 在 pseh2.h(file:///d:/reactos/sdk/lib/pseh/include/pseh/pseh2.h) 中定义,根据编译器类型采用不同的实现策略:

1. MSVC 编译器(原生支持)

c 复制代码
#if defined(_USE_NATIVE_SEH) || defined(_MSC_VER)
#define _SEH2_TRY __try
#define _SEH2_FINALLY __finally
#define _SEH2_EXCEPT(...) __except(__VA_ARGS__)
#define _SEH2_END
#define _SEH2_GetExceptionInformation() (GetExceptionInformation())
#define _SEH2_GetExceptionCode() (GetExceptionCode())
#define _SEH2_AbnormalTermination() (AbnormalTermination())
#define _SEH2_LEAVE __leave

MSVC 编译器原生支持 SEH,因此 PSEH2 只是简单地将宏映射到编译器的关键字。编译器会自动生成 EXCEPTION_REGISTRATION_RECORD 的栈分配和链表操作代码。

2. GCC 编译器(x86 架构)

GCC 不原生支持 SEH,因此 PSEH2 使用 GNU C 的扩展特性(嵌套函数、__label__goto)来模拟 SEH 行为:

c 复制代码
#elif defined(__GNUC__) && !defined(__clang__) && defined(_M_IX86)

#define _SEH2_TRY                                   \
{                                                   \
    __label__ __seh2_scope_end__;                   \
    struct _SEH2_TRY_CONTEXT __seh2_context;        \
    __seh2_context.Handler = __seh2_handler;        \
    __seh2_context.Prev = RtlpGetExceptionList();   \
    RtlpSetExceptionList(&__seh2_context);          \
    /* 嵌套函数作为 handler */                       \
    EXCEPTION_DISPOSITION __seh2_handler(           \
        struct _EXCEPTION_RECORD *ExceptionRecord,  \
        void *EstablisherFrame,                     \
        struct _CONTEXT *ContextRecord,             \
        void *DispatcherContext)                    \
    {                                               \
        /* handler 实现 */

#define _SEH2_EXCEPT(...)                           \
        if ((0 && (__VA_ARGS__)) || 1)              \
            return ExceptionContinueSearch;         \
        return ExceptionContinueExecution;          \
    }                                               \
    if (1)                                          \
    {

#define _SEH2_END                                   \
    }                                               \
    __seh2_scope_end__:                             \
    RtlpSetExceptionList(__seh2_context.Prev);      \
}

关键实现技巧

  1. 嵌套函数 :GCC 允许在函数内部定义嵌套函数,PSEH2 利用这一特性在 _SEH2_TRY 块中定义 handler 函数
  2. __label__ 声明 :使用 __label__ __seh2_scope_end__ 声明局部标签,实现 _SEH2_LEAVE 的跳转
  3. 手动链表操作 :通过 RtlpGetExceptionList()RtlpSetExceptionList() 手动维护 FS:[0] 链表
  4. 异常过滤器求值__VA_ARGS__ 参数在 if 条件中求值,模拟 __except 过滤器表达式

PSEH2 的运行时开销

与 MSVC 的原生 SEH 相比,PSEH2 在 GCC 下的实现有以下开销:

  1. 栈空间 :每个 _SEH2_TRY 块需要在栈上分配 _SEH2_TRY_CONTEXT 结构(约 8 字节)
  2. 链表操作 :每次进入/退出 _SEH2_TRY 块都需要修改 FS:[0],涉及内存写入
  3. 嵌套函数开销:GCC 的嵌套函数需要通过 static chain 传递外层函数的栈帧指针

性能优化建议

  • 避免在热路径(hot path)中使用 _SEH2_TRY,特别是循环内部
  • 对于只需要保护少量内存访问的场景,考虑使用 ProbeForRead / ProbeForWrite 替代
  • 在内核模式下,PSEH2 的开销相对较小,因为内核代码通常已经有较好的异常处理

PSEH2 与 MSVC SEH 的兼容性差异

尽管 PSEH2 力求与 MSVC SEH 语义一致,但仍存在一些差异:

特性 MSVC SEH PSEH2 (GCC)
__finally 块执行 编译器自动生成 cleanup 代码 需要手动在 _SEH2_FINALLY 中实现
AbnormalTermination() 返回是否异常终止 始终返回 0(未完全实现)
嵌套 __try 完全支持 支持,但嵌套函数可能导致栈增长
异常过滤器中的局部变量 完全支持 可能因 GCC 优化导致问题
__leave 语句 跳转到 __finally 之前 通过 goto 实现,语义一致

实际使用示例

c 复制代码
// 内核模式下的 PSEH2 使用
NTSTATUS
MyDriverReadWrite(
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP Irp)
{
    NTSTATUS Status = STATUS_SUCCESS;
    PVOID UserBuffer = Irp->UserBuffer;
    
    _SEH2_TRY
    {
        // 受保护的内存访问
        RtlCopyMemory(UserBuffer, KernelBuffer, Length);
    }
    _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
    {
        // 捕获访问违例
        Status = _SEH2_GetExceptionCode();
        DbgPrint("Memory access failed: 0x%08lx\n", Status);
    }
    _SEH2_END;
    
    return Status;
}

8.1.11 RtlDispatchException 的完整实现分析

RtlDispatchException 是用户态异常分派的核心函数,定义于 i386/except.c(file:///d:/reactos/sdk/lib/rtl/i386/except.c#L65-L223)。它负责遍历 EXCEPTION_REGISTRATION_RECORD 链表,调用每个 handler 并处理返回值。

异常分派的完整流程

c 复制代码
BOOLEAN
NTAPI
RtlDispatchException(
    IN PEXCEPTION_RECORD ExceptionRecord,
    IN PCONTEXT Context)
{
    PEXCEPTION_REGISTRATION_RECORD RegistrationFrame, NestedFrame = NULL;
    DISPATCHER_CONTEXT DispatcherContext;
    EXCEPTION_RECORD ExceptionRecord2;
    EXCEPTION_DISPOSITION Disposition;
    ULONG_PTR StackLow, StackHigh;
    ULONG_PTR RegistrationFrameEnd;

    // 第一步:调用向量化异常处理器(VEH)
    if (RtlCallVectoredExceptionHandlers(ExceptionRecord, Context))
    {
        // VEH 处理了异常,调用 VCH(Vectored Continue Handlers)
        RtlCallVectoredContinueHandlers(ExceptionRecord, Context);
        return TRUE;  // 异常已处理
    }

    // 第二步:获取栈边界和链表头
    RtlpGetStackLimits(&StackLow, &StackHigh);
    RegistrationFrame = RtlpGetExceptionList();  // 读取 FS:[0]

    // 第三步:遍历异常处理链表
    while (RegistrationFrame != EXCEPTION_CHAIN_END)
    {
        // 安全检查:确保 RegistrationFrame 在合法栈范围内
        RegistrationFrameEnd = (ULONG_PTR)RegistrationFrame +
                                sizeof(EXCEPTION_REGISTRATION_RECORD);
        
        if ((RegistrationFrameEnd > StackHigh) ||
            ((ULONG_PTR)RegistrationFrame < StackLow) ||
            ((ULONG_PTR)RegistrationFrame & 0x3))  // 4 字节对齐检查
        {
            // 检查是否在 DPC 栈上(内核模式特殊情况)
            if (RtlpHandleDpcStackException(RegistrationFrame,
                                            RegistrationFrameEnd,
                                            &StackLow,
                                            &StackHigh))
            {
                continue;  // 使用 DPC 栈边界重试
            }

            // 栈无效,标记并返回
            ExceptionRecord->ExceptionFlags |= EXCEPTION_STACK_INVALID;
            return FALSE;
        }

        // TODO: 实现 RtlIsValidHandler 支持 SafeSEH
        // 这是防止 SEH 覆盖攻击的安全机制

        // 调用异常日志(如果启用)
        RtlpCheckLogException(ExceptionRecord,
                              Context,
                              RegistrationFrame,
                              sizeof(*RegistrationFrame));

        // 调用 handler
        Disposition = RtlpExecuteHandlerForException(
            ExceptionRecord,
            RegistrationFrame,
            Context,
            &DispatcherContext,
            RegistrationFrame->Handler);

        // 处理嵌套异常标记
        if (RegistrationFrame == NestedFrame)
        {
            ExceptionRecord->ExceptionFlags &= ~EXCEPTION_NESTED_CALL;
            NestedFrame = NULL;
        }

        // 根据 handler 返回值决定下一步动作
        switch (Disposition)
        {
            case ExceptionContinueExecution:
            {
                // 检查异常是否可继续
                if (ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE)
                {
                    // 不可继续,抛出新异常
                    ExceptionRecord2.ExceptionRecord = ExceptionRecord;
                    ExceptionRecord2.ExceptionCode = STATUS_NONCONTINUABLE_EXCEPTION;
                    ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
                    ExceptionRecord2.NumberParameters = 0;
                    RtlRaiseException(&ExceptionRecord2);
                }
                else
                {
                    // 调用 VCH,然后继续执行
                    RtlCallVectoredContinueHandlers(ExceptionRecord, Context);
                    return TRUE;
                }
            }

            case ExceptionContinueSearch:
                // 继续搜索下一个 handler
                if (ExceptionRecord->ExceptionFlags & EXCEPTION_STACK_INVALID)
                {
                    return FALSE;  // 栈无效,停止搜索
                }
                break;

            case ExceptionNestedException:
            {
                // handler 内部又抛出了异常
                ExceptionRecord->ExceptionFlags |= EXCEPTION_NESTED_CALL;
                
                // 更新嵌套帧指针
                if (DispatcherContext.RegistrationPointer > NestedFrame)
                {
                    NestedFrame = DispatcherContext.RegistrationPointer;
                }
                break;
            }

            default:
            {
                // 无效的 disposition,抛出异常
                ExceptionRecord2.ExceptionRecord = ExceptionRecord;
                ExceptionRecord2.ExceptionCode = STATUS_INVALID_DISPOSITION;
                ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
                ExceptionRecord2.NumberParameters = 0;
                RtlRaiseException(&ExceptionRecord2);
                break;
            }
        }

        // 移动到下一个 frame
        RegistrationFrame = RegistrationFrame->Next;
    }

    // 所有 handler 都拒绝处理
    return FALSE;
}

栈边界检查的安全意义

RtlDispatchException 中的栈边界检查是防止 SEH 覆盖攻击(SEH overwrite attack) 的关键安全措施:

c 复制代码
if ((RegistrationFrameEnd > StackHigh) ||
    ((ULONG_PTR)RegistrationFrame < StackLow) ||
    ((ULONG_PTR)RegistrationFrame & 0x3))

这个检查确保:

  1. RegistrationFrame 的结束地址不超过栈顶(StackHigh
  2. RegistrationFrame 的起始地址不低于栈底(StackLow
  3. RegistrationFrame 地址是 4 字节对齐的

攻击场景 :如果攻击者通过缓冲区溢出覆盖了栈上的 EXCEPTION_REGISTRATION_RECORD,可以将 Handler 指针指向恶意代码。栈边界检查可以检测到这种异常,因为被覆盖的 frame 通常会指向栈外的地址。

SafeSEH 机制 :代码中的 TODO 注释提到了 RtlIsValidHandler,这是 Windows XP SP2 引入的 SafeSEH 机制。它维护一个合法 handler 地址的表,在调用 handler 之前验证其是否在表中。ReactOS 尚未完全实现这一机制。

DPC 栈异常的特殊处理

内核模式下的 DPC(Deferred Procedure Call)使用独立的栈,当异常发生在 DPC 例程中时,RegistrationFrame 可能不在常规线程栈范围内。RtlpHandleDpcStackException 函数检测这种情况,并调整栈边界为 DPC 栈的范围:

c 复制代码
if (RtlpHandleDpcStackException(RegistrationFrame,
                                RegistrationFrameEnd,
                                &StackLow,
                                &StackHigh))
{
    continue;  // 使用新的栈边界重试
}

这个机制确保了内核模式下的异常处理在 DPC 上下文中也能正常工作。


8.1.12 栈展开(Stack Unwinding)机制

栈展开是 SEH 的另一个核心功能,当异常被处理(ExceptionContinueExecutionExceptionExecuteHandler)或线程退出时,需要沿着 EXCEPTION_REGISTRATION_RECORD 链表反向调用每个 handler 的清理代码。

RtlUnwind 的实现

RtlUnwind 定义于 i386/except.c(file:///d:/reactos/sdk/lib/rtl/i386/except.c#L230-L402),其签名如下:

c 复制代码
VOID
NTAPI
RtlUnwind(
    IN PVOID TargetFrame OPTIONAL,      // 目标 frame(可选)
    IN PVOID TargetIp OPTIONAL,         // 目标 IP(可选)
    IN PEXCEPTION_RECORD ExceptionRecord OPTIONAL,  // 异常记录
    IN PVOID ReturnValue)               // 返回值(用于 __finally)

参数说明

  • TargetFrame:指定展开到哪个 frame。如果为 NULL,则展开整个链表(线程退出时使用)
  • TargetIp:展开完成后跳转的目标地址(通常不使用)
  • ExceptionRecord:描述展开原因的异常记录。如果为 NULLRtlUnwind 会构造一个默认的
  • ReturnValue:传递给 __finally 块的值(在 MSVC 中通过 EAX 寄存器传递)

栈展开的完整流程

c 复制代码
VOID
NTAPI
RtlUnwind(
    IN PVOID TargetFrame OPTIONAL,
    IN PVOID TargetIp OPTIONAL,
    IN PEXCEPTION_RECORD ExceptionRecord OPTIONAL,
    IN PVOID ReturnValue)
{
    PEXCEPTION_REGISTRATION_RECORD RegistrationFrame, OldFrame;
    DISPATCHER_CONTEXT DispatcherContext;
    EXCEPTION_RECORD ExceptionRecord2, ExceptionRecord3;
    EXCEPTION_DISPOSITION Disposition;
    ULONG_PTR StackLow, StackHigh;
    ULONG_PTR RegistrationFrameEnd;
    CONTEXT LocalContext;
    PCONTEXT Context;

    // 获取栈边界
    RtlpGetStackLimits(&StackLow, &StackHigh);

    // 如果没有提供异常记录,构造一个默认的
    if (!ExceptionRecord)
    {
        ExceptionRecord = &ExceptionRecord3;
        ExceptionRecord3.ExceptionCode = STATUS_UNWIND;
        ExceptionRecord3.ExceptionFlags = 0;
        ExceptionRecord3.ExceptionRecord = NULL;
        ExceptionRecord3.NumberParameters = 0;
    }

    // 设置展开标志
    ExceptionRecord->ExceptionFlags |= EXCEPTION_UNWINDING;
    if (!TargetFrame)
    {
        ExceptionRecord->ExceptionFlags |= EXCEPTION_EXIT_UNWIND;
    }

    // 捕获当前上下文
    RtlpCaptureContext(&LocalContext);
    Context = &LocalContext;

    // 设置返回值(EAX)
    Context->Eax = (ULONG)ReturnValue;

    // 从当前 frame 开始遍历
    RegistrationFrame = RtlpGetExceptionList();

    while (RegistrationFrame != EXCEPTION_CHAIN_END)
    {
        // 栈边界检查(与 RtlDispatchException 相同)
        RegistrationFrameEnd = (ULONG_PTR)RegistrationFrame +
                                sizeof(EXCEPTION_REGISTRATION_RECORD);
        
        if ((RegistrationFrameEnd > StackHigh) ||
            ((ULONG_PTR)RegistrationFrame < StackLow) ||
            ((ULONG_PTR)RegistrationFrame & 0x3))
        {
            // 栈无效,抛出异常
            ExceptionRecord2.ExceptionRecord = ExceptionRecord;
            ExceptionRecord2.ExceptionCode = STATUS_INVALID_UNWIND_TARGET;
            ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
            ExceptionRecord2.NumberParameters = 0;
            RtlRaiseException(&ExceptionRecord2);
        }

        // 检查是否到达目标 frame
        if (TargetFrame && RegistrationFrame == TargetFrame)
        {
            // 到达目标,设置 TARGET_UNWIND 标志
            ExceptionRecord->ExceptionFlags |= EXCEPTION_TARGET_UNWIND;
        }

        // 调用 handler(展开模式)
        Disposition = RtlpExecuteHandlerForUnwind(
            ExceptionRecord,
            RegistrationFrame,
            Context,
            &DispatcherContext,
            RegistrationFrame->Handler);

        // 展开时 handler 应该返回 ExceptionContinueSearch
        if (Disposition != ExceptionContinueSearch)
        {
            // 无效的 disposition
            ExceptionRecord2.ExceptionRecord = ExceptionRecord;
            ExceptionRecord2.ExceptionCode = STATUS_INVALID_DISPOSITION;
            ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
            ExceptionRecord2.NumberParameters = 0;
            RtlRaiseException(&ExceptionRecord2);
        }

        // 保存当前 frame,移动到下一个
        OldFrame = RegistrationFrame;
        RegistrationFrame = RegistrationFrame->Next;

        // 从链表中移除已展开的 frame
        RtlpSetExceptionList(RegistrationFrame);

        // 如果到达目标 frame,停止展开
        if (TargetFrame && OldFrame == TargetFrame)
        {
            break;
        }
    }

    // 展开完成,恢复到目标地址
    if (TargetIp)
    {
        // 修改上下文中的 EIP,然后调用 NtContinue
        Context->Eip = (ULONG)TargetIp;
        NtContinue(Context, FALSE);
    }
}

__finally 块的执行机制

__finally 块在栈展开时执行,其 handler 需要区分两种情况:

  1. 正常执行完成 :从 _SEH2_TRY 块正常退出
  2. 异常终止:由于异常而展开

在 PSEH2 中,__finally 块通过检查 ExceptionRecord->ExceptionFlags 来判断:

c 复制代码
_SEH2_TRY
{
    // 受保护代码
    if (SomeCondition)
        _SEH2_LEAVE;  // 提前退出
}
_SEH2_FINALLY
{
    // 清理代码
    if (AbnormalTermination())
    {
        // 异常终止
        DbgPrint("Abnormal termination\n");
    }
    else
    {
        // 正常终止
        DbgPrint("Normal termination\n");
    }
}
_SEH2_END;

注意 :PSEH2 的 AbnormalTermination() 实现不完整(始终返回 0),这是与 MSVC SEH 的一个差异。

嵌套展开(Collided Unwind)

如果在栈展开过程中,某个 handler 内部又调用了 RtlUnwind,就会发生嵌套展开。EXCEPTION_COLLIDED_UNWIND 标志用于标记这种情况:

c 复制代码
if (DispatcherContext.RegistrationPointer != RegistrationFrame)
{
    // 嵌套展开
    ExceptionRecord->ExceptionFlags |= EXCEPTION_COLLIDED_UNWIND;
}

嵌套展开的处理比较复杂,需要确保两个展开过程不会相互干扰。RtlUnwind 通过检查 DispatcherContext.RegistrationPointer 来检测这种情况。


8.1.13 异常处理的安全机制

SEH 机制虽然强大,但也带来了安全风险。攻击者可以利用 SEH 覆盖来执行恶意代码。ReactOS 和 Windows 都实现了多种安全机制来防范这些攻击。

SEH 覆盖攻击原理

在 x86 Windows 中,EXCEPTION_REGISTRATION_RECORD 存储在栈上,其布局如下:

复制代码
高地址
+---------------------------+
| EXCEPTION_REGISTRATION_RECORD |
| .Next -> 下一个 frame          |
| .Handler -> handler 地址       |
+---------------------------+
| 局部变量缓冲区               |
| (可能被溢出)                 |
+---------------------------+
低地址

如果局部变量缓冲区发生溢出,攻击者可以覆盖 .Next.Handler 字段,将 .Handler 指向恶意代码。当异常发生时,RtlDispatchException 会调用这个恶意 handler,从而执行攻击者的代码。

栈边界检查

RtlDispatchException 中的栈边界检查是第一道防线:

c 复制代码
if ((RegistrationFrameEnd > StackHigh) ||
    ((ULONG_PTR)RegistrationFrame < StackLow) ||
    ((ULONG_PTR)RegistrationFrame & 0x3))
{
    ExceptionRecord->ExceptionFlags |= EXCEPTION_STACK_INVALID;
    return FALSE;
}

这个检查确保 RegistrationFrame 在合法栈范围内,并且是 4 字节对齐的。如果攻击者将 .Handler 指向栈外的地址,这个检查会失败。

局限性 :栈边界检查无法防止攻击者将 .Handler 指向栈内的恶意代码(例如通过 ROP gadget)。

SafeSEH 机制

SafeSEH 是 Windows XP SP2 引入的安全机制,它维护一个合法 handler 地址的表,在调用 handler 之前验证其是否在表中。

实现原理

  1. 编译器在编译时收集所有合法的 handler 地址,生成一个表
  2. 链接器将所有模块的 handler 表合并,存储在 PE 文件的 .loadcfg 节中
  3. RtlDispatchException 在调用 handler 之前,调用 RtlIsValidHandler 验证地址
c 复制代码
// ReactOS 中的 TODO(尚未实现)
// if (!RtlIsValidHandler(RegistrationFrame->Handler))
// {
//     ExceptionRecord->ExceptionFlags |= EXCEPTION_STACK_INVALID;
//     return FALSE;
// }

ReactOS 现状:SafeSEH 机制尚未在 ReactOS 中完全实现,代码中有 TODO 注释标记。

SEH 验证(SEH Validation)

Windows Vista 引入了更严格的 SEH 验证机制,包括:

  1. Handler 地址范围检查:确保 handler 地址在可执行代码段内
  2. Cookie 验证 :在 EXCEPTION_REGISTRATION_RECORD 中添加 cookie,防止覆盖
  3. 栈回溯验证:验证调用栈的完整性

ReactOS 尚未实现这些高级安全机制,但栈边界检查提供了基本的保护。

调试技巧:检测 SEH 覆盖攻击

在调试器中,可以通过以下方法检测 SEH 覆盖攻击:

  1. 检查 FS:0 链表

    复制代码
    kd> !teb
    kd> dps fs:[0] L20  ; 查看前 20 个 entry
  2. 验证 handler 地址

    复制代码
    kd> ub <handler_address>  ; 反汇编 handler 前的代码
    kd> ln <handler_address>  ; 查看 handler 的符号
  3. 检查栈边界

    复制代码
    kd> !stack  ; 查看当前栈范围
    kd> dps <stack_low> L<stack_size>/4  ; 查看栈内容
  4. 使用 WinDbg 的 !safeseh 扩展(如果可用):

    复制代码
    kd> !safeseh <module_name>  ; 查看模块的 SafeSEH 表

8.1.14 实际应用场景与最佳实践

SEH 在 ReactOS 中有广泛的应用,理解其使用场景和最佳实践对于编写健壮的系统代码至关重要。

场景 1:用户模式缓冲区访问

内核模式代码访问用户模式缓冲区时,必须使用 SEH 保护,因为用户地址可能无效:

c 复制代码
NTSTATUS
MyDeviceIoControl(
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP Irp)
{
    PIO_STACK_LOCATION IrpSp = IoGetCurrentIrpStackLocation(Irp);
    PVOID UserBuffer = Irp->AssociatedIrp.MethodBuffer;
    ULONG InputLength = IrpSp->Parameters.DeviceIoControl.InputBufferLength;
    NTSTATUS Status = STATUS_SUCCESS;

    _SEH2_TRY
    {
        // 验证用户缓冲区可读
        ProbeForRead(UserBuffer, InputLength, 1);

        // 访问用户缓冲区
        RtlCopyMemory(KernelBuffer, UserBuffer, InputLength);
    }
    _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
    {
        // 捕获访问违例
        Status = _SEH2_GetExceptionCode();
        DbgPrint("Failed to access user buffer: 0x%08lx\n", Status);
    }
    _SEH2_END;

    return Status;
}

最佳实践

  • 始终使用 ProbeForRead / ProbeForWrite 在访问前验证地址
  • 使用 EXCEPTION_EXECUTE_HANDLER 捕获所有异常
  • __except 块中记录错误信息,便于调试

场景 2:资源清理

__finally 块用于确保资源在异常或正常退出时都能被清理:

c 复制代码
PVOID
AllocateAndProcessData(
    IN ULONG Size)
{
    PVOID Buffer = NULL;
    HANDLE FileHandle = NULL;
    IO_STATUS_BLOCK IoStatus;
    OBJECT_ATTRIBUTES ObjectAttributes;
    NTSTATUS Status;

    // 分配缓冲区
    Buffer = ExAllocatePoolWithTag(PagedPool, Size, 'tseT');
    if (!Buffer)
        return NULL;

    _SEH2_TRY
    {
        // 打开文件
        InitializeObjectAttributes(&ObjectAttributes, ...);
        Status = ZwCreateFile(&FileHandle, ...);
        if (!NT_SUCCESS(Status))
            _SEH2_LEAVE;

        // 读取数据
        Status = ZwReadFile(FileHandle, ...);
        if (!NT_SUCCESS(Status))
            _SEH2_LEAVE;

        // 处理数据
        ProcessData(Buffer, Size);
    }
    _SEH2_FINALLY
    {
        // 清理资源
        if (FileHandle)
            ZwClose(FileHandle);

        if (!NT_SUCCESS(Status))
        {
            // 异常终止,释放缓冲区
            ExFreePoolWithTag(Buffer, 'tseT');
            Buffer = NULL;
        }
    }
    _SEH2_END;

    return Buffer;
}

最佳实践

  • 使用 _SEH2_LEAVE 提前退出,而不是 return
  • __finally 块中检查状态,决定是否需要清理
  • 避免在 __finally 块中抛出异常

场景 3:嵌套异常处理

SEH 支持嵌套,内层的异常可以向外层传播:

c 复制代码
NTSTATUS
OuterFunction(VOID)
{
    NTSTATUS Status = STATUS_SUCCESS;

    _SEH2_TRY
    {
        Status = InnerFunction();
    }
    _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
    {
        // 捕获内层未处理的异常
        Status = _SEH2_GetExceptionCode();
        DbgPrint("Outer handler caught: 0x%08lx\n", Status);
    }
    _SEH2_END;

    return Status;
}

NTSTATUS
InnerFunction(VOID)
{
    _SEH2_TRY
    {
        // 可能抛出异常的代码
        *((PULONG)0) = 0;  // 访问违例
    }
    _SEH2_EXCEPT(EXCEPTION_CONTINUE_SEARCH)
    {
        // 不处理,继续向外层传播
        return EXCEPTION_CONTINUE_SEARCH;
    }
    _SEH2_END;

    return STATUS_SUCCESS;
}

最佳实践

  • 只在能够处理异常的层捕获异常
  • 使用 EXCEPTION_CONTINUE_SEARCH 将异常传递给外层
  • 避免在 __except 块中吞掉异常而不记录

场景 4:内核模式调试

在内核模式下,SEH 与调试器协作,支持断点和单步调试:

c 复制代码
VOID
KdBreakPoint(VOID)
{
    _SEH2_TRY
    {
        __debugbreak();  // 触发 STATUS_BREAKPOINT
    }
    _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
    {
        // 调试器处理了断点
        DbgPrint("Breakpoint handled by debugger\n");
    }
    _SEH2_END;
}

最佳实践

  • 使用 __debugbreak() 插入断点
  • __except 块中处理调试器未附加的情况
  • 避免在发布版本中保留调试断点

10 问为什么:深入理解 SEH 的设计与实现

问题 1:为什么 SEH 异常处理链表要存储在栈上,而不是堆上或全局变量中?

:这是由 SEH 的核心设计需求决定的------异常处理的作用域必须与函数的生命周期绑定

每个 _SEH2_TRY 块对应一个 EXCEPTION_REGISTRATION_RECORD,它被分配在当前函数的栈帧中。当函数返回时,栈帧自动回收,对应的 handler 也自动从链表中移除。这实现了以下关键特性:

  1. 自动生命周期管理:函数返回 = handler 自动注销,无需手动清理
  2. 天然支持嵌套:每个函数栈帧可以有自己的 handler,形成链式结构
  3. 零额外内存分配 :不需要调用 malloc/ExAllocatePool,避免在异常路径上引入新的失败点
  4. 线程安全 :每个线程有独立的栈和独立的 FS:[0],天然隔离

如果放在堆上,就需要手动管理注册/注销,一旦忘记注销就会导致悬空指针;如果放在全局变量中,就无法支持嵌套和多线程。

在 ReactOS 源码中,RtlpSetExceptionList()RtlpGetExceptionList() 直接操作 FS:[0](即 NtCurrentTib()->ExceptionList),开销仅为一次内存读写。


问题 2:为什么 EXCEPTION_REGISTRATION_RECORDPrev 字段指向的是"下一个"而不是"上一个"?

:这是命名约定造成的混淆,实际上是单链表的栈结构

在 Wine 风格的定义中(wine/winnt.h(file:///d:/reactos/sdk/include/wine/winnt.h)),字段名为 Prev;在 Windows DDK 中,同一字段名为 Next。两者含义完全相同------指向栈底方向的下一个节点

复制代码
FS:[0] → [当前frame] → [外层frame] → [更外层frame] → EXCEPTION_CHAIN_END(-1)
              ↑
         栈顶(最新注册的)

之所以叫 Prev(而不是 Next),是因为 Wine 开发者从"时间顺序"的角度命名:Prev 表示"在我之前注册的 frame"。而微软从"遍历方向"的角度命名:Next 表示"遍历时访问的下一个节点"。

ReactOS 的选择 :ReactOS 同时兼容两种命名,因为头文件中有 #define Next Prev 之类的兼容宏。在阅读代码时,关键是理解这个链表是从栈顶向栈底遍历的。


问题 3:为什么 RtlDispatchException 要先调用 VEH(向量化异常处理器),再遍历注册链表?

:VEH 和基于链表的 SEH 是两套不同层次的异常处理机制,它们的设计目标不同:

特性 VEH(向量化异常处理) SEH(基于链表的异常处理)
注册方式 全局链表(RtlAddVectoredExceptionHandler 栈上链表(FS:[0]
作用域 进程级,与函数调用栈无关 函数级,与栈帧绑定
典型用途 全局异常监控、日志记录、反调试 局部异常恢复、资源清理
优先级 最先被调用 VEH 拒绝后才调用

先调用 VEH 的原因:

  1. 全局拦截需求:某些组件(如反调试器、崩溃报告工具)需要在任何局部 handler 之前看到异常
  2. 不依赖栈完整性:VEH 存储在全局链表中,即使栈被破坏也能工作
  3. 性能考虑 :如果没有注册任何 VEH handler,RtlCallVectoredExceptionHandlers 会快速返回,开销可忽略

在 ReactOS 中(i386/except.c(file:///d:/reactos/sdk/lib/rtl/i386/except.c#L78-L85)):

c 复制代码
if (RtlCallVectoredExceptionHandlers(ExceptionRecord, Context))
{
    RtlCallVectoredContinueHandlers(ExceptionRecord, Context);
    return TRUE;  // VEH 处理了,直接返回
}
// VEH 拒绝,继续遍历 SEH 链表

问题 4:为什么硬件异常(如除零)和软件异常(如 RtlRaiseException)要走同一条分派路径?

:这是 Windows NT 架构的统一异常模型的核心设计决策。

从 CPU 的角度看,硬件异常(如 #DE 除零异常、#PF 缺页异常)通过 IDT 进入内核;而软件异常通过系统调用(NtRaiseException)进入内核。两者在进入 KiDispatchException 后,使用完全相同的分派逻辑。

统一的好处

  1. 代码复用:一套分派逻辑处理所有异常,减少代码量和 bug 面
  2. 语义一致 :无论是硬件还是软件触发的,STATUS_ACCESS_VIOLATION 的行为完全相同
  3. 可测试性:可以用软件模拟硬件异常,便于测试 handler
  4. 调试器统一接口:调试器只需要 hook 一个入口点就能拦截所有异常

在 ReactOS 中,硬件 trap 通过 KiTrap00HandlerKiDispatchException0ArgsKiDispatchException;软件异常通过 NtRaiseExceptionKiRaiseExceptionKiDispatchException。两条路径在 KiDispatchException 处合流。

唯一的区别 :硬件异常会先经过 KeTrapFrameToContextKTRAP_FRAME 转换为 CONTEXT;软件异常则直接使用调用者提供的 CONTEXT


问题 5:为什么 EXCEPTION_DISPOSITION 只有 4 个值?这够吗?

:4 个值覆盖了异常处理中所有可能的决策分支

含义 使用场景
ExceptionContinueExecution (0) 已处理,从修改后的 CONTEXT 继续执行 修复了异常原因(如修正了指针)
ExceptionContinueSearch (1) 不处理,继续搜索下一个 handler 当前 handler 无法处理此异常
ExceptionNestedException (2) handler 内部又抛出了新异常 handler 代码本身触发了异常
ExceptionCollidedUnwind (3) 展开过程中发生冲突 两个展开过程碰撞

为什么不需要更多值?

这 4 个值实际上对应了 4 种互斥的情况:

  • "我处理了" → ContinueExecution
  • "我处理不了" → ContinueSearch
  • "我出事了" → NestedException
  • "展开冲突了" → CollidedUnwind

任何其他语义都可以用这 4 个值的组合来表达。例如,ExceptionExecuteHandler(MSVC 中的过滤器返回值)实际上是在过滤器表达式层面使用的,最终仍会映射到这 4 个值之一。

在 ReactOS 的 RtlDispatchException 中(i386/except.c(file:///d:/reactos/sdk/lib/rtl/i386/except.c#L150-L215)),switch(Disposition) 只有这 4 个 case,任何其他的值都会触发 STATUS_INVALID_DISPOSITION 异常。


问题 6:为什么 KiDispatchException 要对 STATUS_BREAKPOINTEip-- 的特殊处理?

:这是 x86 架构的 INT 3 指令特性决定的。

STATUS_BREAKPOINTINT 30xCC)指令触发。当 CPU 执行 INT 3 时,会将下一条指令的地址 压入栈中作为返回地址。也就是说,trap frame 中的 Eip 指向的是 INT 3 之后的那条指令。

但对于断点调试,我们希望在断点处 重新执行(调试器会替换回原始字节),所以需要把 Eip 减 1,让它重新指向 INT 3 指令本身:

c 复制代码
case STATUS_BREAKPOINT:
    Context.Eip--;  // 回退到 INT 3 指令处
    break;

在 ReactOS 中(i386/exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c#L831-L835)),这个处理在 KeTrapFrameToContext 之后、分派之前完成。

对比其他异常

  • STATUS_ACCESS_VIOLATION:不需要调整 Eip,因为出错的指令应该被 handler 处理或终止
  • STATUS_INTEGER_OVERFLOWINTO 指令):也不需要调整,因为 INTO 的语义不同
  • STATUS_SINGLE_STEP:Eip 已经指向下一条要执行的指令,不需要调整

问题 7:为什么 RtlUnwind 在展开时要从链表中移除已经处理过的 frame?

:这是为了防止重复展开和悬空指针

RtlUnwind 的展开过程是从栈顶向栈底遍历,依次调用每个 handler 的清理代码。在处理完一个 frame 后,立即将它从 FS:[0] 链表中移除:

c 复制代码
OldFrame = RegistrationFrame;
RegistrationFrame = RegistrationFrame->Next;
RtlpSetExceptionList(RegistrationFrame);  // 更新 FS:[0]

为什么要这样做?

  1. 防止重入 :如果 handler 的清理代码本身又触发了异常,新的异常分派会从更新后的 FS:[0] 开始,不会再次调用已经执行过的 handler
  2. 保持一致性 :展开过程中的栈帧正在被"销毁",如果链表仍然指向它们,其他代码(如 RtlDispatchException)可能会访问已失效的 frame
  3. 支持嵌套展开 :如果清理代码中调用了另一个 RtlUnwind(嵌套展开),新的展开过程看到的是正确的链表状态

在 ReactOS 中(i386/except.c(file:///d:/reactos/sdk/lib/rtl/i386/except.c#L230-L402)),这个移除操作是原子性的------先保存下一个 frame 的指针,再更新 FS:[0]


问题 8:为什么内核模式异常处理有三 strike 规则(FirstChance → SecondChance → BugCheck)?

:三 strike 规则是内核稳定性的最后防线

复制代码
FirstChance:  调试器 → 内核 handler 链 → 用户态(如果是用户异常)
SecondChance: 调试器(最后机会)
ThirdStrike:  KeBugCheckEx(蓝屏)

为什么需要多级机会?

  1. FirstChance :给调试器和局部 handler 一个修复问题的机会。很多异常是"可恢复的"------例如调试器设置断点触发的 STATUS_BREAKPOINT,或者 handler 修复了错误指针后可以继续执行
  2. SecondChance:如果 FirstChance 没有人处理,说明异常是"不可恢复的"。此时给调试器最后一次机会来收集诊断信息(dump 内存、记录调用栈)
  3. BugCheck:如果连调试器都无法处理,说明系统处于不一致状态,继续运行可能导致数据损坏。此时必须蓝屏停止

在 ReactOS 中(i386/exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c#L795-L1050)),内核模式异常处理的逻辑是:

c 复制代码
if (FirstChance) {
    // 第一次机会:尝试调试器 + RtlDispatchException
    if (KiDebugRoutine(...)) return;  // 调试器处理了
    if (RtlDispatchException(...)) return;  // handler 处理了
}
// 第二次机会:仅调试器
if (KiDebugRoutine(...)) return;
// 三振出局:蓝屏
KeBugCheckEx(KMODE_EXCEPTION_NOT_HANDLED, ...);

用户模式的区别 :用户模式异常在 FirstChance 失败后,会通过 DbgkForwardException 转发给调试器;如果调试器也不处理,进程会被终止(NtTerminateProcess),但不会蓝屏。


问题 9:为什么 PSEH2 要用 GCC 的嵌套函数来模拟 SEH,而不是用 setjmp/longjmp

setjmp/longjmp 和 SEH 虽然都能实现"非局部跳转",但它们在语义上有本质区别:

特性 setjmp/longjmp SEH
跳转方向 只能向上(回到 setjmp 的位置) 可以向上(展开)或原地(继续执行)
清理代码 不执行中间函数的清理 执行所有 __finally
异常信息 只有一个整数 完整的 EXCEPTION_RECORD + CONTEXT
多级处理 不支持 支持链表遍历
硬件异常 无法捕获 可以捕获(通过 IDT)

PSEH2 选择嵌套函数的原因

  1. 需要真正的 handler 函数 :SEH 的 handler 需要接收 4 个参数(ExceptionRecord、EstablisherFrame、ContextRecord、DispatcherContext),并返回 EXCEPTION_DISPOSITIONsetjmp/longjmp 无法提供这种接口
  2. 需要支持展开模式 :handler 需要区分"异常分派"和"栈展开"两种调用模式,这在 setjmp/longjmp 中无法表达
  3. 需要与系统 SEH 兼容 :PSEH2 生成的 EXCEPTION_REGISTRATION_RECORD 必须能被 RtlDispatchException 正确识别和调用,这要求 handler 是一个真正的函数指针

GCC 嵌套函数的优势

  • 可以访问外层函数的局部变量(通过 static chain)
  • 可以生成真正的函数指针(虽然是非标准的 GCC 扩展)
  • 支持在函数内部定义,实现了"局部 handler"的语义

代价 :嵌套函数不是标准 C,Clang 和 MSVC 不支持。PSEH2 对 Clang 使用了 dummy 实现(_USE_DUMMY_PSEH),实际上不提供异常保护。


问题 10:为什么 RtlDispatchException 要检查 RegistrationFrame 是否在合法栈范围内?

:这是安全防御措施,用于检测栈损坏和 SEH 覆盖攻击。

EXCEPTION_REGISTRATION_RECORD 存储在栈上,如果程序存在缓冲区溢出漏洞,攻击者可以覆盖栈上的 handler 结构,将 Handler 指针指向恶意代码。当异常发生时,RtlDispatchException 会调用这个恶意 handler,实现任意代码执行。

栈边界检查是第一道防线:

c 复制代码
if ((RegistrationFrameEnd > StackHigh) ||
    ((ULONG_PTR)RegistrationFrame < StackLow) ||
    ((ULONG_PTR)RegistrationFrame & 0x3))
{
    ExceptionRecord->ExceptionFlags |= EXCEPTION_STACK_INVALID;
    return FALSE;
}

这个检查确保:

  1. frame 的结束地址不超过栈顶(StackHigh
  2. frame 的起始地址不低于栈底(StackLow
  3. frame 地址是 4 字节对齐的(& 0x3

为什么这很重要?

在正常的程序执行中,EXCEPTION_REGISTRATION_RECORD 一定在栈上(因为它是局部变量)。如果 frame 地址不在栈范围内,说明:

  • 栈被破坏了(缓冲区溢出、栈溢出)
  • 或者攻击者故意构造了假的 frame

局限性:栈边界检查只能防止 frame 指向栈外的情况。如果攻击者将 handler 指向栈内的 shellcode(例如通过 ROP),栈边界检查无法检测。这就是为什么 Windows 后来引入了 SafeSEH 和 SEHOP 等更高级的防护机制。

在 ReactOS 中(i386/except.c(file:///d:/reactos/sdk/lib/rtl/i386/except.c#L101-L119)),如果检测到栈无效,还会尝试 RtlpHandleDpcStackException,因为内核 DPC 例程使用独立的栈,frame 可能不在常规线程栈范围内。


总结

本章 8.1 节建立了 SEH 框架的全局视图。关键要点:

  1. 三层结构 :编译器层(__try/PSEH2)→ 操作系统 RTL 层(RtlDispatchException/RtlUnwind)→ 硬件/内核层(IDT + KiDispatchException)。
  2. 三个核心结构
    • EXCEPTION_RECORD ------ "发生了什么"(异常描述)
    • EXCEPTION_REGISTRATION_RECORD ------ "谁能处理"(链表节点)
    • EXCEPTION_DISPOSITION ------ "处理结果"(handler 返回值)
  3. 三个转化点
    • 硬件 → KTRAP_FRAMEKiEnterTrap
    • KTRAP_FRAMECONTEXTKeTrapFrameToContext
    • CONTEXT → 用户栈(KiDispatchException 复制)
  4. 两条异常流 :硬件 trap(int 0~13)vs 软异常(RtlRaiseException / RtlRaiseStatus),但都汇入 KiDispatchException
  5. 多级机会:FirstChance(调试器+用户 handler)→ SecondChance(仅调试器)→ 蓝屏。

8.2 节将深入到 系统空间 (内核态)异常分派的实现细节,包括 IDT 入口、trap handler 家族、KiDispatchException 的两大分支;8.3 节则转入 用户空间RtlDispatchException / RtlUnwind 链表遍历;8.4 节讨论 软异常 的发起与 STATUS 码体系。


本章代码索引

文件 内容
rtltypes.h(file:///d:/reactos/sdk/include/xdk/rtltypes.h) EXCEPTION_RECORDEXCEPTION_POINTERS 定义
winnt_old.h(file:///d:/reactos/sdk/include/xdk/winnt_old.h) EXCEPTION_MAXIMUM_PARAMETERSEXCEPTION_* 标志
wine/winnt.h(file:///d:/reactos/sdk/include/wine/winnt.h) EXCEPTION_REGISTRATION_RECORD 定义
vcruntime/excpt.h(file:///d:/reactos/sdk/include/vcruntime/excpt.h) EXCEPTION_DISPOSITION 枚举
pseh2.h(file:///d:/reactos/sdk/include/vcruntime/pseh/pseh2.h) PSEH2 宏(_SEH2_TRY 等)
except.c(file:///d:/reactos/ntoskrnl/ke/except.c) 平台无关 SEH(KiContinueKiRaiseExceptionNtRaiseExceptionNtContinue
i386/exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c) x86 内核异常分派(KiDispatchException 等)
i386/traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) x86 trap handler 家族(KiTrap00-13
i386/trap.s(file:///d:/reactos/ntoskrnl/ke/i386/trap.s) x86 IDT 入口桩、TRAP_ENTRY 宏
i386/except.c(file:///d:/reactos/sdk/lib/rtl/i386/except.c) x86 用户态分派(RtlDispatchExceptionRtlUnwind
exception.c(file:///d:/reactos/sdk/lib/rtl/exception.c) RTL 公共 SEH(RtlRaiseExceptionRtlUnhandledExceptionFilter
vectoreh.c(file:///d:/reactos/sdk/lib/rtl/vectoreh.c) 向量化异常处理(VEH)
i386/except_asm.s(file:///d:/reactos/sdk/lib/rtl/i386/except_asm.s) handler 调用的汇编包装
ntosifs.h(file:///d:/reactos/sdk/include/ntosifs.h) EXCEPTION_CHAIN_ENDExRaiseStatus
psdk/ntstatus.h(file:///d:/reactos/sdk/include/psdk/ntstatus.h) 所有 STATUS_* 异常码定义
相关推荐
caimouse4 小时前
Reactos 第 9 章 设备驱动 — 9.1 Windows的设备驱动框架
windows
宸丶一4 小时前
Day 10:LangGraph - Agent 的图执行引擎
java·windows·python
ylscode4 小时前
GreatXML BitLocker绕过漏洞深度解析:Windows Defender离线扫描如何被改造成本地提权后门
windows·安全
caimouse6 小时前
Reactos 第 7 章 视窗报文 — 7.1 视窗线程与 Win32k 扩充系统调用
windows
caimouse7 小时前
Reactos 第 9 章 设备驱动 — 9.7 一个过滤设备驱动模块的示例
windows
caimouse7 小时前
Reactos 第 7 章 视窗报文 — 7.7 鼠标器输入线程
windows
caimouse8 小时前
Reactos 第 7 章 视窗报文 — 7.2 视窗报文的接收
windows
caimouse8 小时前
Reactos 第 8 章 结构化异常处理 — 8.3 用户空间的结构化异常处理
windows
caimouse8 小时前
Reactos 第 9 章 设备驱动 — 9.6 中断处理
网络·windows