Delphi的SEH异常机制

一直想弄明白Delphi异常处理原理,在网上找到下面一篇文章

SEH简介

       SEH(struct exception handling)结构化异常处理是WIN32系统提供一种与语言无关的的异常处理机制。编程语言通过对SEH的包装,使程序异常处理更加简单,代码结构更加清晰。常见的如,delphi用到的 try exception end, try finally end,C++用到的_try{} _finally{} 和_try{} _except {} 结构都是对SEH的包装。

       SEH提供了两种方式供开发者使用,一种是线程级的,通过设置线程的SEH链表结构。线程的TIB信息保存在FS:[0],而TIB的第一项就是指向SEH链表,所以,FS:[0]就是指向SEH链表,关于SEH结构后面介绍。第二种是进程级的,通过API函数SetUnhandledExceptionFilter设置过滤器函数来获取异常,注意的是,这种方式只有在前面的异常机制都不予以处理的时候才会被触发。

       关于更详细的SEH相关内容,请参见大牛Matt Pietrek的文章:

A Crash Course on the Depths of Win32 Structured Exception Handling (原文)

A Crash Course on the Depths of Win32 Structured Exception Handling (翻译)

SEH链表的结构如下:

Delphi打造的最简单的SEH示例

program Project1;

   

{$APPTYPE CONSOLE}

   

uses

 SysUtils, Windows;

   

type

 PEXCEPTION_HANDLER = ^EXCEPTION_HANDLER;

   

 PEXCEPTION_REGISTRATION = ^EXCEPTION_REGISTRATION;

 _EXCEPTION_REGISTRATION = record

    Prev: PEXCEPTION_REGISTRATION;

    Handler: PEXCEPTION_HANDLER;

 end;

   

 EXCEPTION_REGISTRATION = _EXCEPTION_REGISTRATION;

   

 _EXCEPTION_HANDLER = record

    ExceptionRecord: PExceptionRecord;

    SEH: PEXCEPTION_REGISTRATION;

    Context: PContext;

    DispatcherContext: Pointer;

 end;

   

 EXCEPTION_HANDLER = _EXCEPTION_HANDLER;

   

const

 EXCEPTION_CONTINUE_EXECUTION = 0; ///恢复CONTEXT里的寄存器环境,继续执行

 EXCEPTION_CONTINUE_SEARCH    = 1; ///拒绝处理这个异常,请调用下个异常处理函数

 EXCEPTION_NESTED_EXCEPTION   = 2; ///函数中出发了新的异常

 EXCEPTION_COLLIDED_UNWIND    = 3; ///发生了嵌套展开操作

   

 EH_NONE                      = 0;

 EH_NONCONTINUABLE            = 1;

 EH_UNWINDING                 = 2;

 EH_EXIT_UNWIND               = 4;

 EH_STACK_INVALID             = 8;

 EH_NESTED_CALL               = 16;

   

 STATUS_ACCESS_VIOLATION         = $C0000005; ///访问非法地址

 STATUS_ARRAY_BOUNDS_EXCEEDED    = $C000008C;

 STATUS_FLOAT_DENORMAL_OPERAND   = $C000008D;

 STATUS_FLOAT_DIVIDE_BY_ZERO     = $C000008E;

 STATUS_FLOAT_INEXACT_RESULT     = $C000008F;

 STATUS_FLOAT_INVALID_OPERATION = $C0000090;

 STATUS_FLOAT_OVERFLOW           = $C0000091;

 STATUS_FLOAT_STACK_CHECK        = $C0000092;

 STATUS_FLOAT_UNDERFLOW          = $C0000093;

 STATUS_INTEGER_DIVIDE_BY_ZERO   = $C0000094; ///除0错误

 STATUS_INTEGER_OVERFLOW         = $C0000095;

 STATUS_PRIVILEGED_INSTRUCTION   = $C0000096;

 STATUS_STACK_OVERFLOW           = $C00000FD;

 STATUS_CONTROL_C_EXIT           = $C000013A;

   

   

var

 G_TEST: DWORD;

   

procedure Log(LogMsg: string);

begin

 Writeln(LogMsg);

end;

   

function ExceptionHandler(ExceptionHandler: EXCEPTION_HANDLER): LongInt; cdecl;

