Structured Exception Handling (SEH) 详细解析
Structured Exception Handling (SEH) 是 Windows 操作系统提供的一种异常处理机制,它允许程序在运行时处理硬件和软件异常。下面我将从多个方面非常详细地解释 SEH。
1. 异常处理的基本概念
1.1 什么是异常
异常是程序执行过程中发生的意外事件,它打断了正常的指令流。异常可以分为:
- 硬件异常:由 CPU 检测到的异常(如除零、访问违规、页面错误等)
- 软件异常:由程序显式触发的异常(如
RaiseExceptionAPI 调用)
1.2 异常处理的目的
异常处理机制允许程序:
- 从错误中恢复或优雅地退出
- 防止程序崩溃
- 分离正常代码和错误处理代码
- 提供统一的错误处理方式
2. SEH 的核心组件
2.1 EXCEPTION_REGISTRATION_RECORD 结构
这是 SEH 的基本构建块,定义如下:
struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD* Next;
PEXCEPTION_ROUTINE Handler;
};
- Next:指向下一个异常处理记录的指针,形成链表结构
- Handler:异常处理函数的指针
2.2 异常处理函数原型
EXCEPTION_DISPOSITION __cdecl _except_handler(
_In_ struct _EXCEPTION_RECORD* _ExceptionRecord,
_In_ void* _EstablisherFrame,
_Inout_ struct _CONTEXT* _ContextRecord,
_Inout_ void* _DispatcherContext
);
返回值为 EXCEPTION_DISPOSITION 枚举,决定如何处理异常:
ExceptionContinueExecution:继续执行ExceptionContinueSearch:继续搜索其他处理程序ExceptionNestedException:嵌套异常ExceptionCollidedUnwind:碰撞展开
3. SEH 的工作流程
3.1 异常分发流程
- 异常发生:CPU 检测到异常或程序调用
RaiseException - 内核接管:控制权转移到内核异常分发器
- 用户模式回调:内核将异常分发给用户模式异常处理程序
- 遍历 SEH 链:从线程的 SEH 链顶部开始,依次调用每个处理程序
- 处理或传播:处理程序决定处理异常或继续传播
- 未处理异常:如果没有处理程序捕获异常,调用未处理异常过滤器
3.2 展开(Unwind)操作
当异常被处理时,系统执行展开操作:
- 从当前帧回溯到处理程序所在的帧
- 对每个跳过的帧,调用其异常处理程序的展开操作
- 清理栈上的局部对象
- 执行
__finally块中的代码
4. SEH 的实现级别
4.1 编译器级 SEH (__try/__except/__finally)
Microsoft C/C++ 编译器提供的语法扩展:
__try {
// 可能引发异常的代码
}
__except (filter_expression) {
// 异常处理代码
}
__try {
// 代码块
}
__finally {
// 清理代码,总是执行
}
4.1.1 过滤器表达式
__except 的括号内是一个过滤器表达式,可以是:
EXCEPTION_EXECUTE_HANDLER(1):执行处理块EXCEPTION_CONTINUE_SEARCH(0):继续搜索处理程序EXCEPTION_CONTINUE_EXECUTION(-1):继续执行
4.2 操作系统级 SEH
直接使用 Windows API:
// 注册异常处理程序
EXCEPTION_REGISTRATION_RECORD seh;
seh.Handler = my_handler;
seh.Next = (EXCEPTION_REGISTRATION_RECORD*)__readfsdword(0);
__writefsdword(0, (DWORD)&seh);
// 异常处理函数
EXCEPTION_DISPOSITION my_handler(...) {
// 处理异常
}
5. SEH 的底层细节
5.1 FS 寄存器的作用
在 x86 架构中,FS 寄存器指向线程信息块 (TIB),其偏移 0 处是当前线程的 SEH 链头指针。
5.2 异常记录结构
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD* ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
重要字段:
ExceptionCode:异常类型(如 0xC0000094 表示除零)ExceptionFlags:标志位(如 0x01 表示不可继续)ExceptionAddress:异常发生地址
5.3 上下文记录结构
CONTEXT 结构包含异常发生时所有寄存器的状态,用于恢复执行或检查状态。
6. SEH 的安全考虑
6.1 SEH 覆盖攻击
攻击者可能覆盖 SEH 链中的处理程序指针,控制程序执行流。现代 Windows 提供了缓解措施:
- SafeSEH:只允许调用模块中预注册的异常处理程序
- SEHOP (SEH Overwrite Protection):验证整个 SEH 链的完整性
6.2 编码最佳实践
- 避免在异常处理程序中执行复杂操作
- 确保异常处理程序本身不会引发异常
- 谨慎使用
EXCEPTION_CONTINUE_EXECUTION - 在驱动程序中使用 SEH 时要特别小心
7. 高级主题
7.1 向量化异常处理 (VEH)
Windows XP 引入的扩展机制,允许注册全局异常处理程序,在 SEH 之前调用。
PVOID AddVectoredExceptionHandler(
ULONG First,
PVECTORED_EXCEPTION_HANDLER Handler
);
7.2 顶层异常过滤器
通过 SetUnhandledExceptionFilter 设置未处理异常过滤器,捕获未被任何 SEH 处理的异常。
7.3 软件异常与硬件异常
- 软件异常通过
RaiseException触发 - 硬件异常由 CPU 检测并转换为 Windows 异常代码
7.4 异常代码格式
Windows 异常代码是 32 位值,格式如下:
3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
+-+-+-+-+-+-+-+-----------------------+-------------------------------+
|S|R|C|N|r| Facility | Code |
+-+-+-+-+-+-+-+-----------------------+-------------------------------+
- S - 严重程度 (1=错误, 0=成功)
- R - 保留位
- C - 客户代码标志 (Microsoft/客户定义)
- N - NTSTATUS 标志
8. 实际示例
8.1 使用 __try/__except
__try {
// 危险操作
int* p = NULL;
*p = 42; // 访问违规
}
__except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_SEARCH) {
printf("捕获访问违规异常\n");
}
8.2 手动注册 SEH 处理程序
EXCEPTION_DISPOSITION __cdecl MyHandler(
EXCEPTION_RECORD* pRecord,
void* pFrame,
CONTEXT* pCtx,
void* pDispatch)
{
if (pRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
printf("处理访问违规\n");
return ExceptionContinueExecution;
}
return ExceptionContinueSearch;
}
void TestSEH()
{
// 手动构建 SEH 帧
EXCEPTION_REGISTRATION_RECORD seh;
seh.Handler = MyHandler;
seh.Next = NULL;
// 注册到 FS:[0]
__asm {
push seh.Handler
push fs:[0]
mov fs:[0], esp
}
// 触发异常
int* p = NULL;
*p = 42;
// 恢复 SEH 链
__asm {
mov eax, [esp]
mov fs:[0], eax
add esp, 8
}
}
9. SEH 与 C++ 异常的关系
- C++ 异常在 Windows 上通常使用 SEH 实现
catch块被转换为 SEH 处理程序- 抛出异常时,编译器生成对
CxxThrowException的调用,后者使用RaiseException
10. 调试 SEH
调试 SEH 相关问题时可以:
- 查看 FS:[0] 获取当前 SEH 链
- 使用 WinDbg 的
!exchain命令显示异常链 - 设置断点于
kernel32!UnhandledExceptionFilter - 检查
EXCEPTION_RECORD和CONTEXT结构
总结
Structured Exception Handling 是 Windows 平台上强大而复杂的异常处理机制,从编译器语法糖到操作系统底层实现,涉及多个层次的协作。理解 SEH 对于开发健壮的 Windows 应用程序、调试复杂问题以及理解系统安全机制都至关重要。