当前位置:天才代写 > tutorial > C语言/C++ 教程 > Win32布局化异常处理惩罚(SEH)探秘(上)

Win32布局化异常处理惩罚(SEH)探秘(上)

2017-11-04 08:00 星期六 所属: C语言/C++ 教程 浏览:411

副标题#e#

在 Win32 操纵系统提供的所有成果中,利用最遍及但最缺乏文档描写的也许就是布局化异常处理惩罚了(SEH),当你思量 Win32 布局化异常处理惩罚时,你也许会想到诸如 _try,_finally 以及 _except 这些术语。你能在任何有关 Win32 的书中发明对 SEH 很好的描写(纵然是 remedial)。即即是 Win32 SDK 也具备有相当完整的利用 _try,_finally 和 _except 举办布局化异常处理惩罚的概述。

有了这些文档,那为何还说 SEH 缺乏文档呢?其实,Win32 布局化异常处理惩罚是操纵系统提供的一个处事。你能找到的关于 SEH 的所有文档都是描写特定编译器的运行时库,这个运行库对操纵系统实现举办包装。_try,_finally 和 _except 这些要害字没有任何神奇的处所。微软的操纵系统及其编译器系列界说这些要害字和用法。其他的编译器提供商则只是沿用这些语义。固然借助编译器层的 SEH 可以挽回一些原始操纵系统级 SEH 处理惩罚不良口碑,但在公共眼里对原始操纵系统 SEH 细节的处理惩罚感受依旧。

我收到人们大量的e-mail,都是想要实现编译器级的 SEH 处理惩罚,又无法找到操纵系统成果提供的相关文档。凡是我都是发起参考 Visual C++ 可能 Borland C++ 运行库源代码。唉,出于一些未知的原因,编译器级的 SEH 好像是一个大的奥秘,微软和 Borland 都不提供其对 SEH 支持的焦点层源代码。

在本文中,我将一层一层对 SEH 举办剖解,以便揭示其最根基的观念。我规划通过代码发生和运行时库支持将操纵系统提供的成果和编译器提供的成果分隔。当我深入代码考查要害的操纵系统例程时,我将利用 Intel 平台上的 Windows NT4.0 作为基本。但我将要描写的大大都内容同样合用于其它处理惩罚器上运行的应用。

我规划制止涉及到真正的 C++ 异常处理惩罚,它们利用 catch(),而不是 _except。其实,真正的 C++ 异常处理惩罚实现很是雷同于本文中描写的内容。可是 C++ 异常处理惩罚有一些特另外巨大性会影响我想要涉及的观念。

通过深入研究艰涩的 .H 和 .INC 文件来归纳 Win32 SEH 组成,我发明有一个信息源之一就是 IBM OS/2 头文件(尤其是 BSEXCPT.H)。为此你不要以为大惊小怪。。此处描写的 SEH 机制在其源头被界说时,微软仍然开拓 OS/2 平台(译注: OS/2 平台起初是IBM 和 微软配合研发的,厥后由于各种原因两个公司没有再继承下去)。所以你会发明Win32 下的 SEH 和 OS/2 下的 SEH 极其相似。

SEH 浅析

从整体来看,SEH 的可谓不行一世,绝对名列前茅,我将从细微之处开始,用我本身的方法一层一层研究。假如你是一张白纸,以前从没打仗过布局化异常处理惩罚,那就最好不外了。假如你以前利用过 SEH。那就实验清理你脑子中的 _try,GetExceptionCode 和 EXCEPTION_EXECUTE_HANDLER 等诸如此类的词,权当本身是个新手。做一个深呼吸,筹备好了吗?好,我们开始。

想象一下,我汇报你某个线程堕落了,操纵系统给你一个时机通知了这个线程错误,可能再详细一点,当线程堕落伍,操纵系统挪用某个用户界说的回调函数。这个回调函数可以所任何它想做的工作。譬喻,它可以修复任何原因导致的错误,可能播放一个 .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_”开始的 #defines 内容便可以获得一系列差异的异常编码。譬喻 STATUS_ACCESS_VIOLATION 是各人再熟悉不外的异常编码了,其值是 0xC0000005。更巨大的异常编码可以从 Windows NT DDK 的 NTSTATUS.H 文件中找到。EXCEPTION_RECORD 布局中的第四个元素是异常产生的地点。剩下的 EXCEPTION_RECORD 域此刻可以忽略,不消管它。

_except_handler 回调函数的第二个参数是指向成立者框架(establisher frame)布局的指针,在 SEH 中它是一个至关重要的参数,但此刻可以不消体贴它。

_except_handler 回调函数的第三个参数是 CONTEXT 布局的指针。CONTEXT 布局在 WINNT.H 中界说,它暗示特定线程异常产生时寄存器的值:

#p#分页标题#e#

typedef struct _CONTEXT
{
DWORD ContextFlags;
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
FLOATING_SAVE_AREA FloatSave;
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
DWORD Ebp;
DWORD Eip;
DWORD SegCs;
DWORD EFlags;
DWORD Esp;
DWORD SegSs;
} CONTEXT;


#p#副标题#e#

另外,这个 CONTEXT 布局与 GetThreadContext 和 SetThreadContext API 函数利用的布局是沟通的。

_except_handler 回调函数的第四个参数是 DispatcherContext。此刻也可以忽略它。

为了简化起见,当异常产生时,你有一个回调函数被挪用。此回调函数带四个参数,个中三个是布局指针。在这些布局中,某些域是很重要的,其余的不是那么重要。要害是 _except_handler 回调函数吸收

许多信息,好比产生了什么范例的异常,在那边产生的。操作这些信息,异常回调机制需要确定要做什么。

固然我迫不急但地想抛出例子措施示范 _except_handler 回调的运行,但尚有一些工作不能遗漏,需要说明。出格是当错误产生时,操纵系统如何知道到那边挪用?谜底仍然涉及别的一个布局 EXCEPTION_REGISTRATION。你将自始自终在本文中看到这个布局,所以不要擦过这部门内容。我能找到正式界说 EXCEPTION_REGISTRATION 布局的独一处所是 EXSUP.INC 文件,该文件来自 Visual C++ 运行库的源:

_EXCEPTION_REGISTRATION struc
prev dd ?
handler dd ?
_EXCEPTION_REGISTRATION ends

你还将看到该布局在 WINNT.H 文件中界说的 NT_TIB 布局中被引用为 _EXCEPTION_REGISTRATION_RECORD。唉,除此之外,没有什么处所能找到 _EXCEPTION_REGISTRATION_RECORD 的界说,所以我只能利用 EXSUP.INC 文件中界说的汇编语言布局。这也是我为什么在本文前述内容中说过的 SEH 缺乏文档的一个例证。

