一直想弄明白Delphi异常处理原理,在网上找到下面一篇文章
SEH简介
SEH(struct exception handling)结构化异常处理是WIN32系统提供一种与语言无关的的异常处理机制。编程语言通过对SEH的包装,使程序异常处理更加简单,代码结构更加清晰。常见的如,delphi用到的 try exception end, try finally end,C++用到的_try{} _finally{} 和_try{} _except {} 结构都是对SEH的包装。
关于更详细的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);
RaiseExceptionProc := @RaiseException;
InitContext.InitTable := InitTable;
MainInstance := Module.Instance;
{$IFNDEF PC_MAPPED_EXCEPTIONS}
00472007 83C4F0 add esp,-$10 //注意这里,分配了16个字节的堆栈,其中的12个字节是用来存储顶层异常结构的SEH内容
0047200A B8C41D4700 mov eax,$00471dc4
0047200F E81844F9FF call @InitExe // InitExe 在Sysinit单元里,我就不贴了,InitExe 接着就是调用_StartExe
00472044 E89F21F9FF call @Halt0
00472049 8D4000 lea eax,[eax+$00]
procedure SetExceptionHandler;
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 }
LEA EDX, [EBX]._ExceptionHandler
MOV [EAX].TExcFrame.desc,offset _ExceptionHandler ///异常处理函数
MOV [EBX].InitContext.ExcFrame,EAX
1: ( ConstructedObject: Pointer );
当ExceptionHandler捕获到了异常时,VCL就没的选择了,弹出一个错误对话框,显示错误信息,点击确定就结束进程了。
procedure TWinControl.MainWndProc(var Message: TMessage);
Application.HandleException(Self);
一旦消息处理过程中发生了异常DELPHI将跳至Application.HandleException(Self);
procedure TApplication.HandleException(Sender: TObject);
if GetCapture <> 0 then SendMessage(GetCapture, WM_CANCELMODE, 0, 0);
if ExceptObject is Exception then
if not (ExceptObject is EAbort) then
if Assigned(FOnException) then
FOnException(Sender, Exception(ExceptObject))
ShowException(Exception(ExceptObject));
SysUtils.ShowException(ExceptObject, ExceptAddr);
如果用户挂上了application.onexception事件,VCL就会将错误交给事件处理,如果没有,VCL将会弹出错误对话框警告用户,但是不会结束程序。
这种方式的好处就是,软件不会因为异常而直接中止,开发者可以轻松的在onexception里接管所有的异常,坏处就是它破坏了系统提供的SEH异常处理结构,使得别的模块无法获得异常。
3、Try except end 和try finally end做了什么
Try except end和try finally end在实现上其实没有本质的区别,先介绍下第一个。
PASSCAL代码(使用3个Sleep主要是用了观看汇编代码时比较方便隔开编译器生成的代码):
///挂上SEH,将异常处理函数指向到00408D0E 实际上这个地址就直接跳转到了HandleAnyException(后面再介绍这个函数)
00408CF1 55 push ebp ///保存了EBP指针
00408CF2 680E8D4000 push $00408d0e
00408CF7 64FF30 push dword ptr fs:[eax]
00408CFA 648920 mov fs:[eax],esp
00408CFF E8F8C1FFFF call Sleep
00408D09 648910 mov fs:[eax],edx
///没有发生异常,跳转到00408D1F继续执行下面的代码
///如果在异常处理里用了on E:Exception 语法的话会交给另外一个函数
00408D0E E9ADAAFFFF jmp @HandleAnyException
///发生异常后,HandleAnyException处理完毕,交给开发者处理
00408D15 E8E2C1FFFF call Sleep
00408D1A E881ACFFFF call @DoneExcept
00408D21 E8D6C1FFFF call Sleep
///挂上SEH,将异常处理函数指向到00408D0E 实际上这个地址就直接跳转到了HandleFinally
00408CEF 68168D4000 push $00408d16
00408CF4 64FF30 push dword ptr fs:[eax]
00408CF7 648920 mov fs:[eax],esp
00408CFC E8FBC1FFFF call Sleep
00408D06 648910 mov fs:[eax],edx
///将try finally end结构后的用户代码放在栈顶,为后面ret指令所作的工作
00408D09 681D8D4000 push $00408d1d
00408D10 E8E7C1FFFF call Sleep
///弹回到$00408d1d处,就是try finally end后的代码
///处理异常HandleFinally处理完毕后,会跳转到00408D16的下一段代码,
MOV ECX,[EDX].TExcFrame.desc ///将错误处理函数保存在ECX
MOV [EDX].TExcFrame.desc,offset @@exit
ADD ECX,TExcDesc.instructions ///将ECX指向下段代码
CALL ECX ///调用ECX,实际上就是00408D1B
////////////////////////////////////
00408D16 E9D1ABFFFF jmp @HandleFinally
00408D1F E8D8C1FFFF call Sleep
在Delphi开发的时候,经常会重载构造函数constractor,构造函数是创造对象的过程,如果这个时候出现异常VCL会怎么办呢?看代码吧:
function _ClassCreate(AClass: TClass; Alloc: Boolean): TObject;
{ <- EAX = pointer to instance }
CALL DWORD PTR [EAX] + VMTOFFSET TObject.NewInstance
{$IFNDEF PC_MAPPED_EXCEPTIONS}
MOV [ECX].TExcFrame.desc,offset @desc
MOV [ECX].TexcFrame.ConstructedObject,EAX { trick: remember copy to instance }
{$IFNDEF PC_MAPPED_EXCEPTIONS}
///发生异常先交给HandleAnyException处理
MOV EAX,[EAX].TExcFrame.ConstructedObject
CALL DWORD PTR [ECX] + VMTOFFSET TObject.Destroy