Delphi的try...except...end对SEH的封装

先对SEH简要说明一下。寄存器FS:[0]存储着一个异常处理链表第一个元素的指针,该结构定义如下:

 

PExcFrame = ^TExcFrame;
TExcFrame 
= record
  next: PExcFrame;
  desc: Pointer;
end;
 

 

其中TExcFrame.next指向下一个元素,TExcFrame.desc是处理异常的函数地址。

当程序发生异常时,系统调用TExcFrame.desc指向的函数,该函数原型定义如下:

 

 TSEHExceptionHandler = function(ExceptionRecord: PExceptionRecord;
   EstablisherFrame: PExcFrame; ContextRecord: PThreadContext;
   DispatcherContext: Pointer): EXCEPTION_DISPOSITION; 
cdecl;


介绍一下TSEHExceptionHandler函数。参数ExceptionRecord是异常信息(异常代码、引发异常的CPU指令地址等);参数EstablisherFrame当前处理函数的相关信息;参数ContextRecord是异常发生时线程的执行环境(各寄存器的值);参数DispatcherContext本文忽略不讨论。系统根据TSEHExceptionHandler的返回值,进行下一步处理,或将处理函数修改过的ContextRecord恢复为线程执行环境并重新从引发异常的CPU指令处执行,或继续调用异常处理链表中的下一个处理函数。以下是TSEHExceptionHandler参数类型和返回值类型的定义:

FLOATING_SAVE_AREA = packed record
  ControlWord: LongWord;
  StatusWord: LongWord;
  TagWord: LongWord;
  ErrorOffset: LongWord;
  ErrorSelector: LongWord;
  DataOffset: LongWord;
  DataSelector: LongWord;
  RegisterArea: 
array [0..80 - 1of 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..14of 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后面,这个能很方便的做到。请看这段经典的注册异常处理函数的汇编代码:

 

asm
  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控制台程序代码:

 1 program Project1;
 2 
 3 {$APPTYPE CONSOLE}
 4 
 5 begin
 6   try
 7     raise TObject.Create();
 8   except
 9     Writeln('except');
10   end;
11 end.

 

调试状态下切换到CPU窗口,看到以下汇编代码(已经除去了无关代码):

 

Project1.dpr.39: try
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
00405115 pop ebx
00405116 call @Halt0

 

先看这段:

004050C7 xor eax,eax
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')的反汇编:

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

可以看到,在Writeln('except')后,编译器魔法加入了调用@DoneExcept的指令。

 

@HandleAnyException和@DoneExcept分别是System.pas里的函数_HandleAnyException和_DoneExcept,都是纯汇编编写,笔者研究之后,写出了对应的Pascal代码。不过有的地方不得内嵌一点汇编,比如要直接修改寄存器或不经过ret直接跳出函数的时候。

 

type
  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) <> 0then
  
begin
    Result :
= ExceptionContinueSearch;
    Exit;
  
end;

  
if (ExceptionRecord.ExceptionCode <> cDelphiException) then
  
begin
    
asm
      CLD
      CALL    _FpuInit
    
end;
    
if (ExceptObjProc = nilthen
    
begin
      Result :
= ExceptionContinueSearch;
      Exit;
    
end;

    ExceptAddr :
= ExceptionRecord.ExceptionAddress;
    ExceptObj :
= TExceptObjProc(ExceptObjProc)(ExceptionRecord);

    
if (ExceptObj = nilthen
    
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 <= 1or (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;


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2011-02-12 16:17  地质灾害  阅读(787)  评论(0编辑  收藏  举报