不管奈何,让我们回得手头的问题,当某个异常产生时,OS 如何知道到那边挪用回调函数?EXCEPTION_REGISTRATION 由两个域组成,第一个你此刻可以忽略。第二个域是句柄,它包括 _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 函数,如图二所示:

Win32机关化异常处理惩罚处罚(SEH)探秘(上)

图二 _except_handler 函数

通过前面的描写,我写了一个小措施来对操纵系统层的布局化异常举办示范。措施代码如下:

 //==================================================
  // MYSEH - Matt Pietrek 1997
  // Microsoft Systems Journal, January 1997
  // FILE: MYSEH.CPp
  // To compile: CL MYSEH.CPp
  //==================================================
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>

DWORD scratch;

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

// Indicate that we made it to our exception handler
   printf( "Hello from an exception handler\n" );

#p#分页标题#e#

// Change EAX in the context record so that it points to someplace
   // where we can successfully write
   ContextRecord->Eax = (DWORD)&scratch;

// Tell the OS to restart the faulting instruction
   return ExceptionContinueExecution;
}

int main()
{
   DWORD handler = (DWORD)_except_handler;
   __asm
   {
     // 建设 EXCEPTION_REGISTRATION 布局:
     push handler
// handler函数的地点
     push FS:[0] 
// 前一个handler函数的地点
     mov FS:[0],ESp
// 装入新的EXECEPTION_REGISTRATION布局
   }
   __asm
   {
     mov eax,0
// EAX清零
     mov [eax], 1
// 写EAX指向的内存从而存心激发一个错误
   }
   printf( "After writing!\n" );
   __asm
   {
     // 移去我们的 EXECEPTION_REGISTRATION 布局记录
     mov eax,[ESP]  
// 获取前一个布局
     mov FS:[0], EAX 
// 装入前一个布局
     add esp, 8
// 将 EXECEPTION_REGISTRATION 弹出仓库
   }
   return 0;
}

#p#副标题#e#

代码中只有两个函数,main 函数利用了三部门内联汇编块 ASM。第一个 ASM 块通过两个 PUSH 指令(即:“PUSH handler”和“PUSH FS:[0]”)在仓库上成立一个 EXCEPTION_REGISTRATION 布局。PUSH FS:[0] 生存以前 FS:[0] 的值,它是布局的一部门,但今朝这个值对我们不重要。重要的是在仓库上有一个 8-byte 的 EXCEPTION_REGISTRATION 布局。紧接着的指令(MOV FS:[0],ESP)是让线程信息块中的第一个 DWORD 指到新的 EXCEPTION_REGISTRATION 指令。

假如你想知道为什么我要在仓库上成立这个 EXCEPTION_REGISTRATION 布局,而不是利用全局变量,有一个很好的来由。当你利用编译器的 _try/_except 时,编译器也会在仓库上成立 EXCEPTION_REGISTRATION 布局。我只是向你简腹地展现你利用 _try/_except 时编译器所做的工作。让我们回到 main 函数,下一个 __asm 块是通过把 EAX 寄存器清零(MOV EAX,0),然后把此寄存器的值作为内存地点让下一条指令(MOV [EAX],1)向此地点写入数据而存心激发一个错误。最后一个 __asm 块是排除这个简朴的异常处理惩罚例程:首先它规复以前的 FS:[0] 内容,然后它将 EXCEPTION_REGISTRATION 布局记录从仓库中弹出(ADD ESP,8)。

此刻,假设你正在运行 MYSEH.EXE 并会看到所产生的工作。当 MOV [EAX],1 指令执行时,它导致一个数据会见违例。系统察看 TIB 中的  FS:[0] 并找到 EXCEPTION_REGISTRATION 布局指针。此布局中则有一个指向 MYSEH.CPP 中 _except_handler 函数的指针。系统则将四个必需的参数(我在前面描写过这四个参数)压入仓库并挪用 _except_handler 函数。

一旦进入 _except_handler,代码首先通过 printf 指示“哈!这里是我干的!”。接着,_except_handler 修复导致堕落的问题。即 EAX 寄存器指向某个不能写入的内存地点(地点 0)。修复要领是在改变 CONTEXT 布局中的 EAX 的值,以便它指向某个答允举办写入操纵的位置。在这个简朴的措施中,DWORD 变量(scratch)是存心为此而设计的。_except_handler 函数最后一个行动时返回 ExceptionContinueExecution 值,它在尺度的 EXCPT.H 文件中界说。

当操纵系统看到返回值为 ExceptionContinueExecution。它就认为你已经修复了问题,而且引起错误的指令应该被从头执行。因为我的 _except_handler 函数强制 EAX 寄存器指向正当内存,MOV EAX,1 指令再次执行,函数 main 一切正常。看,这并不巨大,不是吗?

进一步深入

有了前面的最简朴的例子,让我们再回过甚去填补一些空缺。固然这个异常回调机制很棒,但它并不是一个完美的办理方案。对付稍微巨大一些的应用措施来说,仅用一个函数就能处理惩罚措施中任那里所都大概产生的异常是相当坚苦的。一个更实用的方案应该是有多个异常处理惩罚例程,每个例程针对措施的特定部门。不知你是否知道,实际上,操纵系统提供的正是这个成果。

#p#分页标题#e#

还记得系统用来查找异常回调函数的 EXCEPTION_REGISTRATION 布局吗?这个布局的第一个成员,称为 prev,前面我们曾把它忽略掉了。它实际上是一个指向别的一个 EXCEPTION_REGISTRATION 布局的指针。这第二个 EXCEPTION_REGISTRATION 布局可以有一个完全差异的处理惩罚函数。然后呢,它的 prev 域可以指向第三个 EXCEPTION_REGISTRATION 布局,依次类推。简朴地说,就是有一个 EXCEPTION_REGISTRATION 布局链表。线程信息块的第一个 DWORD(在基于 Intel CPU 的呆板上是 FS:[0])老是指向这个链表的头部。

操纵系统要这个 EXCEPTION_REGISTRATION 布局链表做什么呢?本来,当异常产生时,系统遍历这个链表以便查找个中的一个EXCEPTION_REGISTRATION 布局,其例程回调(异常处理惩罚措施)同意处理惩罚该异常。在 MYSEH.CPP 的例子中,异常处理惩罚措施通过返回ExceptionContinueExecution 暗示它同意处理惩罚这个异常。异常回调函数也可以拒绝处理惩罚这个异常。在这种环境下,系统移向链表的下一个EXCEPTION_REGISTRATION 布局并询问它的异常回调函数,看它是否愿意处理惩罚这个异常。图四显示了这个进程:

Win32机关化异常处理惩罚处罚(SEH)探秘(上)

