Reactos 第 8 章 结构化异常处理 — 8.2 系统空间的结构化异常处理

第 8 章 结构化异常处理 --- 8.2 系统空间的结构化异常处理

本节深入剖析系统空间(内核态 Ring 0)下 ReactOS 的结构化异常处理实现。 系统空间的 SEH 是整个 SEH 框架的"上游"------所有硬件 trap、所有用户态抛出的异常、所有驱动程序的 SEH,最终都要汇入 KiDispatchException 这一个核心函数。理解 KiDispatchException 的两个分支(内核模式 vs 用户模式)、三阶段机会(FirstChance → SecondChance → BlueScreen)、以及与 KeUserExceptionDispatcher 的协作,是读懂 Windows NT 内核异常分派的关键。


概述

系统空间的 SEH 由 三个层次 协作完成:

  1. 硬件入口层 :x86 IDT(Interrupt Descriptor Table)将每个异常向量指向 KiTrap00 ~ KiTrap13 等汇编入口桩。
  2. Trap 处理层 :每个 trap 桩保存现场(KiEnterTrap)、启用中断、调用 KiDispatchException0Args/1Args/2Args 包装函数。
  3. 异常分派层KiDispatchException 是内核态 SEH 的中央枢纽,它根据 PreviousModeFirstChance 决定走向内核态 handler 链、用户态分派器、调试器还是蓝屏。

这三层共同回答了三个核心问题:

  • 异常 从哪里来?(硬件 trap / 软件 API)
  • 异常 归谁处理 ?(内核态的 RtlDispatchException / 用户态的 KeUserExceptionDispatcher / 调试器)
  • 异常 处理失败怎么办?(重试、二次机会、蓝屏)

本节内容概览

  • 8.2.0 框架图:内核态 SEH 完整数据流
  • 8.2.1 IDT 与 trap 入口桩
  • 8.2.2 KTRAP_FRAMEKEXCEPTION_FRAME 结构
  • 8.2.3 Trap handler 家族(KiTrap00-13
  • 8.2.4 包装宏家族:KiDispatchException0/1/2ArgsKiDispatchExceptionFromTrapFrame
  • 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) KiDispatchExceptionKiDispatchExceptionFromTrapFrame
平台无关 SEH API ntoskrnl/ke/except.c(file:///d:/reactos/ntoskrnl/ke/except.c) KiContinueKiRaiseExceptionNtContinueNtRaiseException
软中断服务 ntoskrnl/ke/i386/traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) KiDebugServiceHandlerKiRaiseAssertionHandler
调试器回调 ntoskrnl/ke/i386/traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) KiDebugHandlerKiDebugRoutine
架构特定 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) 中定义,其核心任务是:

  1. 在栈上压入一个 伪错误码(如果 CPU 没自动压)
  2. 跳转到公共的 KiEnterTrap 代码
  3. KiEnterTrap 把所有寄存器保存到一个 KTRAP_FRAME 结构中
  4. 调用对应的 C 函数 KiTrap0XHandler

8.2.2 KTRAP_FRAMEKEXCEPTION_FRAME 结构

x86 平台上,当 trap 发生时,CPU 会自动把 EIPCSEFLAGSESPSS 压栈,再由 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_FRAMECONTEXT 的转换由 exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c) 中的两个函数完成:

  • KeTrapFrameToContext(TrapFrame, ExceptionFrame, &Context):把 trap frame 转换为 CONTEXT
  • KeContextToTrapFrame(&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 的工作流都是:

  1. KiEnterTrap(TrapFrame):保存硬件上下文到 KTRAP_FRAME
  2. 检查 VDM 模式(虚拟 86 模式,遗留 16 位应用)
  3. _enable() 启用中断(内核态 trap 期间中断默认关闭)
  4. 调用 KiDispatchExceptionNArgs 系列包装函数

下表列出每个 trap 的目标 STATUS 码:

Trap 来源 Eip 调整 状态码
KiTrap00 除零异常 0 STATUS_INTEGER_DIVIDE_BY_ZERO
KiTrap01 调试异常(单步) 0 STATUS_SINGLE_STEP
KiTrap02 NMI - KiHandleNmi → 蓝屏
KiTrap03 断点(int 3) -1 KiDebugHandlerSTATUS_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 MmAccessFaultSTATUS_ACCESS_VIOLATION / STATUS_GUARD_PAGE_VIOLATION / STATUS_STACK_OVERFLOW / STATUS_IN_PAGE_ERROR
KiTrap10 x87 FPU 错误 0 KiNpxHandlerSTATUS_FLOAT_*
KiTrap11 对齐检查 0 KiSystemFatalException
KiTrap13 SIMD 异常 0 STATUS_FLOAT_MULTIPLE_TRAPS

例如 KiTrap0EHandler(页错误)会先调用内存管理器 MmAccessFault 尝试解决缺页,只有当 MmAccessFault 返回失败时才把异常转交给 SEH 框架。


8.2.4 包装宏家族:KiDispatchException0/1/2ArgsKiDispatchExceptionFromTrapFrame

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_BREAKPOINTSTATUS_ASSERTION_FAILURESTATUS_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);
}

