Reactos 第 8 章 结构化异常处理 — 8.4 软异常

第 8 章 结构化异常处理 --- 8.4 软异常

本节深入剖析软异常(Software Exception)的生成、传播与处理。 与硬件异常(Hardware Exception)由 CPU 在指令执行期间自动触发不同,软异常完全由 软件(API、指令、断言、调试器) 主动生成。本节回答三个核心问题:什么是软异常?它如何与硬件异常统一到同一套 SEH 框架?常见的软异常码有哪些、它们各自代表什么语义?


概述

软异常是与 硬件异常 对应的一类异常,它们的本质区别在于 触发源

  • 硬件异常:CPU 在执行某条指令时检测到错误条件(除零、访问违例、断点等),自动触发异常
  • 软异常:由软件主动调用 API、嵌入 int 3/int 2D 指令、断言宏或调试器而触发的异常

虽然触发源不同,ReactOS 内核却把它们 统一 到同一个 KiDispatchException 入口(见 8.2 节)------这是 SEH 框架设计的重要优势。理解这种统一性的关键,是把握 软异常的"统一入口"KiDispatchExceptionFromTrapFrame

软异常的几个 主要来源

  1. 调试器断点int 3STATUS_BREAKPOINT)、int 2DSTATUS_BREAKPOINT)、int 1STATUS_SINGLE_STEP
  2. API 主动抛出RtlRaiseExceptionRtlRaiseStatusExRaiseStatus
  3. 断言失败NT_ASSERTRtlAssertASSERT
  4. C++ 异常throw(底层调用 _CxxThrowException
  5. 栈检查失败STATUS_STACK_BUFFER_OVERRUN/GS 保护)
  6. STATUS 转换:内核函数检查到错误状态后抛出

本节内容概览

  • 8.4.0 框架图:软异常的来源与流向
  • 8.4.1 软异常定义与分类
  • 8.4.2 软异常统一入口:KiDispatchExceptionFromTrapFrame
  • 8.4.3 常见软异常码(按 NTSTATUS 分类)
  • 8.4.4 RtlRaiseException / RtlRaiseStatus 软异常抛出
  • 8.4.5 调试器软中断(int 3、int 2D、int 1)
  • 8.4.6 断言触发的软异常
  • 8.4.7 C++ 异常与 SEH 的统一
  • 8.4.8 STATUS 与异常码的对应体系
  • 8.4.9 软异常传递流程详解