begin

 Result := EXCEPTION_CONTINUE_SEARCH;

 if ExceptionHandler.ExceptionRecord.ExceptionFlags = EH_NONE then

 begin

    case ExceptionHandler.ExceptionRecord.ExceptionCode of

      STATUS_ACCESS_VIOLATION:

        begin

          Log('发现异常为非法内存访问,尝试修复EBX,继续执行');

          ExceptionHandler.Context.Ebx := DWORD(@G_TEST);

          Result := EXCEPTION_CONTINUE_EXECUTION;

        end;

      else

        Log('这个异常我无法处理,请让别人处理吧');

    end;

   

 end else if ExceptionHandler.ExceptionRecord.ExceptionFlags = EH_UNWINDING then

    Log('异常展开操作');

end;

   

begin

 asm

      ///设置SEH

       XOR   EAX, EAX

      PUSH   OFFSET ExceptionHandler

      PUSH   FS:[EAX]

       MOV   FS:[EAX], ESP

   

       ///产生内存访问错误

       XOR   EBX, EBX

       MOV   [EBX], 0

   

      ///取消SEH

       XOR   EAX, EAX

       MOV   ECX, [ESP]

       MOV   FS:[EAX], ECX

       ADD   ESP, 8

 end;

   

 Readln;

end.

这个例子演示了最简单的异常处理,首先,通过PUSH handler 和 prev两个字段创建一个EXCEPTION_REGISTRATION结构体。再将ESP所指的新的REGISTRATION结构体赋值给FS:[0],这样就挂上了我们自己的SEH处理结构。当MOV   [EBX], 0发生内存访问错后,系统挂起,查找SEH处理链表,通知ExceptionHandler进行处理,ExceptionHandler中,将EBX修复到一个可以访问的内存位置,再通知系统恢复环境继续执行。当处理完后恢复原来的SEH结构,再还原堆栈,处理完毕。

VCL对SEH的封装

       在Delphi里我们通常使用try except end 和 try finally end 来处理异常,那么在VCL里是怎么来实现的呢?

      1、VCL的顶层异常捕获

DELPHI开发的程序中,出错的时候,我们很少看到出现一个错误对话框,提示点确定结束程序,点取消调试。而在VC或VB里就很常见,这是为什么呢?这是因为VCL的理念是,只要能够继续运行,就尽量不结束程序,而VC或VB里则认为,一旦出错,而开发者又不处理的话将会导致更严重的错误,所以干脆结束了事。至于二者之间的优劣我们就不讨论了,总之,有好有坏,关键要应用得当。

注意:后面的代码都是以EXE程序来讨论的,DLL的原理是一样的

VCL的顶层异常捕获是在程序入口函数StartExe处做的:

procedure       _StartExe(InitTable: PackageInfo; Module: PLibModule);

begin

 RaiseExceptionProc := @RaiseException;

 RTLUnwindProc := @RTLUnwind;

{$ENDIF}

 InitContext.InitTable := InitTable;

 InitContext.InitCount := 0;

 InitContext.Module := Module;

 MainInstance := Module.Instance;

{$IFNDEF PC_MAPPED_EXCEPTIONS}

 SetExceptionHandler; ///挂上SEH

{$ENDIF}

 IsLibrary := False;

 InitUnits;

end;

也就是在工程文件的begin处做的:

Project1.dpr.9: begin

00472004 55               push ebp

00472005 8BEC             mov ebp,esp

00472007 83C4F0           add esp,-$10    //注意这里,分配了16个字节的堆栈,其中的12个字节是用来存储顶层异常结构的SEH内容

0047200A B8C41D4700       mov eax,$00471dc4

0047200F E81844F9FF       call @InitExe   // InitExe 在Sysinit单元里,我就不贴了,InitExe 接着就是调用_StartExe

Project1.dpr.13: end.

00472044 E89F21F9FF       call @Halt0

00472049 8D4000           lea eax,[eax+$00]

SetExceptionHandler的代码:

procedure       SetExceptionHandler;