图四 查找处理惩罚异常的 EXCEPTION_REGISTRATION 布局

#p#副标题#e#

一旦系统找到一个处理惩罚该异常的某个回调函数,它就遏制遍历布局链表。

下面的代码 MYSEH2.CPP 就是一个异常处理惩罚函数不处理惩罚某个异常的例子。为了使代码只管简朴,我利用了编译器层面的异常处理惩罚。main 函数只配置了一个 __try/__except块。在__try 块内部挪用了 HomeGrownFrame 函数。这个函数与前面的 MYSEH 措施很是相似。它也是在仓库上建设一个 EXCEPTION_REGISTRATION 布局,而且让 FS:[0] 指向此布局。在成立了新的异常处理惩罚措施之后,这个函数通过向一个 NULL 指针所指向的内存处写入数据而存心激发一个错误:

*(PDWORD)0 = 0;

这个异常处理惩罚回调函数,同样被称为_except_handler,却与前面的谁人截然差异。它首先打印出 ExceptionRecord 布局中的异常代码和符号,这个布局的地点是作为一个指针参数被这个函数吸收的。打印出异常符号的原因稍后就会大白。因为_except_handler 函数并没有规划修复堕落的代码,因此它返回 ExceptionContinueSearch。这导致操纵系统继承在 EXCEPTION_REGISTRATION 布局链表中搜索下一个 EXCEPTION_REGISTRATION布局。接下来安装的异常回调函数是针对 main 函数中的__try/__except块的。__except 块简朴地打印出“Caught the exception in main()”。此时我们只是简朴地忽略这个异常来表白我们已经处理惩罚了它。 以下是 MYSEH2.CPP:

//=================================================
// MYSEH2 - Matt Pietrek 1997
// Microsoft Systems Journal, January 1997
// FILE: MYSEH2.CPp
// 利用呼吁行CL MYSEH2.CPP编译
//=================================================
#define WIN32_LEAN_AND_MEAN 
#include <windows.h>
#include <stdio.h>
EXCEPTION_DISPOSITION
__cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext )
{
printf( "Home Grown handler: Exception Code: %08X Exception Flags %X",
ExceptionRecord->ExceptionCode, ExceptionRecord->ExceptionFlags );
if ( ExceptionRecord->ExceptionFlags & 1 )
printf( " EH_NONCONTINUABLE" );
if ( ExceptionRecord->ExceptionFlags & 2 )
printf( " EH_UNWINDING" );
if ( ExceptionRecord->ExceptionFlags & 4 )
printf( " EH_EXIT_UNWIND" );
if ( ExceptionRecord->ExceptionFlags & 8 )
// 留意这个符号
printf( " EH_STACK_INVALID" );
if ( ExceptionRecord->ExceptionFlags & 0x10 )  // 留意这个符号
printf( " EH_NESTED_CALL" );
printf( "\n" );
// 我们不想处理惩罚这个异常,让其它函数处理惩罚吧
return ExceptionContinueSearch;
}
void HomeGrownFrame( void )
{
DWORD handler = (DWORD)_except_handler;
__asm
{
// 建设EXCEPTION_REGISTRATION布局:
push handler
// handler函数的地点
push FS:[0]    // 前一个handler函数的地点
mov FS:[0],ESp
// 安装新的EXECEPTION_REGISTRATION布局
}
*(PDWORD)0 = 0;
// 写入地点0,从而激发一个错误
printf( "I should never get here!\n" );
__asm
{
// 移去我们的EXECEPTION_REGISTRATION布局
mov eax,[ESP]   
// 获取前一个布局
mov FS:[0], EAX 
// 安装前一个布局
add esp, 8    // 把我们EXECEPTION_REGISTRATION布局弹出仓库
}
}
int main()
{
__try
{
HomeGrownFrame();
}
__except( EXCEPTION_EXECUTE_HANDLER )
{
printf( "Caught the exception in main()\n" );
}
return 0;
}

#p#副标题#e#

#p#分页标题#e#

这里的要害是执行流程。当一个异常处理惩罚措施拒绝处理惩罚某个异常时,它实际上也就拒绝抉择流程最终将从那里规复。只有接管某个异常的异常处理惩罚措施才气抉择待所有异常处理惩罚代码执行完毕之后流程将从那里继承执行。这个法则暗含的意义很是重大,固然此刻还不是显而易见。

当利用布局化异常处理惩罚时,假如一个函数有一个异常处理惩罚措施但它却不处理惩罚某个异常,这个函数就有大概非正常退出。譬喻在 MYSEH2中 HomeGrownFrame 函数就不处理惩罚异常。由于在链表中后头的某个异常处理惩罚措施(这里是 main 函数中的)处理惩罚了这个异常,因此堕落指令后头的 printf 就永远不会执行。从某种水平上说,利用布局化异常处理惩罚与利用 setjmp 和 longjmp 运行时库函数有些雷同。

假如你运行 MYSEH2,会发明其输出有些奇怪。看起来仿佛挪用了两次 _except_handler 函数。按照你现有的常识,第一次挪用虽然可以完全领略。可是为什么会有第二次呢?

Home Grown handler: Exception Code: C0000005 Exception Flags 0
Home Grown handler: Exception Code: C0000027 Exception Flags 2 EH_UNWINDING
Caught the Exception in main()

较量一下以“Home Grown Handler”开头的两行,就会看出它们之间有明明的区别。第一次异常符号是0,而第二次是2。这个问题说来话就长了。实际上,当一个异常处理惩罚回调函数拒绝处理惩罚某个异常时,它会被再一次挪用。可是这次回调并不是当即产生的。这有点巨大。我需要把异常产生时的景象好好梳理一下。

当异常产生时,系统遍历 EXCEPTION_REGISTRATION 布局链表,直到它找到一个处理惩罚这个异常的处理惩罚措施。一旦找到,系统就再次遍历这个链表,直处处理惩罚这个异常的结点为止。在这第二次遍历中,系统将再次挪用每个异常处理惩罚函数。要害的区别是,在第二次挪用中,异常符号被配置为2。这个值被界说为 EH_UNWINDING。(EH_UNWINDING 的界说在 Visual C++ 运行时库源代码文件 EXCEPT.INC 中,但 Win32 SDK 中并没有与之等价的界说。)

EH_UNWINDING 暗示什么意思呢?本来,当一个异常处理惩罚回调函数被第二次挪用时(带 EH_UNWINDING 符号),操纵系统给这个函数一个最后清理的时机。什么样的清理呢?一个绝好的例子是 C++ 类的析构函数。当一个函数的异常处理惩罚措施拒绝处理惩罚某个异常时,凡是执行流程并不会正常地从谁人函数退出。此刻,想像一下界说了一个C++类的实例作为局部变量的函数。C++类型划定析构函数必需被挪用。这带 EH_UNWINDING 符号的第二次回调就给这个函数一个时机去做一些雷同于挪用析构函数和__finally 块之类的清理事情。

