前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Delphi异常机制与SEH

Delphi异常机制与SEH

作者头像
战神伽罗
发布2019-07-24 16:20:26
1.1K0
发布2019-07-24 16:20:26
举报

介绍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的文章:

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;

结语

以上就是我所了解到delphi里关于SEH的处理了,内容基本是自己摸索出来的心得,有不当之处,欢迎指正。

参考资料

A Crash Course on the Depths of Win32 Structured Exception Handling

联系方式

Mail: heroyin@gmail.com

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档