学习目标

  • 能够列出 5 种以上软异常的来源
  • 理解 KiDispatchExceptionFromTrapFrame 在软异常中的作用
  • 能够区分 STATUS_BREAKPOINTSTATUS_SINGLE_STEPSTATUS_ASSERTION_FAILURE 等常见软异常码
  • 了解 RtlRaiseException / RtlRaiseStatus 的差异与典型用途
  • 理解 NT_ASSERT / RtlAssert 宏的工作机制
  • 了解 SEH 与 C++ 异常的统一(/EHa

涉及的内核子系统

子系统 头文件/源文件 核心作用
软异常统一入口 ntoskrnl/ke/i386/exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c) KiDispatchExceptionFromTrapFrame
软异常 trap handler ntoskrnl/ke/i386/traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) KiDebugServiceHandler(int 2D)、KiRaiseAssertionHandler(int 2C)、KiRaiseSecurityCheckFailureHandler(int 29)
用户态抛出 API sdk/lib/rtl/exception.c(file:///d:/reactos/sdk/lib/rtl/exception.c) RtlRaiseExceptionRtlRaiseStatus
断言实现 sdk/lib/rtl/assert.c(file:///d:/reactos/sdk/lib/rtl/assert.c) RtlAssert
内核抛出 API sdk/include/ntosifs.h(file:///d:/reactos/sdk/include/ntosifs.h) ExRaiseStatusExRaiseAccessViolation
STATUS 码定义 sdk/include/psdk/ntstatus.h(file:///d:/reactos/sdk/include/psdk/ntstatus.h) 所有 STATUS_* 异常码
异常标志 sdk/include/xdk/winnt_old.h(file:///d:/reactos/sdk/include/xdk/winnt_old.h) EXCEPTION_* 标志
PSEH2 宏 sdk/include/vcruntime/pseh/pseh2.h(file:///d:/reactos/sdk/include/vcruntime/pseh/pseh2.h) _SEH2_TRY 等(内核/用户态共享)
调试宏 sdk/include/ntoskrnl/internal/debug.h(file:///d:/reactos/sdk/include/ntoskrnl/internal/debug.h) ASSERTASSERTMSG

8.4.0 框架图

复制代码
        软异常的来源
        ------------

  +-----------------+
  | 用户代码 int 3  | (断点指令)
  +-----------------+
          |
          v
  +-----------------+      +-------------------+
  | 编译插入 int 3  |      | 调试器主动 Dbg-   |
  | (编译器 /GS)     |      | BreakPoint()      |
  +-----------------+      +-------------------+
          |                       |
          +-----------+-----------+
                      |
                      v
              +---------------+
              | int 3 软中断  |
              | KiTrap03-     |
              |   Handler     |
              | KiDebug-      |
              |   Handler     |
              | STATUS_BREAK- |
              |   POINT       |
              +---------------+
                      |
                      v
  +-----------------------------------+
  | KiDispatchExceptionFromTrapFrame |
  | (软异常统一入口)                  |
  |   构造 EXCEPTION_RECORD           |
  |   调整 Eip (跳过 int 3 指令)      |
  +-----------------------------------+
                      |
                      v
  +-----------------------------------+
  | KiDispatchException (与硬件异常共用)|
  |   内核/用户分派                    |
  +-----------------------------------+


  +-----------------+
  | RtlRaise-       | (用户态 API)
  |   Exception     |
  | RtlRaiseStatus  |
  +-----------------+
          |
          v
  +-----------------+
  | 构造 CONTEXT    |
  | 填 ExceptionRec |
  +-----------------+
          |
          v
  +-----------------+
  | RtlDispatch-    | (用户态链表遍历)
  |   Exception     |
  +-----------------+
          |
          v
  (失败时) -> ZwRaiseException -> 内核 -> KeUserExceptionDispatcher


  +-----------------+
  | 断言失败        |
  | NT_ASSERT       |
  | RtlAssert       |
  | ASSERT          |
  +-----------------+
          |
          v
  +-----------------+
  | RtlRaiseStatus  | (STATUS_ASSERTION_FAILURE)
  | (在 RtlAssert)  |
  +-----------------+
          |
          v
  走 RtlRaiseStatus 路径


  +-----------------+
  | C++ throw       |
  +-----------------+
          |
          v
  +-----------------+
  | _CxxThrow-      | (MSVC CRT)
  |   Exception     |
  +-----------------+
          |
          v
  +-----------------+
  | RtlRaise-       | (带异常码 0xE06D7363)
  |   Exception     |
  +-----------------+
          |
          v
  走 RtlRaiseException 路径
  C++ handler 捕获后处理


  +-----------------+
  | /GS 栈 cookie   |
  | 失败            |
  +-----------------+
          |
          v
  +-----------------+
  | int 29          | (KiRaiseSecurity-   |
  | __report_gs-    |  CheckFailure-      |
  |   failure       |  Handler)           |
  +-----------------+
          |
          v
  +-----------------+
  | KiDispatch-     | STATUS_STACK_
  | ExceptionFrom-  | BUFFER_OVERRUN
  | TrapFrame       |
  +-----------------+

8.4.1 软异常定义与分类

软异常 (Software Exception)是指 由软件主动生成 的异常,与硬件异常(由 CPU 在执行指令时自动检测)相对。

与硬件异常对比

维度 硬件异常 软异常
触发源 CPU 内部(除零、访问违例、断点) 软件(API、int 指令、断言、throw)
触发时机 指令执行期间 任意软件位置
异常码 固定的硬件异常码 任意 NTSTATUS
异常地址 Eip 指向故障指令 通常是 Eip = _ReturnAddress()(调用方)
主要用途 反映程序错误 反映程序状态/控制流

软异常的四大类别

  1. 控制流类STATUS_BREAKPOINT(int 3)、STATUS_SINGLE_STEP(int 1)、STATUS_ASSERTION_FAILURE(int 2C)
  2. 主动抛出类RtlRaiseException / RtlRaiseStatus 发起的任意 NTSTATUS
  3. 安全检查类STATUS_STACK_BUFFER_OVERRUN(int 29,/GS 失败)
  4. C++ 异常类throw 编译为 _CxxThrowExceptionRtlRaiseException(异常码=0xE06D7363)

软异常的内核态路径

软异常在内核态通常走 与硬件异常相同的 KiDispatchException 路径------这是 SEH 框架的关键设计点。具体来说:

  1. 软异常通过 KiDispatchExceptionFromTrapFrame(见 8.4.2)构造一个 EXCEPTION_RECORD
  2. 然后调用 KiDispatchException 把它当作"硬件异常"处理
  3. 之后的 FirstChance/SecondChance/KeBugCheckEx 流程与硬件异常完全相同

这种统一性带来的好处是:SEH handler 不知道也不需要知道 这个异常是硬件产生的还是软件产生的------它们看到的是统一的 EXCEPTION_RECORD


8.4.2 软异常统一入口:KiDispatchExceptionFromTrapFrame

定义于 ntoskrnl/ke/i386/exp.c:1055(file:///d:/reactos/ntoskrnl/ke/i386/exp.c#L1052-L1100):

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

参数

参数 含义
Code 异常码(STATUS_*
Flags 异常标志(EXCEPTION_*
Address 异常发生地址(一般是 TrapFrame->EipEip - 1
ParameterCount 异常参数个数(0-3)
Parameter1/2/3 异常参数
TrapFrame 当前 trap frame(用于决定 PreviousMode)

关键设计

  1. 自动判断 PreviousModeKiUserTrap(TrapFrame) 通过 SegCs 段选择子决定用户态还是内核态
  2. FirstChance = TRUE:默认走"第一次机会"路径
  3. 复用 KiDispatchException:避免在 trap handler 中重复实现 FirstChance/SecondChance 逻辑

使用示例

KiDebugHandler(int 1/int 3)调用:

c 复制代码
DECLSPEC_NORETURN
VOID FASTCALL
KiDebugHandler(IN PKTRAP_FRAME TrapFrame,
               IN ULONG Parameter1,
               IN ULONG Parameter2,
               IN ULONG Parameter3)
{
    if (KiUserTrap(TrapFrame)) _enable();

    /* Dispatch the exception. Fix EIP in case its a break breakpoint */
    KiDispatchExceptionFromTrapFrame(STATUS_BREAKPOINT,
                                     0,
                                     TrapFrame->Eip - (Parameter1 == BREAKPOINT_BREAK),
                                     3,
                                     Parameter1,
                                     Parameter2,
                                     Parameter3,
                                     TrapFrame);
}

KiRaiseAssertionHandler(int 2C)调用:

c 复制代码
DECLSPEC_NORETURN
VOID FASTCALL
KiRaiseAssertionHandler(IN PKTRAP_FRAME TrapFrame)
{
    KiEnterTrap(TrapFrame);
    TrapFrame->Eip -= 2;  /* Decrement EIP to point to the INT2C instruction (2 bytes, not 1 like INT3) */

    /* Dispatch the exception */
    KiDispatchException0Args(STATUS_ASSERTION_FAILURE,
                             TrapFrame->Eip,
                             TrapFrame);
}

KiRaiseSecurityCheckFailureHandler(int 29)调用:

c 复制代码
VOID FASTCALL
KiRaiseSecurityCheckFailureHandler(IN PKTRAP_FRAME TrapFrame)
{
    KiEnterTrap(TrapFrame);
    TrapFrame->Eip -= 2;  /* Decrement EIP to point to the INT29 instruction (2 bytes, not 1 like INT3) */

    if (KiUserTrap(TrapFrame))
    {
        KiDispatchExceptionFromTrapFrame(STATUS_STACK_BUFFER_OVERRUN,
                                         EXCEPTION_NONCONTINUABLE,
                                         TrapFrame->Eip,
                                         1,
                                         TrapFrame->Ecx,
                                         0, 0,
                                         TrapFrame);
    }
    else
    {
        /* Bugcheck the system */
        KeBugCheckWithTf(KERNEL_SECURITY_CHECK_FAILURE, ...);
    }
}

注意这些 trap handler 都 先调用 KiEnterTrap 把 trap frame 构造成完整格式,然后 调整 Eip (int 3 是 1 字节,int 2C/int 29 是 2 字节),再调用 KiDispatchException* 系列。


8.4.3 常见软异常码

定义于 sdk/include/psdk/ntstatus.h(file:///d:/reactos/sdk/include/psdk/ntstatus.h)。ReactOS 中常见的软异常码按语义分类:

控制流类

异常码 触发源 含义
STATUS_BREAKPOINT 0x80000003 int 3 / DbgBreakPoint 调试断点
STATUS_SINGLE_STEP 0x80000004 int 1 / TF 标志 单步执行
STATUS_ASSERTION_FAILURE 0xC0000479 int 2C / RtlAssert 断言失败
STATUS_BREAKPOINT(int 2D) 0x80000003 int 2D / DbgDebugPrompt 调试器调试服务

错误类

异常码 含义
STATUS_ACCESS_VIOLATION 0xC0000005 访问违例(读/写/执行保护)
STATUS_INTEGER_DIVIDE_BY_ZERO 0xC0000094 整数除零
STATUS_INTEGER_OVERFLOW 0xC0000095 整数溢出(带 into 指令)
STATUS_ARRAY_BOUNDS_EXCEEDED 0xC000008C BOUND 越界
STATUS_ILLEGAL_INSTRUCTION 0xC000001D 非法指令
STATUS_PRIVILEGED_INSTRUCTION 0xC0000096 特权指令
STATUS_STACK_OVERFLOW 0xC00000FD 栈溢出
STATUS_STACK_BUFFER_OVERRUN 0xC0000409 /GS 栈检查失败
STATUS_GUARD_PAGE_VIOLATION 0x80000001 守卫页违例
STATUS_IN_PAGE_ERROR 0xC0000006 缺页错误
STATUS_INVALID_HANDLE 0xC0000008 非法句柄
STATUS_INVALID_PARAMETER 0xC000000D 非法参数
STATUS_NO_MEMORY 0xC0000017 内存不足
STATUS_INVALID_DISPOSITION 0xC0000022 handler 返回非法值
STATUS_NONCONTINUABLE_EXCEPTION 0xC0000025 对不可继续异常继续
STATUS_INVALID_UNWIND_TARGET 0xC0000026 unwind 目标非法
STATUS_BAD_STACK 0xC0000028 栈状态非法

浮点类

异常码 含义
STATUS_FLOAT_INVALID_OPERATION 0xC0000090 FPU 非法操作
STATUS_FLOAT_STACK_CHECK 0xC0000092 FPU 栈检查
STATUS_FLOAT_DIVIDE_BY_ZERO 0xC000008E FPU 除零
STATUS_FLOAT_OVERFLOW 0xC0000091 FPU 上溢
STATUS_FLOAT_UNDERFLOW 0xC0000093 FPU 下溢
STATUS_FLOAT_INEXACT_RESULT 0xC000008F FPU 不精确
STATUS_FLOAT_DENORMAL_OPERAND 0xC000008D FPU 非正规操作数
STATUS_FLOAT_MULTIPLE_TRAPS 0xC00002C9 SIMD 多重陷阱

C++ 异常

异常码 含义
0xE06D7363 0xE06D7363 MSVC C++ 异常魔数("msc" 的小端)

信息类(部分 NTSTATUS 可作为软异常)

异常码 含义
STATUS_BUFFER_OVERFLOW 0x80000005 缓冲区溢出(信息)
STATUS_NO_MORE_FILES 0x80000006 没有更多文件
STATUS_WAKE_SYSTEM_DEBUGGER 0x80000007 唤醒系统调试器
STATUS_HANDLES_CLOSED 0x8000000A 句柄已关闭

NTSTATUS 编码规则

NTSTATUS 编码遵循 Severity + Customer + Reserved + Facility + Code 的位段规则:

位段 范围 含义
Severity 31 0 = Success, 1 = Information/Warning/Error
Customer 30 1 = 客户定义(应用程序)
Reserved 29 必须为 0
Facility 28-16 13 位设施码(如 FACILITY_KERNEL = 0,FACILITY_RUNTIME = 2)
Code 15-0 16 位子代码

STATUS_BREAKPOINT = 0x80000003:

  • Severity = 1(Information)
  • Facility = 0(FACILITY_NULL)
  • Code = 3

STATUS_ACCESS_VIOLATION = 0xC0000005:

  • Severity = 1(Error)
  • Facility = 0(FACILITY_NULL)
  • Code = 5

8.4.4 RtlRaiseException / RtlRaiseStatus 软异常抛出

这两个 API 是用户态 主动抛出异常 的标准方式,已在 8.3.4 节详细描述。本节再次从"软异常"的角度强调其特性。

RtlRaiseException 的特点

  • 可继续ExceptionFlags = 0,handler 可以修改 CONTEXT 后继续执行
  • 带 record :调用者控制完整 EXCEPTION_RECORD
  • 典型用途
    • C++ throw 的底层调用
    • 显式 SEH 处理(虽然不推荐)
    • 把 NTSTATUS 转换为异常
    • 自定义错误码传递给上层

RtlRaiseStatus 的特点

  • 不可继续ExceptionFlags = EXCEPTION_NONCONTINUABLE,handler 不能继续执行
  • 仅 NTSTATUS:调用者只提供状态码
  • 典型用途
    • STATUS_ASSERTION_FAILURE
    • 状态检查失败时抛出不可恢复异常

内核态对应函数

  • ExRaiseStatus(Status):内核态主动抛出(设置 EXCEPTION_NONCONTINUABLE
  • ExRaiseAccessViolation():抛出 STATUS_ACCESS_VIOLATION
  • ExRaiseDatatypeMisalignment():抛出 STATUS_DATATYPE_MISALIGNMENT
  • ExRaiseStatus 的实现位于 ntoskrnl/ex/init.c(file:///d:/reactos/ntoskrnl/ex/init.c) 周围

内核态陷阱

内核态调用 ExRaise* 系列时,编译器会生成 KiRaiseException 调用(不是 RtlRaiseException),最终通过 KiDispatchException 走内核异常分派路径。详见 8.2.9 节。


8.4.5 调试器软中断

Windows NT 内核预留了若干 软中断向量 供调试器和工具使用,每个对应一个软异常:

int 1(#DB)--- STATUS_SINGLE_STEP

当 EFlags 的 TF(Trap Flag)位被设置时,CPU 在每条指令执行后触发 int 1。调试器用单步跟踪程序时使用。

int 3(#BP)--- STATUS_BREAKPOINT

int 3 是 1 字节指令(机器码 0xCC),编译器/调试器最常用的断点指令。KiDebugHandler 处理:

c 复制代码
DECLSPEC_NORETURN
VOID FASTCALL
KiDebugHandler(IN PKTRAP_FRAME TrapFrame,
               IN ULONG Parameter1,
               IN ULONG Parameter2,
               IN ULONG Parameter3)
{
    if (KiUserTrap(TrapFrame)) _enable();

    /* Dispatch the exception. Fix EIP in case its a break breakpoint */
    KiDispatchExceptionFromTrapFrame(STATUS_BREAKPOINT,
                                     0,
                                     TrapFrame->Eip - (Parameter1 == BREAKPOINT_BREAK),
                                     3,
                                     Parameter1,
                                     Parameter2,
                                     Parameter3,
                                     TrapFrame);
}

Parameter1 区分 #BP#DBEip - 1 把 EIP 退回 int 3 指令。

int 2C --- STATUS_ASSERTION_FAILURE

int 2C 是 2 字节指令(0xCD 0x2C),专供断言失败使用。KiRaiseAssertionHandler 处理:

c 复制代码
DECLSPEC_NORETURN
VOID FASTCALL
KiRaiseAssertionHandler(IN PKTRAP_FRAME TrapFrame)
{
    KiEnterTrap(TrapFrame);
    TrapFrame->Eip -= 2;  /* INT2C is 2 bytes */

    KiDispatchException0Args(STATUS_ASSERTION_FAILURE,
                             TrapFrame->Eip,
                             TrapFrame);
}

STATUS_ASSERTION_FAILURE 的值是 0xC0000479(Severity=Error, Facility=0, Code=0x479)。

int 29 --- STATUS_STACK_BUFFER_OVERRUN

int 29 是 2 字节指令,由 MSVC /GS 栈保护生成。当栈 cookie 检查失败时,编译器插入的代码会执行 int 29(或 Windows Vista+ 的 __report_gsfailure)。KiRaiseSecurityCheckFailureHandler 处理:

c 复制代码
VOID FASTCALL
KiRaiseSecurityCheckFailureHandler(IN PKTRAP_FRAME TrapFrame)
{
    KiEnterTrap(TrapFrame);
    TrapFrame->Eip -= 2;

    if (KiUserTrap(TrapFrame))
    {
        KiDispatchExceptionFromTrapFrame(STATUS_STACK_BUFFER_OVERRUN,
                                         EXCEPTION_NONCONTINUABLE,
                                         TrapFrame->Eip,
                                         1,
                                         TrapFrame->Ecx, 0, 0,
                                         TrapFrame);
    }
    else
    {
        /* Kernel-side: bugcheck directly */
        KeBugCheckWithTf(KERNEL_SECURITY_CHECK_FAILURE, ...);
    }
}

注意:内核态的 /GS 失败直接蓝屏(KERNEL_SECURITY_CHECK_FAILURE),而用户态失败走 STATUS_STACK_BUFFER_OVERRUN 软异常。

int 2D --- STATUS_BREAKPOINT(调试器服务)

int 2D 是 Windows 调试器内部使用的"调试服务"软中断,由 KiDebugServiceHandler 处理。DbgPrintDebugPrompt 等内核调试器 API 内部使用:

c 复制代码
DECLSPEC_NORETURN
VOID FASTCALL
KiDebugServiceHandler(IN PKTRAP_FRAME TrapFrame)
{
    KiEnterTrap(TrapFrame);
    TrapFrame->Eip++;  /* INT3 is 1 byte */

    KiDebugHandler(TrapFrame, TrapFrame->Eax, TrapFrame->Ecx, TrapFrame->Edx);
}

其他软中断

  • int 0x2A(KiGetTickCountHandler):获取 tick count,遗留支持
  • int 0x2B(KiCallbackReturnHandler):用户态回调返回

8.4.6 断言触发的软异常

ReactOS 中有 多个层次的断言宏,它们在失败时触发软异常。

RtlAssert --- 用户态断言

定义于 sdk/lib/rtl/assert.c(file:///d:/reactos/sdk/lib/rtl/assert.c#L21-L91):

c 复制代码
VOID NTAPI
RtlAssert(IN PVOID FailedAssertion,
          IN PVOID FileName,
          IN ULONG LineNumber,
          IN PCHAR Message OPTIONAL)
{
    CHAR Action[2];
    CONTEXT Context;

    RtlCaptureContext(&Context);

    for (;;)
    {
        DbgPrint("\n*** Assertion failed: %s%s\n"
                 "***   Source File: %s, line %lu\n\n",
                 ...);

        if (RtlGetNtGlobalFlags() & FLG_DISABLE_DEBUG_PROMPTS)
        {
            RtlRaiseStatus(STATUS_ASSERTION_FAILURE);
        }

        DbgPrompt("Break repeatedly, break Once, Ignore, "
                  "terminate Process or terminate Thread (boipt)? ",
                  Action, sizeof(Action));
        switch (Action[0])
        {
            case 'B': case 'b': case 'O': case 'o':
                DbgPrint("Execute '.cxr %p' to dump context\n", &Context);
                DbgBreakPoint();
                if ((Action[0] == 'B') || (Action[0] == 'b')) break;
                // fall through
            case 'I': case 'i':
                return;
            case 'P': case 'p':
                ZwTerminateProcess(ZwCurrentProcess(), STATUS_UNSUCCESSFUL);
                break;
            case 'T': case 't':
                ZwTerminateThread(ZwCurrentThread(), STATUS_UNSUCCESSFUL);
                break;
            default:
                break;
        }
    }
}

RtlAssert 的工作流:

  1. 打印断言失败信息
  2. 检查 FLG_DISABLE_DEBUG_PROMPTS(自动化测试用)
    • 如果设置了 → RtlRaiseStatus(STATUS_ASSERTION_FAILURE) 触发软异常
  3. 提示用户选择动作:
    • B / b --- 持续断点(多次)
    • O / o --- 一次断点
    • I / i --- 忽略(继续执行)
    • P / p --- 终止进程
    • T / t --- 终止线程

ASSERT / ASSERTMSG

定义于 sdk/include/ntoskrnl/internal/debug.h(file:///d:/reactos/sdk/include/ntoskrnl/internal/debug.h):

c 复制代码
#if DBG
#define ASSERT(x) do { if (!(x)) { RtlAssert(#x, __FILE__, __LINE__, NULL); } } while(0)
#define ASSERTMSG(x, msg) do { if (!(x)) { RtlAssert(#x, __FILE__, __LINE__, msg); } } while(0)
#else
#define ASSERT(x) ((void)0)
#define ASSERTMSG(x, msg) ((void)0)
#endif

ASSERT 在 release 构建中被完全消除(无开销),在 debug 构建中调用 RtlAssert

NT_ASSERT / NT_VERIFY

定义于 sdk/include/wudfwdm.h(file:///d:/reactos/sdk/include/wudfwdm.h) 中,是 WDF 引入的现代断言宏:

c 复制代码
#define NT_ASSERT(x) do { if (!(x)) { NT_ASSERTMSG(#x, NULL); } } while(0)
#define NT_ASSERTMSG(expr, msg) do { \
    DbgPrint("ASSERTION %s failed at %s:%d\n", expr, __FILE__, __LINE__); \
    RtlRaiseStatus(STATUS_ASSERTION_FAILURE); \
} while(0)

NT_ASSERT 与传统 ASSERT 的关键差异:

维度 ASSERT NT_ASSERT
Release 构建 完全消除 仍然有效(不消除)
触发机制 交互式提示 直接 RtlRaiseStatus
失败行为 可选择继续 强制抛出 STATUS_ASSERTION_FAILURE
用途 仅 debug 始终检查(如参数验证)

NT_VERIFY 类似 NT_ASSERT 但在 release 中保留表达式求值:

c 复制代码
#define NT_VERIFY(x) do { if (!(x)) { NT_ASSERTMSG(#x, NULL); } } while(0)

内核态的 ASSERT 实现

内核态的 ASSERT 宏触发的是 RtlAssert(虽然名字像 RTL,但 RtlAssert 也可被内核态调用)。在 ReactOS 中,内核态 ASSERT 失败时:

  1. 调用 RtlAssert(如果链接了 RTL)
  2. 或者直接调用 KeBugCheckEx(ASSERTION_FAILURE, ...) 蓝屏

8.4.7 C++ 异常与 SEH 的统一

C++ throw 在 MSVC 上底层调用 _CxxThrowException,最终通过 RtlRaiseException 走 SEH 框架。

_CxxThrowException 流程

c 复制代码
void __stdcall _CxxThrowException(void* pExceptionObject, _s__ThrowInfo* pThrowInfo)
{
    EH_EXCEPTION_RECORD ExceptionRecord = { 0xE06D7363, 0, NULL, NULL, 0, NULL };
    ExceptionRecord.params[0] = (ULONG_PTR)pExceptionObject;
    ExceptionRecord.params[1] = (ULONG_PTR)pThrowInfo;
    ExceptionRecord.params[2] = (ULONG_PTR)((char*)_ReturnAddress() - (char*)ImageBase);

    RtlRaiseException(&ExceptionRecord);
}

0xE06D7363 是 MSVC C++ 异常的魔数("msc" 的小端表示)。

catch 块如何识别

编译器为每个 catch(...) 块生成一个异常 handler,当 RtlDispatchException 调用 handler 时,handler 检查 ExceptionRecord->ExceptionCode

  • 如果是 0xE06D7363 → 调用 MSVC C++ EH 运行时
  • C++ EH 运行时从 ExceptionInformation[0](异常对象指针)和 ExceptionInformation[1]ThrowInfo*)还原异常类型并匹配 catch 块

编译选项

MSVC 提供三个相关编译选项:

选项 含义
/EHsc C++ 异常不与 SEH 混合(默认)
/EHa C++ 异常可以捕获 SEH 异常(混合模式)
/EHs C++ 异常不捕获异步异常(系统生成的 SEH)

/EHa 模式下:

  • catch(...) 可以捕获硬件异常
  • __try / __except 可以捕获 C++ 异常
  • 二者共享同一套 SEH 框架

ReactOS 中 C++ 异常的处理

ReactOS 的 C++ 异常通过 msvcrt 的 _CxxThrowExceptionRtlRaiseException 实现,与 SEH 完全统一。catch 块由编译器转换为 EXCEPTION_DISPOSITION 返回的 handler。


8.4.8 STATUS 与异常码的对应体系

NTSTATUS 是一个 32 位整数,但只有特定的 NTSTATUS 值可以 作为异常码 出现在 EXCEPTION_RECORD.ExceptionCode 中。

异常码合法性

合法异常码应满足:

  • Severity 为 1(即 0x8_xxxx_xxxx 或 0xC_xxxx_xxxx)
  • 不是 0(无异常)
  • 不是 STATUS_SUCCESS(0x00000000)

具体可分为:

  • Success(0x0_xxxx_xxxx):不是异常
  • Information(0x8_xxxx_xxxx):可以作为信息性异常(如断点)
  • Warning(0x8_xxxx_xxxx 中高 16 位非零):警告
  • Error(0xC_xxxx_xxxx):错误

与异常码相关的特殊值

c 复制代码
// 成功
#define STATUS_SUCCESS                   ((NTSTATUS)0x00000000L)

// 信息性 / 自定义
#define STATUS_BREAKPOINT                ((NTSTATUS)0x80000003L)
#define STATUS_SINGLE_STEP               ((NTSTATUS)0x80000004L)
#define STATUS_BUFFER_OVERFLOW           ((NTSTATUS)0x80000005L)
#define STATUS_NO_MORE_FILES             ((NTSTATUS)0x80000006L)
#define STATUS_WAKE_SYSTEM_DEBUGGER      ((NTSTATUS)0x80000007L)

// 错误(Facility = 0,最常见)
#define STATUS_ACCESS_VIOLATION          ((NTSTATUS)0xC0000005L)
#define STATUS_IN_PAGE_ERROR             ((NTSTATUS)0xC0000006L)
#define STATUS_INVALID_HANDLE            ((NTSTATUS)0xC0000008L)
#define STATUS_NO_MEMORY                 ((NTSTATUS)0xC0000017L)
#define STATUS_ILLEGAL_INSTRUCTION       ((NTSTATUS)0xC000001DL)
#define STATUS_NONCONTINUABLE_EXCEPTION  ((NTSTATUS)0xC0000025L)
#define STATUS_INVALID_DISPOSITION       ((NTSTATUS)0xC0000022L)
#define STATUS_INVALID_UNWIND_TARGET     ((NTSTATUS)0xC0000026L)
#define STATUS_BAD_STACK                 ((NTSTATUS)0xC0000028L)
#define STATUS_FLOAT_INVALID_OPERATION   ((NTSTATUS)0xC0000090L)
#define STATUS_INTEGER_DIVIDE_BY_ZERO    ((NTSTATUS)0xC0000094L)
#define STATUS_PRIVILEGED_INSTRUCTION    ((NTSTATUS)0xC0000096L)
#define STATUS_STACK_OVERFLOW            ((NTSTATUS)0xC00000FDL)
#define STATUS_ARRAY_BOUNDS_EXCEEDED     ((NTSTATUS)0xC000008CL)
#define STATUS_STACK_BUFFER_OVERRUN      ((NTSTATUS)0xC0000409L)
#define STATUS_ASSERTION_FAILURE         ((NTSTATUS)0xC0000479L)

// C++ 异常魔数
#define CPLUSPLUS_EXCEPTION  0xE06D7363

用户自定义异常码

应用程序可以自由使用 0xE_xxxx_xxxx(Customer 标志位)作为自定义异常码:

c 复制代码
#define MY_APP_EXCEPTION  ((NTSTATUS)0xE0000001L)

这允许应用程序在 SEH 框架内传递自定义信息。


8.4.9 软异常传递流程详解

下图展示了 一个典型的软异常(RtlRaiseException(STATUS_MY_ERROR))从用户态抛出到被处理的完整路径

复制代码
+------------------------+
| 用户代码                |
| RtlRaiseException(&Rec)| <-- STATUS_MY_ERROR, ExceptionFlags=0
+-----------+------------+
            |
            v
+-------------------------------------+
| RtlRaiseException (i386/except_asm) |
|   - RtlCaptureContext               |
|   - 设置 ExceptionAddress            |
|   - 检查 RtlpCheckForActiveDebugger  |
+-----------+-------------------------+
            |
            v
+-------------------------------------+
| 1) 尝试 RtlDispatchException        |
|    - VEH                            |
|    - 遍历 SEH 链表                  |
|    - 若找到 handler 处理 ->         |
|        RtlCallVectoredContinue-     |
|        Handlers + NtContinue        |
+-----------+-------------------------+
            |
            | (失败)
            v
+-------------------------------------+
| 2) 系统调用 ZwRaiseException        |
|    FirstChance = FALSE              |
+-----------+-------------------------+
            |
            v
+-------------------------------------+
| 内核 NtRaiseException                |
| (ntoskrnl/ke/except.c:172)           |
+-----------+-------------------------+
            |
            v
+-------------------------------------+
| KiRaiseException                    |
|   - ProbeForRead Context/Record     |
|   - KeContextToTrapFrame            |
|   - 清除 KI_EXCEPTION_INTERNAL      |
|   - KiDispatchException(,           |
|       PreviousMode, FALSE)          |
+-----------+-------------------------+
            |
            v
+-------------------------------------+
| KiDispatchException (LastChance)    |
|   - KeTrapFrameToContext            |
|   - (PreviousMode == UserMode)      |
|   - FirstChance=FALSE ->            |
|     DbgkForwardException(...,       |
|       FirstChance=FALSE,            |
|       SecondChance=TRUE)            |
|   - 若 LastChance 仍失败:           |
|     ZwTerminateProcess + 蓝屏       |
+-----------+-------------------------+
            |
            v
+-------------------------------------+
| 进程被终止                          |
+-------------------------------------+

关键路径节点

  1. RtlRaiseException :构造 CONTEXT,调用本地 RtlDispatchException
  2. 本地分派失败 :调用 ZwRaiseException 系统调用
  3. 内核 NtRaiseExceptionKiRaiseException
    • 探测用户态指针
    • 复制到内核栈
    • 构造 trap frame
  4. KiDispatchException(FirstChance=FALSE)
    • 走 SecondChance 路径
    • 调用 DbgkForwardException 通知调试器
    • 若仍无人处理 → ZwTerminateProcess 杀进程

与硬件异常的对比路径

路径 硬件异常 软异常(RtlRaiseException
入口 IDT → KiTrap0X KiRaiseException
trap 构造 KiEnterTrap KeContextToTrapFrame
异常记录 由 trap handler 构造 由调用者构造
KiDispatchException 调用 由 trap handler 调用 KiRaiseException 调用
FirstChance TRUE(trap 默认) 调用者决定(FirstChance 参数)
异常地址 TrapFrame->Eip 调用者提供

两路径在 KiDispatchException 内部完全合流,这正是 SEH 框架设计优雅之处。


深入剖析:软异常的关键设计决策

软中断指令长度的历史演进

x86 架构中,不同软中断指令的长度不同,这直接影响了 trap handler 中 Eip 的调整方式:

中断 机器码 字节数 Eip 调整 用途
int 3 0xCC 1 字节 Eip - 1 调试断点
int 1 硬件触发 N/A 无需调整 单步/TSS 调试
int 2C 0xCD 0x2C 2 字节 Eip - 2 断言失败
int 2D 0xCD 0x2D 2 字节 Eip - 2(int 2D handler 中 Eip++ 实际是 +1) 调试服务
int 29 0xCD 0x1D 2 字节 Eip - 2 /GS 栈保护

为什么 int 3 只有 1 字节? 这是 x86 架构的特殊设计。int 30xCC)被设计为单字节指令,目的是允许调试器在内存中直接替换目标字节为 0xCC,而不会破坏相邻指令。如果使用 2 字节的 int N 指令,替换时会覆盖下一条指令的第一个字节。

int 2C/29 为什么是 2 字节? 这些中断使用通用的 int N 指令格式(0xCD + 中断号),所以是 2 字节。它们不需要"就地替换"的特性,因为编译器在编译时就知道要在哪里插入这些指令。

KiDebugServiceHandler 的特殊处理 :注意 KiDebugServiceHandler(int 2D)中 Eip 的调整是 Eip++(加 1),而不是减 2。这是因为 int 2D 实际上是通过 KiDebugHandler 间接处理的,而 KiDebugHandler 内部会根据 Parameter1 判断是否需要减 1。KiDebugServiceHandler 先加 1 是为了补偿 int 2D 指令本身只有 1 字节有效偏移的情况。

RtlAssert 的交互式调试循环

RtlAssertboipt 提示循环是 ReactOS 调试基础设施的核心组件。它的设计反映了早期 Windows NT 的调试哲学:开发者与调试器实时交互

复制代码
Break repeatedly, break Once, Ignore, terminate Process or terminate Thread (boipt)?

每个选项的深层含义:

  1. Break repeatedly(B) :反复断点。每次执行到断言失败处都会停下来。适合需要多次观察同一断言的场景。底层调用 DbgBreakPoint()(int 3),调试器接管后可以检查变量、调用栈等。

  2. Break Once(O) :单次断点。断一次后继续执行,如果再次到达断言处则自动忽略。这通过 if 条件判断实现------断点后的 break 语句跳出 switch,回到 for(;;) 循环,但下次进入时 Action[0] 已经不是 'B' 了。

  3. Ignore(I) :忽略断言,继续执行。RtlAssert 直接 return,调用方继续执行。这非常危险------断言条件为假意味着程序状态已经不一致,继续执行可能导致更严重的问题。但在某些调试场景下,开发者知道断言是误报,可以选择忽略。

  4. terminate Process(P) :终止整个进程。调用 ZwTerminateProcess,进程立即结束,所有线程都被杀死。适合断言失败影响全局状态的场景。

  5. terminate Thread(T) :仅终止当前线程。调用 ZwTerminateThread,其他线程继续运行。适合断言失败只影响局部线程的场景。

FLG_DISABLE_DEBUG_PROMPTS 标志 :当 NtGlobalFlags 中设置了 FLG_DISABLE_DEBUG_PROMPTS 时,RtlAssert 跳过交互式提示,直接调用 RtlRaiseStatus(STATUS_ASSERTION_FAILURE)。这个标志主要用于:

  • 自动化测试环境(无法人工交互)
  • 远程调试(串口调试器无法显示提示)
  • 生产环境(不应该出现交互式提示)

C++ 异常魔数 0xE06D7363 的由来

C++ 异常码 0xE06D7363 看起来神秘,但实际上是 ASCII 字符串 "msc" 的小端表示加上异常标志位:

复制代码
0xE06D7363
  ││││││││
  │││││└┴┴┴── 'm' = 0x6D, 's' = 0x73, 'c' = 0x63
  │└┴┴──────── "msc" (Microsoft C/C++)
  └─────────── E = 1110b → Customer=1, Severity=3(Error)

为什么是 "msc"? 因为 MSVC(Microsoft C/C++ Compiler)的缩写就是 "msc"。微软选择这个魔数来标识 C++ 异常,使得 SEH handler 可以区分 C++ 异常和其他异常。

NTSTATUS 编码分析

  • Bit 31 = 1(Error severity)
  • Bit 30 = 1(Customer bit,表示非微软标准异常)
  • Bit 29 = 0(Reserved)
  • Facility = 0x6D7(实际上被编码在低 16 位中)

这种编码使得 C++ 异常在 SEH 框架中具有唯一的标识,handler 可以通过检查 ExceptionCode == 0xE06D7363 来判断是否是 C++ 异常。

/GS 栈保护的内核态与用户态差异

/GS(Buffer Security Check)是 MSVC 编译器的安全特性,通过在栈帧中插入 cookie 来检测缓冲区溢出。当 cookie 被覆盖时,说明发生了栈溢出,程序会触发安全异常。

用户态处理

c 复制代码
// 用户态 /GS 失败
KiDispatchExceptionFromTrapFrame(STATUS_STACK_BUFFER_OVERRUN,
                                 EXCEPTION_NONCONTINUABLE,
                                 TrapFrame->Eip,
                                 1, TrapFrame->Ecx, 0, 0,
                                 TrapFrame);

用户态走正常的异常分派路径,最终可能导致进程终止。

内核态处理

c 复制代码
// 内核态 /GS 失败
KeBugCheckWithTf(KERNEL_SECURITY_CHECK_FAILURE, ...);

内核态直接蓝屏!这是因为:

  1. 内核栈 cookie 被覆盖意味着内核状态已经被破坏
  2. 继续执行可能导致数据损坏或安全漏洞
  3. 蓝屏是保护系统完整性的最后手段

ECX 参数的含义 :在 KiRaiseSecurityCheckFailureHandler 中,TrapFrame->Ecx 被作为异常参数传递。在 MSVC 的 /GS 实现中,ECX 通常包含失败函数的地址或 cookie 值,用于事后分析。

NtRaiseException 系统调用的安全验证

NtRaiseException 系统调用(ntoskrnl/ke/except.c(file:///d:/reactos/ntoskrnl/ke/except.c))在将用户态的异常记录传递给内核之前,需要进行严格的安全验证:

c 复制代码
NTSTATUS NTAPI
NtRaiseException(IN PEXCEPTION_RECORD ExceptionRecord,
                 IN PCONTEXT Context,
                 IN BOOLEAN FirstChance)
{
    // 1. 验证用户态指针
    ProbeForRead(ExceptionRecord, sizeof(EXCEPTION_RECORD), sizeof(ULONG));
    ProbeForRead(Context, sizeof(CONTEXT), sizeof(ULONG));

    // 2. 复制到内核栈
    RtlCopyMemory(&LocalRecord, ExceptionRecord, sizeof(EXCEPTION_RECORD));
    RtlCopyMemory(&LocalContext, Context, sizeof(CONTEXT));

    // 3. 验证异常记录合法性
    if (LocalRecord.ExceptionFlags & ~EXCEPTION_NONCONTINUABLE)
        return STATUS_INVALID_PARAMETER;

    // 4. 调用内核分派
    KiRaiseException(&LocalRecord, &LocalContext, ...);
}

安全验证的必要性

  1. ProbeForRead:防止用户态传入无效指针导致内核崩溃
  2. 复制到内核栈:避免 TOCTOU(Time-of-check-to-time-of-use)攻击------用户态代码可能在验证后修改数据
  3. 标志验证 :只允许 EXCEPTION_NONCONTINUABLE 标志,其他位必须为 0
  4. ContextFlags 验证:确保 Context 中的段选择子是合法的用户态值

软异常的调试技巧

在 WinDbg 中调试软异常时,以下命令非常有用:

1. 查看所有异常断点

复制代码
sxe *    ; 在第一次机会时中断所有异常
sxd *    ; 忽略第一次机会,在第二次机会时中断

2. 查看当前异常记录

复制代码
!exr -1  ; 显示最近的异常记录

3. 查看异常地址的反汇编

复制代码
!exr -1  ; 获取 ExceptionAddress
ub <addr>  ; 反汇编异常地址前的指令

4. 追踪 RtlRaiseException 调用

复制代码
bu ntdll!RtlRaiseException  ; 在 RtlRaiseException 上设置断点

5. 查看 SEH 链表

复制代码
!teb       ; 查看 TEB,获取 ExceptionList 偏移
dps fs:[0] ; 查看 SEH 链表

6. 模拟软异常

复制代码
.int3      ; 在调试器中触发断点异常
r eip=<addr>  ; 修改 EIP 到目标地址

10问为什么:深入理解软异常

问题 1:为什么软异常要和硬件异常共用同一个 KiDispatchException 入口?

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

从 SEH handler 的角度看,它不需要也不应该关心异常的来源。无论是 CPU 检测到除零,还是程序主动调用 RtlRaiseStatus,handler 看到的都是相同的 EXCEPTION_RECORD 结构。这种统一性带来了:

  1. 代码复用:一套分派逻辑处理所有异常,减少代码量和 bug 面
  2. 语义一致__except 块不需要区分异常来源
  3. 调试器统一接口:调试器只需 hook 一个入口就能拦截所有异常
  4. 可扩展性:新增异常类型不需要修改分派逻辑

如果分开处理,每个 handler 都需要同时处理硬件和软件两种路径,代码量翻倍,且容易出现不一致。

问题 2:为什么 int 3 指令要设计成 1 字节,而不是和其他 int N 一样是 2 字节?

:这是为了支持调试器的断点替换机制

调试器设置断点的过程是:

  1. 读取目标地址的原始字节(假设为 0x55,即 push ebp
  2. 将该字节替换为 0xCC(int 3)
  3. 当 CPU 执行到 0xCC 时触发断点异常
  4. 调试器恢复原始字节 0x55,让程序继续执行

如果 int 3 是 2 字节指令(如 0xCD 0x03),替换时会覆盖相邻指令的第一个字节,破坏程序逻辑。1 字节的 0xCC 确保替换是原子性的------只影响一个字节。

此外,0xCC 作为单字节指令还有一个好处:可以在任何位置插入,不需要考虑对齐问题。

问题 3:为什么 C++ 异常码要用 0xE06D7363 这个"魔数",而不是直接用 STATUS_* 常量?

:这是为了区分 C++ 异常和系统异常

C++ 异常和系统 SEH 异常在语义上有本质区别:

  • C++ 异常是类型化 的(catch(int) 只捕获 int 类型)
  • SEH 异常是码化 的(__except 根据异常码决定)

如果使用标准的 STATUS 码,SEH handler 可能会错误地"捕获"C++ 异常,导致 C++ 的 catch 块无法执行。通过使用 0xE06D7363 这个特殊的魔数:

  1. SEH handler 可以识别 :检查 ExceptionCode == 0xE06D7363 来判断是否是 C++ 异常
  2. 不会与系统异常冲突:系统异常的 Facility 通常是 0,而 C++ 异常的编码完全不同
  3. Customer 标志位:Bit 30 = 1 表示这是"客户定义"的异常,不是微软标准异常

问题 4:为什么内核态 /GS 失败直接蓝屏,而用户态走异常分派?

:这是由内核态和用户态的容错策略不同决定的。

用户态

  • 进程是独立的,一个进程崩溃不影响其他进程
  • 走异常分派可以给调试器一个收集信息的机会
  • 最终结果是进程终止,系统继续运行

内核态

  • 内核是共享的,一个内核组件的状态损坏可能影响整个系统
  • 栈 cookie 被覆盖意味着栈已经被破坏,继续执行可能导致:
    • 返回地址被篡改(ROP 攻击)
    • 局部变量被覆盖(数据损坏)
    • 函数指针被修改(控制流劫持)
  • 蓝屏(KERNEL_SECURITY_CHECK_FAILURE)是保护系统完整性的最后手段

这种差异反映了 Windows NT 的安全哲学:用户态可以崩溃,内核态不能继续执行

问题 5:为什么 RtlAssert 要提供 boipt 五个选项,而不是直接终止进程?

:这是为了支持交互式调试的工作流。

在早期 Windows NT 开发中,调试器(如 WinDbg)通常通过串口连接,开发者需要实时与断言失败交互:

  1. Break repeatedly(B):适合需要多次观察同一断言的场景。例如,断言在循环中失败,开发者想观察每次迭代的变量变化。

  2. Break Once(O):适合只想看一次现场,然后让程序继续执行的场景。

  3. Ignore(I) :适合断言是误报的场景。例如,某些断言在特定条件下不成立,但程序逻辑是正确的。

  4. terminate Process(P):适合断言失败影响全局状态,需要完全重启的场景。

  5. terminate Thread(T):适合断言失败只影响局部线程,其他线程可以继续工作的场景。

如果直接终止进程,开发者就失去了灵活调试的能力。当然,FLG_DISABLE_DEBUG_PROMPTS 标志允许在自动化环境中跳过交互。

问题 6:为什么 NtRaiseException 需要 ProbeForRead 验证用户态指针?

:这是为了防止恶意用户态代码攻击内核

NtRaiseException 接收用户态传入的 EXCEPTION_RECORD*CONTEXT* 指针。如果不验证:

  1. 空指针:内核读取地址 0 会触发缺页异常,导致蓝屏
  2. 内核地址 :用户态传入内核地址(如 0x80000000),内核"读取"的实际上是用户态控制的数据
  3. TOCTOU 攻击:用户态代码在一个线程中修改指针指向的数据,在另一个线程中触发 NtRaiseException

ProbeForRead 确保:

  • 指针在用户态地址范围内
  • 指针是正确对齐的
  • 指针指向的内存是可读的

然后 RtlCopyMemory 将数据复制到内核栈,后续操作都基于内核副本,避免了 TOCTOU 攻击。

问题 7:为什么 int 2C(断言)和 int 29(/GS)都是 2 字节指令,但 Eip 调整方式不同?

:虽然都是 2 字节,但它们的处理流程不同

int 2C(KiRaiseAssertionHandler)

c 复制代码
TrapFrame->Eip -= 2;  // 回退到 int 2C 指令处

直接减 2,让异常地址指向 int 2C 指令本身。

int 2D(KiDebugServiceHandler)

c 复制代码
TrapFrame->Eip++;  // 实际上是 +1

加 1 是因为 int 2D 通过 KiDebugHandler 间接处理,而 KiDebugHandler 内部会根据 Parameter1 判断是否需要减 1。

int 29(KiRaiseSecurityCheckFailureHandler)

c 复制代码
TrapFrame->Eip -= 2;  // 回退到 int 29 指令处

与 int 2C 相同,直接减 2。

差异的根本原因是:int 2D 的处理路径更复杂(经过 KiDebugServiceHandlerKiDebugHandler),需要额外的补偿。

问题 8:为什么 C++ throw 要通过 RtlRaiseException 而不是直接调用 SEH?

:这是为了复用 SEH 的分派和展开机制

C++ 异常和 SEH 在底层共享同一套基础设施:

  • 分派RtlDispatchException 遍历 SEH 链表,调用每个 handler
  • 展开RtlUnwind 反向遍历链表,调用 __finally 块进行清理

如果 C++ 异常直接实现自己的分派逻辑:

  1. 需要重新实现链表遍历
  2. 需要处理与 SEH 的交互(如 /EHa 模式)
  3. 需要处理嵌套异常和展开冲突

通过调用 RtlRaiseException,C++ 异常可以:

  • 复用 SEH 的分派逻辑
  • 与 SEH handler 协作(__except 可以捕获 C++ 异常)
  • 确保 __finally 块在栈展开时正确执行

问题 9:为什么 STATUS_BREAKPOINT 的 Severity 是 Information(0x8),而不是 Error(0xC)?

:这是因为断点不是错误,而是控制流机制

STATUS_BREAKPOINT(0x80000003)的 Severity 位是 0(Information),这反映了它的语义:

  1. 断点是预期的:开发者主动插入断点,用于调试目的
  2. 断点可以被处理:调试器处理断点后,程序可以继续执行
  3. 断点不是错误:程序状态没有损坏,不需要恢复

如果 Severity 是 Error(0xC),SEH handler 可能会认为这是不可恢复的错误,从而终止程序。

类似的,STATUS_SINGLE_STEP(0x80000004)也是 Information severity,因为单步执行也是正常的调试行为。

问题 10:为什么 FLG_DISABLE_DEBUG_PROMPTS 标志要单独存在,而不是直接删除 RtlAssert 的交互代码?

:这是为了兼容不同的调试场景

RtlAssert 的交互代码在以下场景中非常有用:

  • 本地调试(开发者通过 WinDbg 交互)
  • 手动测试(开发者观察断言失败)

但在以下场景中,交互代码会导致问题:

  • 自动化测试(没有人工干预)
  • 远程调试(串口调试器无法显示提示)
  • 生产环境(不应该出现交互式提示)

FLG_DISABLE_DEBUG_PROMPTS 标志允许在编译时保留交互代码(便于本地调试),在运行时通过标志禁用(适应自动化环境)。

如果直接删除交互代码,本地调试时就失去了灵活性;如果不提供禁用机制,自动化环境就会卡住。


总结

8.4 节建立了软异常的完整视图。关键要点:

  1. 软异常定义:由软件主动生成的异常,与硬件异常(CPU 自动触发)相对
  2. 软异常统一入口KiDispatchExceptionFromTrapFrame 接收异常码/标志/地址/参数,构造 EXCEPTION_RECORD 后调用 KiDispatchException
  3. 四大类别
    • 控制流(int 1/3/2C/2D/29)
    • 主动抛出(RtlRaiseException / RtlRaiseStatus
    • 断言失败(RtlAssert / NT_ASSERT / ASSERT
    • C++ 异常(_CxxThrowException
  4. 统一框架 :所有软异常在 KiDispatchException 内部与硬件异常完全合流
  5. NTSTATUS 编码:Severity + Customer + Reserved + Facility + Code
  6. /GS 保护STATUS_STACK_BUFFER_OVERRUN 软异常
  7. C++ 异常魔数0xE06D7363("msc")
  8. 调试器软中断:int 1/3/2C/2D/29 都有专门的 trap handler
  9. RtlAssert 提示循环:boipt(break/once/ignore/process/thread)交互式
  10. 跨架构一致性:软异常路径在 x86/x64/ARM 上完全一致

至此第 8 章完整覆盖了 ReactOS 结构化异常处理的全部内容:8.1 框架、8.2 内核态、8.3 用户态、8.4 软异常。


本章代码索引

文件 内容
exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c) KiDispatchExceptionFromTrapFrameKiDispatchException
traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) KiDebugServiceHandler(int 2D)、KiRaiseAssertionHandler(int 2C)、KiRaiseSecurityCheckFailureHandler(int 29)、KiDebugHandler(int 1/3)
except.c(file:///d:/reactos/ntoskrnl/ke/except.c) KiContinueKiRaiseExceptionNtRaiseExceptionNtContinue
exception.c(file:///d:/reactos/sdk/lib/rtl/exception.c) RtlRaiseExceptionRtlRaiseStatusRtlUnhandledExceptionFilter
assert.c(file:///d:/reactos/sdk/lib/rtl/assert.c) RtlAssert(boipt 提示循环)
psdk/ntstatus.h(file:///d:/reactos/sdk/include/psdk/ntstatus.h) 所有 STATUS_* 异常码
xdk/winnt_old.h(file:///d:/reactos/sdk/include/xdk/winnt_old.h) EXCEPTION_MAXIMUM_PARAMETERSEXCEPTION_* 标志
xdk/rtltypes.h(file:///d:/reactos/sdk/include/xdk/rtltypes.h) EXCEPTION_RECORD
ntosifs.h(file:///d:/reactos/sdk/include/ntosifs.h) ExRaiseStatusExRaiseAccessViolationExRaiseDatatypeMisalignment
pseh2.h(file:///d:/reactos/sdk/include/vcruntime/pseh/pseh2.h) _SEH2_TRY 等(与软异常配合使用)
ntoskrnl/internal/debug.h(file:///d:/reactos/sdk/include/ntoskrnl/internal/debug.h) ASSERTASSERTMSG
wudfwdm.h(file:///d:/reactos/sdk/include/wudfwdm.h) NT_ASSERTNT_VERIFY
dll/ntdll(file:///d:/reactos/dll/ntdll) _CxxThrowException 实现、user-side soft exception
dll/ntdll/ldr/ldrpe.c(file:///d:/reactos/dll/ntdll/ldr/ldrpe.c) ntdll 加载与异常初始化
相关推荐
艾莉丝努力练剑1 小时前
【Qt】界面优化:绘图API
linux·运维·开发语言·网络·qt·tcp/ip·udp
方便面不加香菜1 小时前
Linux--基础IO(二)
linux·运维·服务器
牛油果子哥q1 小时前
队列(Queue)深度精讲,先进先出原理、顺序/链式/循环队列、STL queue底层、栈队列互模拟与面试考点全解
开发语言·c++·面试
艾莉丝努力练剑1 小时前
【Linux网络】NAT、内网穿透、内网打洞
linux·运维·服务器·网络·计算机网络·udp·php
聆风吟º1 小时前
【Python编程日志】Python基础数据类型完整梳理
开发语言·python·数据类型
无忧.芙桃1 小时前
Linux信号机制(中)
linux·运维·服务器
liming4951 小时前
Maven中央库迁移
服务器·前端·maven
chushiyunen1 小时前
codex笔记、thinkai中转站
windows
IT小黄人_9992 小时前
联想服务器更换硬盘后手动重建
运维·服务器