第 8 章 结构化异常处理 --- 8.4 软异常
本节深入剖析软异常(Software Exception)的生成、传播与处理。 与硬件异常(Hardware Exception)由 CPU 在指令执行期间自动触发不同,软异常完全由 软件(API、指令、断言、调试器) 主动生成。本节回答三个核心问题:什么是软异常?它如何与硬件异常统一到同一套 SEH 框架?常见的软异常码有哪些、它们各自代表什么语义?
概述
软异常是与 硬件异常 对应的一类异常,它们的本质区别在于 触发源:
- 硬件异常:CPU 在执行某条指令时检测到错误条件(除零、访问违例、断点等),自动触发异常
- 软异常:由软件主动调用 API、嵌入 int 3/int 2D 指令、断言宏或调试器而触发的异常
虽然触发源不同,ReactOS 内核却把它们 统一 到同一个 KiDispatchException 入口(见 8.2 节)------这是 SEH 框架设计的重要优势。理解这种统一性的关键,是把握 软异常的"统一入口"KiDispatchExceptionFromTrapFrame。
软异常的几个 主要来源:
- 调试器断点 :
int 3(STATUS_BREAKPOINT)、int 2D(STATUS_BREAKPOINT)、int 1(STATUS_SINGLE_STEP) - API 主动抛出 :
RtlRaiseException、RtlRaiseStatus、ExRaiseStatus等 - 断言失败 :
NT_ASSERT、RtlAssert、ASSERT宏 - C++ 异常 :
throw(底层调用_CxxThrowException) - 栈检查失败 :
STATUS_STACK_BUFFER_OVERRUN(/GS保护) - 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_BREAKPOINT、STATUS_SINGLE_STEP、STATUS_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) | RtlRaiseException、RtlRaiseStatus |
| 断言实现 | 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) | ExRaiseStatus、ExRaiseAccessViolation 等 |
| 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) | ASSERT、ASSERTMSG |
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()(调用方) |
| 主要用途 | 反映程序错误 | 反映程序状态/控制流 |
软异常的四大类别
- 控制流类 :
STATUS_BREAKPOINT(int 3)、STATUS_SINGLE_STEP(int 1)、STATUS_ASSERTION_FAILURE(int 2C) - 主动抛出类 :
RtlRaiseException/RtlRaiseStatus发起的任意 NTSTATUS - 安全检查类 :
STATUS_STACK_BUFFER_OVERRUN(int 29,/GS 失败) - C++ 异常类 :
throw编译为_CxxThrowException→RtlRaiseException(异常码=0xE06D7363)
软异常的内核态路径
软异常在内核态通常走 与硬件异常相同的 KiDispatchException 路径------这是 SEH 框架的关键设计点。具体来说:
- 软异常通过
KiDispatchExceptionFromTrapFrame(见 8.4.2)构造一个EXCEPTION_RECORD - 然后调用
KiDispatchException把它当作"硬件异常"处理 - 之后的 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->Eip 或 Eip - 1) |
ParameterCount |
异常参数个数(0-3) |
Parameter1/2/3 |
异常参数 |
TrapFrame |
当前 trap frame(用于决定 PreviousMode) |
关键设计
- 自动判断 PreviousMode :
KiUserTrap(TrapFrame)通过SegCs段选择子决定用户态还是内核态 - FirstChance = TRUE:默认走"第一次机会"路径
- 复用
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 转换为异常
- 自定义错误码传递给上层
- C++
RtlRaiseStatus 的特点
- 不可继续 :
ExceptionFlags = EXCEPTION_NONCONTINUABLE,handler 不能继续执行 - 仅 NTSTATUS:调用者只提供状态码
- 典型用途 :
STATUS_ASSERTION_FAILURE- 状态检查失败时抛出不可恢复异常
内核态对应函数
ExRaiseStatus(Status):内核态主动抛出(设置EXCEPTION_NONCONTINUABLE)ExRaiseAccessViolation():抛出STATUS_ACCESS_VIOLATIONExRaiseDatatypeMisalignment():抛出STATUS_DATATYPE_MISALIGNMENTExRaiseStatus的实现位于 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 与 #DB,Eip - 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 处理。DbgPrint、DebugPrompt 等内核调试器 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 的工作流:
- 打印断言失败信息
- 检查
FLG_DISABLE_DEBUG_PROMPTS(自动化测试用)- 如果设置了 →
RtlRaiseStatus(STATUS_ASSERTION_FAILURE)触发软异常
- 如果设置了 →
- 提示用户选择动作:
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 失败时:
- 调用
RtlAssert(如果链接了 RTL) - 或者直接调用
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 的 _CxxThrowException → RtlRaiseException 实现,与 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
+-------------------------------------+
| 进程被终止 |
+-------------------------------------+
关键路径节点
RtlRaiseException:构造 CONTEXT,调用本地RtlDispatchException- 本地分派失败 :调用
ZwRaiseException系统调用 - 内核
NtRaiseException→KiRaiseException:- 探测用户态指针
- 复制到内核栈
- 构造 trap frame
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 3(0xCC)被设计为单字节指令,目的是允许调试器在内存中直接替换目标字节为 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 的交互式调试循环
RtlAssert 的 boipt 提示循环是 ReactOS 调试基础设施的核心组件。它的设计反映了早期 Windows NT 的调试哲学:开发者与调试器实时交互。
Break repeatedly, break Once, Ignore, terminate Process or terminate Thread (boipt)?
每个选项的深层含义:
-
Break repeatedly(B) :反复断点。每次执行到断言失败处都会停下来。适合需要多次观察同一断言的场景。底层调用
DbgBreakPoint()(int 3),调试器接管后可以检查变量、调用栈等。 -
Break Once(O) :单次断点。断一次后继续执行,如果再次到达断言处则自动忽略。这通过
if条件判断实现------断点后的break语句跳出switch,回到for(;;)循环,但下次进入时Action[0]已经不是 'B' 了。 -
Ignore(I) :忽略断言,继续执行。
RtlAssert直接return,调用方继续执行。这非常危险------断言条件为假意味着程序状态已经不一致,继续执行可能导致更严重的问题。但在某些调试场景下,开发者知道断言是误报,可以选择忽略。 -
terminate Process(P) :终止整个进程。调用
ZwTerminateProcess,进程立即结束,所有线程都被杀死。适合断言失败影响全局状态的场景。 -
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, ...);
内核态直接蓝屏!这是因为:
- 内核栈 cookie 被覆盖意味着内核状态已经被破坏
- 继续执行可能导致数据损坏或安全漏洞
- 蓝屏是保护系统完整性的最后手段
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, ...);
}
安全验证的必要性:
- ProbeForRead:防止用户态传入无效指针导致内核崩溃
- 复制到内核栈:避免 TOCTOU(Time-of-check-to-time-of-use)攻击------用户态代码可能在验证后修改数据
- 标志验证 :只允许
EXCEPTION_NONCONTINUABLE标志,其他位必须为 0 - 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 结构。这种统一性带来了:
- 代码复用:一套分派逻辑处理所有异常,减少代码量和 bug 面
- 语义一致 :
__except块不需要区分异常来源 - 调试器统一接口:调试器只需 hook 一个入口就能拦截所有异常
- 可扩展性:新增异常类型不需要修改分派逻辑
如果分开处理,每个 handler 都需要同时处理硬件和软件两种路径,代码量翻倍,且容易出现不一致。
问题 2:为什么 int 3 指令要设计成 1 字节,而不是和其他 int N 一样是 2 字节?
答 :这是为了支持调试器的断点替换机制。
调试器设置断点的过程是:
- 读取目标地址的原始字节(假设为
0x55,即push ebp) - 将该字节替换为
0xCC(int 3) - 当 CPU 执行到
0xCC时触发断点异常 - 调试器恢复原始字节
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 这个特殊的魔数:
- SEH handler 可以识别 :检查
ExceptionCode == 0xE06D7363来判断是否是 C++ 异常 - 不会与系统异常冲突:系统异常的 Facility 通常是 0,而 C++ 异常的编码完全不同
- Customer 标志位:Bit 30 = 1 表示这是"客户定义"的异常,不是微软标准异常
问题 4:为什么内核态 /GS 失败直接蓝屏,而用户态走异常分派?
答 :这是由内核态和用户态的容错策略不同决定的。
用户态:
- 进程是独立的,一个进程崩溃不影响其他进程
- 走异常分派可以给调试器一个收集信息的机会
- 最终结果是进程终止,系统继续运行
内核态:
- 内核是共享的,一个内核组件的状态损坏可能影响整个系统
- 栈 cookie 被覆盖意味着栈已经被破坏,继续执行可能导致:
- 返回地址被篡改(ROP 攻击)
- 局部变量被覆盖(数据损坏)
- 函数指针被修改(控制流劫持)
- 蓝屏(
KERNEL_SECURITY_CHECK_FAILURE)是保护系统完整性的最后手段
这种差异反映了 Windows NT 的安全哲学:用户态可以崩溃,内核态不能继续执行。
问题 5:为什么 RtlAssert 要提供 boipt 五个选项,而不是直接终止进程?
答 :这是为了支持交互式调试的工作流。
在早期 Windows NT 开发中,调试器(如 WinDbg)通常通过串口连接,开发者需要实时与断言失败交互:
-
Break repeatedly(B):适合需要多次观察同一断言的场景。例如,断言在循环中失败,开发者想观察每次迭代的变量变化。
-
Break Once(O):适合只想看一次现场,然后让程序继续执行的场景。
-
Ignore(I) :适合断言是误报的场景。例如,某些断言在特定条件下不成立,但程序逻辑是正确的。
-
terminate Process(P):适合断言失败影响全局状态,需要完全重启的场景。
-
terminate Thread(T):适合断言失败只影响局部线程,其他线程可以继续工作的场景。
如果直接终止进程,开发者就失去了灵活调试的能力。当然,FLG_DISABLE_DEBUG_PROMPTS 标志允许在自动化环境中跳过交互。
问题 6:为什么 NtRaiseException 需要 ProbeForRead 验证用户态指针?
答 :这是为了防止恶意用户态代码攻击内核。
NtRaiseException 接收用户态传入的 EXCEPTION_RECORD* 和 CONTEXT* 指针。如果不验证:
- 空指针:内核读取地址 0 会触发缺页异常,导致蓝屏
- 内核地址 :用户态传入内核地址(如
0x80000000),内核"读取"的实际上是用户态控制的数据 - 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 的处理路径更复杂(经过 KiDebugServiceHandler → KiDebugHandler),需要额外的补偿。
问题 8:为什么 C++ throw 要通过 RtlRaiseException 而不是直接调用 SEH?
答 :这是为了复用 SEH 的分派和展开机制。
C++ 异常和 SEH 在底层共享同一套基础设施:
- 分派 :
RtlDispatchException遍历 SEH 链表,调用每个 handler - 展开 :
RtlUnwind反向遍历链表,调用__finally块进行清理
如果 C++ 异常直接实现自己的分派逻辑:
- 需要重新实现链表遍历
- 需要处理与 SEH 的交互(如
/EHa模式) - 需要处理嵌套异常和展开冲突
通过调用 RtlRaiseException,C++ 异常可以:
- 复用 SEH 的分派逻辑
- 与 SEH handler 协作(
__except可以捕获 C++ 异常) - 确保
__finally块在栈展开时正确执行
问题 9:为什么 STATUS_BREAKPOINT 的 Severity 是 Information(0x8),而不是 Error(0xC)?
答 :这是因为断点不是错误,而是控制流机制。
STATUS_BREAKPOINT(0x80000003)的 Severity 位是 0(Information),这反映了它的语义:
- 断点是预期的:开发者主动插入断点,用于调试目的
- 断点可以被处理:调试器处理断点后,程序可以继续执行
- 断点不是错误:程序状态没有损坏,不需要恢复
如果 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 节建立了软异常的完整视图。关键要点:
- 软异常定义:由软件主动生成的异常,与硬件异常(CPU 自动触发)相对
- 软异常统一入口 :
KiDispatchExceptionFromTrapFrame接收异常码/标志/地址/参数,构造EXCEPTION_RECORD后调用KiDispatchException - 四大类别 :
- 控制流(int 1/3/2C/2D/29)
- 主动抛出(
RtlRaiseException/RtlRaiseStatus) - 断言失败(
RtlAssert/NT_ASSERT/ASSERT) - C++ 异常(
_CxxThrowException)
- 统一框架 :所有软异常在
KiDispatchException内部与硬件异常完全合流 - NTSTATUS 编码:Severity + Customer + Reserved + Facility + Code
- /GS 保护 :
STATUS_STACK_BUFFER_OVERRUN软异常 - C++ 异常魔数 :
0xE06D7363("msc") - 调试器软中断:int 1/3/2C/2D/29 都有专门的 trap handler
- RtlAssert 提示循环:boipt(break/once/ignore/process/thread)交互式
- 跨架构一致性:软异常路径在 x86/x64/ARM 上完全一致
至此第 8 章完整覆盖了 ReactOS 结构化异常处理的全部内容:8.1 框架、8.2 内核态、8.3 用户态、8.4 软异常。
本章代码索引
| 文件 | 内容 |
|---|---|
| exp.c(file:///d:/reactos/ntoskrnl/ke/i386/exp.c) | KiDispatchExceptionFromTrapFrame、KiDispatchException |
| 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) | KiContinue、KiRaiseException、NtRaiseException、NtContinue |
| exception.c(file:///d:/reactos/sdk/lib/rtl/exception.c) | RtlRaiseException、RtlRaiseStatus、RtlUnhandledExceptionFilter |
| 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_PARAMETERS、EXCEPTION_* 标志 |
| xdk/rtltypes.h(file:///d:/reactos/sdk/include/xdk/rtltypes.h) | EXCEPTION_RECORD |
| ntosifs.h(file:///d:/reactos/sdk/include/ntosifs.h) | ExRaiseStatus、ExRaiseAccessViolation、ExRaiseDatatypeMisalignment |
| 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) | ASSERT、ASSERTMSG |
| wudfwdm.h(file:///d:/reactos/sdk/include/wudfwdm.h) | NT_ASSERT、NT_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 加载与异常初始化 |