首页
社区
课程
招聘
[原创]浅析64位Windows的SEH机制
发表于: 2026-3-29 22:01 4427

[原创]浅析64位Windows的SEH机制

2026-3-29 22:01
4427

笔者花费了一些时间,学习了64位WindowsSEH机制的底层原理。现写为博客,希望对大家有帮助。

注意:本文是从逆向工程的角度分析64位Windows的SEH机制,阅读本文的前提是能够从程序员(开发)的角度理解SEH机制。

如果读者从未了解过SEH机制,请先阅读以下文章:

Windows SEH机制(一)

Windows SEH之全局展开

Windows SEH机制(二)

要使SEH发挥作用,必须得到编译器、硬件和操作系统的配合支持。SEH 在特定平台上的具体实现方式可能因架构而异。

下图总结了Windows异常处理机制的基本框架(摘自《加密与解密》):

图片描述

这里先给出一个基本的示例程序,便于我们讨论:

64位中的SEH机制不再把异常处理下相关信息放到栈上,而是由编译器生成相关信息,并在放到PE文件中一个特定的节区中:.pdata。在64位SEH机制中需要学习的第一个结构就是RUNTIME_FUNCTION。若干RUNTIME_FUNCTION结构体组成一个数组,放在.pdata节区中。该结构体定义如下:

该结构体包含三个字段:

通过PE工具,我们可以查看到该结构体数组。例如,对于上面的程序,使用cff程序查看异常目录:

图片描述

这个结构体的数量和__try块的数量无关,而与包含__try块的函数有关(不管函数中有多少个__try块)。并且可以看到,RUNTIME_FUNCTION在.pdata中的排序方式是以BeginAddress为准的升序排序。

为什么这样排列?这是为了支持 O(log n) 时间复杂度的二分查找算法。当异常发生时,内核异常分发器(RtlLookupFunctionEntry)需要通过异常地址 RIP快速定位所属函数。二分查找是唯一能在对数时间内完成此任务的高效算法,而二分查找的前提条件就是数据集有序

注意,上面的三个地址都是相对于ImageBase的偏移量。所以尽管是在64位系统上,这三个地址也只有32位。

我们使用IDA来查看main函数对应的RUNTIME_FUNCTION条目。通过快捷键g就可以快速定位到main函数的RUNTIME_FUNCTION:

图片描述

可以将前两个字段的值放到放到反汇编窗口中查看,就是main函数的起始和结束地址。

接下来说说UnwindData指向的UNWIND_INFO结构体。

这个结构体十分关键,其中的内容是SEH的核心。UNWIND_INFO结构体定义如下:

UNWIND_INFO结构体定义较为复杂,我们只关注其中的关键部分。

根据Flags的不同,UNWIND_INFO结构体的匿名联合体字段有不同的解释,ExceptionData字段是否有效也于Flags字段有关。

下面的UNWIND_INFO结构体的定义更好的展示了Flags字段的影响:

**为了快速讲清楚重点,这里我们只讨论Flags设置为UNW_FLAG_EHANDLER的情况。**一个典型的Flags被设置为UNW_FLAG_EHANDLER的函数如下:

当Flags设置为UNW_FLAG_EHANDLER时,此时union中的ExceptionHandler(编程语言相关)字段有效,由编译器负责填写。如果发生异常且指令指针为>= BeginAddress和< EndAddress,就调用该ExceptionHandler

ExceptionHandler会解析ExceptionData字段来确定如何处理发生的异常。

SCOPE译为范围,能力

ExceptionData是指向C_SCOPE_TABLE结构体的指针的偏移量(也就是指向C_SCOPE_TABLE的指针),该结构体定义如下(务必仔细阅读):

RUNTIME_FUNCTION描述了包含SEH的函数的整个范围,而SCOPE_TABLE描述了函数内每个单独的__try/__except块。

以上面的代码为例,生成的汇编代码大致如下:

套到RUMTIME_FUNCTION和UNWIND_INFO结构体如下:

图片描述

到现在为止,我们可以总结一下x64的SEH机制如何定位到发生异常的地方:假设发生异常的地址为EA,先通过二分查找在RUNTIME_FUNCTION(比较规则:BeginAddress < EA < EndAddress)定位到发生异常的函数,然后遍历ExceptionData指向的C_SCOPE_TABLE_ENTRY数组,定位到发生异常的try块(比较规则:Begin < EA < End)。

这是查找的基本流程,但是我们可以再深入一些。

每当发生异常时,就会调用内部 Windows 函数 RtlDispatchException。该函数在用户模式异常的 NTDLL 模块中实现,在内核模式异常的 NTOSKRNL 模块中实现,方式略有不同。该函数通过执行一些初始检查来开始执行:如果存在用户模式VEH,则将调用该异常处理程序;否则将进行标准 SEH 处理。

标准SEH处理:复制异常时的线程上下文,并利用RtlLookupFunctionEntry函数来执行一项重要任务:获取PE文件的ImageBase和RUNTIME_FUNCTION结构。

