更進一步認識SEH

轉載自-http://blog.programfan.com/article.asp?id=9837

上一篇文章阿愚對結構化異常處理(Structured Exception Handling,SEH)有了初步的認識,而且也知道了SEH是__try,__except,__finally,__leave異常模型機制和try,catch,throw方式的C++異常模型的奠基石。

  爲了更進一步認識SEH機制,更深刻的理解SEH與__try,__except,__finally,__leave異常模型機制的區別。本篇文章特別對狹義上的SEH進行一些極爲細緻的講解。

SEH設計思路

  SEH機制大致被設計成這樣一種工作流程:用戶應用程序對一些可能出現異常錯誤的代碼模塊,創建一個相對應的監控函數(也即回調函數),並向操作系統註冊;這樣應用程序在執行過程中,如果什麼異常也沒出現的話,那麼程序將按正常的執行流順序來完成相對應的工作任務;否則,如果受監控的代碼模塊中,在運行時出現一個預知或未預知的異常錯誤,那麼操作系統將捕獲住這個異常,並暫停程序的執行過程(實際上是出現異常的工作線程被暫停了),然後,操作系統也收集一些有關異常的信息(例如,異常出現的地點,線程的工作環境,異常的錯誤種類,以及其它一些特殊的字段等),接着,操作系統根據先前應用程序註冊的監控性質的回調函數,來查詢當前模塊所對應的監控函數,找到之後,便立即來回調它,並且傳遞一些必要的異常的信息作爲監控函數的參數。此時,用戶註冊的監控函數便可以根據異常錯誤的種類和嚴重程度來進行分別處理,例如終止程序,或者試圖恢復錯誤後,再使程序正常運行。

  細心的朋友們現在可能想到,用戶應用程序如何來向操作系統註冊一系列的監控函數呢?其實SEH設計的巧妙之處就在與此,它這裏有兩個關鍵之處。其一,就是每個線程爲一個完全獨立的註冊主體,線程間互不干擾,也即每個線程所註冊的所有監控回調函數會連成一個鏈表,並且鏈表頭被保存在與線程本地存儲數據相關的區域(也即FS數據段區域,這是Windows操作系統的設計範疇,FS段中的數據一般都是一些線程相關的本地數據信息,例如FS:[0]就是保存監控回調函數數據結構體的鏈表頭。有關線程相關的本地數據,這裏不再詳細贅述,感興趣的朋友可以參考其它更爲詳細地資料);其二,那就是每個存儲監控回調函數指針的數據結構體,實際上它們一般並不是被存儲在堆(Heap)中,而是被存儲在棧(Stack)中。大家還記得在《第9集 C++的異常對象如何傳送》中,有關“函數調用棧”的佈局,呵呵!那只是比較理想化的棧佈局,實際上,無論是C++還是C程序中,如果函數模塊中,存在異常處理機制的情況下,那麼棧佈局都會略有些變化,會變得更爲複雜一些,因爲在棧中,需要插入一些“存儲監控回調函數指針的數據結構體”數據信息。例如典型的帶SEH機制的棧佈局如下圖所示。

  上圖中,注意其中綠線部分所連成鏈表數據結構,這就是用戶應用程序向操作系統註冊的一系列的監控函數。如果某個函數中聲明瞭異常處理機制,那麼在函數幀棧中將分配一個數據結構體(EXCEPTION_REGISTRATION),這個數據結構體有點類似與局部變量的性質,它包含兩個字段,其中一個是指向監控函數的指針(handler function address);另一個就是鏈表指針(previous EXCEPTION_REGISTRATION)。特別需要注意的是,並不是每個函數幀棧中都有EXCEPTION_REGISTRATION數據結構。另外鏈表頭指針被保存到FS:[0]中,這樣無論是操作系統,還是應用程序都能夠很好操縱這個鏈表數據體變量。

EXCEPTION_REGISTRATION的定義如下:
typedef struct _EXCEPTION_REGISTRATION
{
struct _EXCEPTION_REGISTRATION *prev;
DWORD handler;
}EXCEPTION_REGISTRATION, *PEXCEPTION_REGISTRATION;

通過一個簡單例子,來理解SEH機制

  也許上面的論述過於抽象化和理論化了,還是看一個簡單的例子吧!這樣也很容易來理解SEH的工作機制原來是那麼的簡單。示例代碼如下:

//seh.c
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>


typedef struct _EXCEPTION_REGISTRATION
{
struct _EXCEPTION_REGISTRATION *prev;
DWORD handler;
}EXCEPTION_REGISTRATION, *PEXCEPTION_REGISTRATION;


// 異常監控函數
EXCEPTION_DISPOSITION myHandler(
EXCEPTION_RECORD *ExcRecord,
void * EstablisherFrame,
CONTEXT *ContextRecord,
void * DispatcherContext)
{
printf("進入到異常處理模塊中/n");
printf("不進一步處理異常,程序直接終止退出/n");

abort();
return ExceptionContinueExecution;
}