asm

 XOR     EDX,EDX                 { using [EDX] saves some space over [0] }

 LEA     EAX,[EBP-12]                 ///这里就是直接将begin处分配的内存指针传给EAX,指向一个TExcFrame结构体

 MOV     ECX,FS:[EDX]            { ECX := head of chain                  }

 MOV     FS:[EDX],EAX            { head of chain := @exRegRec            }

   

 MOV     [EAX].TExcFrame.next,ECX

{$IFDEF PIC}

 LEA     EDX, [EBX]._ExceptionHandler

 MOV     [EAX].TExcFrame.desc, EDX

{$ELSE}

 MOV     [EAX].TExcFrame.desc,offset _ExceptionHandler ///异常处理函数

{$ENDIF}

 MOV     [EAX].TExcFrame.hEBP,EBP ///保存EBP寄存器,EBP寄存器是一个非常关键的寄存器,一般用来保存进入函数时候的栈顶指针,当函数执行完后用来恢复堆栈,一旦这个寄存器被修改或无法恢复,用明叔的话说就是:windows很生气,后果很严重!

{$IFDEF PIC}

 MOV     [EBX].InitContext.ExcFrame,EAX

{$ELSE}

 MOV     InitContext.ExcFrame,EAX

{$ENDIF}

end;

介绍一下TExcFrame:

 PExcFrame = ^TExcFrame;

 TExcFrame = record

    next: PExcFrame;

    desc: PExcDesc;

    hEBP: Pointer;

    case Integer of

    0: ( );

    1: ( ConstructedObject: Pointer );

    2: ( SelfOfMethod: Pointer );

 end;

TExcFrame其实相当于在EXCEPTION_REGISTRATION基础上扩展了hEBP和另外一个指针,这是符合规范的,因为系统只要求前两位就行了。一般的编程语言都会扩展几个字段来保存一些关键寄存器或者其他信息方便出错后能够恢复现场。

当ExceptionHandler捕获到了异常时,VCL就没的选择了,弹出一个错误对话框,显示错误信息,点击确定就结束进程了。

2、消息处理时候的异常处理

大家可能有疑问了,那不是意味着程序里没有TRY EXCEPT END的话,出现异常就会直接退出?那么我在button的事件里抛出一个错误为什么没有退出呢?这是因为,DELPHI几乎在所有的消息函数处理位置加了异常保护,以controls为例子:

procedure TWinControl.MainWndProc(var Message: TMessage);

begin

 try

    try

      WindowProc(Message);

    finally

      FreeDeviceContexts;

      FreeMemoryContexts;

    end;

 except

    Application.HandleException(Self);

 end;

end;

一旦消息处理过程中发生了异常DELPHI将跳至Application.HandleException(Self);

进行处理:

procedure TApplication.HandleException(Sender: TObject);

begin

 if GetCapture <> 0 then SendMessage(GetCapture, WM_CANCELMODE, 0, 0);

 if ExceptObject is Exception then

 begin

    if not (ExceptObject is EAbort) then

      if Assigned(FOnException) then

        FOnException(Sender, Exception(ExceptObject))

      else

        ShowException(Exception(ExceptObject));

 end else

    SysUtils.ShowException(ExceptObject, ExceptAddr);

end;

       如果用户挂上了application.onexception事件,VCL就会将错误交给事件处理,如果没有,VCL将会弹出错误对话框警告用户,但是不会结束程序。

       这种方式的好处就是,软件不会因为异常而直接中止,开发者可以轻松的在onexception里接管所有的异常,坏处就是它破坏了系统提供的SEH异常处理结构,使得别的模块无法获得异常。

3、Try except end 和try finally end做了什么

       Try except end和try finally end在实现上其实没有本质的区别,先介绍下第一个。

   

try except end的实现:

       PASSCAL代码(使用3个Sleep主要是用了观看汇编代码时比较方便隔开编译器生成的代码):

 try

    Sleep(1);

 except

    Sleep(1);

 end;

 Sleep(1);

   

编译后代码:

SEHSample.dpr.89: try

///挂上SEH,将异常处理函数指向到00408D0E 实际上这个地址就直接跳转到了HandleAnyException(后面再介绍这个函数)

   

00408CEF 33C0             xor eax,eax

00408CF1 55               push ebp ///保存了EBP指针

00408CF2 680E8D4000       push $00408d0e