在异常已经被处理惩罚完毕,而且所有前面的异常帧都已经被展开之后,流程从处理惩罚异常的谁人回调函数抉择的处所开始继承执行。必然要记着,仅仅把指令指针配置到所需的代码处就开始执行是不可的。流程规复执行处的代码的仓库指针和栈帧指针(在Intel CPU上是 ESP 和EBP)也必需被规复成它们在处理惩罚这个异常的函数的栈帧上的值。因此,这个处理惩罚异常的回调函数必需认真把仓库指针和栈帧指针规复成它们在包括处理惩罚这个异常的 SEH 代码的函数的仓库上的值。

凡是,展开操纵导致仓库上处理惩罚异常的帧以下的仓库区域上的所有内容都被移除了,就仿佛我们从来没有挪用过这些函数一样。展开的别的一个结果就是 EXCEPTION_REGISTRATION 布局链表上处理惩罚异常的谁人布局之前的所有 EXCEPTION_REGISTRATION 布局都被移除了。这很好领略,因为这些 EXCEPTION_REGISTRATION 布局凡是都被建设在仓库上。在异常被处理惩罚后,仓库指针和栈帧指针在内存中比那些从 EXCEPTION_REGISTRATION 布局链表上移除的 EXCEPTION_REGISTRATION 布局高。图六显示了我说的环境。

Win32机关化异常处理惩罚处罚(SEH)探秘(上)

图六 从异常展开

帮帮我!没有人处理惩罚它!

迄今为止,我实际上一直在假设操纵系统老是能在 EXCEPTION_REGISTRATION 布局链表中 的某个处所找到一个异常处理惩罚措施。假如找不到怎么办呢?实际上,这险些不行能产生。因为操纵系统黑暗已经为每个线程都提供了一个默认的异常处理惩罚措施。这个默认的异常处理惩罚措施老是链表的最后一个结点,而且它老是选择处理惩罚异常。它举办的操纵与其它正常的异常处理惩罚回调函数有些差异,下面我会说明。

让我们来看一下系统是在什么时候插入了这个默认的、最后一个异常处理惩罚措施。很明明它需要在线程执行的早期,在任何用户代码开始执行之前。

#p#分页标题#e#

下面是我为 BaseProcessStart 函数写的伪代码。它是 Windows NT KERNEL32.DLL 的一个内部例程。这个函数带一个参数——线程进口点函数的地点。BaseProcessStart 运行在新历程的上下文情况中,而且从该历程的第一个线程的进口点函数开始执行。 

 BaseProcessStart 伪码
 BaseProcessStart( PVOID lpfnEntryPoint )
 {
   DWORD retValue
   DWORD currentESP;
   DWORD exceptionCode;
   currentESP = ESP;
   _try
   {
     NtSetInformationThread( GetCurrentThread(),
                 ThreadQuerySetWin32StartAddress,
                 &lpfnEntryPoint, sizeof(lpfnEntryPoint) );
     retValue = lpfnEntryPoint();
     ExitThread( retValue );
   }
   _except(// 过滤器-表达式代码
       exceptionCode = GetExceptionInformation(),
       UnhandledExceptionFilter( GetExceptionInformation() ) )
   {
     ESP = currentESP;
     if ( !_BaseRunningInServerProcess )     // 通例历程
       ExitProcess( exceptionCode );
     else                    // 处事
       ExitThread( exceptionCode );
   }
 }

#p#副标题#e#

在这段伪码中,留意对 lpfnEntryPoint 的挪用被封装在一个__try 和 __except 块中。正是此__try 块安装了默认的、异常处理惩罚措施链表上的最后一个异常处理惩罚措施。所有厥后注册的异常处理惩罚措施都被安装在此链表中这个结点的前面。假如 lpfnEntryPoint 函数返回,那么表白线程一直运行到完成而且没有激发异常。这时 BaseProcessStart 挪用 ExitThread 使线程退出。

另一方面,假如线程激发了一个异常可是没有异常处理惩罚措施来处理惩罚它时,该怎么办呢?这时,执行流程转到 __except 要害字后头的括号中。在 BaseProcessStart 中,这段代码挪用 UnhandledExceptionFilter 这个 API,稍后我会讲到它。此刻对付我们来说,重要的是 UnhandledExceptionFilter 这个API包括了默认的异常处理惩罚措施。

假如 UnhandledExceptionFilter 返回 EXCEPTION_EXECUTE_HANDLER,这时 BaseProcessStart 中的__except 块开始执行。而__except块所做的只是挪用 ExitProcess 函数去终止当前历程。稍微想一下你就会领略了。知识汇报我们,假如一个历程激发了一个错误而没有异常处理惩罚措施去处理惩罚它,这个历程就会被系统终止。你在伪代码中看到的正是这些。

对付上述内容我尚有一点要增补。假如激发错误的线程是作为处事来运行的,而且是基于线程的处事,那么__except 块并不挪用 ExitProcess,而是挪用 ExitThread。不能仅仅因为一个处事堕落就终止整个处事历程。

UnhandledExceptionFilter 中的默认异常处理惩罚措施都做了什么呢?当我在一个技能讲座上问起这个问题时,响应者凤毛麟角。险些没有人知道当未处理惩罚异常产生时,到底操纵系统的默认行为是什么。简朴地演示一下这个默认的行为也许会让许多人豁然开朗。我运行一个存心激发错误的措施,其功效如下(如图八)。

Win32机关化异常处理惩罚处罚(SEH)探秘(上)

图八 未处理惩罚异常对话框

外貌上看,UnhandledExceptionFilter 显示了一个对话框汇报你产生了一个错误。这时,你被给以了一个时秘密么终止堕落历程,要么调试它。可是幕后产生了很多工作,我会在文章最后具体报告它。

正如我让你看到的那样,当异常产生时,用户写的代码可以(而且凡是是这样)得到时机执行。同样,在操纵进程中,用户写的代码可以执行。此用户编写的代码也大概有缺陷并大概激发另一个异常。由于这个原因,异常处理惩罚回调函数也可以返回别的两个值: ExceptionNestedException 和 ExceptionCollidedUnwind。很明明,它们很重要。但这长短常巨大的问题,我并不规划在这里具体报告它们。要想领略其根基观念真的太坚苦了。

编译器级的SEH

#p#分页标题#e#

