Windows异常世界历险记(一)——Windows系统用户级结构化异常处理机制(SEH)的基础知识和Unwind展开操作

这篇文章写于14年10月,彼时正对Windows的SEH机制感兴趣,且国庆假期时间比较充裕,便记了几篇相关的笔记。后来逐渐忙于课业便没继续分析下去,3年来多次想完成这个系列,但每次刚一开头总会被各种琐事打断,一直未能如愿。恰逢近来重读《Windows核心编程》,决心分析清楚Visual C++异常处理机制的工作原理,并在此基础上对比Delphi的异常处理机制。由于此二者的异常处理机制都是建立在Windows用户级结构化异常处理机制的基础上,因此将笔记整理后贴出,希望这一次能如愿以偿地完成分析吧。

Windows系统的用户级结构化异常处理机制(SEH)是基于线程的,因此可为不同的线程指派不同的异常处理函数。且由于其形成了链结构,还可据此为一个线程指定多个异常处理函数,异常发生后,操作系统会沿此链表调用各异常处理函数,直到某个异常处理函数修复了该异常或已达末尾。通常处理完异常后,需要执行展开(Unwind)操作,该操作先通知目标结点前的各异常处理函数释放资源,然后将之前的SEH链全部删除。该操作通常由各高级语言Rtl模块来完成,Win32汇编操作时既可以不展开,也可以手工展开,还可以使用RtlUnwind函数展开。

SEH的基础知识

在Win32环境下,FS寄存器总指向线程环境块_TEB,_TEB的首项中存放的是线程信息块NT_TIB,其首项中即为SHE链的第一个节点地址。下面用Windbg查看以上两个结构。

_TEB结构:

0:000> dt _TEB
ntdll!_TEB
+0x000 NtTib : _NT_TIB //指向_NT_TIB结构
+0x01c EnvironmentPointer : Ptr32 Void
+0x020 ClientId : _CLIENT_ID
+0x028 ActiveRpcHandle : Ptr32 Void
+0x02c ThreadLocalStoragePointer : Ptr32 Void
+0x030 ProcessEnvironmentBlock : Ptr32 _PEB
+0x034 LastErrorValue : Uint4B
.....

_NT_TIB结构:

0:000> dt _NT_TIB
ConsoleApplication1!_NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase : Ptr32 Void
+0x008 StackLimit : Ptr32 Void
+0x00c SubSystemTib : Ptr32 Void
+0x010 FiberData : Ptr32 Void
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32 Void
+0x018 Self : Ptr32 _NT_TIB

故可用fs:[0]来索引SEH链的第一个节点。注意,fs:[4]和fs:[8]分别是线程的栈基址和栈顶限制地址,只有当StackLimit<地址<StackBase时,该地址才位于栈上。这在后面的SafeSEH机制中将被用作合法性检验。

SEH的每个节点都是一个_EXCEPTION_REGISTRATION_RECORD结构,标准结构定义如下:

0:000> dt _EXCEPTION_REGISTRATION_RECORD
ConsoleApplication1!_EXCEPTION_REGISTRATION_RECORD
+0x000 Next : Ptr32 _EXCEPTION_REGISTRATION_RECORD   //指向下一个_EXCEPTION_REGISTRATION_RECORD结构
+0x004 Handler : Ptr32 _EXCEPTION_DISPOSITION       //指向异常处理函数,其返回值为_EXCEPTION_DISPOSITION枚举结构

_EXCEPTION_DISPOSITION枚举结构定义如下:

0:000> dt _EXCEPTION_DISPOSITION
ntdll!_EXCEPTION_DISPOSITION
ExceptionContinueExecution = 0n0 //已处理异常,程序可继续执行
ExceptionContinueSearch = 0n1 //函数不处理该异常,继续搜寻异常处理函数链
ExceptionNestedException = 0n2 //在处理异常的过程中产生了新的异常
ExceptionCollidedUnwind = 0n3 //嵌套展开

异常处理函数定义如下:

EXCEPTION_DISPOSITION __cdecl except_handler (
    struct _EXCEPTION_RECORD *_ExceptionRecord, //指向_EXCEPTION_RECORD
    void * _EstablisherFrame, //指向_EXCEPTION_REGISTRATION_RECORD
    struct _CONTEXT *_ContextRecord, //指向_CONTEXT
    void * _DispatcherContext);
  • 其第一个参数指向一个_EXCEPTION_RECORD结构,该结构将传入异常信息:
0:000> dt _EXCEPTION_RECORD
ConsoleApplication1!_EXCEPTION_RECORD
+0x000 ExceptionCode : Uint4B            //异常类型
+0x004 ExceptionFlags : Uint4B            //异常标志
+0x008 ExceptionRecord : Ptr32 _EXCEPTION_RECORD        //辅助结构
+0x00c ExceptionAddress : Ptr32 Void            //异常发生的地址
+0x010 NumberParameters : Uint4B            //下面传了几个参数
+0x014 ExceptionInformation : [15] Uint4B    //参数具体意义——后两个参数主要适用于RaiseException
/*ExceptionCode 异常类型,最可能遇到的几种类型如下:
              C0000005h----读写内存冲突
              C0000094h----非法除0
              C00000FDh----堆栈溢出或者说越界
              80000001h----由Virtual Alloc建立起来的属性页冲突
              C0000025h----不可持续异常,程序无法恢复执行,异常处理例程不应处理这个异常
              C0000026h----在异常处理过程中系统使用的代码,如果系统从某个例程莫名奇妙的返回,则出现此代码,例如调用RtlUnwind时没有Exception Record参数时产生的异常填入的就是这个代码
              80000003h----调试时因代码中int3中断
              80000004h----处于被单步调试状态

      位:       31~30            29~28           27~16          15~0
          _____________________________________________________________________+
    含义:     严重程度          29位            功能代码        异常代码
              0==成功         0==Mcrosoft     MICROSOFT定义   用户定义
              1==通知         1==客户
              2==警告          283==错误         被保留必须为0
ExceptionFlags 异常标志
0 可修复异常
#define EXCEPTION_NONCONTINUABLE 0x1    // Noncontinuable exception
#define EXCEPTION_UNWINDING 0x2         // Unwind is in progress
#define EXCEPTION_EXIT_UNWIND 0x4       // Exit unwind is in progress
#define EXCEPTION_STACK_INVALID 0x8     // Stack out of limits or unaligned
#define EXCEPTION_NESTED_CALL 0x10      // Nested exception handler call
#define EXCEPTION_TARGET_UNWIND 0x20    // Target unwind in progress
#define EXCEPTION_COLLIDED_UNWIND 0x40  // Collided exception handler call

#define EXCEPTION_UNWIND (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND | \
                          EXCEPTION_TARGET_UNWIND | EXCEPTION_COLLIDED_UNWIND) */
  • 第二个参数指向我们自己定义的_EXCEPTION_REGISTRATION_RECORD结构,除了上述介绍的标准结构的之外,通常我们还会附加一些数据,不同的环境如VC和SDK以及DDK中,都有不同的附加数据。但前两个元素类型一定是固定的。

  • 第三个参数指向一个CONTEXT结构,该结构储存有异常发生时各寄存器的值,可以修改其中的某些寄存器诸如EIP的值,来控制异常成功被处理后程序的执行流程,或清空DR0-4以躲避调试器。

  • 最后一个参数主要用于Unwinding操作。

当异常发生后,Windows将沿着SEH链按次序访问各_EXCEPTION_REGISTRATION_RECORD结构,取得异常处理函数地址后调用它,直到到达链尾或某个异常处理函数报告它已解决了该异常。

SEH的使用和Unwinding的必要性

我们通常在栈中构建SEH的_EXCEPTION_REGISTRATION_RECORD结构,示例代码如下:

push    一堆附加数据
push   offset _Handler
push   fs:[0]
mov    fs:[0],esp

首先在栈中构造了一个_EXCEPTION_REGISTRATION_RECORD,然后将其地址(esp指向栈顶)传给fs:[0],这样,便向SEH链首插入了一个节点。

在该函数中,我们可以对_EXCEPTION_RECORD的ExceptionCode和ExceptionFlags进行判断,如果为预期的异常类型和异常标志,则进行处理然后返回ExceptionContinueExecution;若不为预期的异常则直接返回ExceptionContinueSearch。

下面我们来谈一谈SEH链的展开操作,首先,为什么要展开呢?

我们假设在程序中,主函数调用了func1(),func1又调用了func2();func1()注册了名为SEH1的异常处理函数,该函数会将执行流程修正到func1()的某处;func2()注册了名为SEH2的异常处理函数,该函数会将执行流程修正到func2()的某处。我们假设在执行到func2()函数的某条语句时发生了异常,且只有SEH1能处理该异常。

一方面,假设func2()函数中在异常发生前分配了资源,由于SEH1在处理该异常时直接将地址修正到func1的领空,将造成资源泄露;另外一种情况,若在面向对象编程中,则可能无法执行某个类的析构函数。

另一方面,我们来看看当时的堆栈情况:
这里写图片描述
可以看出,如果SEH1将esp和ebp修正到func1栈桢上后,func2的栈桢将变为未使用状态,若后续再调用其他函数或使用局部变量,可能会覆盖func2注册的_EXCEPTION_REGISTRATION_RECORD结构。当下次再出现异常,系统仍会在改地址取得一个不存在的异常处理函数地址并执行,这是很危险的!

因此,在通常情况下,应调用SEH链上之前的所有异常处理函数,通知其进行清理收尾工作;另一方面,将本节点地址传给fs:[0]。我们把这个过程称为展开(Unwind)操作。

Unwind操作的步骤

Unwind操作分为两种,一种是局部展开,另一种是全局展开(也称退出展开)。

局部展开见诸于某个异常处理函数成功解决了异常后从FS:[0]指向的_EXCEPTION_REGISTRATION_RECORD开始,依次以
EXCEPTION_RECORD.ExceptionCode = 0xC0000027
EXCEPTION_RECORD.ExceptionFlags = 0x2(EXCEPTION_UNWINDING)
调用SEH链上位置在它前面的那些函数。这个操作需要由异常处理函数自己完成。

全局展开则由系统自动进行,发生于SEH链上的所有函数都拒绝处理异常的情况下。此时,系统按SEH链次序以
EXCEPTION_RECORD.ExceptionCode = 0xC0000027
EXCEPTION_RECORD.ExceptionFlags = 0x6(EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)
调用各异常处理函数。Unwind操作中,被调用的函数在完成了清理动作后应返回ExceptionContinueSearch。

如果确定异常处理函数要响应Unwind操作,则应在函数内增加相应的代码。下图为罗云彬前辈给出的一个完整的异常处理函数框架:
SEH函数的框架

好在Windows已经为我们提供了实现展开的API函数RtlUnwind,其原型如下:

void WINAPI RtlUnwind(
  _In_opt_  PVOID TargetFrame,   //目标SEH节点地址,输入则为局部展开;NULL为退出展开
  _In_opt_  PVOID TargetIp,  //局部展开后EIP值
  _In_opt_  PEXCEPTION_RECORD ExceptionRecord,  //附加的ExceptionRecord记录,外部调用一般不需要
  _In_      PVOID ReturnValue  //返回值,留空即可
);

在下一篇中,我们将分析Windows提供的RtlUnwind函数的实现机制,进而引出SafeSEH机制。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值