00408CF7 64FF30           push dword ptr fs:[eax]

00408CFA 648920           mov fs:[eax],esp

   

SEHSample.dpr.90: Sleep(1);

00408CFD 6A01             push $01

00408CFF E8F8C1FFFF       call Sleep

   

///如果没有发生异常,取消SEH,恢复堆栈

00408D04 33C0             xor eax,eax

00408D06 5A               pop edx

00408D07 59               pop ecx

00408D08 59               pop ecx

00408D09 648910           mov fs:[eax],edx

///没有发生异常,跳转到00408D1F继续执行下面的代码

00408D0C EB11             jmp +$11

   

///如果在异常处理里用了on E:Exception 语法的话会交给另外一个函数

_HandleOnException处理,这里不详细介绍HandleAnyException的实现了,其中的很大一个作用就是把异常翻译成DELPHI的EXCEPTION对象交给开发者处理,这就是为什么你只是声明了个E:Exception没有构造就直接可以使用,而且也不用释放,其实是VCL帮你做了创建和释放工作。

00408D0E E9ADAAFFFF       jmp @HandleAnyException

   

///发生异常后,HandleAnyException处理完毕,交给开发者处理

SEHSample.dpr.92: Sleep(1);

00408D13 6A01             push $01

00408D15 E8E2C1FFFF       call Sleep

   

///执行清理工作,释放异常对象,取消SEH,恢复EBP

00408D1A E881ACFFFF       call @DoneExcept

   

SEHSample.dpr.94: Sleep(1);

00408D1F 6A01             push $01

00408D21 E8D6C1FFFF       call Sleep

当代码进入try except end 结构时,首先挂上SEH,如果代码正常执行,在执行完毕后取消SEH,这种情况比较简单。如果出现了异常,那么代码就会跳到错误处理函数位置,首先会交给HandleAnyException处理,再返回到开发者代码,最后执行DoneExcept进行清理工作。

   

Try finally end 的实现:

       Passcal代码:

try

    Sleep(1);

 finally

    Sleep(1);

 end;

 Sleep(1);

编译后代码:

SEHSample.dpr.89: try

///挂上SEH,将异常处理函数指向到00408D0E 实际上这个地址就直接跳转到了HandleFinally

00408CEC 33C0             xor eax,eax

00408CEE 55               push ebp

00408CEF 68168D4000       push $00408d16

00408CF4 64FF30           push dword ptr fs:[eax]

00408CF7 648920           mov fs:[eax],esp

   

SEHSample.dpr.90: Sleep(1);

00408CFA 6A01             push $01

00408CFC E8FBC1FFFF       call Sleep

   

///如果没有发生异常,取消SEH,恢复堆栈

00408D01 33C0             xor eax,eax

00408D03 5A               pop edx

00408D04 59               pop ecx

00408D05 59               pop ecx

00408D06 648910           mov fs:[eax],edx

   

///将try finally end结构后的用户代码放在栈顶,为后面ret指令所作的工作

00408D09 681D8D4000       push $00408d1d

   

SEHSample.dpr.92: Sleep(1);

00408D0E 6A01             push $01

00408D10 E8E7C1FFFF       call Sleep

   

///弹回到$00408d1d处,就是try finally end后的代码

00408D15 C3               ret

   

///处理异常HandleFinally处理完毕后,会跳转到00408D16的下一段代码,

           HandleFinally:                     

MOV     ECX,[EDX].TExcFrame.desc ///将错误处理函数保存在ECX

        MOV     [EDX].TExcFrame.desc,offset @@exit

   

        PUSH    EBX

        PUSH    ESI

        PUSH    EDI

        PUSH    EBP

   

        MOV     EBP,[EDX].TExcFrame.hEBP

ADD     ECX,TExcDesc.instructions ///将ECX指向下段代码

   

        CALL    NotifyExceptFinally

        CALL    ECX   ///调用ECX,实际上就是00408D1B

   

00408D16 E9D1ABFFFF       jmp @HandleFinally

///跳到00408D0E处,就是FINALLY内的代码处

00408D1B EBF1             jmp -$0f

   

SEHSample.dpr.94: Sleep(1);

00408D1D 6A01             push $01

