Delphi异常机制与SEH

转摘地址


Delphi异常机制与SEH
书呆子
介绍SEH机制的文章很多,但基本都是C++的,关于Delphi的很少。最近项目需要,仔细阅读了VCL关于异常的处理,有些心得体会,希望和大家一起分享。
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 的文章
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里是怎么来实现的呢?
      1VCL的顶层异常捕获
在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异常处理结构,使得别的模块无法获得异常。
3Try 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结构化异常处理的优点,复杂的东西编译器都给你弄好了,开发者面对的东西相对简单。
4VCL对象构造时的异常处理
       在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;
结语
       以上就是我所了解到delphi里关于SEH的处理了,内容基本是自己摸索出来的心得,有不当之处,欢迎指正。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值