int main()
{
DWORD prev;
EXCEPTION_REGISTRATION reg, *preg;

// 建立異常結構幀(EXCEPTION_REGISTRATION)
reg.handler = (DWORD)myHandler;

// 把異常結構幀插入到鏈表中
__asm
{
mov eax, fs:[0]
mov prev, eax
}
reg.prev = (EXCEPTION_REGISTRATION*) prev;

// 註冊監控函數
preg = &reg;
__asm
{
mov eax, preg
mov fs:[0], eax
}

{
int* p;
p = 0;

// 下面的語句被執行,將導致一個異常
*p = 45;
}

printf("這裏將不會被執行到./n");

return 0;
}

  上面的程序運行結果如下:



  通過上面的演示的簡單例程,現在應該非常清楚了Windows操作系統提供的SEH機制的基本原理和控制流轉移的具體過程。另外,這裏分別詳細介紹一下exception_handler回調函數的各個參數的涵義。其中第一個參數爲EXCEPTION_RECORD類型,它記錄了一些與異常相關的信息。它的定義如下:
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
UINT_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

   第二個參數爲PEXCEPTION_REGISTRATION類型,既當前的異常幀指針。第三個參數爲指向CONTEXT數據結構的指針,CONTEXT數據結構體中記錄了異常發生時,線程當時的上下文環境,主要包括寄存器的值,這一點有點類似於setjmp函數的作用。第四個參數DispatcherContext,它也是一個指針,表示調度的上下文環境,這個參數一般不被用到。
最後再來看一看exception_handler回調函數的返回值有何意義?它基本上有兩種返回值,一種就是返回ExceptionContinueExecution,表示異常已經被恢復了,程序可以正常繼續執行。另一種就是ExceptionContinueSearch,它表示當前的異常回調處理函數不能有效處理這個異常錯誤,系統將會根據EXCEPTION_REGISTRATION數據鏈表,繼續查找下一個異常處理的回調函數。上面的例程的詳細分析如下圖所示:



來一個稍微複雜一點例子,來更深入理解SEH機制

  現在,相信大家已經對SEH機制,既有了非常理性的理解,也有非常感性的認識。實際上,從用戶角度上來分析,SEH機制確是比較簡單。它首先是用戶註冊一系列的異常回調函數(也即監控函數),操作系統爲每個線程維護一個這樣的鏈表,每當程序中出現異常的時候,操作系統便獲得控制權,並紀錄一些與異常相關的信息,接着系統便依次搜索上面的鏈表,來查找並調用相應的異常回調函數。

  說到這裏,也許朋友們有點疑惑了?上一篇文章中講述到,“無論是__try,__except,__finally,__leave異常模型機制,或是try,catch,throw方式的C++異常模型,它們都是在SEH基礎上來實現的”。但是從這裏看來,好像上面描述的SEH機制與try,catch,throw方式的C++異常模型不太相關。是的,也許表面上看起來區別是比較大的,但是SEH機制,它的的確確是上面講到的其它兩種異常處理模型的基礎。這一點,在深入分析C++異常模型的實現時,會再做詳細的敘述。這裏爲了更深入理解SEH機制,主人公阿愚設計了一個稍微複雜一點例子。它仍然只有SEH機制,沒有__try,__except,__finally,__leave異常模型的任何影子,但是它與真實的__try,__except,__finally,__leave異常模型的實現卻有幾分相似之處。

// seh.c
#include <windows.h>
#include <stdio.h>


typedef struct _EXCEPTION_REGISTRATION
{
struct _EXCEPTION_REGISTRATION *prev;
DWORD handler;
}EXCEPTION_REGISTRATION, *PEXCEPTION_REGISTRATION;

#define SEH_PROLOGUE(pFunc_exception) /
{ /
DWORD pFunc = (DWORD)pFunc_exception; /
_asm mov eax, FS:[0] /
_asm push pFunc /
_asm push eax /
_asm mov FS:[0], esp /
}

#define SEH_EPILOGUE() /
{ /
_asm pop FS:[0] /
_asm pop eax /
}

void printfErrorMsg(int ex_code)
{
char msg[20];

memset(msg, 0, sizeof(msg));
switch (ex_code)
{
case EXCEPTION_ACCESS_VIOLATION :
strcpy(msg, "存儲保護異常");
break;
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED :
strcpy(msg, "數組越界異常");
break;
case EXCEPTION_BREAKPOINT :
strcpy(msg, "斷點異常");
break;
case EXCEPTION_FLT_DIVIDE_BY_ZERO :
case EXCEPTION_INT_DIVIDE_BY_ZERO :
strcpy(msg, "被0除異常");
break;
default :
strcpy(msg, "其它異常");
}

printf("/n");
printf("%s,錯誤代碼爲:0x%x/n", msg, ex_code);
}

