[轉][翻譯]深入理解Win32結構化異常處理(一)

白谷逸發表於2024-06-06

在所有的Win32作業系統提供的功能裡,最常用但是描述最不全的(underdocument)恐怕就是結構化異常處理了(structured exception handling (SEH))。當你想到Win32的結構化異常處理,你會想到 _try, _finally, 和 _except這些東西,你可以從任何一本Win32的書中找到SEH的很好的描述,即使是Win32SDK也有一個非常完備的關於_try, _finally, 和 _except等的結構化異常處理的概述。既然有這麼多關於關於SEH的書,為什麼還說它描述不全呢,那是因為本質上講Win32的結構化異常處理是作業系統提供的服務。所有你能找到的關於SEH的書都是描述一種包裝了作業系統內部實現的特定編譯器的執行時庫。微軟的作業系統或者編譯器廠商定義_try, _finally, 和 _except等關鍵字用以表意相關的操作,其他的編譯器廠商完全可以定義其他的關鍵字進行相同的表意。也就是說編譯器級的SEH封裝了作業系統原生的SEH,使得我們無法接觸到原生SEH的細節。也不知道為什麼,編譯器級別的SEH就像是一個大秘密,Microsoft的Visual C++和Borland的Borland C++都沒有提供它們SEH的最低層的程式碼。這篇文章中,我們從編譯器提供的SEH(透過程式碼生成和執行時庫提供)中剝離作業系統提供的SEH深入探究SEH最基本的概念。我會避免使用真正的C++異常處理(真正的C++異常處理使用catch()代替_except),實際上真正的異常處理的實現方式和本文討論的非常相似(當然了真正的C++異常處理會有一些額外的複雜的東西,討論這些東西會掩蓋SEH的本質,故略去不講)。

當一個執行緒故障發生時,作業系統會提供一個機會告知錯誤資訊。具體點說就是,當一個執行緒錯誤發生,作業系統會呼叫一個使用者定義的回撥函式,這個回撥函式定義一些使用者想要的操作,比如讓蜂鳴器發聲,或者播放一段.wav格式的提示音。不管這個回撥函式幹什麼,它最後的操作是返回一個值告訴作業系統下一步要幹什麼。Win32的異常回撥函式格式(來源於標準的Win32標頭檔案EXCPT.h)如下:

 EXCEPTION_DISPOSITION __cdecl _except_handler(     struct _EXCEPTION_RECORD *ExceptionRecord,     void * EstablisherFrame,     struct _CONTEXT *ContextRecord,     void * DispatcherContext     );


這個異常回撥函式的第一個引數是一個指向結構體EXCEPTION_RECORD的指標,這個結構體定義在WINNT.H裡如下:

typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; }  EXCEPTION_RECORD;

第一個引數ExceptionCode是一個由作業系統分配給異常的數值,在WINNT.H裡用#define定義了一系列的由STATUS_為字首的異常程式碼,比如STATUS_ACCESS_VIOLATION 的異常程式碼是 0xC0000005,我們可以從Windows NT DDK的標頭檔案NTSTATUS.H中找到更加完備的異常程式碼。

第四個引數ExceptionAddress異常發生的地址。

其他的引數可以暫時忽略。

異常回撥函式_except_handler的第二個引數是一個指向establisher frame結構體的指標,這是SEH中一個很重要的引數,但是現在暫時忽略。

第三個引數是一個指向結構體CONTEXT的指標,CONTEXT結構體定義在WINNT.H裡,它代表了特定執行緒的註冊值。當用在SEH時,CONTEXT就表示異常發生時的註冊值。順帶說一句,這個CONTEXT結構體與GetThreadContext和SetThreadContext所使用的結構體是同一個。

第四個引數DispatcherContext也可以暫時忽略。

CONTEXT結構體:

typedef struct _CONTEXT {     DWORD ContextFlags;     DWORD   Dr0;     DWORD   Dr1;     DWORD   Dr2;     DWORD   Dr3;     DWORD   Dr6;     DWORD   Dr7;     FLOATING_SAVE_AREA FloatSave;     DWORD   SegGs;     DWORD   SegFs;     DWORD   SegEs;     DWORD   SegDs;     DWORD   Edi;     DWORD   Esi;     DWORD   Ebx;     DWORD   Edx;     DWORD   Ecx;     DWORD   Eax;     DWORD   Ebp;     DWORD   Eip;     DWORD   SegCs;     DWORD   EFlags;     DWORD   Esp;     DWORD   SegSs; } CONTEXT;

簡單歸結一下前邊所說的:當一個異常發生時,一個回撥函式就會被呼叫,這個回撥函式有四個引數,其中三個是指向結構體的指標。_except_handler回撥函式接收豐富的異常資訊(比如什麼型別的異常發生了,在哪發生的),透過這些資訊,異常回撥函式決定要做什麼。

這裡留有一個疑問,當異常發生時,作業系統怎麼知道從哪呼叫這個回撥函式呢。答案是EXCEPTION_REGISTRATION。我們唯一能找到EXCEPTION_REGISTRATION定義的地方是Visual C++的執行時庫的EXSUP.INC。

EXCEPTION_REGISTRATION struc     prev    dd      ?     handler dd      ? _EXCEPTION_REGISTRATION ends

你也可以從WINNT.H的NT_TIB結構體的定義中看到一個被稱為_EXCEPTION_REGISTRATION_RECORD的資料型別,但是我們找不到任何關於這個資料型別的定義資訊,這也就是為什麼說SEH是underdocumented未被文件化的。
讓我們回到剛才的問題,作業系統怎麼知道當異常發生時要從哪呼叫回撥函式呢,EXCEPTION_REGISTRATION有兩部分,第一部分暫時先忽略,第二部分handler是一個指向_except_ handler回撥函式的指標。但是問題是作業系統怎麼找到這個EXCEPTION_REGISTRATION呢?

為了回答這個問題,讓我們重申一下:結構化異常處理工作在每個獨立的執行緒裡的,也就是說,每個執行緒都有自己的異常處理回撥函式。thread information block(也叫TEB,或者TIB)是一個重要的Win32資料結構它儲存了當前執行的執行緒的資訊。TIB裡的第一個DWORD就是一個指向該執行緒EXCEPTION_REGISTRATION結構體。在Intel的Win32平臺上,FS登錄檔總是指向當前的TIB,也就是說在FS:[0]處你可以找到指向EXCEPTION_REGISTRATION的指標。

總結一下:當異常發生時,作業系統查詢異常執行緒的TIB,從中取得指向EXCEPTION_REGISTRATION的指標,在EXCEPTION_REGISTRATION中可以找到指向異常回撥函式 _except_handler的指標。

透過上述資訊我寫了一小程式簡要描述一下系統級的結構化異常處理。

Figure 3   MYSEH.CPP //==================================================// MYSEH - Matt Pietrek 1997// Microsoft Systems Journal, January 1997// FILE: MYSEH.CPP// To compile: CL MYSEH.CPP//==================================================#define WIN32_LEAN_AND_MEAN#include <windows.h>#include <stdio.h>DWORD  scratch;EXCEPTION_DISPOSITION__cdecl_except_handler(    struct _EXCEPTION_RECORD *ExceptionRecord,    void * EstablisherFrame,    struct _CONTEXT *ContextRecord,    void * DispatcherContext ){    unsigned i;    // Indicate that we made it to our exception handler    printf( "Hello from an exception handler\n" );    // Change EAX in the context record so that it points to someplace    // where we can successfully write    ContextRecord->Eax = (DWORD)&scratch;    // Tell the OS to restart the faulting instruction    return ExceptionContinueExecution;}int main(){    DWORD handler = (DWORD)_except_handler;    __asm    {                           // Build EXCEPTION_REGISTRATION record:        push    handler         // Address of handler function        push    FS:[0]          // Address of previous handler        mov     FS:[0],ESP      // Install new EXECEPTION_REGISTRATION    }    __asm    {        mov     eax,0           // Zero out EAX        mov     [eax], 1        // Write to EAX to deliberately cause a fault    }    printf( "After writing!\n" );    __asm    {                           // Remove our EXECEPTION_REGISTRATION record        mov     eax,[ESP]       // Get pointer to previous record        mov     FS:[0], EAX     // Install previous record        add     esp, 8          // Clean our EXECEPTION_REGISTRATION off stack    }    return 0;}


