Structured Exception Handling (SEH) 详细解析

366 阅读5分钟

Structured Exception Handling (SEH) 详细解析

Structured Exception Handling (SEH) 是 Windows 操作系统提供的一种异常处理机制,它允许程序在运行时处理硬件和软件异常。下面我将从多个方面非常详细地解释 SEH。

1. 异常处理的基本概念

1.1 什么是异常

异常是程序执行过程中发生的意外事件,它打断了正常的指令流。异常可以分为:

  • 硬件异常:由 CPU 检测到的异常(如除零、访问违规、页面错误等)
  • 软件异常:由程序显式触发的异常(如 RaiseException API 调用)

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 异常分发流程

  1. 异常发生:CPU 检测到异常或程序调用 RaiseException
  2. 内核接管:控制权转移到内核异常分发器
  3. 用户模式回调:内核将异常分发给用户模式异常处理程序
  4. 遍历 SEH 链:从线程的 SEH 链顶部开始,依次调用每个处理程序
  5. 处理或传播:处理程序决定处理异常或继续传播
  6. 未处理异常:如果没有处理程序捕获异常,调用未处理异常过滤器

3.2 展开(Unwind)操作

当异常被处理时,系统执行展开操作:

  1. 从当前帧回溯到处理程序所在的帧
  2. 对每个跳过的帧,调用其异常处理程序的展开操作
  3. 清理栈上的局部对象
  4. 执行 __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 相关问题时可以:

  1. 查看 FS:[0] 获取当前 SEH 链
  2. 使用 WinDbg 的 !exchain 命令显示异常链
  3. 设置断点于 kernel32!UnhandledExceptionFilter
  4. 检查 EXCEPTION_RECORDCONTEXT 结构

总结

Structured Exception Handling 是 Windows 平台上强大而复杂的异常处理机制,从编译器语法糖到操作系统底层实现,涉及多个层次的协作。理解 SEH 对于开发健壮的 Windows 应用程序、调试复杂问题以及理解系统安全机制都至关重要。