内核态分派只有 三阶段

  1. FirstChance(第一次机会)
    • 调用 KiDebugRoutine 给内核调试器一次处理机会
    • 如果调试器不处理,调用 RtlDispatchException 走内核的 EXCEPTION_REGISTRATION_RECORD 链表
  2. SecondChance(第二次机会)
    • 再次调用 KiDebugRoutine 给调试器最后机会
  3. 三次失败 → 蓝屏
    • 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, ...);
}

用户模式分派的 关键路径

  1. 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 走用户态链表
  2. SecondChance
    • 再次 DbgkForwardException(..., SecondChance=TRUE)
  3. 三次失败 → 进程终止
    • ZwTerminateProcess(NtCurrentProcess(), ExceptionCode) 杀掉当前进程
    • 备用:调用 KeBugCheckEx

注意代码中的 DispatchToUser 标签:这是处理 栈溢出 的特殊机制。如果在复制 CONTEXT/EXCEPTION_RECORD 到用户栈时探测栈失败(_SEH2_EXCEPT 触发),且异常码是 STATUS_STACK_OVERFLOW,则重新尝试(因为已经换了一个栈布局)。

8.2.5.3 关键辅助函数

  • KiUserTrap(TrapFrame):通过 SegCs 判断是否是用户态 trap
  • KiIsFrameEdited(TrapFrame):检查 SegCs & FRAME_EDITED
  • KiUserTrapDispatcher / KeUserExceptionDispatcher:ntdll 中提供的用户态入口
  • KeTrapFrameToContext / KeContextToTrapFrame:双向转换

8.2.6 用户态分派路径:KeUserExceptionDispatcher 跳转

KeUserExceptionDispatcherntdll.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);

它把异常发送给内核调试器(通过 KdpTrapKdSendPacket 等),等待调试器响应(继续/单步/处理/忽略)。返回 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 在以下情况调用它:

  1. 内核模式:第三次机会仍失败
  2. 用户模式:第三次机会仍失败,且 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软异常 的内核入口。它做以下工作:

  1. PreviousMode 检查 :如果调用者来自用户态,需要 ProbeForRead 验证 ExceptionRecordContext 都合法
  2. 拷贝到内核栈:避免在用户态异常处理过程中用户栈被修改
  3. 构造 trap frame :调用 KeContextToTrapFrame 把 CONTEXT 写入 trap frame
  4. 清除 KI_EXCEPTION_INTERNAL 标志ExceptionRecord->ExceptionCode &= ~KI_EXCEPTION_INTERNAL;
  5. 调用 KiDispatchException:把所有参数传过去

注意 SearchFrames 参数:当 TRUE 时表示继续搜索用户态 handler;FALSE 时表示只走内核态路径。

NtRaiseException

定义于 except.c:172(file:///d:/reactos/ntoskrnl/ke/except.c#L171-L212),是 KiRaiseException 的系统调用包装,由 ntdll 的 ZwRaiseException 调用。FirstChance 参数决定走 KiDispatchExceptionFirstChance=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

主要差异:

  1. 统一的 KTRAP_FRAME :x64 没有 KEXCEPTION_FRAME 这一层,所有异常数据都在一个 KTRAP_FRAME
  2. 段寄存器简化 :x64 大多数段寄存器无意义(CS/SS/FS/GS 保留),KiDispatchException 不再需要设置段寄存器
  3. 使用 KUSER_SHARED_DATA:x64 模式下用户态/内核态共享数据布局不同
  4. 更大的 CONTEXTCONTEXT 包含 XSAVE 区域(约 1.5KB)
  5. MSR 寄存器 :通过 LSTAR MSR 控制系统调用入口

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 入口汇编

主要差异:

  1. Vector table 模式:ARM 有"高向量"与"低向量"两种 trap 入口模式
  2. Banked 寄存器:ARM 有 USR/SYS/SVC/IRQ/FIQ 多种模式,每个模式有自己的 SP/LR
  3. 指令集差异:Thumb/ARM 模式切换会影响 Eip(PC)计算

虽然实现细节不同,但 算法框架 完全一致:

  • KiDispatchException 函数签名相同
  • 内核态/用户态分支逻辑相同
  • FirstChance/SecondChance/KeBugCheckEx 三阶段处理相同
  • 异常分派依赖 EXCEPTION_RECORD / CONTEXT 数据结构相同

这就是为什么 平台无关except.c 可以被 x86/x64/ARM 共享------所有架构特定的工作都收敛在 KiDispatchExceptionKiContinue 中。


深入剖析:系统空间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;

这种内部/外部异常码分离的设计有几个好处:

  1. 延迟转换:在分派前才转换,允许内核代码使用内部码进行特殊处理
  2. 架构抽象:不同架构可能有不同的内部表示,但对外暴露统一的STATUS码
  3. 安全考虑:防止用户态代码直接触发内部异常处理路径

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)的回调函数,它实现了内核调试器与异常分派之间的通信协议。