EXCEPTION_DISPOSITION my_exception_Handler(
EXCEPTION_RECORD *ExcRecord,
void * EstablisherFrame,
CONTEXT *ContextRecord,
void * DispatcherContext)
{
int _ebp;

printfErrorMsg(ExcRecord->ExceptionCode);

printf("跳過出現異常函數,返回到上層函數中繼續執行/n");
printf("/n");

_ebp = ContextRecord->Ebp;
_asm
{
// 恢復上一個異常幀
mov eax, EstablisherFrame
mov eax, [eax]
mov fs:[0], eax

// 返回到上一層的調用函數
mov esp, _ebp
pop ebp
mov eax, -1
ret
}

// 下面將絕對不會被執行到
exit(0);
return ExceptionContinueExecution;
}

EXCEPTION_DISPOSITION my_RaiseException_Handler(
EXCEPTION_RECORD *ExcRecord,
void * EstablisherFrame,
CONTEXT *ContextRecord,
void * DispatcherContext)
{
int _ebp;

printfErrorMsg(ExcRecord->ExceptionCode);

printf("跳過出現異常函數,返回到上層函數中繼續執行/n");
printf("/n");

_ebp = ContextRecord->Ebp;
_asm
{
// 恢復上一個異常幀
mov eax, EstablisherFrame
mov eax, [eax]
mov fs:[0], eax

// 返回到上一層的調用函數
mov esp, _ebp
pop ebp
mov esp, ebp
pop ebp
mov eax, -1
ret
}

// 下面將絕對不會被執行到
exit(0);
return ExceptionContinueExecution;
}


void test1()
{
SEH_PROLOGUE(my_exception_Handler);

{
int zero;
int j;
zero = 0;

// 下面的語句被執行,將導致一個異常
j = 10 / zero;

printf("在test1()函數中,這裏將不會被執行到.j=%d/n", j);
}

SEH_EPILOGUE();
}

void test2()
{
SEH_PROLOGUE(my_exception_Handler);

{
int* p;
p = 0;

printf("在test2()函數中,調用test1()函數之前/n");
test1();
printf("在test2()函數中,調用test1()函數之後/n");
printf("/n");


// 下面的語句被執行,將導致一個異常
*p = 45;

printf("在test2()函數中,這裏將不會被執行到/n");
}

SEH_EPILOGUE();
}

void test3()
{
SEH_PROLOGUE(my_RaiseException_Handler);

{
// 下面的語句被執行,將導致一個異常
RaiseException(0x999, 0x888, 0, 0);

printf("在test3()函數中,這裏將不會被執行到/n");
}

SEH_EPILOGUE();
}

int main()
{
printf("在main()函數中,調用test1()函數之前/n");
test1();
printf("在main()函數中,調用test1()函數之後/n");

printf("/n");

printf("在main()函數中,調用test2()函數之前/n");
test2();
printf("在main()函數中,調用test2()函數之後/n");

printf("/n");

printf("在main()函數中,調用test3()函數之前/n");
test3();
printf("在main()函數中,調用test3()函數之後/n");

return 0;
}

上面的程序運行結果如下:
在main()函數中,調用test1()函數之前

被0除異常,錯誤代碼爲:0xc0000094
跳過出現異常函數,返回到上層函數中繼續執行

在main()函數中,調用test1()函數之後

在main()函數中,調用test2()函數之前
在test2()函數中,調用test1()函數之前

被0除異常,錯誤代碼爲:0xc0000094
跳過出現異常函數,返回到上層函數中繼續執行

在test2()函數中,調用test1()函數之後


存儲保護異常,錯誤代碼爲:0xc0000005
跳過出現異常函數,返回到上層函數中繼續執行

在main()函數中,調用test2()函數之後

在main()函數中,調用test3()函數之前

其它異常,錯誤代碼爲:0x999
跳過出現異常函數,返回到上層函數中繼續執行

在main()函數中,調用test3()函數之後
Press any key to continue

總結

  本文所講到的異常處理機制,它就是狹義上的SEH,雖然它很簡單,但是它是Windows系列操作系統平臺上其它所有異常處理模型實現的奠基石。有了它就有了基本的物質保障,
另外,通常一般所說的SEH,它都是指在本篇文章中所闡述的狹義上的SEH機制基礎之上,實現的__try,__except,__finally,__leave異常模型,因此從下一篇文章中,開始全面介紹__try,__except,__finally,__leave異常模型,實際上,它也即廣義上的SEH。此後所有的文章內容中,如沒有特別註明,SEH機制都表示__try,__except,__finally,__leave異常模型,這也是爲了與try,catch,throw方式的C++異常模型相區分開。

  朋友們!有點疲勞了吧!可千萬不要放棄,繼續到下一篇的文章中,可要知道,__try,__except,__finally,__leave異常模型,它可以說是最優先的異常處理模型之一,甚至比C++的異常模型還好,功能還強大!即便是JAVA的異常處理模型也都從它這裏繼承了許多優點,所以不要錯過呦,Let’s go!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章