structured:Structured Exception Handling

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

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

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

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

  SEHheBuff

  若将SEH细节都放到起讨论任务实在艰巨因此我会从简单开始层往深里讲如果的前从未使用过结构化异常处理则正好心无杂念若是用过那就要努力将 _try、GetExceptionCode和

  EXCEPTION_EXECUTE_HANDLER从脑子中扫出假装这是个全新概念Areyouready?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需要 4个参数

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

typedefstruct_EXCEPTION_RECORD{
  DWORDExceptionCode;
  DWORDExceptionFlags;
  struct_EXCEPTION_RECORD*ExceptionRecord;
  PVOIDExceptionAddress;
  DWORDNumberParameters;
  DWORDExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
  参数ExceptionCode是操作系统分配给异常在WINNT.H文件中查找开头为“STATUS_”宏就能找到大堆这样异常代号例如大家熟知STATUS_ACCESS_VIOLATION代号就是0xC0000005更为完整异常代号可以从WindowsNTDDK中NTSTATUS.H文件里找到EXCEPTION_RECORD结构体第 4个元素是异常发生处地址其余EXCEPTION_RECORD域目前都可以忽略掉_except_handler第 2个参数是个指向establisherframe结构体指针在SEH里这可是个重要参数不过现在先不用管它第 3个参数是个指向CONTEXT结构体指针CONTEXT结构体定义在WINNT.H文件中它保存着某线程寄存器Figure1即为CONTEXT结构体当用于SEH时CONTEXT结构体保存着发生异常时各寄存器无独有偶GetThreadContext和SetThreadContext使用也是相同CONTEXT结构体第 4个也是最后个参数叫做DispatcherContext现在先不去管它

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

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

_EXCEPTION_REGISTRATIONstruc
  prev  dd   ?
  handlerdd   ?
_EXCEPTION_REGISTRATIONends
  可以看到在WINNT.HNT_TIB结构体定义中这个结构体被称为_EXCEPTION_REGISTRATION_RECORD然而_EXCEPTION_REGISTRATION_RECORD定义是没有因此我所能用只能是EXSUP.INC中汇编语言struc定义对于我前面提到SEH未公开这就是

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

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

  把目前这小点儿东西凑到我写了个小来演示所讲到这个非常简单OS级结构化异常处理Figure3所示就是MYSEH.CPP它只有两个使用了 3个内嵌ASM块个块使用两条PUSH指令(“PUSHhandler”和“PUSHFS:[0]”)在堆栈上构建了个EXCEPTION_REGISTRATION结构体PUSHFS:[0]将FS:[0]个值保存为结构体部分但是目前并不重要重要是堆栈上有个8字节EXCEPTION_REGISTRATION结构体条指令(MOVFS:[0],ESP)将线程信息块个DWORD指向新EXCEPTION_REGISTRATION结构体

  在堆栈上构建EXCEPTION_REGISTRATION结构体而不是使用全局变量是由原因当使用编译器_try/_except语义时编译器也会在堆栈上构建EXCEPTION_REGISTRATION结构体我只是要介绍说明使用_try/_except后编译器所做最起码工作回到个__asm块清零了EAX寄存器(MOVEAX,0)然后将寄存器值作为内存地址而下条指令就向这个地址进行写入(MOV[EAX],1)这就引发了异常最后__asm块移除这个简单异常处理:首先恢复以前FS:[0]内容然后从堆栈中弹出EXCEPTION_REGISTRATION记录(ADDESP,8)

  现在假设正在运行MYSEH.EXE执行情况MOV[EAX],1指令执行引发了个accessviolation系统察看TIBFS:[0]并找到指向EXCEPTION_REGISTRATION结构体指针结构体中有个指向MYSEH.CPP文件中_except_handler指针系统将所需 4个参数入栈并_except_handler进入_except_handler代码首先用条prf语句打印“Yo!Imadeithere!”然后_except_handler修复引起异常问题问题在于EAX指向了不可写内存地址(地址0)所做修复就是修改CONTEXT中EAX使其指向个可写内存单元在这个简单个DWORD类型变量(scratch)就是用于此目_except_handler最后动作就是返回ExceptionContinueExecution类型这个结构体定义在标准EXCPT.H文件中

  当操作系统看到所返回ExceptionContinueExecution时就认为问题已被解决并重新执行引起异常指令_except_handler修改了EAX寄存器使其指向了有效内存MOVEAX,1就再次执行正常继续并不很复杂不是吗?

  MovingInaLittleDeeper

  有了这个最简单情形我们再回来填补几个空白尽管异常回调如此伟大但并不完美对于任意大小编写来处理中可能发生所有异常那这个恐怕会是团糟更为可行情形是能有多个异常处理都用于特定部分操作系统提供了这个功能

  还记得系统查找异常处理回调所用EXCEPTION_REGISTRATION结构体吧?此结构体个参数就是我前面忽略那个它叫做prev它确实是指向另个EXCEPTION_REGISTRATION结构体指针这个第 2个EXCEPTION_REGISTRATION结构体可以有个完全区别处理而且它prev域还可以指向第 3个EXCEPTION_REGISTRATION结构体依次类推简单讲就是个EXCEPTION_REGISTRATION结构体链表此链表表头总是由线程信息块个DWORD(Intel机器上FS:[0])所指向

  操作系统用这个EXCEPTION_REGISTRATION结构体链表做什么?当异常发生时系统遍历此链表并查找回调和异常相符EXCEPTION_REGISTRATION对于MYSEH.CPP来说回调返回ExceptionContinueExecution型和异常相符合回调也可能不适合所发生异常这时系统就移向链表中下个EXCEPTION_REGISTRATION结构体并询问异常回调是否要处理此异常Figure4所示即为此过程旦系统找到了处理此异常回调就停止对EXCEPTION_REGISTRATION链表遍历

  我给出了个异常回调不能处理异常例子见Figure5MYSEH2.CPP为简单起见我用了点编译器级异常处理只是建立个_try/_except块_try块中个对HomeGrownFrame和前面MYSEH代码很类似它在堆栈上创建了个EXCEPTION_REGISTRATION记录并使FS:[0]指向此纪录在建立了新处理主动引起异常向NULL指针处进行写入:

  *(PDWORD)0=0;

  这里异常回调也就是_except_handler和前面那个很不代码先打印出ExceptionRecord参数异常代号和标志后面会介绍说明打印此异常标志原因这个_except_handler并不能修复引起异常代码它就返回ExceptionContinueSearch这就使得操作系统继续查找链表中个EXCEPTION_REGISTRATION记录个异常回调是用于_try/_except代码_except块只是打印“Caughttheexceptionin此处异常处理就是简单地将其忽略此处个关键问题就是执行控制流当处理不能处理异常时就是在拒绝使控制流在此处继续接受异常处理则在所有异常处理代码完成的后决定控制流在哪里继续点并不那么显而易见

  当使用结构化异常处理时如果场处理没能处理异常可以用种非正常方式退出例如MYSEH2HomeGrownFrame处理并没有处理异常异常处理链中后面某个处理()处理了此异常所以引起异常指令的后prf从未获得执行从某种意义上说使用结构化异常处理和使用运行时库jmp和longjmp差不多

  若是运行MYSEH2其输出可能会令人惊讶看上去似乎了两次_except_handler次是可以理解那第 2次又是如何回事呢?

HomeGrownhandler:ExceptionCode:C0000005ExceptionFlags0
HomeGrownhandler:ExceptionCode:C0000027ExceptionFlags2
                       EH_UNWINDING
CaughttheExceptionin
  比较由“HomeGrownHandler”开始两行其区别是显然即第异常标志为0而第 2次则为2这里就需要提到unwinding概念步讲当异常回调拒绝处理异常时就又被这次回调并没有立即发生 2是更为复杂我还需要再进步明确异常发生时情景

  当异常发生时系统遍历EXCEPTION_REGISTRATION结构体链表直至找到处理此异常处理旦找到了处理系统再次遍历此链表直到处理异常节点在第 2次遍历中系统对所有异常处理进行第 2次关键区别就是在第 2次异常标志被设为值2这个值对应着EH_UNWINDING(EH_UNWINDING定义在VisualC运行时库源代码EXCEPT.INC里但Win32SDK里并没有等价定义)

  EH_UNWINDING是什么意思呢?当异常回调被第 2次时(带有EH_UNWINDING标志)操作系统就给处理次做所需清理机会什么样清理呢?个很好例子就是C析构异常处理拒绝处理异常时控制流般并不会以正常方式从中退出现在考虑声明了个局部CC规范标准指出析构是必须被第 2次标志为EH_UNWINDING异常处理回调就是为做析构和_finally块此类清理工作提供机会在异常被处理并且所有的前exceptionframes都被以进行unwind的后从回调选择地方继续但是记住这并不等于将指令指针设为所要代码地址并继续执行继续执行出代码要求堆栈和帧指针(IntelCPUESP和EBP寄存器)都设为在处理异常堆栈帧中相应因此接受某异常处理负责将堆栈指针和堆栈帧指针设为包含处理异常SEH代码堆栈帧中

  Figure6UnwindingfromanException

  更般地说从异常中unwinding使得位于处理帧堆栈区域的下所有东西都被移除几乎相当于从未过那些unwinding个效果就是链表中位于处理异常EXCEPTION_REGISTRATION的前所有EXCEPTION_REGISTRATIONs都被从链表中移除这是有意义这些EXCEPTION_REGISTRATION般都是在堆栈上构建在异常被处理后堆栈指针和堆栈帧指针在内存中地址要比从链表中移除那些EXCEPTION_REGISTRATIONs高Figure6所示即为所述

  Help!NobodyHandledIt!

  到目前为止我都是假设操作系统总能在 EXCEPTION_REGISTRATION链表中找到处理要是没有相应处理如何办?这种情况几乎不会发生原因是操作系统私地下为每个线程都准备了个默认异常处理这个默认异常处理总是链表最后个节点并总被选来处理异常行为和异常回调有些区别我的后会介绍说明

  我们来看下系统在那里安插这个默认、最终异常处理这显然要在线程执行前期进行要在任何用户代码执行的前Figure7为我为BaseProcessStart写伪码BaseProcessStart是WindowsNTKERNEL32.DLL个内部BaseProcessStart需要个参数即线程入口地址BaseProcessStart运行在新进程上下文中并入口点来启动进程第个线程

  注意在伪码中对lpfnEntryPo被封装在了对_try和_except中这个_try块就是用来在异常处理链表中安装那个默认最终异常处理所有的后注册异常处理都会插在链表中这个处理前面若lpfnEntryPo返回线程就运行至完成而不引起异常若是这样BaseProcessStartExitThread来结束线程

  要是另种情况即线程发生了异常却再也没有异常处理了如何办?在这种情况下控制流流进_except关键字后大括号里在BaseProcessStart里这段代码叫UnhandledExceptionFilterAPI我在后面还会回来介绍它现在关键是UnhandledExceptionFilterAPI包含着默认异常处理

  若UnhandledExceptionFilter返回是EXCEPTION_EXECUTE_HANDLERBaseProcessStart_except块就执行_except块代码所作就是ExitProcess来结束当前进程仔细考虑这样做还是有意义个常识就是如果引起了异常又没有处理能处理此异常系统就结束该进程伪码中所展示正是这种情况

  还要最后补充如果引发异常线程是作为服务运行且是用于个基于线程服务则_except块并不会ExitProcess而是ExitThread没有人会个服务出错而结束整个服务进程

  UnhandledExceptionFilter中默认异常处理又作了些什么呢?当我在讨论班上提出这个问题时没几个人能猜出未处理异常发生时操作系统默认行为通过对默认处理行为演示答案点即明人们就都明白了我只是运行了个主动引起异常并指出其结果(见Figure8)

  Figure8UnhandledExceptionDialog

  UnhandledExceptionFilter显示了个对话框告诉你发生了个异常此时要么可以结束进程要么就调试引发异常进程在这幕后还有相当多操作我在本文结束前再来讲这些东西正如我所提到当异常发生时用户编写代码可以得到执行(通常是这样)类似地在unwind操作过程中用户编写代码也可以得到执行用户代码可能仍有问题并引起另个异常因此异常回调还可以返回另外两个值:ExceptionNestedException和ExceptionCollidedUnwind显然这些内容就很深了我并不想在这里介绍其对于理解基本事实来说太难了

  Compiler-levelSEH

  尽管我偶尔会使用_try和_except但目前我所讲到都是由操作系统实现然而看看我那两个使用纯操作系统SEH变态样子编译器对此封装实在是必要我们来看下VisualC是如何在操作系统级SEH支持的上构建其结构化异常处理

  在继续进行的前要记住件重要那就是另种编译器可能会和纯操作系统级SEH做法完全区别没有人说过必须要实现Win32SDK文档所描述_try/_except模型例如VisualBasic5.0在其运行时代码里使用了结构化异常处理但其数据结构和算法和我这里所讲完全区别若读下Win32SDK文档有关结构化异常处理描述就会找到所谓“frame-based”异常处理语义其形式如下:

try{
  //guardedbodyofcode
}
except(filter-expression){
  //exception-handlerblock
}
  简单讲try中所有代码都被个构建在堆栈帧上EXCEPTION_REGISTRATION保护起来入口EXCEPTION_REGISTRATION被放入异常处理链表表头在_try块结尾处其EXCEPTION_REGISTRATION被从链表头移除如前所述异常处理链表头保存在FS:[0]因此若在调试器中汇编代码中单步执行就会看到以下指令:

  MOVDWORDPTRFS:[00000000],ESP

  或是

  MOVDWORDPTRFS:[00000000],ECX

  可以十分确信代码正在建立或撤除个_try/_except块现在知道了个_try块对应着堆栈上个EXCEPTION_REGISTRATION结构体那EXCEPTION_REGISTRATION里回调呢?使用Win32术语异常回调对应着filter-expression代号filter-expression就是关键字_except后括号中代码正是这个filter-expression代号决定了是否执行后面{}块中代码

Tags:  控件exception exceptioncode exception structured

延伸阅读

最新评论

发表评论