第 8 章 结构化异常处理 --- 8.2 系统空间的结构化异常处理
本节深入剖析系统空间(内核态 Ring 0)下 ReactOS 的结构化异常处理实现。 系统空间的 SEH 是整个 SEH 框架的"上游"------所有硬件 trap、所有用户态抛出的异常、所有驱动程序的 SEH,最终都要汇入 KiDispatchException 这一个核心函数。理解 KiDispatchException 的两个分支(内核模式 vs 用户模式)、三阶段机会(FirstChance → SecondChance → BlueScreen)、以及与 KeUserExceptionDispatcher 的协作,是读懂 Windows NT 内核异常分派的关键。
概述
系统空间的 SEH 由 三个层次 协作完成:
- 硬件入口层 :x86 IDT(Interrupt Descriptor Table)将每个异常向量指向
KiTrap00~KiTrap13等汇编入口桩。 - Trap 处理层 :每个 trap 桩保存现场(
KiEnterTrap)、启用中断、调用KiDispatchException0Args/1Args/2Args包装函数。 - 异常分派层 :
KiDispatchException是内核态 SEH 的中央枢纽,它根据PreviousMode和FirstChance决定走向内核态 handler 链、用户态分派器、调试器还是蓝屏。
这三层共同回答了三个核心问题:
- 异常 从哪里来?(硬件 trap / 软件 API)
- 异常 归谁处理 ?(内核态的
RtlDispatchException/ 用户态的KeUserExceptionDispatcher/ 调试器) - 异常 处理失败怎么办?(重试、二次机会、蓝屏)
本节内容概览
- 8.2.0 框架图:内核态 SEH 完整数据流
- 8.2.1 IDT 与 trap 入口桩
- 8.2.2
KTRAP_FRAME与KEXCEPTION_FRAME结构 - 8.2.3 Trap handler 家族(
KiTrap00-13) - 8.2.4 包装宏家族:
KiDispatchException0/1/2Args与KiDispatchExceptionFromTrapFrame - 8.2.5 内核态分派核心:
KiDispatchException(两分支) - 8.2.6 用户态分派路径:
KeUserExceptionDispatcher跳转 - 8.2.7 调试器与
KiDebugRoutine/DbgkForwardException - 8.2.8 蓝屏与
KeBugCheckEx - 8.2.9 平台无关 SEH:
KiContinue/KiRaiseException/NtRaiseException/NtContinue - 8.2.10 x64 与 ARM 实现差异对比
学习目标
- 能够画出 IDT → trap 桩 → trap handler →
KiDispatchException的完整调用链 - 理解
KiDispatchException三大分支(内核态/用户态/软异常)的处理流程 - 能够解释
FirstChance/SecondChance/LastChance的语义 - 理解
KeUserExceptionDispatcher是如何被注入到用户态返回路径的 - 能够描述 CONTEXT 与 KTRAP_FRAME 的转换关系
涉及的内核子系统
| 子系统 | 头文件/源文件 | 核心作用 |
|---|---|---|
| IDT 与 trap 入口 | ntoskrnl/ke/i386/trap.s(file:///d:/reactos/ntoskrnl/ke/i386/trap.s) | 21 个异常向量入口 |
| Trap handler 家族 | ntoskrnl/ke/i386/traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) | KiTrap00 ~ KiTrap13 C 函数 |
| 异常分派核心 | ntoskrnl/ke/i386/exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c) | KiDispatchException、KiDispatchExceptionFromTrapFrame |
| 平台无关 SEH API | ntoskrnl/ke/except.c(file:///d:/reactos/ntoskrnl/ke/except.c) | KiContinue、KiRaiseException、NtContinue、NtRaiseException |
| 软中断服务 | ntoskrnl/ke/i386/traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) | KiDebugServiceHandler、KiRaiseAssertionHandler 等 |
| 调试器回调 | ntoskrnl/ke/i386/traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) | KiDebugHandler、KiDebugRoutine |
| 架构特定 SEH | ntoskrnl/ke/amd64/except.c(file:///d:/reactos/ntoskrnl/ke/amd64/except.c) | x64 实现 |
| 架构特定 SEH | ntoskrnl/ke/arm/exp.c(file:///d:/reactos/ntoskrnl/ke/arm/exp.c) | ARM 实现 |
| 系统调用接口 | sdk/include/ntosifs.h(file:///d:/reactos/sdk/include/ntosifs.h) | ExRaiseStatus 等 |
8.2.0 框架图
硬件事件
(CPU trap / 软件 API / 系统调用)
|
v
+-----------------+ IDT
| KiTrap00-13 |<--+-------------------+
| (汇编入口桩) | | |
+-----------------+ | |
| | |
v | |
+-----------------+ | |
| KiEnterTrap | 保存寄存器到 |
| 构造 KTRAP_FRAME | KTRAP_FRAME |
+-----------------+ | |
| | |
v | |
+-----------------+ | |
| KiTrap0XHandler | | |
| (C 函数) | | |
+-----------------+ | |
| | |
v | |
+-----------------+ | |
| KiDispatch- | | |
| Exception0Args/ | | |
| 1Args/2Args | | |
+-----------------+ | |
| | |
v | |
+-----------------+ | |
| KiDispatch- | | 软异常统一入口 |
| Exception |<--+<------------------+
| (中央枢纽) | | KiDispatch- |
+-----------------+ | ExceptionFrom- |
| | TrapFrame |
v |
三大分支: |
(A) 内核态 |
(B) 用户态 |
(C) 软异常直通 |
| |
v |
(A) KernelMode 分派
FirstChance → KiDebugRoutine → RtlDispatchException
SecondChance → KiDebugRoutine
三次失败 → KeBugCheckEx(KMODE_EXCEPTION_NOT_HANDLED)
|
v
(B) UserMode 分派
FirstChance → KiDebugRoutine → DbgkForwardException
→ 复制 CONTEXT/EXCEPTION_RECORD 到用户栈
→ 设置 EIP = KeUserExceptionDispatcher
→ _SEH2_YIELD(return)
SecondChance → DbgkForwardException
三次失败 → ZwTerminateProcess + KeBugCheckEx
|
v
回到用户态后 KeUserExceptionDispatcher 调用 RtlDispatchException
遍历 EXCEPTION_REGISTRATION_RECORD 链表
|
v
(C) 软异常直通
KiDispatchExceptionFromTrapFrame 构造 EXCEPTION_RECORD
然后走 (A) 或 (B) 同一条 KiDispatchException 路径
8.2.1 IDT 与 trap 入口桩
x86 CPU 通过 IDT(Interrupt Descriptor Table) 把每个异常/中断向量指向一个处理函数。ReactOS 在 trap.s(file:///d:/reactos/ntoskrnl/ke/i386/trap.s) 中定义 IDT 入口:
asm
idt _KiTrap00, INT_32_DPL0 /* INT 00: Divide Error (#DE) */
idt _KiTrap01, INT_32_DPL0 /* INT 01: Debug Exception (#DB) */
idt _KiTrap02, INT_32_DPL0 /* INT 02: NMI Interrupt */
idt _KiTrap03, INT_32_DPL3 /* INT 03: Breakpoint Exception (#BP) */
idt _KiTrap04, INT_32_DPL3 /* INT 04: Overflow Exception (#OF) */
idt _KiTrap05, INT_32_DPL0 /* INT 05: BOUND Range Exceeded (#BR) */
idt _KiTrap06, INT_32_DPL0 /* INT 06: Invalid Opcode Code (#UD) */
idt _KiTrap07, INT_32_DPL0 /* INT 07: Device Not Available (#NM) */
idt _KiTrap08, INT_32_DPL0 /* INT 08: Double Fault Exception (#DF) */
idt _KiTrap09, INT_32_DPL0 /* INT 09: RESERVED */
idt _KiTrap0A, INT_32_DPL0 /* INT 0A: Invalid TSS Exception (#TS) */
idt _KiTrap0B, INT_32_DPL0 /* INT 0B: Segment Not Present (#NP) */
idt _KiTrap0C, INT_32_DPL0 /* INT 0C: Stack Fault Exception (#SS) */
idt _KiTrap0D, INT_32_DPL0 /* INT 0D: General Protection (#GP) */
idt _KiTrap0E, INT_32_DPL0 /* INT 0E: Page-Fault Exception (#PF) */
idt _KiTrap10, INT_32_DPL0 /* INT 10: x87 FPU Error (#MF) */
idt _KiTrap11, INT_32_DPL0 /* INT 11: Align Check Exception (#AC) */
注意 DPL(Descriptor Privilege Level)的差异:INT_32_DPL3 表示该中断可以被用户态代码(Ring 3)通过 int n 主动触发(如 int 3 断点),而 INT_32_DPL0 仅限内核态。
每个 _KiTrapXX 是一个汇编入口桩,由 TRAP_ENTRY 宏展开:
asm
TRAP_ENTRY KiTrap00, KI_PUSH_FAKE_ERROR_CODE
TRAP_ENTRY KiTrap01, KI_PUSH_FAKE_ERROR_CODE
TASK_ENTRY KiTrap02, KI_NMI ; NMI 用任务门
TRAP_ENTRY KiTrap03, KI_PUSH_FAKE_ERROR_CODE
TRAP_ENTRY KiTrap04, KI_PUSH_FAKE_ERROR_CODE
TRAP_ENTRY KiTrap05, KI_PUSH_FAKE_ERROR_CODE
TRAP_ENTRY KiTrap06, KI_PUSH_FAKE_ERROR_CODE
TRAP_ENTRY KiTrap07, KI_PUSH_FAKE_ERROR_CODE
TASK_ENTRY KiTrap08, 0 ; 双重错误用任务门
TRAP_ENTRY KiTrap0A, 0 ; 错误码由 CPU 压栈
TRAP_ENTRY KiTrap0B, 0
TRAP_ENTRY KiTrap0C, 0
TRAP_ENTRY KiTrap0D, 0
TRAP_ENTRY KiTrap0E, 0
TRAP_ENTRY 宏在 trap.s(file:///d:/reactos/ntoskrnl/ke/i386/trap.s) 中定义,其核心任务是:
- 在栈上压入一个 伪错误码(如果 CPU 没自动压)
- 跳转到公共的
KiEnterTrap代码 KiEnterTrap把所有寄存器保存到一个KTRAP_FRAME结构中- 调用对应的 C 函数
KiTrap0XHandler
8.2.2 KTRAP_FRAME 与 KEXCEPTION_FRAME 结构
x86 平台上,当 trap 发生时,CPU 会自动把 EIP、CS、EFLAGS、ESP、SS 压栈,再由 KiEnterTrap 加上通用寄存器、段寄存器、调试寄存器等,构成一个 KTRAP_FRAME:
c
typedef struct _KTRAP_FRAME
{
ULONG DbgEbp; // 调试 EBP
ULONG DbgEip; // 调试 EIP
ULONG DbgArgMark; // 调试参数标记
ULONG DbgArgPointer; // 调试参数指针
ULONG TempSegCs; // 临时 CS
ULONG TempEsp; // 临时 ESP
ULONG Ebp;
ULONG Eip;
ULONG SegCs;
ULONG EFlags;
ULONG HardwareEsp; // 用户栈或内核栈
ULONG HardwareSegSs;
ULONG V86Es, V86Ds, V86Fs, V86Gs; // V86 模式段
ULONG Es;
ULONG Ds;
ULONG Fs;
ULONG ExceptionList; // SEH 链表头(FS:[0] 快照)
ULONG PreviousPreviousMode;
ULONG Edi, Esi, Ebx, Edx, Ecx, Eax; // 通用寄存器
ULONG ErrCode; // 错误码
ULONG Eip_ofs; // 隐藏字段
ULONG SegCs_ofs;
ULONG EFlags_ofs;
ULONG HardwareEsp_ofs;
ULONG HardwareSegSs_ofs;
ULONG Dr0, Dr1, Dr2, Dr3, Dr6, Dr7; // 调试寄存器
ULONG V86Es_ofs, V86Ds_ofs, V86Fs_ofs, V86Gs_ofs;
ULONG Es_ofs, Ds_ofs, Fs_ofs;
ULONG Reserved[7];
} KTRAP_FRAME;
KEXCEPTION_FRAME 是 x86 早期 NT 内核遗留的二级栈 frame,仅在某些特定路径(如 NtContinue)使用。在 AMD64/ARM 上已合并入单一 trap frame。
KTRAP_FRAME 与 CONTEXT 的转换由 exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c) 中的两个函数完成:
KeTrapFrameToContext(TrapFrame, ExceptionFrame, &Context):把 trap frame 转换为 CONTEXTKeContextToTrapFrame(&Context, ExceptionFrame, TrapFrame, ContextFlags, PreviousMode):把 CONTEXT 写回 trap frame
这个转换是双向的:在分派异常前 KiDispatchException 把 trap frame 转成 CONTEXT 调用 handler;在恢复执行前 KiContinue 把 CONTEXT 转回 trap frame 然后 iret。
8.2.3 Trap handler 家族(KiTrap00-13)
每个 trap 都有一个对应的 C 函数入口,位于 traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c)。它们有非常相似的模板:
c
DECLSPEC_NORETURN
VOID
FASTCALL
KiTrap00Handler(IN PKTRAP_FRAME TrapFrame)
{
/* Save trap frame */
KiEnterTrap(TrapFrame);
/* Check for VDM trap */
ASSERT(KiVdmTrap(TrapFrame) == FALSE);
/* Enable interrupts */
_enable();
/* Dispatch the exception */
KiDispatchException0Args(STATUS_INTEGER_DIVIDE_BY_ZERO,
TrapFrame->Eip,
TrapFrame);
}
每个 handler 的工作流都是:
KiEnterTrap(TrapFrame):保存硬件上下文到KTRAP_FRAME- 检查 VDM 模式(虚拟 86 模式,遗留 16 位应用)
_enable()启用中断(内核态 trap 期间中断默认关闭)- 调用
KiDispatchExceptionNArgs系列包装函数
下表列出每个 trap 的目标 STATUS 码:
| Trap | 来源 | Eip 调整 | 状态码 |
|---|---|---|---|
KiTrap00 |
除零异常 | 0 | STATUS_INTEGER_DIVIDE_BY_ZERO |
KiTrap01 |
调试异常(单步) | 0 | STATUS_SINGLE_STEP |
KiTrap02 |
NMI | - | KiHandleNmi → 蓝屏 |
KiTrap03 |
断点(int 3) | -1 | KiDebugHandler → STATUS_BREAKPOINT |
KiTrap04 |
溢出(into) | -1 | STATUS_INTEGER_OVERFLOW |
KiTrap05 |
BOUND 越界 | 0 | STATUS_ARRAY_BOUNDS_EXCEEDED |
KiTrap06 |
非法操作码 | 0 | STATUS_ILLEGAL_INSTRUCTION |
KiTrap07 |
FPU 不可用 | 0 | NPX/FPU 状态恢复或 KiNpxHandler |
KiTrap08 |
双重错误 | - | KeBugCheckEx(UNEXPECTED_KERNEL_MODE_TRAP) |
KiTrap09 |
协处理器段超限 | - | KiSystemFatalException |
KiTrap0A |
无效 TSS | - | KiSystemFatalException |
KiTrap0B |
段不存在 | - | KiSystemFatalException |
KiTrap0C |
栈错误 | - | KiSystemFatalException |
KiTrap0D |
通用保护 (GPF) | 0 | STATUS_PRIVILEGED_INSTRUCTION / STATUS_ACCESS_VIOLATION / 蓝屏 |
KiTrap0E |
页错误 | 0 | MmAccessFault → STATUS_ACCESS_VIOLATION / STATUS_GUARD_PAGE_VIOLATION / STATUS_STACK_OVERFLOW / STATUS_IN_PAGE_ERROR |
KiTrap10 |
x87 FPU 错误 | 0 | KiNpxHandler → STATUS_FLOAT_* |
KiTrap11 |
对齐检查 | 0 | KiSystemFatalException |
KiTrap13 |
SIMD 异常 | 0 | STATUS_FLOAT_MULTIPLE_TRAPS |
例如 KiTrap0EHandler(页错误)会先调用内存管理器 MmAccessFault 尝试解决缺页,只有当 MmAccessFault 返回失败时才把异常转交给 SEH 框架。
8.2.4 包装宏家族:KiDispatchException0/1/2Args 与 KiDispatchExceptionFromTrapFrame
KiDispatchException 需要 5 个参数:异常记录、异常 frame、trap frame、PreviousMode、FirstChance。但 trap handler 通常手头只有 trap frame 和简单的异常码、参数。为了减少样板代码,ReactOS 提供了一组包装函数(在 exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c) 中定义):
c
VOID KiDispatchException0Args(IN NTSTATUS Code,
IN ULONG_PTR Address,
IN PKTRAP_FRAME TrapFrame);
VOID KiDispatchException1Args(IN NTSTATUS Code,
IN ULONG_PTR Address,
IN ULONG_PTR Parameter,
IN PKTRAP_FRAME TrapFrame);
VOID KiDispatchException2Args(IN NTSTATUS Code,
IN ULONG_PTR Address,
IN ULONG_PTR Parameter1,
IN ULONG_PTR Parameter2,
IN PKTRAP_FRAME TrapFrame);
这些函数内部构造一个 EXCEPTION_RECORD 并调用 KiDispatchException。例如:
c
VOID KiDispatchException2Args(IN NTSTATUS Code,
IN ULONG_PTR Address,
IN ULONG_PTR Parameter1,
IN ULONG_PTR Parameter2,
IN PKTRAP_FRAME TrapFrame)
{
EXCEPTION_RECORD ExceptionRecord;
PKEXCEPTION_FRAME ExceptionFrame;
ExceptionRecord.ExceptionCode = Code;
ExceptionRecord.ExceptionFlags = 0;
ExceptionRecord.ExceptionRecord = NULL;
ExceptionRecord.ExceptionAddress = (PVOID)Address;
ExceptionRecord.NumberParameters = 2;
ExceptionRecord.ExceptionInformation[0] = Parameter1;
ExceptionRecord.ExceptionInformation[1] = Parameter2;
ExceptionFrame = (PKEXCEPTION_FRAME)(TrapFrame + 1);
KiDispatchException(&ExceptionRecord,
ExceptionFrame,
TrapFrame,
KiUserTrap(TrapFrame) ? UserMode : KernelMode,
TRUE);
}
注意:包装函数默认 FirstChance = TRUE,因为 trap 本身是"第一次机会"。KiUserTrap(TrapFrame) 通过 SegCs 段选择子判断 CPL 决定 PreviousMode。
另一个重要的入口是 `KiDispatchExceptionFromTrapFrame`(file:///d:/reactos/ntoskrnl/ke/i386/exp.c#L1055):
c
DECLSPEC_NORETURN
VOID NTAPI
KiDispatchExceptionFromTrapFrame(IN NTSTATUS Code,
IN ULONG Flags,
IN ULONG_PTR Address,
IN ULONG ParameterCount,
IN ULONG_PTR Parameter1,
IN ULONG_PTR Parameter2,
IN ULONG_PTR Parameter3,
IN PKTRAP_FRAME TrapFrame)
{
EXCEPTION_RECORD ExceptionRecord;
/* Build the exception record */
ExceptionRecord.ExceptionCode = Code;
ExceptionRecord.ExceptionFlags = Flags;
ExceptionRecord.ExceptionRecord = NULL;
ExceptionRecord.ExceptionAddress = (PVOID)Address;
ExceptionRecord.NumberParameters = ParameterCount;
if (ParameterCount)
{
ExceptionRecord.ExceptionInformation[0] = Parameter1;
ExceptionRecord.ExceptionInformation[1] = Parameter2;
ExceptionRecord.ExceptionInformation[2] = Parameter3;
}
/* Dispatch the exception */
KiDispatchException(&ExceptionRecord,
(PKEXCEPTION_FRAME)(TrapFrame + 1),
TrapFrame,
KiUserTrap(TrapFrame) ? UserMode : KernelMode,
TRUE);
}
这是 软异常 的统一入口------很多软件触发的异常(STATUS_BREAKPOINT、STATUS_ASSERTION_FAILURE、STATUS_STACK_BUFFER_OVERRUN)都通过它发出。
8.2.5 内核态分派核心:KiDispatchException
KiDispatchException 是整个系统空间 SEH 的 中央枢纽,定义于 exp.c:795(file:///d:/reactos/ntoskrnl/ke/i386/exp.c#L795-L1050)。它的整体结构是:
c
VOID
NTAPI
KiDispatchException(IN PEXCEPTION_RECORD ExceptionRecord,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame,
IN KPROCESSOR_MODE PreviousMode,
IN BOOLEAN FirstChance)
{
CONTEXT Context;
EXCEPTION_RECORD LocalExceptRecord;
/* Increase number of Exception Dispatches */
KeGetCurrentPrcb()->KeExceptionDispatchCount++;
/* Set the context flags */
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
if ((PreviousMode == UserMode) || (KeGetPcr()->KdVersionBlock))
{
Context.ContextFlags |= CONTEXT_FLOATING_POINT;
if (KeI386FxsrPresent) Context.ContextFlags |= CONTEXT_EXTENDED_REGISTERS;
}
/* Get a Context */
KeTrapFrameToContext(TrapFrame, ExceptionFrame, &Context);
/* Look at our exception code */
switch (ExceptionRecord->ExceptionCode)
{
case STATUS_BREAKPOINT:
Context.Eip--; // 跳过 INT3 指令
break;
case KI_EXCEPTION_ACCESS_VIOLATION:
ExceptionRecord->ExceptionCode = STATUS_ACCESS_VIOLATION;
break;
}
/* Handle kernel-mode first, it's simpler */
if (PreviousMode == KernelMode)
{
// ... 见 8.2.5.1
}
else
{
// ... 见 8.2.5.2
}
Handled:
/* Convert the context back into Trap/Exception Frames */
KeContextToTrapFrame(&Context, ExceptionFrame, TrapFrame, Context.ContextFlags, PreviousMode);
return;
}
8.2.5.1 内核模式分支
c
if (PreviousMode == KernelMode)
{
/* Check if this is a first-chance exception */
if (FirstChance != FALSE)
{
if (KiDebugRoutine(TrapFrame, ExceptionFrame, ExceptionRecord, &Context,
PreviousMode, FALSE))
{
goto Handled;
}
if (RtlDispatchException(ExceptionRecord, &Context)) goto Handled;
}
/* This is a second-chance exception, only for the debugger */
if (KiDebugRoutine(TrapFrame, ExceptionFrame, ExceptionRecord, &Context,
PreviousMode, TRUE))
{
goto Handled;
}
/* Third strike; you're out */
KeBugCheckEx(KMODE_EXCEPTION_NOT_HANDLED,
ExceptionRecord->ExceptionCode,
(ULONG_PTR)ExceptionRecord->ExceptionAddress,
(ULONG_PTR)TrapFrame,
0);
}
内核态分派只有 三阶段:
- FirstChance(第一次机会) :
- 调用
KiDebugRoutine给内核调试器一次处理机会 - 如果调试器不处理,调用
RtlDispatchException走内核的EXCEPTION_REGISTRATION_RECORD链表
- 调用
- SecondChance(第二次机会) :
- 再次调用
KiDebugRoutine给调试器最后机会
- 再次调用
- 三次失败 → 蓝屏 :
KeBugCheckEx(KMODE_EXCEPTION_NOT_HANDLED, ...)触发蓝屏死机
注意:内核态虽然调用 RtlDispatchException,但这与用户态的链表遍历不同------内核代码很少注册链表节点,因此绝大多数内核异常会直接走到第二次机会。
8.2.5.2 用户模式分支
c
else /* UserMode */
{
if (FirstChance)
{
if ((!(PsGetCurrentProcess()->DebugPort) && !(KdIgnoreUmExceptions)) ||
(KdIsThisAKdTrap(ExceptionRecord, &Context, PreviousMode)))
{
if (KiDebugRoutine(TrapFrame, ExceptionFrame, ExceptionRecord, &Context,
PreviousMode, FALSE))
{
goto Handled;
}
}
/* Forward exception to user mode debugger */
if (DbgkForwardException(ExceptionRecord, TRUE, FALSE)) return;
/* Set up the user-stack */
DispatchToUser:
_SEH2_TRY
{
ULONG Size;
ULONG_PTR Stack, NewStack;
if ((TrapFrame->HardwareSegSs != (KGDT_R3_DATA | RPL_MASK)) ||
(TrapFrame->EFlags & EFLAGS_V86_MASK))
{
LocalExceptRecord.ExceptionCode = STATUS_ACCESS_VIOLATION;
LocalExceptRecord.ExceptionFlags = 0;
LocalExceptRecord.NumberParameters = 0;
RtlRaiseException(&LocalExceptRecord);
}
/* Align context size and get stack pointer */
Size = (sizeof(CONTEXT) + 3) & ~3;
Stack = (Context.Esp & ~3) - Size;
/* Probe stack and copy Context */
ProbeForWrite((PVOID)Stack, Size, sizeof(ULONG));
RtlCopyMemory((PVOID)Stack, &Context, sizeof(CONTEXT));
/* Align exception record size and get stack pointer */
Size = (sizeof(EXCEPTION_RECORD) -
(EXCEPTION_MAXIMUM_PARAMETERS - ExceptionRecord->NumberParameters) *
sizeof(ULONG) + 3) & ~3;
NewStack = Stack - Size;
/* Probe stack and copy exception record */
ProbeForWrite((PVOID)(NewStack - 2 * sizeof(ULONG_PTR)),
Size + 2 * sizeof(ULONG_PTR), sizeof(ULONG));
RtlCopyMemory((PVOID)NewStack, ExceptionRecord, Size);
/* Now write the two params for the user-mode dispatcher */
*(PULONG_PTR)(NewStack - 1 * sizeof(ULONG_PTR)) = Stack;
*(PULONG_PTR)(NewStack - 2 * sizeof(ULONG_PTR)) = NewStack;
/* Set new Stack Pointer */
KiSsToTrapFrame(TrapFrame, KGDT_R3_DATA);
KiEspToTrapFrame(TrapFrame, NewStack - 2 * sizeof(ULONG_PTR));
/* Force correct segments */
TrapFrame->SegCs = Ke386SanitizeSeg(KGDT_R3_CODE, PreviousMode);
TrapFrame->SegDs = Ke386SanitizeSeg(KGDT_R3_DATA, PreviousMode);
TrapFrame->SegEs = Ke386SanitizeSeg(KGDT_R3_DATA, PreviousMode);
TrapFrame->SegFs = Ke386SanitizeSeg(KGDT_R3_TEB, PreviousMode);
TrapFrame->SegGs = 0;
/* Set EIP to the User-mode Dispatcher */
TrapFrame->Eip = (ULONG)KeUserExceptionDispatcher;
/* Dispatch exception to user-mode */
_SEH2_YIELD(return);
}
_SEH2_EXCEPT((RtlCopyMemory(...), EXCEPTION_EXECUTE_HANDLER))
{
if ((NTSTATUS)LocalExceptRecord.ExceptionCode == STATUS_STACK_OVERFLOW)
{
LocalExceptRecord.ExceptionAddress = ExceptionRecord->ExceptionAddress;
RtlCopyMemory(ExceptionRecord, &LocalExceptRecord, sizeof(EXCEPTION_RECORD));
_SEH2_YIELD(goto DispatchToUser);
}
}
_SEH2_END;
}
/* Try second chance */
if (DbgkForwardException(ExceptionRecord, TRUE, TRUE)) return;
else if (DbgkForwardException(ExceptionRecord, FALSE, TRUE)) return;
/* 3rd strike, kill the process */
ZwTerminateProcess(NtCurrentProcess(), ExceptionRecord->ExceptionCode);
KeBugCheckEx(KMODE_EXCEPTION_NOT_HANDLED, ...);
}
用户模式分派的 关键路径:
- FirstChance :
- 若当前进程没有用户态调试器(
DebugPort == NULL)且未忽略用户态异常,调用KiDebugRoutine给内核调试器 DbgkForwardException(..., FirstChance=TRUE, SecondChance=FALSE)给用户态调试器一次机会- 核心 :构造一个 用户态调用栈 ,把 CONTEXT 和 EXCEPTION_RECORD 复制到用户栈,然后 修改 trap frame :
TrapFrame->Eip = KeUserExceptionDispatcher(ntdll 中的入口)TrapFrame->Esp指向用户栈构造的"调用参数"
_SEH2_YIELD(return):异常处理完成后通过 iret 直接"返回"到KeUserExceptionDispatcher,它会读取栈上的参数(EXCEPTION_RECORD*和CONTEXT*),然后调用RtlDispatchException走用户态链表
- 若当前进程没有用户态调试器(
- SecondChance :
- 再次
DbgkForwardException(..., SecondChance=TRUE)
- 再次
- 三次失败 → 进程终止 :
ZwTerminateProcess(NtCurrentProcess(), ExceptionCode)杀掉当前进程- 备用:调用
KeBugCheckEx
注意代码中的 DispatchToUser 标签:这是处理 栈溢出 的特殊机制。如果在复制 CONTEXT/EXCEPTION_RECORD 到用户栈时探测栈失败(_SEH2_EXCEPT 触发),且异常码是 STATUS_STACK_OVERFLOW,则重新尝试(因为已经换了一个栈布局)。
8.2.5.3 关键辅助函数
KiUserTrap(TrapFrame):通过SegCs判断是否是用户态 trapKiIsFrameEdited(TrapFrame):检查SegCs & FRAME_EDITED位KiUserTrapDispatcher/KeUserExceptionDispatcher:ntdll 中提供的用户态入口KeTrapFrameToContext/KeContextToTrapFrame:双向转换
8.2.6 用户态分派路径:KeUserExceptionDispatcher 跳转
KeUserExceptionDispatcher 是 ntdll.dll 提供的一个用户态函数,由 ReactOS 在 dll/ntdll(file:///d:/reactos/dll/ntdll) 中实现。它的签名大致为:
c
VOID NTAPI KeUserExceptionDispatcher(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT Context);
KiDispatchException 在用户模式分支构造的栈布局(从高到低):
+-----------------+
| ... | (用户态原栈顶)
+-----------------+
| Stack (CONTEXT*)| <- 这是 KeUserExceptionDispatcher 看到的参数 2
+-----------------+
| NewStack (EXC*) | <- 这是 KeUserExceptionDispatcher 看到的参数 1
+-----------------+
| 实际 EXCEPTION_RECORD 数据 |
+-----------------+
| 实际 CONTEXT 数据 |
+-----------------+
当 KiDispatchException 通过 _SEH2_YIELD(return) 返回时,KiExceptionExit(或 KiEoiHelper)会把修改后的 trap frame iret 到 KeUserExceptionDispatcher。此时栈上的前两个 ULONG_PTR 分别是 EXCEPTION_RECORD* 和 CONTEXT*,正好对应 KeUserExceptionDispatcher 的两个参数。
KeUserExceptionDispatcher 内部调用 RtlDispatchException 遍历用户态链表(详见 8.3 节)。
8.2.7 调试器与 KiDebugRoutine / DbgkForwardException
KiDebugRoutine
KiDebugRoutine 是 内核态调试器回调(Windows NT Kernel Debugger / KD),由 traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) 实现:
c
KiDebugRoutine(IN PKTRAP_FRAME TrapFrame,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PEXCEPTION_RECORD ExceptionRecord,
IN PCONTEXT Context,
IN KPROCESSOR_MODE PreviousMode,
IN BOOLEAN SecondChance);
它把异常发送给内核调试器(通过 KdpTrap、KdSendPacket 等),等待调试器响应(继续/单步/处理/忽略)。返回 TRUE 表示调试器已处理,异常可以"恢复"。
KiDispatchException 在内核模式分支的两次 KiDebugRoutine 调用:
- 第一次:
SecondChance = FALSE - 第二次:
SecondChance = TRUE
DbgkForwardException
DbgkForwardException 在 ps/win32k 子系统中实现,作用是把异常 转发给用户态调试器 (如 WinDbg、VS 调试器)。它通过 DbgkpSendApiMessage 与调试器通信,调试器可以通过修改 CONTEXT 来影响后续执行。
签名:
c
BOOLEAN DbgkForwardException(IN PEXCEPTION_RECORD ExceptionRecord,
IN BOOLEAN FirstChance,
IN BOOLEAN SecondChance);
KiDispatchException 在用户模式分支调用:
DbgkForwardException(ExceptionRecord, TRUE, FALSE)(第一次机会)DbgkForwardException(ExceptionRecord, TRUE, TRUE)(第二次机会)DbgkForwardException(ExceptionRecord, FALSE, TRUE)(仅第二次机会,备用)
8.2.8 蓝屏与 KeBugCheckEx
KeBugCheckEx 是 不可恢复错误 的最终处理。KiDispatchException 在以下情况调用它:
- 内核模式:第三次机会仍失败
- 用户模式:第三次机会仍失败,且
ZwTerminateProcess后
签名:
c
VOID KeBugCheckEx(IN ULONG BugCheckCode,
IN ULONG_PTR BugCheckParameter1,
IN ULONG_PTR BugCheckParameter2,
IN ULONG_PTR BugCheckParameter3,
IN ULONG_PTR BugCheckParameter4);
最常见的 SEH 相关 bugcheck:
| 错误码 | 含义 |
|---|---|
KMODE_EXCEPTION_NOT_HANDLED (0x1E) |
内核态异常未被处理 |
UNEXPECTED_KERNEL_MODE_TRAP (0x7F) |
意外的内核 trap(如双重错误) |
KERNEL_SECURITY_CHECK_FAILURE (0x139) |
内核态 /GS cookie 检查失败 |
IRQL_NOT_LESS_OR_EQUAL (0xA) |
在 DISPATCH_LEVEL 以上访问分页内存 |
TRAP_CAUSE_UNKNOWN (0x12) |
未知 trap 原因 |
KeBugCheckEx 会冻结所有 CPU、显示蓝屏、写入 dump 文件、调用 HAL 的 HalEndOfBoot 等终结操作。
8.2.9 平台无关 SEH:KiContinue / KiRaiseException / NtRaiseException / NtContinue
这些函数位于 ntoskrnl/ke/except.c(file:///d:/reactos/ntoskrnl/ke/except.c),是 平台无关 的高层 API,供 ntdll、驱动、内核代码使用。
KiContinue
定义于 except.c:42(file:///d:/reactos/ntoskrnl/ke/except.c#L42-L86):
c
NTSTATUS
NTAPI
KiContinue(IN PCONTEXT Context,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame)
{
NTSTATUS Status = STATUS_SUCCESS;
KIRQL OldIrql = APC_LEVEL;
KPROCESSOR_MODE PreviousMode = KeGetPreviousMode();
if (KeGetCurrentIrql() < APC_LEVEL) KeRaiseIrql(APC_LEVEL, &OldIrql);
_SEH2_TRY
{
if (PreviousMode != KernelMode)
{
KiContinuePreviousModeUser(Context, ExceptionFrame, TrapFrame);
}
else
{
KeContextToTrapFrame(Context, ExceptionFrame, TrapFrame,
Context->ContextFlags, KernelMode);
}
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
if (OldIrql < APC_LEVEL) KeLowerIrql(OldIrql);
return Status;
}
KiContinue 把用户提供的 CONTEXT 写回 trap frame。它通过 PSEH2 验证上下文(防止恶意上下文),并根据 PreviousMode 走不同路径。失败时返回 STATUS 错误码(不是抛出异常)。
KiRaiseException
定义于 except.c:90(file:///d:/reactos/ntoskrnl/ke/except.c#L88-L167):
c
NTSTATUS
NTAPI
KiRaiseException(_In_ PEXCEPTION_RECORD ExceptionRecord,
_In_ PCONTEXT Context,
_Out_ PKEXCEPTION_FRAME ExceptionFrame,
_Out_ PKTRAP_FRAME TrapFrame,
_In_ BOOLEAN SearchFrames)
KiRaiseException 是 软异常 的内核入口。它做以下工作:
- PreviousMode 检查 :如果调用者来自用户态,需要
ProbeForRead验证ExceptionRecord和Context都合法 - 拷贝到内核栈:避免在用户态异常处理过程中用户栈被修改
- 构造 trap frame :调用
KeContextToTrapFrame把 CONTEXT 写入 trap frame - 清除
KI_EXCEPTION_INTERNAL标志 :ExceptionRecord->ExceptionCode &= ~KI_EXCEPTION_INTERNAL; - 调用
KiDispatchException:把所有参数传过去
注意 SearchFrames 参数:当 TRUE 时表示继续搜索用户态 handler;FALSE 时表示只走内核态路径。
NtRaiseException
定义于 except.c:172(file:///d:/reactos/ntoskrnl/ke/except.c#L171-L212),是 KiRaiseException 的系统调用包装,由 ntdll 的 ZwRaiseException 调用。FirstChance 参数决定走 KiDispatchException 的 FirstChance=TRUE 还是 FALSE 路径。
NtContinue
定义于 except.c:216(file:///d:/reactos/ntoskrnl/ke/except.c#L214-L251),是 KiContinue 的系统调用包装:
c
NTSTATUS NTAPI
NtContinue(_In_ PCONTEXT Context, _In_ BOOLEAN TestAlert)
{
PKTHREAD Thread;
NTSTATUS Status;
PKTRAP_FRAME TrapFrame;
PKEXCEPTION_FRAME ExceptionFrame;
Thread = KeGetCurrentThread();
TrapFrame = Thread->TrapFrame;
Thread->TrapFrame = KiGetLinkedTrapFrame(TrapFrame);
Status = KiContinue(Context, ExceptionFrame, TrapFrame);
if (!NT_SUCCESS(Status)) return Status;
if (TestAlert) KeTestAlertThread(Thread->PreviousMode);
KiExceptionExit(TrapFrame, ExceptionFrame);
}
KiExceptionExit 是 不会返回 的,它 iret 到新上下文中。KiGetLinkedTrapFrame(TrapFrame) 用于把当前的 trap frame "链接" 到调用者之前的状态,形成 trap frame 链。
8.2.10 x64 与 ARM 实现差异对比
x64 实现(ntoskrnl/ke/amd64/)
x64 平台的 SEH 实现位于:
- ntoskrnl/ke/amd64/except.c(file:///d:/reactos/ntoskrnl/ke/amd64/except.c):平台无关 API
- ntoskrnl/ke/amd64/traphandler.c(file:///d:/reactos/ntoskrnl/ke/amd64/traphandler.c):trap handler C 部分
- ntoskrnl/ke/amd64/trap.S(file:///d:/reactos/ntoskrnl/ke/amd64/trap.S):trap 入口汇编
- ntoskrnl/ke/amd64/stubs.c(file:///d:/reactos/ntoskrnl/ke/amd64/stubs.c):辅助 stub
主要差异:
- 统一的
KTRAP_FRAME:x64 没有KEXCEPTION_FRAME这一层,所有异常数据都在一个KTRAP_FRAME中 - 段寄存器简化 :x64 大多数段寄存器无意义(CS/SS/FS/GS 保留),
KiDispatchException不再需要设置段寄存器 - 使用
KUSER_SHARED_DATA:x64 模式下用户态/内核态共享数据布局不同 - 更大的 CONTEXT :
CONTEXT包含 XSAVE 区域(约 1.5KB) - MSR 寄存器 :通过
LSTARMSR 控制系统调用入口
ARM 实现(ntoskrnl/ke/arm/)
ARM 平台的 SEH 实现位于:
- ntoskrnl/ke/arm/exp.c(file:///d:/reactos/ntoskrnl/ke/arm/exp.c):ARM 异常分派
- ntoskrnl/ke/arm/trapc.c(file:///d:/reactos/ntoskrnl/ke/arm/trapc.c):trap handler C 部分
- ntoskrnl/ke/arm/trap.s(file:///d:/reactos/ntoskrnl/ke/arm/trap.s):trap 入口汇编
主要差异:
- Vector table 模式:ARM 有"高向量"与"低向量"两种 trap 入口模式
- Banked 寄存器:ARM 有 USR/SYS/SVC/IRQ/FIQ 多种模式,每个模式有自己的 SP/LR
- 指令集差异:Thumb/ARM 模式切换会影响 Eip(PC)计算
虽然实现细节不同,但 算法框架 完全一致:
KiDispatchException函数签名相同- 内核态/用户态分支逻辑相同
- FirstChance/SecondChance/KeBugCheckEx 三阶段处理相同
- 异常分派依赖
EXCEPTION_RECORD/CONTEXT数据结构相同
这就是为什么 平台无关 的 except.c 可以被 x86/x64/ARM 共享------所有架构特定的工作都收敛在 KiDispatchException 和 KiContinue 中。
深入剖析:系统空间SEH的关键设计决策
KiDispatchException的异常码转换机制
在 KiDispatchException 中,某些异常码需要在分派前进行转换。这种转换反映了硬件异常与软件异常之间的语义差异。
KI_EXCEPTION_ACCESS_VIOLATION 的处理
ReactOS 内部使用 KI_EXCEPTION_ACCESS_VIOLATION 作为访问违例的内部表示,在分派前转换为标准的 STATUS_ACCESS_VIOLATION:
c
case KI_EXCEPTION_ACCESS_VIOLATION:
ExceptionRecord->ExceptionCode = STATUS_ACCESS_VIOLATION;
if (PreviousMode == UserMode)
{
// 检查是否涉及NX位(不可执行页)
// 这需要额外的处理逻辑
}
break;
这种内部/外部异常码分离的设计有几个好处:
- 延迟转换:在分派前才转换,允许内核代码使用内部码进行特殊处理
- 架构抽象:不同架构可能有不同的内部表示,但对外暴露统一的STATUS码
- 安全考虑:防止用户态代码直接触发内部异常处理路径
STATUS_BREAKPOINT的Eip调整
断点异常需要特殊的Eip回退处理,这源于x86 INT 3指令的特性。当CPU执行INT 3时,会将返回地址设置为INT 3之后的下一条指令。但对于调试器,我们希望在断点处重新执行(调试器会替换回原始字节),因此需要Eip--。
c
case STATUS_BREAKPOINT:
Context.Eip--; // 回退到INT 3指令处
break;
这个看似简单的操作背后有复杂的考量:
- 如果调试器没有处理断点,Eip应该指向INT 3之后,让程序继续执行
- 如果调试器处理了断点(替换了原始字节),Eip应该指向原始指令
- ReactOS选择总是Eip--,将决策权交给调试器
用户态分派的栈构造细节
KiDispatchException 在将异常分派到用户态时,需要在用户栈上构造一个特殊的调用框架。这个过程涉及多个安全检查:
栈对齐验证
c
Size = (sizeof(CONTEXT) + 3) & ~3; // 4字节对齐
Stack = (Context.Esp & ~3) - Size;
用户栈必须是4字节对齐的,这是x86 Windows ABI的要求。如果原始Esp不对齐,会导致后续的函数调用失败。
栈空间探测
c
ProbeForWrite((PVOID)Stack, Size, sizeof(ULONG));
在复制CONTEXT到用户栈之前,必须验证目标地址是可写的。这防止了恶意代码通过构造无效的Esp来攻击内核。
段寄存器验证
c
if ((TrapFrame->HardwareSegSs != (KGDT_R3_DATA | RPL_MASK)) ||
(TrapFrame->EFlags & EFLAGS_V86_MASK))
{
// 无效的SS或V86模式,抛出新的异常
LocalExceptRecord.ExceptionCode = STATUS_ACCESS_VIOLATION;
RtlRaiseException(&LocalExceptRecord);
}
这个检查确保异常来自合法的用户态代码段。如果SS不是用户态数据段,或者处于V86模式,说明可能是恶意代码试图伪造异常。
段寄存器的净化
c
TrapFrame->SegCs = Ke386SanitizeSeg(KGDT_R3_CODE, PreviousMode);
TrapFrame->SegDs = Ke386SanitizeSeg(KGDT_R3_DATA, PreviousMode);
TrapFrame->SegEs = Ke386SanitizeSeg(KGDT_R3_DATA, PreviousMode);
TrapFrame->SegFs = Ke386SanitizeSeg(KGDT_R3_TEB, PreviousMode);
TrapFrame->SegGs = 0;
在跳转到用户态异常处理器之前,必须确保所有段寄存器都是合法的用户态值。Ke386SanitizeSeg 会验证并净化段选择子,防止恶意代码通过段寄存器攻击内核。
KiDebugRoutine的调试器通信协议
KiDebugRoutine 是内核调试器(KD)的回调函数,它实现了内核调试器与异常分派之间的通信协议。
通信流程
- 异常通知 :
KiDebugRoutine将异常信息打包成KD数据包,通过串口/网络发送给调试器 - 等待响应:内核进入等待状态,直到调试器返回处理结果
- 处理决策 :调试器可以返回以下几种响应:
- 继续执行(Continue)
- 单步执行(Single Step)
- 修改上下文(Modify Context)
- 忽略异常(Ignore)
- 终止进程(Terminate)
SecondChance参数的语义
KiDebugRoutine 接收 SecondChance 参数来区分第一次和第二次机会:
SecondChance = FALSE:第一次机会,调试器可以尝试处理异常SecondChance = TRUE:第二次机会,调试器只能收集诊断信息
这种区分允许调试器在第一次机会时尝试修复问题(如修改寄存器值),在第二次机会时只能记录日志。
蓝屏决策的复杂性
KeBugCheckEx 的调用并不是简单的"三次失败就蓝屏",而是涉及复杂的决策逻辑。
内核模式蓝屏
c
KeBugCheckEx(KMODE_EXCEPTION_NOT_HANDLED,
ExceptionRecord->ExceptionCode,
(ULONG_PTR)ExceptionRecord->ExceptionAddress,
(ULONG_PTR)TrapFrame,
0);
内核模式蓝屏的参数包含:
- BugCheckCode:
KMODE_EXCEPTION_NOT_HANDLED - Parameter1:异常码
- Parameter2:异常地址
- Parameter3:TrapFrame指针(用于事后分析)
- Parameter4:保留
用户模式蓝屏
用户模式异常处理失败后,首先尝试终止进程:
c
ZwTerminateProcess(NtCurrentProcess(), ExceptionRecord->ExceptionCode);
KeBugCheckEx(KMODE_EXCEPTION_NOT_HANDLED, ...);
只有在进程终止失败时才会蓝屏。这是因为用户模式异常通常只影响单个进程,不应该导致整个系统崩溃。
蓝屏前的清理工作
KeBugCheckEx 在显示蓝屏之前会执行一系列清理工作:
- 冻结所有其他CPU(多处理器系统)
- 保存系统状态到dump文件
- 调用已注册的bugcheck回调
- 禁用中断和DMA
- 显示蓝屏信息
平台无关SEH的抽象层次
KiContinue 和 KiRaiseException 是平台无关的SEH API,它们屏蔽了x86/x64/ARM的架构差异。
KiContinue的抽象
c
NTSTATUS
NTAPI
KiContinue(IN PCONTEXT Context,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame)
这个函数签名在所有平台上都相同,但内部实现不同:
- x86:调用
KeContextToTrapFrame将CONTEXT转换为KTRAP_FRAME - x64:直接操作统一的KTRAP_FRAME(没有KEXCEPTION_FRAME)
- ARM:处理banked寄存器和模式切换
KiRaiseException的抽象
c
NTSTATUS
NTAPI
KiRaiseException(_In_ PEXCEPTION_RECORD ExceptionRecord,
_In_ PCONTEXT Context,
_Out_ PKEXCEPTION_FRAME ExceptionFrame,
_Out_ PKTRAP_FRAME TrapFrame,
_In_ BOOLEAN SearchFrames)
SearchFrames 参数控制是否搜索用户态handler链:
TRUE:继续搜索用户态handler(用于NtRaiseException)FALSE:只走内核态路径(用于内部异常)
这种抽象使得上层代码(如ntdll、驱动程序)不需要关心底层架构差异。
10问为什么:深入理解系统空间SEH
问题 1:为什么KiDispatchException要先调用KeTrapFrameToContext,而不是直接操作TrapFrame?
答 :这是为了架构抽象 和安全性。
KTRAP_FRAME 是架构特定的数据结构,不同平台的布局完全不同:
- x86:包含段寄存器、调试寄存器、V86字段等
- x64:没有段寄存器,但有更大的浮点保存区
- ARM:使用banked寄存器,布局完全不同
如果让handler直接操作TrapFrame,就需要为每个平台编写不同的handler代码。通过转换为CONTEXT,handler可以使用统一的接口访问寄存器状态。
安全性考虑 :KeTrapFrameToContext 在转换过程中会验证和净化数据,防止恶意TrapFrame影响handler。例如,它会检查段选择子是否合法,EFlags是否包含无效位等。
性能权衡:虽然转换有开销,但异常处理本身是低频操作,这个开销可以接受。而且转换只在分派前进行一次,不会在handler链中重复。
问题 2:为什么内核模式分支要先调用KiDebugRoutine,再调用RtlDispatchException?
答 :这是为了调试器优先 和内核稳定性。
内核调试器(KD)是系统级的调试工具,它需要在任何内核handler之前看到异常。原因:
- 全局视角:调试器可以看到所有内核异常,而内核handler只能看到自己注册的异常
- 修复能力:调试器可以修改寄存器值、内存内容,尝试修复问题
- 诊断信息:即使无法修复,调试器也可以收集调用栈、变量值等诊断信息
如果先调用RtlDispatchException,可能会遇到以下问题:
- 内核handler可能没有正确处理异常,导致异常传播
- 内核handler本身可能触发新的异常(嵌套异常)
- 调试器可能错过关键的异常现场
例外情况 :如果异常是由调试器自己触发的(如断点),KdIsThisAKdTrap 会检测到,并直接传递给调试器,不经过RtlDispatchException。
问题 3:为什么用户模式分支要构造用户栈,而不是直接调用用户态handler?
答 :这是由内核态和用户态的隔离决定的。
内核运行在Ring 0,用户态运行在Ring 3。内核不能直接调用用户态函数,因为:
- 权限级别不同:内核代码不能直接在用户态执行
- 地址空间不同:内核栈和用户栈在不同的地址范围
- 安全边界:直接调用会破坏安全边界,允许用户态代码攻击内核
通过构造用户栈并修改TrapFrame,KiDispatchException 实现了"欺骗"CPU的效果:
- 当iret执行时,CPU会自动切换到用户态
- EIP指向
KeUserExceptionDispatcher,这是用户态代码 - ESP指向构造的用户栈,包含异常参数
- 段寄存器被设置为合法的用户态值
这种方式既保持了安全边界,又实现了异常分派。
问题 4:为什么用户模式分支要用_SEH2_TRY保护栈构造代码?
答 :这是为了处理栈溢出的特殊情况。
在构造用户栈时,代码需要向用户栈写入CONTEXT和EXCEPTION_RECORD。如果用户栈已经溢出(例如递归太深),ProbeForWrite 会触发访问违例。
c
_SEH2_TRY
{
ProbeForWrite((PVOID)Stack, Size, sizeof(ULONG));
RtlCopyMemory((PVOID)Stack, &Context, sizeof(CONTEXT));
// ...
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
if ((NTSTATUS)LocalExceptRecord.ExceptionCode == STATUS_STACK_OVERFLOW)
{
// 重新尝试
goto DispatchToUser;
}
}
这个SEH保护实现了以下逻辑:
- 如果栈探测失败,捕获异常
- 检查是否是栈溢出(
STATUS_STACK_OVERFLOW) - 如果是,重新尝试(因为已经调整了栈布局)
- 如果不是,继续传播异常
这种自举式的异常处理确保了即使栈溢出,也能正确分派异常。
问题 5:为什么KiDispatchException要区分FirstChance和SecondChance?
答 :这是为了实现多级异常处理机会 和调试器协作。
FirstChance(第一次机会):
- 异常刚发生,现场完整
- 调试器可以尝试修复问题
- 用户handler可以尝试处理
- 如果处理成功,程序可以继续执行
SecondChance(第二次机会):
- FirstChance处理失败
- 异常不可恢复
- 调试器只能收集诊断信息
- 最终会导致进程终止或蓝屏
这种区分的好处:
- 修复机会:某些异常是可以修复的(如缺页异常),FirstChance允许修复
- 调试效率:调试器可以在FirstChance时设置断点,在SecondChance时收集dump
- 性能优化:SecondChance路径可以跳过某些耗时的处理
实际例子:
- 调试器在FirstChance时修改变量值,让程序继续执行
- 用户handler在FirstChance时分配缺失的内存,让程序继续
- 如果都失败,SecondChance时调试器记录调用栈,然后终止进程
问题 6:为什么DbgkForwardException要调用两次(FirstChance和SecondChance)?
答 :这是为了用户态调试器的协作。
用户态调试器(如Visual Studio、WinDbg)通过DbgkForwardException接收异常通知。两次调用的语义不同:
第一次调用:DbgkForwardException(ExceptionRecord, TRUE, FALSE)
FirstChance = TRUE:通知调试器这是第一次机会SecondChance = FALSE:不是第二次机会- 调试器可以尝试处理异常(修改寄存器、继续执行)
第二次调用:DbgkForwardException(ExceptionRecord, TRUE, TRUE)
FirstChance = TRUE:仍然是第一次机会(从内核角度)SecondChance = TRUE:但这是调试器的第二次机会- 调试器只能收集信息,不能修复
第三次调用:DbgkForwardException(ExceptionRecord, FALSE, TRUE)
FirstChance = FALSE:不是第一次机会SecondChance = TRUE:第二次机会- 备用路径,用于特殊调试器
这种复杂的调用模式反映了Windows调试架构的历史演进,需要兼容不同版本的调试器。
问题 7:为什么KeUserExceptionDispatcher要放在ntdll中,而不是ntoskrnl中?
答 :这是由用户态和内核态的分离决定的。
KeUserExceptionDispatcher 是用户态代码,它需要:
- 在Ring 3执行
- 访问用户态栈和内存
- 调用
RtlDispatchException(用户态RTL函数)
如果放在ntoskrnl中:
- 内核代码不能直接在用户态执行
- 需要额外的模式切换开销
- 会破坏安全边界
ntdll的角色 :
ntdll是用户态和内核态的桥梁,它包含:
- 系统调用包装(如
NtCreateFile) - 用户态RTL函数(如
RtlDispatchException) - 异常分派入口(如
KeUserExceptionDispatcher)
当KiDispatchException修改TrapFrame,将EIP设置为KeUserExceptionDispatcher时,iret会自动跳转到ntdll中的这个函数,开始用户态异常处理。
问题 8:为什么KiContinue要用_SEH2_TRY保护KeContextToTrapFrame调用?
答 :这是为了防止恶意CONTEXT攻击内核。
KiContinue 接收用户提供的CONTEXT,并将其写回TrapFrame。如果CONTEXT包含无效值(如非法的段选择子、无效的EFlags),KeContextToTrapFrame 可能会触发异常。
c
_SEH2_TRY
{
if (PreviousMode != KernelMode)
{
KiContinuePreviousModeUser(Context, ExceptionFrame, TrapFrame);
}
else
{
KeContextToTrapFrame(Context, ExceptionFrame, TrapFrame,
Context->ContextFlags, KernelMode);
}
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
这个SEH保护实现了以下安全机制:
- 验证CONTEXT :
KeContextToTrapFrame会检查段选择子、EFlags等 - 捕获异常:如果验证失败,触发异常
- 返回错误码 :不蓝屏,而是返回
STATUS_ACCESS_VIOLATION等错误
这种设计允许用户态调试器提供无效的CONTEXT,而不会导致系统崩溃。
问题 9:为什么KiRaiseException要清除KI_EXCEPTION_INTERNAL标志?
答 :这是为了隔离内部异常和外部异常。
ReactOS内部使用KI_EXCEPTION_INTERNAL标志来标记内部异常(如KI_EXCEPTION_ACCESS_VIOLATION)。这些内部异常不应该暴露给用户态代码。
c
ExceptionRecord->ExceptionCode &= ~KI_EXCEPTION_INTERNAL;
清除这个标志的原因:
- 安全考虑:防止用户态代码利用内部异常码攻击系统
- 兼容性:用户态代码期望标准的STATUS码,而不是内部码
- 语义清晰:内部码和外部码有不同的语义,需要转换
例子:
- 内部:
KI_EXCEPTION_ACCESS_VIOLATION(内核使用) - 外部:
STATUS_ACCESS_VIOLATION(用户态看到)
在KiDispatchException中,内部码会被转换为外部码,然后再分派给用户态handler。
问题 10:为什么x64和ARM的KiDispatchException与x86的实现不同,但算法框架相同?
答 :这是为了架构抽象 和代码复用。
架构差异:
- x86:使用段寄存器、KEXCEPTION_FRAME、复杂的TrapFrame
- x64:没有段寄存器、统一的TrapFrame、更大的CONTEXT
- ARM:使用banked寄存器、不同的异常模型
算法框架相同:
- 都有
KiDispatchException函数,签名相同 - 都有内核态/用户态分支
- 都有FirstChance/SecondChance/蓝屏三阶段
- 都依赖
EXCEPTION_RECORD和CONTEXT数据结构
实现不同的原因:
- 寄存器保存:不同架构的寄存器集合不同
- 模式切换:x86用段寄存器,x64用MSR,ARM用模式位
- 栈布局:不同架构的栈对齐要求不同
代码复用的实现:
- 平台无关代码放在
ntoskrnl/ke/except.c - 架构特定代码放在
ntoskrnl/ke/{i386,amd64,arm}/ - 通过函数指针和条件编译实现抽象
这种设计使得ReactOS可以在多个架构上运行,同时保持SEH语义的一致性。
总结
8.2 节建立了系统空间 SEH 的完整视图。关键要点:
- IDT 入口 :
KiTrap00-1321 个汇编入口桩,通过TRAP_ENTRY宏展开为KiEnterTrap通用代码 - Trap Frame :
KTRAP_FRAME是硬件上下文的内核表示,由KiEnterTrap构造 - 包装函数 :
KiDispatchException0/1/2Args和KiDispatchExceptionFromTrapFrame简化 trap handler 样板代码 - 核心枢纽 :
KiDispatchException是 SEH 框架的中央调度器:- 内核态 :FirstChance → 调试器 +
RtlDispatchException→ SecondChance → 调试器 → 蓝屏 - 用户态 :FirstChance → 调试器 + DbgkForwardException → 构造用户栈 + 跳转
KeUserExceptionDispatcher→ SecondChance → 进程终止
- 内核态 :FirstChance → 调试器 +
- 软异常入口 :
KiDispatchExceptionFromTrapFrame是所有软异常(断点、断言、栈溢出等)的统一入口 - 平台无关 API :
KiContinue/KiRaiseException/NtContinue/NtRaiseException屏蔽了架构差异 - CONTEXT ↔ TrapFrame 转换 :
KeTrapFrameToContext与KeContextToTrapFrame是 handler 看到 CPU 状态的关键 - 跨架构一致 :x86/x64/ARM 实现都收敛到
KiDispatchException与KiContinue,算法框架完全一致
8.3 节将进入 用户空间 SEH,剖析 RtlDispatchException / RtlUnwind 如何遍历 EXCEPTION_REGISTRATION_RECORD 链表、调用 handler、并处理 EXCEPTION_DISPOSITION 的四个返回值。
本章代码索引
| 文件 | 内容 |
|---|---|
| trap.s(file:///d:/reactos/ntoskrnl/ke/i386/trap.s) | x86 IDT 入口桩、TRAP_ENTRY 宏、KiEnterTrap、KiCommonExit |
| traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) | x86 trap handler 家族(KiTrap00-13、KiDebugHandler、KiNpxHandler、KiDebugServiceHandler 等) |
| exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c) | x86 异常分派核心(KiDispatchException、KiDispatchExceptionFromTrapFrame、KeContextToTrapFrame、KeTrapFrameToContext、KiDispatchException0/1/2Args) |
| except.c(file:///d:/reactos/ntoskrnl/ke/except.c) | 平台无关 SEH(KiContinue、KiRaiseException、NtRaiseException、NtContinue) |
| context.c(file:///d:/reactos/ntoskrnl/ke/i386/context.c) | KeContextToTrapFrame、KeTrapFrameToContext 实现 |
| traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) | KiDebugServiceHandler(int 2D)、KiRaiseAssertionHandler(int 2C)、KiRaiseSecurityCheckFailureHandler(int 29) |
| bug.c(file:///d:/reactos/ntoskrnl/ke/bug.c) | KeBugCheckEx、KeBugCheckWithTf 实现 |
| ntosifs.h(file:///d:/reactos/sdk/include/ntosifs.h) | ExRaiseStatus、ExRaiseAccessViolation、ExRaiseDatatypeMisalignment 等宏 |
| probe.h(file:///d:/reactos/sdk/include/reactos/probe.h) | ProbeForRead、ProbeForWrite 宏 |
| pseh2.h(file:///d:/reactos/sdk/include/vcruntime/pseh/pseh2.h) | PSEH2 宏(_SEH2_TRY 等) |
| amd64/except.c(file:///d:/reactos/ntoskrnl/ke/amd64/except.c) | x64 平台无关 SEH |
| amd64/traphandler.c(file:///d:/reactos/ntoskrnl/ke/amd64/traphandler.c) | x64 trap handler |
| amd64/trap.S(file:///d:/reactos/ntoskrnl/ke/amd64/trap.S) | x64 trap 入口 |
| arm/exp.c(file:///d:/reactos/ntoskrnl/ke/arm/exp.c) | ARM 异常分派 |
| arm/trapc.c(file:///d:/reactos/ntoskrnl/ke/arm/trapc.c) | ARM trap handler |
| arm/trap.s(file:///d:/reactos/ntoskrnl/ke/arm/trap.s) | ARM trap 入口 |
| ke.h(file:///d:/reactos/sdk/include/ntoskrnl/ke.h) | KeGetTrapFrame、KeExceptionExit 等声明 |
| psdk/ntstatus.h(file:///d:/reactos/sdk/include/psdk/ntstatus.h) | 所有 STATUS_* 异常码 |