Main函式里有三段inline的ASM程式碼塊,第一段透過在("PUSH handler" 和"PUSH FS:[0]")在棧上生成了一個EXCEPTION_REGISTRATION結構體。PUSH FS:[0]儲存了FS:[0]先前的值使之成為結構體的一部分,這樣棧上就有一個8位元組的EXCEPTION_REGISTRATION。下一條指令MOV FS:[0],ESP將當前執行緒資訊塊TIB的第一個DWORD放入新的EXCEPTION_REGISTRATION。

你有可能會奇怪為什麼我在棧上建立EXCEPTION_REGISTRATION而不是採用全域性變數,理由是當你使用編譯器的_try/_except的語法,編譯器也是在棧上建立EXCEPTION_REGISTRATION的,我只是簡單展示一下當你使用_try/_except時編譯器會如何做的一個簡化的版本。
第二個_asm程式碼段主要用於產生一個異常,MOV EAX,0清零EAX暫存器,然後MOV [EAX],1把暫存器的值當成一個記憶體地址,把1賦值給記憶體地址為零的記憶體這樣就會產生一個異常。
最後一個_asm程式碼段移除異常處理:將先前FS:[0]的值還原,然後將EXCEPTION_REGISTRATION彈出堆疊(ADD ESP,8)。

編譯完成後執行,你會發現,當MOV [EAX],1執行,它會引起一個access violation違規訪問異常,作業系統檢視TIB的FS:[0],找到EXCEPTION_REGISTRATION的指標,在這個結構體裡是一個指向_except_handler的指標,作業系統會將前邊所述四個引數入棧,然後呼叫異常處理函式。

在異常函式里首先執行printf,然後異常處理函式會解決異常,也就是EAX暫存器指向了一個不能寫入的地址0,解決方式是更改CONTEXT的EAX的值使他指向一塊可以寫入的記憶體地址(scratch的地址,scrath是為了簡化程式說明問題故意引入的),最後的操作將ExceptionContinueExecution返回。
當作業系統看到ExceptionContinueExecution返回,這就意味著你已經解決了問題,故障指令會重新執行。也就是說異常處理函式改變EAX的值使之指向可寫入記憶體,MOV EAX,1會再次執行,main函式正常執行完畢。

Reference:

http://www.microsoft.com/msj/0197/exception/exception.aspx

http://en.wikipedia.org/wiki/Win32_Thread_Information_Block

由於執行在CLR,C#的異常堆疊資訊,異常處理顯得沒有那麼神秘,畢竟CLR做為一個平臺包辦了這一切,但C++的異常處理怎麼實現呢,一直對這個問題很感興趣,以前去codeproject上看過一些帖子對結構化異常處理稍微瞭解過一點,但是理解的很膚淺。這幾天關注C#5碰到一篇採訪C#架構師Anders Hejlsberg的帖子無意間發現了關於SEH的這個連結,一讀覺著深入淺出,挺有意思,隨性翻譯一下備忘,沒有嚴格按照原文翻譯,而且也沒有翻譯完,以後抽時間做完它。難免有疏漏,歡迎指正,如果有高手指教提供更完備的資料在下感激不盡了。


---------------------
作者:salomon
來源:CNBLOGS
原文:https://www.cnblogs.com/salomon/archive/2012/06/20/2556134.html
版權宣告:本文為作者原創文章,轉載請附上博文連結!
內容解析By:CSDN,CNBLOG部落格文章一鍵轉載外掛

相關文章