固然我在前面偶然也利用了__try 和__except,但迄今为止险些我写的所有内容都是关于操纵系统方面临 SEH 的实现。然而看一下我那两个利用操纵系统的原始 SEH 的小措施别扭的样子,编译器对这个成果举办封装实在长短常有须要的。此刻让我们来看一下 Visual C++ 是如安在操纵系统对 SEH 成果实现的基本上来建设它本身的布局化异常处理惩罚支持的。

在继承往下接头之前,记着其它编译器可以利用原始的系统 SEH 来做一些完全差异的工作这一点长短常重要的。没有谁划定编译器必需实现 Win32 SDK 文档中描写的__try/__except 模子。譬喻 Visual Basic 5.0 在它的运行时代码中利用了布局化异常处理惩罚,可是哪里的数据布局和算法与我这里要讲的完全差异。

假如你把 Win32 SDK 文档中关于布局化异常处理惩罚方面的内容从新到尾读一遍,必然会碰着下面所谓的“基于帧”的异常处理惩罚措施模子:

__try {
// 这里是被掩护的代码
}
__except (过滤器表达式) {
// 这里是异常处理惩罚措施代码
}

简朴地说,某个函数__try 块中的所有代码是由 EXCEPTION_REGISTRATION 布局来掩护的,该布局成立在此函数的仓库帧上。在函数的进口处,这个新的 EXCEPTION_REGISTRATION 布局被放在异常处理惩罚措施链表的头部。在__try 块竣事后,相应的 EXCEPTION_REGISTRATION 布局从这个链表的头部被移除。正如我前面所说,异常处理惩罚措施链表的头部被生存在 FS:[0] 处。因此,假如你在调试器中单步跟踪时能看到雷同下面的指令

MOV DWORD PTR FS:[00000000],ESp
可能
MOV DWORD PTR FS:[00000000],ECX

就能很是确定这段代码正在进入或退出一个__try/__except块。

既然一个__try 块对应着仓库上的一个 EXCEPTION_REGISTRATION 布局,那么 EXCEPTION_REGISTRATION 布局中的回调函数又如何呢?利用 Win32 的术语来说,异常处理惩罚回调函数对应的是过滤器表达式(filter-expression)代码。事实上,过滤器表达式就是__except 要害字后头的小括号中的代码。就是这个过滤器表达式代码抉择了后头的大括号中的代码是否执行。

由于过滤器表达式代码是你本身写的,你虽然可以抉择在你的代码中的某个处所是否处理惩罚某个特定的异常。它可以简朴的只是一句 “EXCEPTION_EXECUTE_HANDLER”,也可以先挪用一个把p计较到20,000,000位的函数,然后再返回一个值来汇报操纵系统下一步做什么。随你的便。要害是你的过滤器表达式代码必需是我前面讲的有效的异常处理惩罚回调函数。

我适才讲的固然相当简朴,但那只不外是隔着有色玻璃看世界而已。现实长短常巨大的。首先,你的过滤器表达式代码并不是被操纵系统直接挪用的。事实上,各个 EXCEPTION_REGISTRATION 布局的 handler 域都指向了同一个函数。这个函数在 Visual C++ 的运行时库中,它被称为__except_handler3。正是这个__except_handler3 挪用了你的过滤器表达式代码,我一会儿再接着说它。

对我前面的简朴描写需要批改的另一个处所是,并不是每次进入或退出一个__try 块时就建设或取消一个 EXCEPTION_REGISTRATION 布局。相反,在利用 SEH 的任何函数中只建设一个 EXCEPTION_REGISTRATION 布局。换句话说,你可以在一个函数中利用多个 __try/__except 块,可是在仓库上只建设一个 EXCEPTION_REGISTRATION 布局。同样,你可以在一个函数中嵌套利用 __try 块,但 Visual C++ 仍旧只是建设一个 EXCEPTION_REGISTRATION 布局。

假如整个 EXE 或 DLL 只需要单个的异常处理惩罚措施(__except_handler3),同时,假如单个的 EXCEPTION_REGISTRATION 布局就能处理惩罚多个__try 块的话,很明明,这内里尚有许多对象我们不知道。这个能力是通过一个凡是环境下看不到的表中的数据来完成的。由于本文的目标就是要深入摸索布局化异常处理惩罚,那就让我们来看一看这些数据布局吧。

#p#副标题#e#

扩展的异常处理惩罚帧

Visual C++ 的 SEH 实现并没有利用原始的 EXCEPTION_REGISTRATION 布局。它在这个布局的末端添加了一些附加数据。这些附加数据正是答允单个函数(__except_handler3)处理惩罚所有异常并将执行流程通报到相应的过滤器表达式和__except 块的要害。我在 Visual C++ 运行时库源代码中的 EXSUP.INC 文件中找到了有关 Visual C++ 扩展的 EXCEPTION_REGISTRATION 布局名目标线索。在这个文件中,你会看到以下界说(已经被注释掉了):

;struct _EXCEPTION_REGISTRATION{
; struct _EXCEPTION_REGISTRATION *prev;
; void (*handler)( PEXCEPTION_RECORD,
; PEXCEPTION_REGISTRATION,
; PCONTEXT,
; PEXCEPTION_RECORD);
; struct scopetable_entry *scopetable;
; int trylevel;
; int _ebp;
; PEXCEPTION_POINTERS xpointers;
;};

#p#分页标题#e#

在前面你已经见过前两个域:prev 和 handler。它们构成了根基的 EXCEPTION_REGISTRATION 布局。后头三个域:scopetable(浸染域表)、trylevel 和_ebp 是新增加的。scopetable 域指向一个 scopetable_entry 布局数组,而 trylevel 域实际上是这个数组的索引。最后一个域_ebp,是 EXCEPTION_REGISTRATION 布局建设之前栈帧指针(EBP)的值。

_ebp 域成为扩展的 EXCEPTION_REGISTRATION 布局的一部门并非偶尔。它是通过 PUSH EBP 这条指令被包括进这个布局中的,而大大都函数开头都是这条指令(凡是编译器并不为利用FPO优化的函数生成尺度的仓库帧,这样其第一条指令大概不是 PUSH EBP。可是假如利用了SEH的话,那么无论你是否利用了FPO优化,编译器必然生成尺度的仓库帧)。这条指令可以使 EXCEPTION_REGISTRATION 布局中所有其它的域都可以用一个相对付栈帧指针(EBP)的负偏移来会见。譬喻 trylevel 域在 [EBP-04]处,scopetable 指针在[EBP-08]处,等等。(也就是说,这个布局是从[EBP-10H]处开始的。)

紧随着扩展的 EXCEPTION_REGISTRATION 布局下面,Visual C++ 压入了别的两个值。紧随着(即[EBP-14H]处)的一个DWORD,是为一个指向 EXCEPTION_POINTERS 布局(一个尺度的Win32 布局)的指针所保存的空间。这个指针就是你挪用 GetExceptionInformation 这个API时返回的指针。尽量SDK文档体现 GetExceptionInformation 是一个尺度的 Win32 API,但事实上它是一个编译器内联函数。当你挪用这个函数时,Visual C++ 生成以下代码:

MOV EAX,DWORD PTR [EBP-14]

GetExceptionInformation 是一个编译器内联函数,与它相关的 GetExceptionCode 函数也是如此。此函数实际上只是返回 GetExceptionInformation 返回的数据布局(EXCEPTION_POINTERS)中的一个布局(EXCEPTION_RECORD)中的一个域(ExceptionCode)的值。当 Visual C++ 为 GetExceptionCode 函数生成下面的指令时,它到底是想干什么?我把这个问题留给读者。(此刻就能领略为什么SDK文档提醒我们要留意这两个函数的利用范畴了。)

MOV EAX,DWORD PTR [EBP-14] ; 执行完毕,EAX指向EXCEPTION_POINTERS布局
MOV EAX,DWORD PTR [EAX] ; 执行完毕,EAX指向EXCEPTION_RECORD布局
MOV EAX,DWORD PTR [EAX] ; 执行完毕,EAX中是ExceptionCode的值

此刻回到扩展的 EXCEPTION_REGISTRATION 布局上来。在这个布局开始前的8个字节处(即[EBP-18H]处),Visual C++ 保存了一个DWORD来生存所有prolog代码执行完毕之后的仓库指针(ESP)的值(实际生成的指令为MOV DWORD PTR [EBP-18H],ESP)。这个DWORD中生存的值是函数执行时ESP寄存器的正常值(除了在筹备挪用其它函数时把参数压入仓库这个进程会改变 ESP寄存器的值并在函数返回时规复它的值外,函数在执行进程中一般不改变ESP寄存器的值)。

看起来仿佛我一下子给你贯注了太多的信息,我认可。在继承下去之前,让我们先暂停,往返首一下 Visual C++ 为利用布局化异常处理惩罚的函数生成的尺度异常仓库帧,它看起来像下面这个样子:

EBP-00 _ebp
EBP-04 trylevel
EBP-08 scopetable数组指针
EBP-0C handler函数地点
EBP-10指向前一个EXCEPTION_REGISTRATION布局
EBP-14 GetExceptionInformation
EBP-18 栈帧中的尺度ESP

在操纵系统看来,只存在构成原始 EXCEPTION_REGISTRATION 布局的两个域:即[EBP-10h]处的prev指针和[EBP-0Ch]处的handler函数指针。栈帧中的其它所有内容是针对付Visual C++的。把这个Visual C++生成的尺度异常仓库帧记到脑筋里之后,让我们来看一下真正实现编译器层面SEH的这个Visual C++运行时库例程——__except_handler3。

__except_handler3 和 scopetable

我真的很但愿让你看一看Visual C++运行时库源代码,让你本身好好研究一下__except_handler3函数,可是我办不到。因为 Microsoft并没有提供。在这里你就迁就着看一下我为__except_handler3函数写的伪代码吧:。

图九 __except_handler3函数的伪代码:

int __except_handler3(
struct _EXCEPTION_RECORD * pExceptionRecord,
struct EXCEPTION_REGISTRATION * pRegistrationFrame,
struct _CONTEXT *pContextRecord,
void * pDispatcherContext )
{
LONG filterFuncRet;
LONG trylevel;
EXCEPTION_POINTERS exceptPtrs;
PSCOPETABLE pScopeTable;
CLD // 将偏向符号复位(不测试任何条件!)
// 假如没有配置EXCEPTION_UNWINDING符号或EXCEPTION_EXIT_UNWIND符号
// 表白这是第一次挪用这个处理惩罚措施(也就是说,并非处于异常展开阶段)
if ( ! (pExceptionRecord->ExceptionFlags
& (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) )
{
// 在仓库上建设一个EXCEPTION_POINTERS布局
exceptPtrs.ExceptionRecord = pExceptionRecord;
exceptPtrs.ContextRecord = pContextRecord;
// 把前面界说的EXCEPTION_POINTERS布局的地点放在比
// establisher栈帧低4个字节的位置上。参考前面我讲
// 的编译器为GetExceptionInformation生成的汇编代
// 码*(PDWORD)((PBYTE)pRegistrationFrame - 4) = &exceptPtrs;
// 获取初始的“trylevel”值
trylevel = pRegistrationFrame->trylevel;
// 获取指向scopetable数组的指针
scopeTable = pRegistrationFrame->scopetable;
search_for_handler:
if ( pRegistrationFrame->trylevel != TRYLEVEL_NONE )
{
if ( pRegistrationFrame->scopetable[trylevel].lpfnFilter )
{
PUSH EBP // 生存这个栈帧指针
// !!!很是重要!!!切换回本来的EBP。正是这个操纵才使得
// 栈帧上的所有局部变量可以或许在异常产生后仍然保持它的值稳定。
EBP = &pRegistrationFrame->_ebp;
// 挪用过滤器函数
filterFuncRet = scopetable[trylevel].lpfnFilter();
POP EBP // 规复异常处理惩罚措施的栈帧指针
if ( filterFuncRet != EXCEPTION_CONTINUE_SEARCH )
{
if ( filterFuncRet < 0 ) // EXCEPTION_CONTINUE_EXECUTION
return ExceptionContinueExecution;
// 假如可以或许执行到这里,说明返回值为EXCEPTION_EXECUTE_HANDLEr
scopetable = pRegistrationFrame->scopetable;
// 让操纵系统清理已经注册的栈帧,这会使本函数被递归挪用
__global_unwind2( pRegistrationFrame );
// 一旦执行到这里,除最后一个栈帧外,所有的栈帧已经
// 被清理完毕,流程要从最后一个栈帧继承执行
EBP = &pRegistrationFrame->_ebp;
__local_unwind2( pRegistrationFrame, trylevel );
// NLG = "non-local-goto" (setjmp/longjmp stuff)
__NLG_Notify( 1 ); // EAX = scopetable->lpfnHandler
// 把当前的trylevel配置成当找到一个异常处理惩罚措施时
// SCOPETABLE中当前正在被利用的那一个元素的内容
pRegistrationFrame->trylevel = scopetable->previousTryLevel;
// 挪用__except {}块,这个挪用并不会返回
pRegistrationFrame->scopetable[trylevel].lpfnHandler();
}
}
scopeTable = pRegistrationFrame->scopetable;
trylevel = scopeTable->previousTryLevel;
goto search_for_handler;
}
else // trylevel == TRYLEVEL_NONE
{
return ExceptionContinueSearch;
}
}
else // 配置了EXCEPTION_UNWINDING符号或EXCEPTION_EXIT_UNWIND符号
{
PUSH EBP // 生存EBp
EBP = &pRegistrationFrame->_ebp; // 为挪用__local_unwind2配置EBp
__local_unwind2( pRegistrationFrame, TRYLEVEL_NONE )
POP EBP // 规复EBp
return ExceptionContinueSearch;
}
}

#p#副标题#e#

#p#分页标题#e#

固然__except_handler3的代码看起来许多,可是记着一点:它只是一个我在文章开头讲过的异常处理惩罚回调函数。它同MYSEH.EXE和 MYSEH2.EXE中的异常回调函数都带有同样的四个参数。__except_handler3概略上可以由第一个if语句分为两部门。这是由于这个函数可以在两种环境下被挪用,一次是正常挪用,另一次是在展开阶段。个中大部门是在非展开阶段的回调。

__except_handler3一开始就在仓库上建设了一个EXCEPTION_POINTERS布局,并用它的两个参数来对这个布局举办初始化。我在伪代码中把这个布局称为 exceptPrts,它的地点被放在[EBP-14h]处。你回想一下前面我讲的编译器为 GetExceptionInformation和 GetExceptionCode 函数生成的汇编代码就会心识到,这实际上初始化了这两个函数利用的指针。

接着,__except_handler3从EXCEPTION_REGISTRATION帧中获取当前的trylevel(在[EBP-04h]处)。 trylevel变量实际是scopetable数组的索引,而正是这个数组才使得一个函数中的多个__try块和嵌套的__try块可以或许仅利用一个 EXCEPTION_REGISTRATION布局。每个scopetable元素布局如下:

typedef struct _SCOPETABLE
{
DWORD previousTryLevel;
DWORD lpfnFilter;
DWORD lpfnHandler;
} SCOPETABLE, *PSCOPETABLE;

#p#分页标题#e#

SCOPETABLE布局中的第二个成员和第三个成员较量容易领略。它们别离是过滤器表达式代码的地点和相应的__except块的地点。可是prviousTryLevel成员有点巨大。总之一句话,它用于嵌套的__try块。这里的要害是函数中的每个__try块都有一个相应的SCOPETABLE布局。

正如我前面所说,当前的 trylevel 指定了要利用的scopetable数组的哪一个元素,最终也就是指定了过滤器表达式和__except块的地点。此刻想像一下两个__try块嵌套的景象。假如内层__try块的过滤器表达式不处理惩罚某个异常,那外层__try块的过滤器表达式就必需处理惩罚它。那此刻要问,__except_handler3是如何知道SCOPETABLE数组的哪个元素相应于外层的__try块的呢?谜底是:外层__try块的索引由 SCOPETABLE布局的previousTryLevel域给出。操作这种机制,你可以嵌套任意层的__try块。previousTryLevel 域就仿佛是一个函数中所有大概的异常处理惩罚措施组成的线性链表中的结点一样。假如trylevel的值为0xFFFFFFFF(实际上就是-1,这个值在 EXSUP.INC中被界说为TRYLEVEL_NONE),符号着这个链表竣事。

回到__except_handler3的代码中。在获取了当前的trylevel之后,它就挪用相应的SCOPETABLE布局中的过滤器表达式代码。假如过滤器表达式返回EXCEPTION_CONTINUE_SEARCH,__exception_handler3 移向SCOPETABLE数组中的下一个元素,这个元素的索引由previousTryLevel域给出。假如遍历完整个线性链表(还记得吗?这个链表是由于在一个函数内部嵌套利用__try块而形成的)都没有找处处理惩罚这个异常的代码,__except_handler3返回DISPOSITION_CONTINUE_SEARCH(原文如此,但按照_except_handler函数的界说,这个返回值应该为ExceptionContinueSearch。实际上这两个常量的值是一样的。我在伪代码中已经将其纠正过来了),这导致系统移向下一个EXCEPTION_REGISTRATION帧(这个链表是由于函数嵌套挪用而形成的)。

假如过滤器表达式返回EXCEPTION_EXECUTE_HANDLER,这意味着异常应该由相应的__except块处理惩罚。它同时也意味着所有前面的EXCEPTION_REGISTRATION帧都应该从链表中移除,而且相应的__except块都应该被执行。第一个任务通过挪用__global_unwind2来完成的,后头我会讲到这个函数。跳过这中间的一些清理代码,流程分开__except_handler3转向__except块。令人奇怪的是,流程并不从__except块中返回,固然是 __except_handler3利用CALL指令挪用了它。

当前的trylevel值是如何被配置的呢?它实际上是由编译器隐含处理惩罚的。编译器很是机智地修改这个扩展的EXCEPTION_REGISTRATION 布局中的trylevel域的值(实际上是生成修改这个域的值的代码)。假如你查抄编译器为利用SEH的函数生成的汇编代码,就会在差异的处所都看到修改这个位于[EBP-04h]处的trylevel域的值的代码。

__except_handler3是如何做到既通过CALL指令挪用__except块而又不让执行流程返回呢?由于CALL指令要向仓库中压入了一个返回地点,你可以想象这有大概粉碎仓库。假如你查抄一下编译器为__except块生成的代码,你会发明它做的第一件事就是将EXCEPTION_REGISTRATION布局下面8个字节处(即[EBP-18H]处)的一个DWORD值加载到ESP寄存器中(实际代码为MOV ESP,DWORD PTR [EBP-18H]),这个值是在函数的 prolog 代码中被生存在这个位置的(实际代码为MOV DWORD PTR [EBP-18H],ESP)。

ShowSEHFrames 措施

假如你此刻以为已经被EXCEPTION_REGISTRATION、scopetable、trylevel、过滤器表达式以及展开等等之类的词搞得晕头转向的话,那和我最初的感受一样。可是编译器层面的布局化异常处理惩罚方面的常识并不适合一点一点的学。除非你从整体上领略它,不然有许多内容单独看并没有什么意义。对面临大堆的理论时,我最自然的做法就是写一些应用我学到的理论方面的措施。假如它可以或许凭据预料的那样事情,我就知道我的领略(凡是)是正确的。

下面是ShowSEHFrame.EXE的源代码。它利用__try/__except块配置了好几个 Visual C++ SEH 帧。然后它显示每一个帧以及Visual C++为每个帧建设的scopetable的相关信息。这个措施自己并不生成也不依赖任何异常。相反,我利用了多个__try块以强制Visual C++生成多个 EXCEPTION_REGISTRATION 帧以及相应的 scopetable。

//ShowSEHFrames.CPp
//=========================================================
// ShowSEHFrames - Matt Pietrek 1997
// Microsoft Systems Journal, February 1997
// FILE: ShowSEHFrames.CPp
// 利用呼吁行CL ShowSehFrames.CPP举办编译
//=========================================================
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
#pragma hdrstop
//-------------------------------------------------------------------
// 本措施仅合用于Visual C++,它利用的数据布局是特定于Visual C++的
//-------------------------------------------------------------------
#ifndef _MSC_VEr
#error Visual C++ Required (Visual C++ specific information is displayed)
#endif
//-------------------------------------------------------------------
// 布局界说
//-------------------------------------------------------------------
// 操纵系统界说的根基异常帧
struct EXCEPTION_REGISTRATION
{
EXCEPTION_REGISTRATION* prev;
FARPROC handler;
};
// Visual C++扩展异常帧指向的数据布局
struct scopetable_entry
{
DWORD previousTryLevel;
FARPROC lpfnFilter;
FARPROC lpfnHandler;
};
// Visual C++利用的扩展异常帧
struct VC_EXCEPTION_REGISTRATION : EXCEPTION_REGISTRATION
{
scopetable_entry * scopetable;
int trylevel;
int _ebp;
};
//----------------------------------------------------------------
// 原型声明
//----------------------------------------------------------------
// __except_handler3是Visual C++运行时库函数,我们想打印出它的地点
// 可是它的原型并没有呈此刻任何头文件中,所以我们需要本身声明它。
extern "C" int _except_handler3(PEXCEPTION_RECORD,
EXCEPTION_REGISTRATION *,
PCONTEXT,
PEXCEPTION_RECORD);
//-------------------------------------------------------------
// 代码
//-------------------------------------------------------------
//
// 显示一个异常帧及其相应的scopetable的信息
//
void ShowSEHFrame( VC_EXCEPTION_REGISTRATION * pVCExcRec )
{
printf( "Frame: %08X Handler: %08X Prev: %08X Scopetable: %08X\n",
pVCExcRec, pVCExcRec->handler, pVCExcRec->prev,
pVCExcRec->scopetable );
scopetable_entry * pScopeTableEntry = pVCExcRec->scopetable;
for ( unsigned i = 0; i <= pVCExcRec->trylevel; i++ )
{
printf( " scopetable[%u] PrevTryLevel: %08X "
"filter: %08X __except: %08X\n", i,
pScopeTableEntry->previousTryLevel,
pScopeTableEntry->lpfnFilter,
pScopeTableEntry->lpfnHandler );
pScopeTableEntry++;
}
printf( "\n" );
}
//
// 遍历异常帧的链表,按顺序显示它们的信息
//
void WalkSEHFrames( void )
{
VC_EXCEPTION_REGISTRATION * pVCExcRec;
// 打印出__except_handler3函数的位置
printf( "_except_handler3 is at address: %08X\n", _except_handler3 );
printf( "\n" );
// 从FS:[0]处获取指向链表头的指针
__asm mov eax, FS:[0] __asm mov [pVCExcRec], EAX 
// 遍历异常帧的链表。0xFFFFFFFF符号着链表的末了
while ( 0xFFFFFFFF != (unsigned)pVCExcRec )
{
ShowSEHFrame( pVCExcRec );
pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev);
}
}
void Function1( void )
{
// 嵌套3层__try块以便强制为scopetable数组发生3个元素
__try
{
__try
{
__try
{
WalkSEHFrames(); // 此刻显示所有的异常帧的信息
} __except( EXCEPTION_CONTINUE_SEARCH )
{}
} __except( EXCEPTION_CONTINUE_SEARCH )
{}
} __except( EXCEPTION_CONTINUE_SEARCH )
{}
}
int main()
{
int i;
// 利用两个__try块(并不嵌套),这导致为scopetable数组生成两个元素
__try
{
i = 0x1234;
} __except( EXCEPTION_CONTINUE_SEARCH )
{
i = 0x4321;
}
__try
{
Function1(); // 挪用一个配置更多异常帧的函数
} __except( EXCEPTION_EXECUTE_HANDLER )
{
// 应该永远不会执行到这里,因为我们并没有规划发生任何异常
printf( "Caught Exception in main\n" );
}
return 0;
}