现在,我们来追踪一个 64 位 Windows 程序发生除零异常 (#DE) 后,从 CPU 陷阱到最终执行 __except 块的完整、详细的系统级调用链和数据结构操作。这将深入到内核和运行时库的内部。

1. 触发异常

2. CPU 硬件操作

3. 内核陷阱处理 (KiDivideErrorFault)

这是最核心的调度器。其内部逻辑复杂,但关键步骤如下:

1. 构建 CONTEXTEXCEPTION_POINTERS

2. 首次尝试:用户模式异常分发内核检查异常地址,发现 RIP 在用户空间。于是调用 RtlDispatchException 函数(这是用户态异常分发的内核入口)。

3. RtlLookupFunctionEntry 的查找过程这个函数是第一阶段查找的具体实现。

4. __C_specific_handler 的工作(用户态回调!)这是 MSVC 运行时库 (vcruntimexxx.dll) 提供的函数。注意:此时 CPU 仍在内核模式,但该函数是用户态代码,内核会临时切换到用户态执行它。

1. 内核收到 ExceptionExecuteHandler 后的操作内核的 KiDispatchException 收到 RtlDispatchException 返回 TRUE,知道找到了处理程序。

2. RtlUnwindEx 的展开过程这是实际的栈帧展开器,它解释执行 UNWIND_CODE 数组。

1. 最终返回 (KiContinuePreviousMode)内核将修改后的 CONTEXT 写回陷阱帧,然后执行 iretq(或 sysret)指令,返回到用户态。但返回的 RIP 不是原来的 idiv 之后,而是 __except 块的地址。

2. 用户态恢复执行

关键数据结构和调用链总结:

调用链总结如下:

上面的代码涉及到了较多的结构体,大家可以参考钱松林老师的《C++反汇编与逆向分析技术揭秘》一书。

在UNWIND_INFO结构体中有一个UnwindCode字段,指向UNWIND_CODE结构体。

UNWIND_CODE 结构体是 64 位 SEH 机制中的“展开脚本”或“逆操作指令集”。它不直接参与异常处理的决策(这是 SCOPE_RECORD 的工作),而是负责异常处理流程中的执行环节——即如何安全、正确地将栈帧恢复到函数调用前的状态。这是实现栈展开(Stack Unwind)的基石。

你可以将函数调用过程想象成搭积木:

关键点:异常可能发生在函数序言之后、尾声之前的任何位置。系统必须能够从任意点将栈恢复到函数入口之前的状态。UNWIND_CODE 提供了完成此操作所需的精确、逐步的逆操作指令

定义位于 winnt.h

字段含义

让我们结合一个具体例子,跟踪流程。假设有以下函数:

编译器会为它生成以下逻辑的 UNWIND_CODE 数组(伪代码表示):

注意CodeOffset 是累加的。第一条对应序言第0字节(push rbx),第二条对应第1字节(push rsi),第三条对应第2字节(sub rsp,0x20)。

场景1:异常发生在序言之后,函数体内部(例如在 sub rsp, 0x20 之后)

场景2:异常发生在序言执行过程中(例如在 push rsi 之后,sub rsp,0x20 之前)

场景3:用于 __finally 的展开

当为 __finally 展开时,过程相同。UNWIND_CODE 确保栈被正确恢复,然后系统会跳转到 __finally 块执行清理代码,最后继续展开。

重要提示:展开时的“pop”操作是逻辑上的。实际过程是:

栈内存本身的内容不会被修改,只是 CONTEXT 被更新,为后续恢复执行做准备。

阶段 关键函数 关键数据结构 操作
硬件陷阱 KiDivideErrorFault KTRAP_FRAME 保存用户上下文
内核分发 KiDispatchException EXCEPTION_RECORD, CONTEXT 构建异常信息
用户分发 RtlDispatchException - 协调分发流程
函数查找 RtlLookupFunctionEntry RUNTIME_FUNCTION 数组 二分查找函数
展开信息 - UNWIND_INFO, UNWIND_CODE 描述栈帧布局
作用域查找 __C_specific_handler SCOPE_RECORD 数组 线性查找 try 块
栈展开 RtlUnwindEx UNWIND_CODE 解释执行展开指令
返回 KiContinuePreviousMode KTRAP_FRAME 恢复用户态上下文
操作码 含义 对应的序言指令示例 展开时的逆操作(由 RtlUnwindEx 执行)
UWOP_PUSH_NONVOL 压入非易失寄存器 push rbx 从栈上弹出值,并恢复 CONTEXT 中对应寄存器的值RSP += 8
UWOP_ALLOC_SMALL 分配小栈空间 sub rsp, 0x20 RSP += 分配大小
UWOP_ALLOC_LARGE 分配大栈空间 sub rsp, 0x1000 RSP += 分配大小
UWOP_SAVE_NONVOL 保存非易失寄存器到栈上 mov [rsp+0x10], rbx 从栈上指定偏移处读取值,恢复 CONTEXT 中对应寄存器的值
UWOP_SAVE_XMM128 保存 XMM 寄存器 movaps [rsp+0x20], xmm6 恢复 CONTEXT 中的 XMM 寄存器。
UWOP_SET_FPREG 建立帧指针 mov rbp, rsp RBP 恢复为 CONTEXT 中保存的调用者值。
UWOP_PUSH_MACHFRAME 压入机器帧(用于硬件中断) 由硬件中断自动完成 特殊中断恢复。

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 6
支持
分享
最新回复 (4)
雪    币: 3917
活跃值: (4756)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2
太硬核了
2026-4-3 10:47
0
雪    币: 1966
活跃值: (1829)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
感谢分享~~
2026-4-7 09:27
0
雪    币: 3769
活跃值: (3726)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
tql
2026-4-8 10:27
0
雪    币: 1966
活跃值: (1829)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
tql
2026-4-9 22:37
0
游客
登录 | 注册 方可回帖
返回