• 当前位置:首页>>编程开发A>>安全防御>>A Crash Course on the Depths of Win32 Structured Exception Handling
  • A Crash Course on the Depths of Win32 Structured Exception Handling
  • 创建时间:2005-03-16
    文章属性:翻译
    文章提交:tombkeeper (t0mbkeeper_at_hotmail.com)

    A Crash Course on the Depths of Win32 Structured Exception Handling

    Matt Pietrek 著  
    董岩 译

    在所有 Win32 操作系统提供的机制中,使用最广泛的未公开的机制恐怕就要数结构化异常处理(structured exception handling,SEH)了。一提到结构化异常处理,可能就会令人想起 _try、_finally 和 _except 之类的词儿。在任何一本不错的 Win32 书中都会有对 SEH 详细的介绍。甚至连 Win32 SDK 里都对使用 _try、_finally 和 _except 进行结构化异常处理作了完整的介绍。既然有这么多地放都提到了 SEH,那我为什么还要说它是未公开的呢?本质上讲,Win32 结构化异常处理是操作系统提供的一种服务。编译器的运行时库对这种服务操作系统实现进行了封装,而所有能找到的介绍 SEH 的文档讲的都是针对某一特定编译器的运行时库。关键字 _try、_finally 和 _except 并没有什么神秘的。微软的 OS 和编译器定义了这些关键字以及它们的行为。其它的 C++ 编译器厂商也只需要尊从它们定好的语义就行了。在编译器的 SEH 层减少了直接使用纯操作系统的 SEH 所带来的危害的同时,也将纯操作系统的 SEH 从大家的面前隐藏了起来。

    我收到过大量的电子邮件说他们都需要实现编译器级的 SEH 但却找不到公开的文档。本来,我可以指着 Visual C++ 和 Borlang C++ 的运行时库的源代码说看一下它们就行了。但是,不知道是什么原因,编译器级的 SEH 仍是个天大的秘密。微软和 Borland 都没有提供 SEH 最内层的源代码。

    在本文中,我会从最基本的概念上讲解结构化异常处理。在讲解的时候,我会将操作系统所提供的与编译器代码生成和运行时库支持的分离开来。当深入关键性操作系统程序的代码时,我基于的都是 Intel 版的 Windows NT 4.0。然而。我所讲的大部分内容同样适用于其它的处理器。

    我会避免提及实际的 C++ 的异常处理,C++ 下用的是 catch() 而不是 _except。其实,真正的 C++ 异常处理的实现方式和我所讲的方式也是极为相似的。但是,真正 C++ 异常处理特有的复杂性会影响到我这里所讲的概念。对于深挖那些晦涩的 .H 和 .INC 文件并拼凑出 Win32 SEH 的相关代码,最好的一个信息来源就是 IBM OS/2 的头文件(特别是 BSEXCPT.H)。这对有相关经验的人并没什么可希奇的,这里讲的 SEH 机制在微软开发 OS/2 时就定义了。因此,Win32 的 SEH 与 OS/2 的极为相似。

    SEH in the Buff

    若将 SEH 的细节都放到一起讨论,任务实在艰巨,因此,我会从简单的开始,一层一层往深里讲。如果之前从未使用过结构化异常处理,则正好心无杂念。若是用过,那就要努力将  _try、GetExceptionCode 和
    EXCEPTION_EXECUTE_HANDLER从脑子中扫出,假装这是一个全新的概念。Are you ready?Good。

    当线程发生异常时,操作系统会将这个异常通知给用户使用户能够得知它的发生。更特别的是,当线程发生异常时,操作系统会调用用户定义的回调函数。这个回调函数想做什么就能做什么。例如,它可以修正引起异常的程序,也可以播放一段 .WAV 文件。无论回调函数干什么,函数最后的动作都是返回一个值告诉系统下面该干些什么(这样说并不严格,但目前可以认为是这样)。既然在用户代码引起异常后,操作系统会回调用户的代码,那这个回调函数又是什么样的呢?换句话说,关于异常都需要知道哪些信息呢?其实无所谓,因为 Win32 已经定义好了。异常的回调函数的样子如下:

    EXCEPTION_DISPOSITION
    __cdecl _except_handler(
         struct _EXCEPTION_RECORD *ExceptionRecord,
         void * EstablisherFrame,
         struct _CONTEXT *ContextRecord,
         void * DispatcherContext
         );

    这个函数原型来自标准 Win32 头文件 EXCPT.H,初看上去让人有点眼晕。如果慢慢看的话,似乎情况还没那么严重。对于初学者来说,大可以忽略返回值的类型 (EXCEPTION_DISPOSITION)。所需知道的就是这个函数叫 _except_handler,需要四个参数。

    第一个参数是一个指向 EXCEPTION_RECORD 的指针。这个结构体定义在 WINNT.H 中,定义如下:

    typedef struct _EXCEPTION_RECORD {
        DWORD ExceptionCode;
        DWORD ExceptionFlags;
        struct _EXCEPTION_RECORD *ExceptionRecord;
        PVOID ExceptionAddress;
        DWORD NumberParameters;
        DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
    }  EXCEPTION_RECORD;

    参数 ExceptionCode 是操作系统分配给异常的号。在 WINNT.H 文件中查找开头为“STATUS_” 的宏就能找到一大堆这样的异常代号。例如,大家熟知的 STATUS_ACCESS_VIOLATION 的代号就是 0xC0000005。更为完整的异常代号可以从 Windows NT DDK 中的 NTSTATUS.H 文件里找到。EXCEPTION_RECORD 结构体的第四个元素是异常发生处的地址。其余的 EXCEPTION_RECORD 域目前都可以忽略掉。_except_handler 函数的第二个参数是一个指向 establisher frame 结构体的指针。在 SEH 里这可是个重要的参数,不过现在先不用管它。第三个参数是一个指向 CONTEXT 结构体的指针。CONTEXT 结构体定义在 WINNT.H 文件中,它保存着某一线程的寄存器的值。Figure 1 即为 CONTEXT 结构体的域。当用于 SEH 时,CONTEXT 结构体保存着发生异常时各寄存器的值。无独有偶,GetThreadContext 和 SetThreadContext 使用的也是相同的 CONTEXT 结构体。第四个也是最后的一个参数叫做 DispatcherContext,现在先不去管它。

    简单总结一下,当发生异常时会调用一个回调函数。这个回调函数需要四个参数,其中三个都是结构体指针。在这些结构体中,有些域重要,有些并不重要。关键的问题是 _except_handler 回调函数收到了大量的信息,比如异常的类型和发生的位置。异常回调函数需要使用这些信息来决定所采取的行动。

    我很想现在就给出一个样例程序来说明 _except_handler,只是仍有一些东西需要解释,即当异常发生时操作系统是如何知道在那里调用回调函数呢?答案在另一个叫 EXCEPTION_REGISTRATION 的结构体中。本文通篇都能见到这个结构体,因此对这部分还是不要囫囵吞枣为好。唯一能找到  EXCEPTION_REGISTRATION 正式定义的地方就是 Visual C++ 运行时库源代码中的 EXSUP.INC 文件:

    _EXCEPTION_REGISTRATION struc
         prev    dd      ?
         handler dd      ?
    _EXCEPTION_REGISTRATION ends

    可以看到,在 WINNT.H 的 NT_TIB 结构体定义中,这个结构体被称为 _EXCEPTION_REGISTRATION_RECORD。然而 _EXCEPTION_REGISTRATION_RECORD 的定义是没有的,因此我所能用的只能是 EXSUP.INC 中的汇编语言的 struc 定义。对于我前面提到的 SEH 的未公开,这就是一例。

    不管怎样,我们回到目前的问题上来。当异常发生时,OS 是如何知道调用位置的呢?EXCEPTION_REGISTRATION结构体有两个域,第一个先不用管。第二个域,handler,为一个指向 _except_ handler 回调函数的指针。有点儿接近答案了,但是还有个问题就是,OS 从哪里能找到这个 EXCEPTION_REGISTRATION 结构体呢?

    为了回答这个问题,需要记住结构化异常处理是以线程为基础的。也就是说,每一个线程都有自己的异常处理回调函数。在1996年5月的专栏中,我讲了一个关键的 Win32 数据结构,线程信息块(TEB 或 TIB)。这个结构体中有一个域对于 Windows NT, Windows 95, Win32s 和 OS/2 都是相同的。TIB 中的第一个 DWORD 是一个指向线程的 EXCEPTION_REGISTRATION 结构体的指针。在 Intel 的 Win32 平台上,FS 寄存器永远指向当前的 TIB,因此,在 FS:[0] 就可以找到指向 EXCEPTION_REGISTRATION 结构体的指针。答案出来了!当异常发生时,系统察看出错线程的 TIB 并取回一个指向 EXCEPTION_REGISTRATION 结构体的指针,从而得到一个指向 _except_handler 回调函数的指针。现在操作系统已经有足够的信息来调用 _except_handler 函数了,见 Figure 2。

    把目前这一小点儿东西凑到一起,我写了一个小程序来演示所讲到的这
  • 上一篇:安全编程: 避免竞争条件
    下一篇:The NT Insider:Stop Interrupting Me -- Of PICs and APICs