#p#副标题#e#

#p#分页标题#e#

ShowSEHFrames措施中较量重要的函数是WalkSEHFrames和ShowSEHFrame。WalkSEHFrames函数首选打印出 __except_handler3的地点,打印它的原因很快就清楚了。接着,它从FS:[0]处获取异常链表的头指针,然后遍历该链表。此链表中每个结点都是一个VC_EXCEPTION_REGISTRATION范例的布局,它是我本身界说的,用于描写Visual C++的异常处理惩罚帧。对付这个链表中的每个结点,WalkSEHFrames都把指向这个结点的指针通报给ShowSEHFrame函数。

#p#分页标题#e#

ShowSEHFrame函数一开始就打印出异常处理惩罚帧的地点、异常处理惩罚回调函数的地点、前一个异常处理惩罚帧的地点以及scopetable的地点。接着,对付每个 scopetable数组中的元素,它都打印出其priviousTryLevel、过滤器表达式的地点以及相应的__except块的地点。我是如何知道scopetable数组中有几多个元素的呢?其实我并不知道。可是我假定VC_EXCEPTION_REGISTRATION布局中的当前trylevel域的值比scopetable数组中的元素总数少1。

图十一是 ShowSEHFrames 的运行功效。首先查抄以“Frame:”开头的每一行,你会发明它们显示的异常处理惩罚帧在仓库上的地点呈递增趋势,而且在前三个帧中,它们的异常处理惩罚措施的地点是一样的(都是004012A8)。再看输出的开始部门,你会发明这个004012A8不是此外,它正是 Visual C++运行时库函数__except_handler3的地点。这证明白我前面所说的单个回调函数处理惩罚所有异常这一点。

Win32机关化异常处理惩罚处罚(SEH)探秘(上)

图十一 ShowSEHFrames运行功效

你大概想知道为什么显着 ShowSEHFrames 措施只有两个函数利用SEH,可是却有三个异常处理惩罚帧利用__except_handler3作为它们的异常回调函数。实际上第三个帧来自 Visual C++ 运行时库。Visual C++ 运行时库源代码中的 CRT0.C 文件清楚地表白了对 main 或 WinMain 的挪用也被一个__try/__except 块封装着。这个__try 块的过滤器表达式代码可以在 WINXFLTR.C文 件中找到。

回到 ShowSEHFrames 措施,留意到最后一个帧的异常处理惩罚措施的地点是 77F3AB6C,这与其它三个差异。仔细调查一下,你会发明这个地点在 KERNEL32.DLL 中。这个出格的帧就是由 KERNEL32.DLL 中的 BaseProcessStart 函数安装的,这在前面我已经说过。

 

    关键字:

天才代写-代写联系方式