Y
yinju
Unregistered / Unconfirmed
GUEST, unregistred user!
Delphi异常机制与SEH<br>书呆子<br>介绍SEH机制的文章很多,但基本都是C++的,关于Delphi的很少。最近项目需要,仔细阅读了VCL关于异常的处理,有些心得体会,希望和大家一起分享。<br>SEH简介<br> SEH(struct exception handling)结构化异常处理是WIN32系统提供一种与语言无关的的异常处理机制。编程语言通过对SEH的包装,使程序异常处理更加简单,代码结构更加清晰。常见的如,delphi用到的 try exception end, try finally end,C++用到的_try{} _finally{} 和_try{} _except {} 结构都是对SEH的包装。<br> SEH提供了两种方式供开发者使用,一种是线程级的,通过设置线程的SEH链表结构。线程的TIB信息保存在FS:[0],而TIB的第一项就是指向SEH链表,所以,FS:[0]就是指向SEH链表,关于SEH结构后面介绍。第二种是进程级的,通过API函数SetUnhandledExceptionFilter设置过滤器函数来获取异常,注意的是,这种方式只有在前面的异常机制都不予以处理的时候才会被触发。<br> 关于更详细的SEH相关内容,请参见大牛Matt Pietrek的文章:<br>A Crash Course on the Depths of Win32 Structured Exception Handling (原文)<br>A Crash Course on the Depths of Win32 Structured Exception Handling (翻译)<br>SEH链表的结构如下:<br><br><br>Delphi打造的最简单的SEH示例<br>program Project1;<br> <br>{$APPTYPE CONSOLE}<br> <br>uses<br> SysUtils, Windows;<br> <br>type<br> PEXCEPTION_HANDLER = ^EXCEPTION_HANDLER;<br> <br> PEXCEPTION_REGISTRATION = ^EXCEPTION_REGISTRATION;<br> _EXCEPTION_REGISTRATION = record<br> Prev: PEXCEPTION_REGISTRATION;<br> Handler: PEXCEPTION_HANDLER;<br> end;<br> <br> EXCEPTION_REGISTRATION = _EXCEPTION_REGISTRATION;<br> <br> _EXCEPTION_HANDLER = record<br> ExceptionRecord: PExceptionRecord;<br> SEH: PEXCEPTION_REGISTRATION;<br> Context: PContext;<br> DispatcherContext: Pointer;<br> end;<br> <br> EXCEPTION_HANDLER = _EXCEPTION_HANDLER;<br> <br>const<br> EXCEPTION_CONTINUE_EXECUTION = 0; ///恢复CONTEXT里的寄存器环境,继续执行<br> EXCEPTION_CONTINUE_SEARCH = 1; ///拒绝处理这个异常,请调用下个异常处理函数<br> EXCEPTION_NESTED_EXCEPTION = 2; ///函数中出发了新的异常<br> EXCEPTION_COLLIDED_UNWIND = 3; ///发生了嵌套展开操作<br> <br> EH_NONE = 0;<br> EH_NONCONTINUABLE = 1;<br> EH_UNWINDING = 2;<br> EH_EXIT_UNWIND = 4;<br> EH_STACK_INVALID = 8;<br> EH_NESTED_CALL = 16;<br> <br> STATUS_ACCESS_VIOLATION = $C0000005; ///访问非法地址<br> STATUS_ARRAY_BOUNDS_EXCEEDED = $C000008C;<br> STATUS_FLOAT_DENORMAL_OPERAND = $C000008D;<br> STATUS_FLOAT_DIVIDE_BY_ZERO = $C000008E;<br> STATUS_FLOAT_INEXACT_RESULT = $C000008F;<br> STATUS_FLOAT_INVALID_OPERATION = $C0000090;<br> STATUS_FLOAT_OVERFLOW = $C0000091;<br> STATUS_FLOAT_STACK_CHECK = $C0000092;<br> STATUS_FLOAT_UNDERFLOW = $C0000093;<br> STATUS_INTEGER_DIVIDE_BY_ZERO = $C0000094; ///除0错误<br> STATUS_INTEGER_OVERFLOW = $C0000095;<br> STATUS_PRIVILEGED_INSTRUCTION = $C0000096;<br> STATUS_STACK_OVERFLOW = $C00000FD;<br> STATUS_CONTROL_C_EXIT = $C000013A;<br> <br> <br>var<br> G_TEST: DWORD;<br> <br>procedure Log(LogMsg: string);<br>begin<br> Writeln(LogMsg);<br>end;<br> <br>function ExceptionHandler(ExceptionHandler: EXCEPTION_HANDLER): LongInt; cdecl;<br>begin<br> Result := EXCEPTION_CONTINUE_SEARCH;<br> if ExceptionHandler.ExceptionRecord.ExceptionFlags = EH_NONE then<br> begin<br> case ExceptionHandler.ExceptionRecord.ExceptionCode of<br> STATUS_ACCESS_VIOLATION:<br> begin<br> Log('发现异常为非法内存访问,尝试修复EBX,继续执行');<br> ExceptionHandler.Context.Ebx := DWORD(@G_TEST);<br> Result := EXCEPTION_CONTINUE_EXECUTION;<br> end;<br> else<br> Log('这个异常我无法处理,请让别人处理吧');<br> end;<br> <br> end else if ExceptionHandler.ExceptionRecord.ExceptionFlags = EH_UNWINDING then<br> Log('异常展开操作');<br>end;<br> <br>begin<br> asm<br> ///设置SEH<br> XOR EAX, EAX<br> PUSH OFFSET ExceptionHandler<br> PUSH FS:[EAX]<br> MOV FS:[EAX], ESP<br> <br> ///产生内存访问错误<br> XOR EBX, EBX<br> MOV [EBX], 0<br> <br> ///取消SEH<br> XOR EAX, EAX<br> MOV ECX, [ESP]<br> MOV FS:[EAX], ECX<br> ADD ESP, 8<br> end;<br> <br> Readln;<br>end.<br>这个例子演示了最简单的异常处理,首先,通过PUSH handler 和 prev两个字段创建一个EXCEPTION_REGISTRATION结构体。再将ESP所指的新的REGISTRATION结构体赋值给FS:[0],这样就挂上了我们自己的SEH处理结构。当MOV [EBX], 0发生内存访问错后,系统挂起,查找SEH处理链表,通知ExceptionHandler进行处理,ExceptionHandler中,将EBX修复到一个可以访问的内存位置,再通知系统恢复环境继续执行。当处理完后恢复原来的SEH结构,再还原堆栈,处理完毕。<br>VCL对SEH的封装<br> 在Delphi里我们通常使用try except end 和 try finally end 来处理异常,那么在VCL里是怎么来实现的呢?<br> 1、VCL的顶层异常捕获<br>在DELPHI开发的程序中,出错的时候,我们很少看到出现一个错误对话框,提示点确定结束程序,点取消调试。而在VC或VB里就很常见,这是为什么呢?这是因为VCL的理念是,只要能够继续运行,就尽量不结束程序,而VC或VB里则认为,一旦出错,而开发者又不处理的话将会导致更严重的错误,所以干脆结束了事。至于二者之间的优劣我们就不讨论了,总之,有好有坏,关键要应用得当。<br>注意:后面的代码都是以EXE程序来讨论的,DLL的原理是一样的<br>VCL的顶层异常捕获是在程序入口函数StartExe处做的:<br>procedure _StartExe(InitTable: PackageInfo; Module: PLibModule);<br>begin<br> RaiseExceptionProc := @RaiseException;<br> RTLUnwindProc := @RTLUnwind;<br>{$ENDIF}<br> InitContext.InitTable := InitTable;<br> InitContext.InitCount := 0;<br> InitContext.Module := Module;<br> MainInstance := Module.Instance;<br>{$IFNDEF PC_MAPPED_EXCEPTIONS}<br> SetExceptionHandler; ///挂上SEH<br>{$ENDIF}<br> IsLibrary := False;<br> InitUnits;<br>end;<br>也就是在工程文件的begin处做的:<br>Project1.dpr.9: begin<br>00472004 55 push ebp<br>00472005 8BEC mov ebp,esp<br>00472007 83C4F0 add esp,-$10 //注意这里,分配了16个字节的堆栈,其中的12个字节是用来存储顶层异常结构的SEH内容<br>0047200A B8C41D4700 mov eax,$00471dc4<br>0047200F E81844F9FF call @InitExe // InitExe 在Sysinit单元里,我就不贴了,InitExe 接着就是调用_StartExe<br>Project1.dpr.13: end.<br>00472044 E89F21F9FF call @Halt0<br>00472049 8D4000 lea eax,[eax+$00]<br>SetExceptionHandler的代码:<br>procedure SetExceptionHandler;<br>asm<br> XOR EDX,EDX { using [EDX] saves some space over [0] }<br> LEA EAX,[EBP-12] ///这里就是直接将begin处分配的内存指针传给EAX,指向一个TExcFrame结构体<br> MOV ECX,FS:[EDX] { ECX := head of chain }<br> MOV FS:[EDX],EAX { head of chain := @exRegRec }<br> <br> MOV [EAX].TExcFrame.next,ECX<br>{$IFDEF PIC}<br> LEA EDX, [EBX]._ExceptionHandler<br> MOV [EAX].TExcFrame.desc, EDX<br>{$ELSE}<br> MOV [EAX].TExcFrame.desc,offset _ExceptionHandler ///异常处理函数<br>{$ENDIF}<br> MOV [EAX].TExcFrame.hEBP,EBP ///保存EBP寄存器,EBP寄存器是一个非常关键的寄存器,一般用来保存进入函数时候的栈顶指针,当函数执行完后用来恢复堆栈,一旦这个寄存器被修改或无法恢复,用明叔的话说就是:windows很生气,后果很严重!<br>{$IFDEF PIC}<br> MOV [EBX].InitContext.ExcFrame,EAX<br>{$ELSE}<br> MOV InitContext.ExcFrame,EAX<br>{$ENDIF}<br>end;<br>介绍一下TExcFrame:<br> PExcFrame = ^TExcFrame;<br> TExcFrame = record<br> next: PExcFrame;<br> desc: PExcDesc;<br> hEBP: Pointer;<br> case Integer of<br> 0: ( );<br> 1: ( ConstructedObject: Pointer );<br> 2: ( SelfOfMethod: Pointer );<br> end;<br>TExcFrame其实相当于在EXCEPTION_REGISTRATION基础上扩展了hEBP和另外一个指针,这是符合规范的,因为系统只要求前两位就行了。一般的编程语言都会扩展几个字段来保存一些关键寄存器或者其他信息方便出错后能够恢复现场。<br>当ExceptionHandler捕获到了异常时,VCL就没的选择了,弹出一个错误对话框,显示错误信息,点击确定就结束进程了。<br>2、消息处理时候的异常处理<br>大家可能有疑问了,那不是意味着程序里没有TRY EXCEPT END的话,出现异常就会直接退出?那么我在button的事件里抛出一个错误为什么没有退出呢?这是因为,DELPHI几乎在所有的消息函数处理位置加了异常保护,以controls为例子:<br>procedure TWinControl.MainWndProc(var Message: TMessage);<br>begin<br> try<br> try<br> WindowProc(Message);<br> finally<br> FreeDeviceContexts;<br> FreeMemoryContexts;<br> end;<br> except<br> Application.HandleException(Self);<br> end;<br>end;<br>一旦消息处理过程中发生了异常DELPHI将跳至Application.HandleException(Self);<br>进行处理:<br>procedure TApplication.HandleException(Sender: TObject);<br>begin<br> if GetCapture <> 0 then SendMessage(GetCapture, WM_CANCELMODE, 0, 0);<br> if ExceptObject is Exception then<br> begin<br> if not (ExceptObject is EAbort) then<br> if Assigned(FOnException) then<br> FOnException(Sender, Exception(ExceptObject))<br> else<br> ShowException(Exception(ExceptObject));<br> end else<br> SysUtils.ShowException(ExceptObject, ExceptAddr);<br>end;<br> 如果用户挂上了application.onexception事件,VCL就会将错误交给事件处理,如果没有,VCL将会弹出错误对话框警告用户,但是不会结束程序。<br> 这种方式的好处就是,软件不会因为异常而直接中止,开发者可以轻松的在onexception里接管所有的异常,坏处就是它破坏了系统提供的SEH异常处理结构,使得别的模块无法获得异常。<br>3、Try except end 和try finally end做了什么<br> Try except end和try finally end在实现上其实没有本质的区别,先介绍下第一个。<br> <br>try except end的实现:<br> PASSCAL代码(使用3个Sleep主要是用了观看汇编代码时比较方便隔开编译器生成的代码):<br> try<br> Sleep(1);<br> except<br> Sleep(1);<br> end;<br> Sleep(1);<br> <br>编译后代码:<br>SEHSample.dpr.89: try<br>///挂上SEH,将异常处理函数指向到00408D0E 实际上这个地址就直接跳转到了HandleAnyException(后面再介绍这个函数)<br> <br>00408CEF 33C0 xor eax,eax<br>00408CF1 55 push ebp ///保存了EBP指针<br>00408CF2 680E8D4000 push $00408d0e<br>00408CF7 64FF30 push dword ptr fs:[eax]<br>00408CFA 648920 mov fs:[eax],esp<br> <br>SEHSample.dpr.90: Sleep(1);<br>00408CFD 6A01 push $01<br>00408CFF E8F8C1FFFF call Sleep<br> <br>///如果没有发生异常,取消SEH,恢复堆栈<br>00408D04 33C0 xor eax,eax<br>00408D06 5A pop edx<br>00408D07 59 pop ecx<br>00408D08 59 pop ecx<br>00408D09 648910 mov fs:[eax],edx<br>///没有发生异常,跳转到00408D1F继续执行下面的代码<br>00408D0C EB11 jmp +$11<br> <br>///如果在异常处理里用了on E:Exception 语法的话会交给另外一个函数<br>_HandleOnException处理,这里不详细介绍HandleAnyException的实现了,其中的很大一个作用就是把异常翻译成DELPHI的EXCEPTION对象交给开发者处理,这就是为什么你只是声明了个E:Exception没有构造就直接可以使用,而且也不用释放,其实是VCL帮你做了创建和释放工作。<br>00408D0E E9ADAAFFFF jmp @HandleAnyException<br> <br>///发生异常后,HandleAnyException处理完毕,交给开发者处理<br>SEHSample.dpr.92: Sleep(1);<br>00408D13 6A01 push $01<br>00408D15 E8E2C1FFFF call Sleep<br> <br>///执行清理工作,释放异常对象,取消SEH,恢复EBP<br>00408D1A E881ACFFFF call @DoneExcept<br> <br>SEHSample.dpr.94: Sleep(1);<br>00408D1F 6A01 push $01<br>00408D21 E8D6C1FFFF call Sleep<br>当代码进入try except end 结构时,首先挂上SEH,如果代码正常执行,在执行完毕后取消SEH,这种情况比较简单。如果出现了异常,那么代码就会跳到错误处理函数位置,首先会交给HandleAnyException处理,再返回到开发者代码,最后执行DoneExcept进行清理工作。<br> <br>Try finally end 的实现:<br> Passcal代码:<br>try<br> Sleep(1);<br> finally<br> Sleep(1);<br> end;<br> Sleep(1);<br>编译后代码:<br>SEHSample.dpr.89: try<br>///挂上SEH,将异常处理函数指向到00408D0E 实际上这个地址就直接跳转到了HandleFinally<br>00408CEC 33C0 xor eax,eax<br>00408CEE 55 push ebp<br>00408CEF 68168D4000 push $00408d16<br>00408CF4 64FF30 push dword ptr fs:[eax]<br>00408CF7 648920 mov fs:[eax],esp<br> <br>SEHSample.dpr.90: Sleep(1);<br>00408CFA 6A01 push $01<br>00408CFC E8FBC1FFFF call Sleep<br> <br>///如果没有发生异常,取消SEH,恢复堆栈<br>00408D01 33C0 xor eax,eax<br>00408D03 5A pop edx<br>00408D04 59 pop ecx<br>00408D05 59 pop ecx<br>00408D06 648910 mov fs:[eax],edx<br> <br>///将try finally end结构后的用户代码放在栈顶,为后面ret指令所作的工作<br>00408D09 681D8D4000 push $00408d1d<br> <br>SEHSample.dpr.92: Sleep(1);<br>00408D0E 6A01 push $01<br>00408D10 E8E7C1FFFF call Sleep<br> <br>///弹回到$00408d1d处,就是try finally end后的代码<br>00408D15 C3 ret <br> <br>///处理异常HandleFinally处理完毕后,会跳转到00408D16的下一段代码,<br> HandleFinally: <br>MOV ECX,[EDX].TExcFrame.desc ///将错误处理函数保存在ECX<br> MOV [EDX].TExcFrame.desc,offset @@exit<br> <br> PUSH EBX<br> PUSH ESI<br> PUSH EDI<br> PUSH EBP<br> <br> MOV EBP,[EDX].TExcFrame.hEBP<br>ADD ECX,TExcDesc.instructions ///将ECX指向下段代码<br> <br> CALL NotifyExceptFinally<br> CALL ECX ///调用ECX,实际上就是00408D1B<br>////////////////////////////////////<br> <br>00408D16 E9D1ABFFFF jmp @HandleFinally<br>///跳到00408D0E处,就是FINALLY内的代码处<br>00408D1B EBF1 jmp -$0f<br> <br>SEHSample.dpr.94: Sleep(1);<br>00408D1D 6A01 push $01<br>00408D1F E8D8C1FFFF call Sleep<br> 当代码进入到try finally end时,首先挂上SEH,如果代码正常执行,取消SEH,将try finally end后的代码地址压入堆栈,再finally里的代码运行完毕后,ret就返回到了该地址。如果发生异常,跳到HandleFinally,HandleFinally处理完后再跳转到finally里的代码,ret返回后,回到HandleFinally,返回EXCEPTION_CONTINUE_SEARCH给系统,异常将会继续交给上层SEH结构处理。<br> <br>从代码可以看出,简单的try except end和try finally end背后,编译器可是做了大量的工作,这也是SEH结构化异常处理的优点,复杂的东西编译器都给你弄好了,开发者面对的东西相对简单。<br>4、VCL对象构造时的异常处理<br> 在Delphi开发的时候,经常会重载构造函数constractor,构造函数是创造对象的过程,如果这个时候出现异常VCL会怎么办呢?看代码吧:<br>function _ClassCreate(AClass: TClass; Alloc: Boolean): TObject;<br>asm<br> { -> EAX = pointer to VMT }<br> { <- EAX = pointer to instance }<br> PUSH EDX<br> PUSH ECX<br> PUSH EBX<br> TEST DL,DL<br> JL @@noAlloc<br> ///首先通过NewInstance构造对象,分配内存<br> CALL DWORD PTR [EAX] + VMTOFFSET TObject.NewInstance<br>@@noAlloc:<br>{$IFNDEF PC_MAPPED_EXCEPTIONS}<br> ///挂上SEH<br> XOR EDX,EDX<br> LEA ECX,[ESP+16]<br> MOV EBX,FS:[EDX]<br> MOV [ECX].TExcFrame.next,EBX<br> MOV [ECX].TExcFrame.hEBP,EBP<br>///将异常处理函数指向@desc节<br> MOV [ECX].TExcFrame.desc,offset @desc<br>///将EAX,也就是对象实例存在在扩展字段里<br> MOV [ECX].TexcFrame.ConstructedObject,EAX { trick: remember copy to instance }<br> MOV FS:[EDX],ECX<br>{$ENDIF}<br>///返回,调用构造函数<br> POP EBX<br> POP ECX<br> POP EDX<br> RET<br> <br>{$IFNDEF PC_MAPPED_EXCEPTIONS}<br>@desc:<br> ///发生异常先交给HandleAnyException处理<br> <br> JMP _HandleAnyException<br> <br> { destroy the object }<br> ///异常处理完毕后,获取对象<br> MOV EAX,[ESP+8+9*4]<br> MOV EAX,[EAX].TExcFrame.ConstructedObject<br>///判断对象是否为空<br> TEST EAX,EAX<br> JE @@skip<br>///调用析构函数,释放对象<br> MOV ECX,[EAX]<br> MOV DL,$81<br> PUSH EAX<br> CALL DWORD PTR [ECX] + VMTOFFSET TObject.Destroy<br> POP EAX<br> CALL _ClassDestroy<br>@@skip:<br> { reraise the exception }<br>///重新抛出异常<br> CALL _RaiseAgain<br>{$ENDIF}<br>end;<br>这也算一个VCL里比较特殊的SEH应用吧,过程大概就是,对构造函数进行保护,如果出现异常就调用析构函数释放。<br>这个地方很容易让开发者犯错误,下面举个例子:<br>type<br> TTest = class<br> private<br> a: TObject;<br> b: TObject;<br> public<br> constructor Create;<br> destructor Destroy; override;<br> end;<br>constructor TTest.Create;<br>begin<br> inherited;<br> a := TObject.Create;<br> b := TObject.Create;<br>end;<br> <br>destructor TTest.Destroy;<br>begin<br> a.Free;<br> b.Free;<br> inherited;<br>end;<br>这段代码看起来没啥问题,可实际上却不然,正常情况下,没有异常可以顺利通过,但如果a := TObject.Create;出现了异常,意味着b := TObject.Create;就不会被运行,b对象就不存在,这个时候VCL又会主动调用析构函数,结果b.free的时候就出错了。所以在析构函数里释放对象的时候,一定要注意判断对象是否存在。改正如下:<br>destructor TTest.Destroy;<br>begin<br> if a <> nil then<br> a.Free;<br> if b <> nil then<br> b.Free;<br> inherited;<br>end;<br>结语<br> 以上就是我所了解到delphi里关于SEH的处理了,内容基本是自己摸索出来的心得,有不当之处,欢迎指正。<br>参考资料<br>A Crash Course on the Depths of Win32 Structured Exception Handling <br>联系方式<br>Mail:heroyin@gmail.com<br><br>原文:http://blog.csdn.net/hero_yin/archive/2008/01/28/2069916.aspx