00408D1F E8D8C1FFFF       call Sleep

       当代码进入到try finally end时,首先挂上SEH,如果代码正常执行,取消SEH,将try finally end后的代码地址压入堆栈,再finally里的代码运行完毕后,ret就返回到了该地址。如果发生异常,跳到HandleFinally,HandleFinally处理完后再跳转到finally里的代码,ret返回后,回到HandleFinally,返回EXCEPTION_CONTINUE_SEARCH给系统,异常将会继续交给上层SEH结构处理。

   

从代码可以看出,简单的try except end和try finally end背后,编译器可是做了大量的工作,这也是SEH结构化异常处理的优点,复杂的东西编译器都给你弄好了,开发者面对的东西相对简单。

4、VCL对象构造时的异常处理

       在Delphi开发的时候,经常会重载构造函数constractor,构造函数是创造对象的过程,如果这个时候出现异常VCL会怎么办呢?看代码吧:

function _ClassCreate(AClass: TClass; Alloc: Boolean): TObject;

asm

        { ->    EAX = pointer to VMT      }

        { <-    EAX = pointer to instance }

        PUSH    EDX

        PUSH    ECX

        PUSH    EBX

        TEST    DL,DL

        JL      @@noAlloc

            ///首先通过NewInstance构造对象,分配内存

        CALL    DWORD PTR [EAX] + VMTOFFSET TObject.NewInstance

@@noAlloc:

{$IFNDEF PC_MAPPED_EXCEPTIONS}

                  ///挂上SEH

        XOR     EDX,EDX

        LEA     ECX,[ESP+16]

        MOV     EBX,FS:[EDX]

        MOV     [ECX].TExcFrame.next,EBX

        MOV     [ECX].TExcFrame.hEBP,EBP

///将异常处理函数指向@desc节

        MOV     [ECX].TExcFrame.desc,offset @desc

///将EAX,也就是对象实例存在在扩展字段里

        MOV     [ECX].TexcFrame.ConstructedObject,EAX   { trick: remember copy to instance }

        MOV     FS:[EDX],ECX

{$ENDIF}

///返回,调用构造函数

        POP     EBX

        POP     ECX

        POP     EDX

        RET

   

{$IFNDEF PC_MAPPED_EXCEPTIONS}

@desc:

           ///发生异常先交给HandleAnyException处理

            

        JMP     _HandleAnyException

   

 {       destroy the object                                                      }

            ///异常处理完毕后,获取对象

        MOV     EAX,[ESP+8+9*4]

        MOV     EAX,[EAX].TExcFrame.ConstructedObject

///判断对象是否为空

        TEST    EAX,EAX

        JE      @@skip

///调用析构函数,释放对象

        MOV     ECX,[EAX]

        MOV     DL,$81

        PUSH    EAX

        CALL    DWORD PTR [ECX] + VMTOFFSET TObject.Destroy

        POP     EAX

        CALL    _ClassDestroy

@@skip:

 {       reraise the exception   }

///重新抛出异常

        CALL    _RaiseAgain

{$ENDIF}

end;

这也算一个VCL里比较特殊的SEH应用吧,过程大概就是,对构造函数进行保护,如果出现异常就调用析构函数释放。

这个地方很容易让开发者犯错误,下面举个例子:

type

 TTest = class

 private

    a: TObject;

    b: TObject;

 public

    constructor Create;

    destructor Destroy; override;

 end;

constructor TTest.Create;

begin

 inherited;

 a := TObject.Create;

 b := TObject.Create;

end;

   

destructor TTest.Destroy;

begin

 a.Free;

 b.Free;

 inherited;

end;

这段代码看起来没啥问题,可实际上却不然,正常情况下,没有异常可以顺利通过,但如果a := TObject.Create;出现了异常,意味着b := TObject.Create;就不会被运行,b对象就不存在,这个时候VCL又会主动调用析构函数,结果b.free的时候就出错了。所以在析构函数里释放对象的时候,一定要注意判断对象是否存在。改正如下:

destructor TTest.Destroy;

begin

    if a <> nil then

 a.Free;

       if b <> nil then

 b.Free;

 inherited;

end;

转自:http://blog.csdn.net/hero_yin/article/details/2069916

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值