Delphi的try...except...end对SEH的封装
先对SEH简要说明一下。寄存器FS:[0]存储着一个异常处理链表第一个元素的指针,该结构定义如下:
TExcFrame = record
next: PExcFrame;
desc: Pointer;
end;
其中TExcFrame.next指向下一个元素,TExcFrame.desc是处理异常的函数地址。
当程序发生异常时,系统调用TExcFrame.desc指向的函数,该函数原型定义如下:
EstablisherFrame: PExcFrame; ContextRecord: PThreadContext;
DispatcherContext: Pointer): EXCEPTION_DISPOSITION; cdecl;
介绍一下TSEHExceptionHandler函数。参数ExceptionRecord是异常信息(异常代码、引发异常的CPU指令地址等);参数EstablisherFrame当前处理函数的相关信息;参数ContextRecord是异常发生时线程的执行环境(各寄存器的值);参数DispatcherContext本文忽略不讨论。系统根据TSEHExceptionHandler的返回值,进行下一步处理,或将处理函数修改过的ContextRecord恢复为线程执行环境并重新从引发异常的CPU指令处执行,或继续调用异常处理链表中的下一个处理函数。以下是TSEHExceptionHandler参数类型和返回值类型的定义:
ControlWord: LongWord;
StatusWord: LongWord;
TagWord: LongWord;
ErrorOffset: LongWord;
ErrorSelector: LongWord;
DataOffset: LongWord;
DataSelector: LongWord;
RegisterArea: array [0..80 - 1] of Byte;
Cr0NpxState: LongWord;
end;
TFloatingSaveArea = FLOATING_SAVE_AREA;
THREAD_CONTEXT = packed record
ContextFlags: LongWord;
Dr0: LongWord;
Dr1: LongWord;
Dr2: LongWord;
Dr3: LongWord;
Dr6: LongWord;
Dr7: LongWord;
FloatSave: FLOATING_SAVE_AREA;
SegGs: LongWord;
SegFs: LongWord;
SegEs: LongWord;
SegDs: LongWord;
Edi: LongWord;
Esi: LongWord;
Ebx: LongWord;
Edx: LongWord;
Ecx: LongWord;
Eax: LongWord;
Ebp: LongWord;
Eip: LongWord;
SegCs: LongWord;
EFlags: LongWord;
Esp: LongWord;
SegSs: LongWord;
end;
TThreadContext = THREAD_CONTEXT;
PThreadContext = ^THREAD_CONTEXT;
PExcFrame = ^TExcFrame;
TExcFrame = record
next: PExcFrame;
desc: PExcDesc;
hEBP: Pointer;
case Integer of
0: ( );
1: ( ConstructedObject: Pointer );
2: ( SelfOfMethod: Pointer );
end;
PExceptionRecord = ^TExceptionRecord;
TExceptionRecord =
record
ExceptionCode : LongWord;
ExceptionFlags : LongWord;
OuterException : PExceptionRecord;
ExceptionAddress : Pointer;
NumberParameters : Longint;
case {IsOsException:} Boolean of
True: (ExceptionInformation : array [0..14] of Longint);
False: (ExceptAddr: Pointer; ExceptObject: Pointer);
end;
EXCEPTION_DISPOSITION = (
ExceptionContinueExecution, {恢复寄存器,继续执行}
ExceptionContinueSearch, {调用处理链表中下一个处理函数}
ExceptionNestedException,
ExceptionCollidedUnwind);
TExceptionDisposition = EXCEPTION_DISPOSITION;
细心的读者应该会发现上面TExcFrame的定义比第一次贴出的有所不同,多出了几个元素。讨论一下多出的hEBP: Pointer。以try...except...end为例,except和end之间的代码在引发异常的函数内,很可能要访问函数的参数和局部变量,稍微懂一点汇编的人都知道,函数和局部变量一般是用ebp偏移量来访问的。而异常发生后,经由系统的处理,再调用异常处理函数(请注意,异常处理函数和except和end之间的代码不是同一码事,except和end之间的代码是由异常处理函数负责调用的),寄存器的值很可能已经与发生异常时大不相同了。所以,在把自己的异常处理函数加到系统异常处理链表时,需要一并将当前的EBP寄存器的值备份。通常把EBP的值保存在TExcFrame.desc后面,这个能很方便的做到。请看这段经典的注册异常处理函数的汇编代码:
push EBP
push handler
push FS:[0]
mov FS:[0],ESP
end;
三次压栈之后,从栈顶ESP开始地址从低到高依次是当前异常处理链表首元素,将要注册的异常处理函数,当前的EBP值,这正好符合TExcFrame中它们的顺序。之后一句mov FS:[0], ESP就注册成功了。
SEH的基础知识就说这么一些了,想更详细地了解SEH,可以参考http://www.vckbase.com/document/viewdoc/?id=1867。
要探讨Delphi的try...except...end语法对SEH的封装,当然要查看编译器为它产生的汇编代码了。以下是笔者用于测试和分析的Delphi控制台程序代码:
2
3 {$APPTYPE CONSOLE}
4
5 begin
6 try
7 raise TObject.Create();
8 except
9 Writeln('except');
10 end;
11 end.
调试状态下切换到CPU窗口,看到以下汇编代码(已经除去了无关代码):
004050C7 xor eax,eax
004050C9 push ebp
004050CA push $004050f0
004050CF push dword ptr fs:[eax]
004050D2 mov fs:[eax],esp
Project1.dpr.40: raise TObject.Create();
004050D5 mov dl,$01
004050D7 mov eax,[$00401000]
004050DC call TObject.Create
004050E1 call @RaiseExcept
004050E6 xor eax,eax
004050E8 pop edx
004050E9 pop ecx
004050EA pop ecx
004050EB mov fs:[eax],edx
004050EE jmp $00405113
004050F0 jmp @HandleAnyException
Project1.dpr.42: Writeln('except');
004050F5 mov eax,[$00406798]
004050FA mov edx,$00405124
004050FF call @Write0LString
00405104 call @WriteLn
00405109 call @_IOTest
0040510E call @DoneExcept
Project1.dpr.47: end.
00405113 pop edi
00405114 pop esi
00405116 call @Halt0
先看这段:
004050C9 push ebp
004050CA push $004050f0
004050CF push dword ptr fs:[eax]
004050D2 mov fs:[eax],esp
这是一段标准的注册异常处理函数的代码,就不细说了。将地址$004050F0注册到系统异常处理链。$004050F0处的指令是jmp @HandleAnyException。
再来看看编译器为except和end之间的异常处理代码Writeln('except')的反汇编:
004050F5 mov eax,[$00406798]
004050FA mov edx,$00405124
004050FF call @Write0LString
00405104 call @WriteLn
00405109 call @_IOTest
0040510E call @DoneExcept
可以看到,在Writeln('except')后,编译器魔法加入了调用@DoneExcept的指令。
@HandleAnyException和@DoneExcept分别是System.pas里的函数_HandleAnyException和_DoneExcept,都是纯汇编编写,笔者研究之后,写出了对应的Pascal代码。不过有的地方不得内嵌一点汇编,比如要直接修改寄存器或不经过ret直接跳出函数的时候。
PRaiseFrame = ^TRaiseFrame;
TRaiseFrame = packed record
NextRaise: PRaiseFrame;
ExceptAddr: Pointer;
ExceptObject: TObject;
ExceptionRecord: PExceptionRecord;
end;
{异常帧}
TRaiseFrameEx = packed record
Base: TRaiseFrame;
ExceptionFrame: PExcFrame;
end;
PRaiseFrameEx = ^TRaiseFrameEx;
{获取线程局部存储中的异常帧}
function GetLastRaiseFrame(): PRaiseFrame;
asm
CALL SysInit.@GetTLS
MOV EAX, [EAX].RaiseListPtr
end;
{设置线程局部存储中异常帧}
procedure SetLastRaiseFrame(RaiseFrame: PRaiseFrame);
asm
PUSH EBX
MOV EBX, EAX
CALL SysInit.@GetTLS
MOV [EAX].RaiseListPtr, EBX
POP EBX
end;
procedure _DoneExcept;
var
FrameEx: PRaiseFrameEx;
Current: PExcFrame;
begin
FrameEx := PRaiseFrameEx(GetLastRaiseFrame());
SetLastRaiseFrame(FrameEx.Base.NextRaise); {还原原来的异常帧}
FrameEx.Base.ExceptObject.Free(); {释放异常对象}
Current := FrameEx.ExceptionFrame; {得到TExcFrame结构指针,就是注册异常时mov FS:[0], ESP中这个ESP}
asm
MOV EDX, [EBP + 4] {懂汇编就知道,这是取得_DonExcept的返回地址}
MOV ESP, Current {恢复发生异常时的ESP,这里直接改变了ESP,所以会影响_DoneExcept内的push, pop, ret等指令,这也是后面用JMP EDX直接跳出函数的原因}
POP EAX {弹出TExcFrame.next,这样就把之前的异常链首元素保存在EAX中了}
MOV DWORD PTR FS:[0], EAX {恢复原来的异常链首元素,这样就删除了我们注册的异常处理函数}
POP EAX {弹出TExcFrame.desc}
POP EBP {弹出TExcFrame.hEBP,恢复了EBP}
JMP EDX {直接跳出_DoneExcept,不按常规方式经由ret退出。就跳到except...end后面的代码了}
end;
end;
function _AnyException(ExceptionRecord: PExceptionRecord;
EstablisherFrame: PExcFrame; ContextRecord: PThreadContext;
DispatcherContext: Pointer): EXCEPTION_DISPOSITION; cdecl;
type
TExceptObjProc = function (P: PExceptionRecord): TObject;
var
ExceptObj: TObject;
ExceptAddr: Pointer;
EBPAddr: Pointer;
Handler: TSEHExceptionHandler;
FrameEx: TRaiseFrameEx;
begin
if (ExceptionRecord.ExceptionFlags and (cUnwinding or cUnwindingForExit) <> 0) then
begin
Result := ExceptionContinueSearch;
Exit;
end;
if (ExceptionRecord.ExceptionCode <> cDelphiException) then
begin
asm
CLD
CALL _FpuInit
end;
if (ExceptObjProc = nil) then
begin
Result := ExceptionContinueSearch;
Exit;
end;
ExceptAddr := ExceptionRecord.ExceptionAddress;
ExceptObj := TExceptObjProc(ExceptObjProc)(ExceptionRecord);
if (ExceptObj = nil) then
begin
Result := ExceptionContinueSearch;
Exit;
end;
if (ExceptionRecord.ExceptionCode <> cCppException) then
else begin
Result := ExceptionContinueSearch;
Exit;
end;
end
else begin
ExceptAddr := ExceptionRecord.ExceptAddr;
ExceptObj := ExceptionRecord.ExceptObject;
end;
if (ExceptionRecord.ExceptionCode = cDelphiException) then
begin
if ((JITEnable <= 1) or (DebugHook > 0)) then
begin
FrameEx.Base.ExceptionRecord := ExceptionRecord;
FrameEx.Base.ExceptObject := ExceptObj;
FrameEx.Base.ExceptAddr := ExceptAddr;
FrameEx.Base.NextRaise := GetLastRaiseFrame();
FrameEx.ExceptionFrame := EstablisherFrame;
SetLastRaiseFrame(@FrameEx);
ExceptionRecord.ExceptionFlags := EXceptionRecord.ExceptionFlags or cUnwinding;
_UnwindException(EstablisherFrame, ExceptionRecord, nil);
EBPAddr := EstablisherFrame.hEBP;
Handler := TSEHExceptionHandler(EstablisherFrame.desc);
asm
MOV EBX, Handler
MOV EBP, EBPAddr
ADD EBX, 5
JMP EBX
end;
end
else begin
end;
end;
end;