介紹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的文章:
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的處理了,內容基本是本身摸索出來的心得,有不當之處,歡迎指正。
參考資料
聯繫方式