一直想弄明白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的文章:
(原文)
(翻译)
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