通信流程

  1. 异常通知KiDebugRoutine 将异常信息打包成KD数据包,通过串口/网络发送给调试器
  2. 等待响应:内核进入等待状态,直到调试器返回处理结果
  3. 处理决策 :调试器可以返回以下几种响应:
    • 继续执行(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 在显示蓝屏之前会执行一系列清理工作:

  1. 冻结所有其他CPU(多处理器系统)
  2. 保存系统状态到dump文件
  3. 调用已注册的bugcheck回调
  4. 禁用中断和DMA
  5. 显示蓝屏信息

平台无关SEH的抽象层次

KiContinueKiRaiseException 是平台无关的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之前看到异常。原因:

  1. 全局视角:调试器可以看到所有内核异常,而内核handler只能看到自己注册的异常
  2. 修复能力:调试器可以修改寄存器值、内存内容,尝试修复问题
  3. 诊断信息:即使无法修复,调试器也可以收集调用栈、变量值等诊断信息

如果先调用RtlDispatchException,可能会遇到以下问题:

  • 内核handler可能没有正确处理异常,导致异常传播
  • 内核handler本身可能触发新的异常(嵌套异常)
  • 调试器可能错过关键的异常现场

例外情况 :如果异常是由调试器自己触发的(如断点),KdIsThisAKdTrap 会检测到,并直接传递给调试器,不经过RtlDispatchException。


问题 3:为什么用户模式分支要构造用户栈,而不是直接调用用户态handler?

:这是由内核态和用户态的隔离决定的。

内核运行在Ring 0,用户态运行在Ring 3。内核不能直接调用用户态函数,因为:

  1. 权限级别不同:内核代码不能直接在用户态执行
  2. 地址空间不同:内核栈和用户栈在不同的地址范围
  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保护实现了以下逻辑:

  1. 如果栈探测失败,捕获异常
  2. 检查是否是栈溢出(STATUS_STACK_OVERFLOW
  3. 如果是,重新尝试(因为已经调整了栈布局)
  4. 如果不是,继续传播异常

这种自举式的异常处理确保了即使栈溢出,也能正确分派异常。


问题 5:为什么KiDispatchException要区分FirstChance和SecondChance?

:这是为了实现多级异常处理机会调试器协作

FirstChance(第一次机会)

  • 异常刚发生,现场完整
  • 调试器可以尝试修复问题
  • 用户handler可以尝试处理
  • 如果处理成功,程序可以继续执行

SecondChance(第二次机会)

  • FirstChance处理失败
  • 异常不可恢复
  • 调试器只能收集诊断信息
  • 最终会导致进程终止或蓝屏

这种区分的好处:

  1. 修复机会:某些异常是可以修复的(如缺页异常),FirstChance允许修复
  2. 调试效率:调试器可以在FirstChance时设置断点,在SecondChance时收集dump
  3. 性能优化: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 是用户态代码,它需要:

  1. 在Ring 3执行
  2. 访问用户态栈和内存
  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保护实现了以下安全机制:

  1. 验证CONTEXTKeContextToTrapFrame 会检查段选择子、EFlags等
  2. 捕获异常:如果验证失败,触发异常
  3. 返回错误码 :不蓝屏,而是返回STATUS_ACCESS_VIOLATION等错误

这种设计允许用户态调试器提供无效的CONTEXT,而不会导致系统崩溃。


问题 9:为什么KiRaiseException要清除KI_EXCEPTION_INTERNAL标志?

:这是为了隔离内部异常和外部异常

ReactOS内部使用KI_EXCEPTION_INTERNAL标志来标记内部异常(如KI_EXCEPTION_ACCESS_VIOLATION)。这些内部异常不应该暴露给用户态代码。

c 复制代码
ExceptionRecord->ExceptionCode &= ~KI_EXCEPTION_INTERNAL;

清除这个标志的原因:

  1. 安全考虑:防止用户态代码利用内部异常码攻击系统
  2. 兼容性:用户态代码期望标准的STATUS码,而不是内部码
  3. 语义清晰:内部码和外部码有不同的语义,需要转换

例子

  • 内部: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_RECORDCONTEXT数据结构

实现不同的原因

  1. 寄存器保存:不同架构的寄存器集合不同
  2. 模式切换:x86用段寄存器,x64用MSR,ARM用模式位
  3. 栈布局:不同架构的栈对齐要求不同

代码复用的实现

  • 平台无关代码放在ntoskrnl/ke/except.c
  • 架构特定代码放在ntoskrnl/ke/{i386,amd64,arm}/
  • 通过函数指针和条件编译实现抽象

这种设计使得ReactOS可以在多个架构上运行,同时保持SEH语义的一致性。


总结

8.2 节建立了系统空间 SEH 的完整视图。关键要点:

  1. IDT 入口KiTrap00-13 21 个汇编入口桩,通过 TRAP_ENTRY 宏展开为 KiEnterTrap 通用代码
  2. Trap FrameKTRAP_FRAME 是硬件上下文的内核表示,由 KiEnterTrap 构造
  3. 包装函数KiDispatchException0/1/2ArgsKiDispatchExceptionFromTrapFrame 简化 trap handler 样板代码
  4. 核心枢纽KiDispatchException 是 SEH 框架的中央调度器:
    • 内核态 :FirstChance → 调试器 + RtlDispatchException → SecondChance → 调试器 → 蓝屏
    • 用户态 :FirstChance → 调试器 + DbgkForwardException → 构造用户栈 + 跳转 KeUserExceptionDispatcher → SecondChance → 进程终止
  5. 软异常入口KiDispatchExceptionFromTrapFrame 是所有软异常(断点、断言、栈溢出等)的统一入口
  6. 平台无关 APIKiContinue / KiRaiseException / NtContinue / NtRaiseException 屏蔽了架构差异
  7. CONTEXT ↔ TrapFrame 转换KeTrapFrameToContextKeContextToTrapFrame 是 handler 看到 CPU 状态的关键
  8. 跨架构一致 :x86/x64/ARM 实现都收敛到 KiDispatchExceptionKiContinue,算法框架完全一致

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 宏、KiEnterTrapKiCommonExit
traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) x86 trap handler 家族(KiTrap00-13KiDebugHandlerKiNpxHandlerKiDebugServiceHandler 等)
exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c) x86 异常分派核心(KiDispatchExceptionKiDispatchExceptionFromTrapFrameKeContextToTrapFrameKeTrapFrameToContextKiDispatchException0/1/2Args
except.c(file:///d:/reactos/ntoskrnl/ke/except.c) 平台无关 SEH(KiContinueKiRaiseExceptionNtRaiseExceptionNtContinue
context.c(file:///d:/reactos/ntoskrnl/ke/i386/context.c) KeContextToTrapFrameKeTrapFrameToContext 实现
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) KeBugCheckExKeBugCheckWithTf 实现
ntosifs.h(file:///d:/reactos/sdk/include/ntosifs.h) ExRaiseStatusExRaiseAccessViolationExRaiseDatatypeMisalignment 等宏
probe.h(file:///d:/reactos/sdk/include/reactos/probe.h) ProbeForReadProbeForWrite
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) KeGetTrapFrameKeExceptionExit 等声明
psdk/ntstatus.h(file:///d:/reactos/sdk/include/psdk/ntstatus.h) 所有 STATUS_* 异常码
相关推荐
caimouse2 小时前
Reactos 第 7 章 视窗报文 — 7.3 Win32k 的用户空间回调机制
windows
caimouse2 小时前
Reactos 第 9 章 设备驱动 — 9.5 一组PnP设备驱动模块的实例
网络·windows
神成12 小时前
vmware 上 win7 系统按照 vmware tool
windows
虾壳云官方3 小时前
OpenClaw 2.7.9 Windows 一键部署教程:零基础也能搭建 AI 自动化助手
运维·人工智能·windows·自动化·openclaw·openclaw一键部署
xcLeigh5 小时前
鸿蒙平台 KeePass 密码管理器适配实战:从 Windows 到 鸿蒙PC 的 Electron 迁移指南
windows·electron·web·harmonyos·加密算法·keepass
caimouse7 小时前
Reactos 第 9 章 设备驱动 — 9.1 Windows的设备驱动框架
windows
宸丶一8 小时前
Day 10:LangGraph - Agent 的图执行引擎
java·windows·python
ylscode8 小时前
GreatXML BitLocker绕过漏洞深度解析:Windows Defender离线扫描如何被改造成本地提权后门
windows·安全
caimouse9 小时前
Reactos 第 8 章 结构化异常处理 — 8.1 结构化异常处理的程序框架
windows