第 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 中由三个相互协作的子系统构成:
- 编译器/语言运行时层 :MSVC 的
__try/__except/__finally、GCC 借助 PSEH2 宏的_SEH2_TRY/_SEH2_EXCEPT; - 操作系统运行时层(RTL) :
RtlDispatchException、RtlUnwind、RtlRaiseException等一组函数; - 硬件/内核异常分派层 :x86 IDT、trap handler、
KiDispatchException。
三个层次的数据通过 EXCEPTION_RECORD、CONTEXT、EXCEPTION_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_DISPOSITION与EXCEPTION_POINTERS - 8.1.6 关键数据结构:
CONTEXT(处理器上下文) - 8.1.7 异常标志位(
EXCEPTION_*)的语义 - 8.1.8 异常处理流程总览(用户态/内核态双向流动)
- 8.1.9 关键 API 索引
学习目标
- 能够画出 SEH 在三层之间的数据流图
- 能够口述
EXCEPTION_RECORD、EXCEPTION_REGISTRATION_RECORD、EXCEPTION_DISPOSITION三个核心结构的作用 - 理解硬件异常如何跨越内核边界进入用户态 handler
- 区分 硬件异常(hardware exception) 与 软异常(software exception)
涉及的内核子系统
| 子系统 | 头文件 | 核心作用 |
|---|---|---|
| HAL / 内核 trap | ntoskrnl/ke/i386/exp.c |
IDT → KiDispatchException |
| 平台无关 SEH | ntoskrnl/ke/except.c |
KiContinue、KiRaiseException、NtRaiseException |
| 用户态 RTL SEH | sdk/lib/rtl/i386/except.c |
RtlDispatchException、RtlUnwind |
| RTL 公共 SEH | sdk/lib/rtl/exception.c |
RtlRaiseException、RtlUnhandledExceptionFilter |
| 向量化异常(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) |
设计目标:
- 统一处理硬件异常与软件异常 (除零、访问违例、
RtlRaiseException都走同一套框架) - 支持跨语言/跨模块(C、C++、MASM、__try/except/__finally 全部统一)
- 支持嵌套异常与栈展开 (析构函数、
__finally块的执行) - 支持多级机会(FirstChance → SecondChance → 蓝屏)
- 支持调试器接入(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 复用同一份链表代码)。它负责:
- 优先调用 VEH(向量化异常处理器) 链
- 沿
EXCEPTION_REGISTRATION_RECORD链表逐层调用 handler - 根据
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;
要点:
ExceptionCode是NTSTATUS类型,常见的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_DISPOSITION4 选 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_DISPOSITION 与 EXCEPTION_POINTERS
EXCEPTION_DISPOSITION 是 handler 函数的返回值枚举,定义于 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_POINTERS 是 handler 收到的参数打包,定义于 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_POINTERS 是 C++/高层 API 使用的便捷打包。GetExceptionInformation() 宏正是返回当前异常对应的 EXCEPTION_POINTERS*。
8.1.6 关键数据结构:CONTEXT(处理器上下文)
CONTEXT 描述异常发生瞬间的 完整 CPU 状态。在 x86 平台上它定义于 NT 头文件中,包含:
- 通用寄存器 :
Eax、Ebx、Ecx、Edx、Esi、Edi、Ebp、Esp - 指令指针 :
Eip - 段寄存器 :
SegCs、SegDs、SegEs、SegFs、SegGs、SegSs - 标志寄存器 :
EFlags - 浮点/FXSAVE 区 :
FloatSave或ExtendedRegisters - 调试寄存器 :
Dr0~Dr7
CONTEXT.ContextFlags 字段决定 哪些字段是有效的。常见的标志:
CONTEXT_INTEGER(0x00001):通用寄存器CONTEXT_CONTROL(0x00001 | 0x00004 等组合):Eip/Esp/Ebp/SegCs/SegSs/EFlagsCONTEXT_SEGMENTS(0x00004 | 0x00008):段寄存器CONTEXT_FLOATING_POINT(0x00008 | 0x00010):x87 FPUCONTEXT_EXTENDED_REGISTERS(0x00020):FXSAVECONTEXT_DEBUG_REGISTERS(0x00010):Dr0-Dr3、Dr6、Dr7CONTEXT_FULL=CONTEXT_INTEGER | CONTEXT_CONTROL | CONTEXT_SEGMENTS
CONTEXT 与 KTRAP_FRAME 之间的转换由 exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c) 中的 KeTrapFrameToContext 和 KeContextToTrapFrame 完成(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 |
展开到链表末尾(无 TargetFrame 的 RtlUnwind) |
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 蓝屏 进程继续运行 / 终止
关键观察:
- 硬件 trap 与软异常在进入
KiDispatchException后路径合流。 - 内核模式异常处理只有"机会"机制(FirstChance/SecondChance),无链表遍历(虽然仍可调用
RtlDispatchException)。 - 用户模式异常处理会回到
KeUserExceptionDispatcher(由 ntdll 实现),由其调用RtlDispatchException遍历链表。 - 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); \
}
关键实现技巧:
- 嵌套函数 :GCC 允许在函数内部定义嵌套函数,PSEH2 利用这一特性在
_SEH2_TRY块中定义 handler 函数 __label__声明 :使用__label__ __seh2_scope_end__声明局部标签,实现_SEH2_LEAVE的跳转- 手动链表操作 :通过
RtlpGetExceptionList()和RtlpSetExceptionList()手动维护FS:[0]链表 - 异常过滤器求值 :
__VA_ARGS__参数在if条件中求值,模拟__except过滤器表达式
PSEH2 的运行时开销
与 MSVC 的原生 SEH 相比,PSEH2 在 GCC 下的实现有以下开销:
- 栈空间 :每个
_SEH2_TRY块需要在栈上分配_SEH2_TRY_CONTEXT结构(约 8 字节) - 链表操作 :每次进入/退出
_SEH2_TRY块都需要修改FS:[0],涉及内存写入 - 嵌套函数开销: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))
这个检查确保:
RegistrationFrame的结束地址不超过栈顶(StackHigh)RegistrationFrame的起始地址不低于栈底(StackLow)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 的另一个核心功能,当异常被处理(ExceptionContinueExecution 或 ExceptionExecuteHandler)或线程退出时,需要沿着 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:描述展开原因的异常记录。如果为NULL,RtlUnwind会构造一个默认的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 需要区分两种情况:
- 正常执行完成 :从
_SEH2_TRY块正常退出 - 异常终止:由于异常而展开
在 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 之前验证其是否在表中。
实现原理:
- 编译器在编译时收集所有合法的 handler 地址,生成一个表
- 链接器将所有模块的 handler 表合并,存储在 PE 文件的
.loadcfg节中 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 验证机制,包括:
- Handler 地址范围检查:确保 handler 地址在可执行代码段内
- Cookie 验证 :在
EXCEPTION_REGISTRATION_RECORD中添加 cookie,防止覆盖 - 栈回溯验证:验证调用栈的完整性
ReactOS 尚未实现这些高级安全机制,但栈边界检查提供了基本的保护。
调试技巧:检测 SEH 覆盖攻击
在调试器中,可以通过以下方法检测 SEH 覆盖攻击:
-
检查 FS:0 链表:
kd> !teb kd> dps fs:[0] L20 ; 查看前 20 个 entry -
验证 handler 地址:
kd> ub <handler_address> ; 反汇编 handler 前的代码 kd> ln <handler_address> ; 查看 handler 的符号 -
检查栈边界:
kd> !stack ; 查看当前栈范围 kd> dps <stack_low> L<stack_size>/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 也自动从链表中移除。这实现了以下关键特性:
- 自动生命周期管理:函数返回 = handler 自动注销,无需手动清理
- 天然支持嵌套:每个函数栈帧可以有自己的 handler,形成链式结构
- 零额外内存分配 :不需要调用
malloc/ExAllocatePool,避免在异常路径上引入新的失败点 - 线程安全 :每个线程有独立的栈和独立的
FS:[0],天然隔离
如果放在堆上,就需要手动管理注册/注销,一旦忘记注销就会导致悬空指针;如果放在全局变量中,就无法支持嵌套和多线程。
在 ReactOS 源码中,RtlpSetExceptionList() 和 RtlpGetExceptionList() 直接操作 FS:[0](即 NtCurrentTib()->ExceptionList),开销仅为一次内存读写。
问题 2:为什么 EXCEPTION_REGISTRATION_RECORD 的 Prev 字段指向的是"下一个"而不是"上一个"?
答 :这是命名约定造成的混淆,实际上是单链表的栈结构。
在 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 的原因:
- 全局拦截需求:某些组件(如反调试器、崩溃报告工具)需要在任何局部 handler 之前看到异常
- 不依赖栈完整性:VEH 存储在全局链表中,即使栈被破坏也能工作
- 性能考虑 :如果没有注册任何 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 后,使用完全相同的分派逻辑。
统一的好处:
- 代码复用:一套分派逻辑处理所有异常,减少代码量和 bug 面
- 语义一致 :无论是硬件还是软件触发的,
STATUS_ACCESS_VIOLATION的行为完全相同 - 可测试性:可以用软件模拟硬件异常,便于测试 handler
- 调试器统一接口:调试器只需要 hook 一个入口点就能拦截所有异常
在 ReactOS 中,硬件 trap 通过 KiTrap00Handler → KiDispatchException0Args → KiDispatchException;软件异常通过 NtRaiseException → KiRaiseException → KiDispatchException。两条路径在 KiDispatchException 处合流。
唯一的区别 :硬件异常会先经过 KeTrapFrameToContext 将 KTRAP_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_BREAKPOINT 做 Eip-- 的特殊处理?
答 :这是 x86 架构的 INT 3 指令特性决定的。
STATUS_BREAKPOINT 由 INT 3(0xCC)指令触发。当 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_OVERFLOW(INTO指令):也不需要调整,因为INTO的语义不同STATUS_SINGLE_STEP:Eip 已经指向下一条要执行的指令,不需要调整
问题 7:为什么 RtlUnwind 在展开时要从链表中移除已经处理过的 frame?
答 :这是为了防止重复展开和悬空指针。
RtlUnwind 的展开过程是从栈顶向栈底遍历,依次调用每个 handler 的清理代码。在处理完一个 frame 后,立即将它从 FS:[0] 链表中移除:
c
OldFrame = RegistrationFrame;
RegistrationFrame = RegistrationFrame->Next;
RtlpSetExceptionList(RegistrationFrame); // 更新 FS:[0]
为什么要这样做?
- 防止重入 :如果 handler 的清理代码本身又触发了异常,新的异常分派会从更新后的
FS:[0]开始,不会再次调用已经执行过的 handler - 保持一致性 :展开过程中的栈帧正在被"销毁",如果链表仍然指向它们,其他代码(如
RtlDispatchException)可能会访问已失效的 frame - 支持嵌套展开 :如果清理代码中调用了另一个
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(蓝屏)
为什么需要多级机会?
- FirstChance :给调试器和局部 handler 一个修复问题的机会。很多异常是"可恢复的"------例如调试器设置断点触发的
STATUS_BREAKPOINT,或者 handler 修复了错误指针后可以继续执行 - SecondChance:如果 FirstChance 没有人处理,说明异常是"不可恢复的"。此时给调试器最后一次机会来收集诊断信息(dump 内存、记录调用栈)
- 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 选择嵌套函数的原因:
- 需要真正的 handler 函数 :SEH 的 handler 需要接收 4 个参数(ExceptionRecord、EstablisherFrame、ContextRecord、DispatcherContext),并返回
EXCEPTION_DISPOSITION。setjmp/longjmp无法提供这种接口 - 需要支持展开模式 :handler 需要区分"异常分派"和"栈展开"两种调用模式,这在
setjmp/longjmp中无法表达 - 需要与系统 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;
}
这个检查确保:
- frame 的结束地址不超过栈顶(
StackHigh) - frame 的起始地址不低于栈底(
StackLow) - 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 框架的全局视图。关键要点:
- 三层结构 :编译器层(
__try/PSEH2)→ 操作系统 RTL 层(RtlDispatchException/RtlUnwind)→ 硬件/内核层(IDT +KiDispatchException)。 - 三个核心结构 :
EXCEPTION_RECORD------ "发生了什么"(异常描述)EXCEPTION_REGISTRATION_RECORD------ "谁能处理"(链表节点)EXCEPTION_DISPOSITION------ "处理结果"(handler 返回值)
- 三个转化点 :
- 硬件 →
KTRAP_FRAME(KiEnterTrap) KTRAP_FRAME→CONTEXT(KeTrapFrameToContext)CONTEXT→ 用户栈(KiDispatchException复制)
- 硬件 →
- 两条异常流 :硬件 trap(int 0~13)vs 软异常(
RtlRaiseException/RtlRaiseStatus),但都汇入KiDispatchException。 - 多级机会: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_RECORD、EXCEPTION_POINTERS 定义 |
| winnt_old.h(file:///d:/reactos/sdk/include/xdk/winnt_old.h) | EXCEPTION_MAXIMUM_PARAMETERS、EXCEPTION_* 标志 |
| 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(KiContinue、KiRaiseException、NtRaiseException、NtContinue) |
| 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 用户态分派(RtlDispatchException、RtlUnwind) |
| exception.c(file:///d:/reactos/sdk/lib/rtl/exception.c) | RTL 公共 SEH(RtlRaiseException、RtlUnhandledExceptionFilter) |
| 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_END、ExRaiseStatus 等 |
| psdk/ntstatus.h(file:///d:/reactos/sdk/include/psdk/ntstatus.h) | 所有 STATUS_* 异常码定义 |