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

白谷逸發表於2024-06-06
__except_handler3 and the scopetable

我真的很希望讓你看一看Visual C++執行時庫原始碼,讓你自己好好研究一下__except_handler3函式,但是我辦不到。因為 Microsoft並沒有提供。在這裡你就將就著看一下我為__except_handler3函式寫的虛擬碼吧

View Code
int __except_handler3(     struct _EXCEPTION_RECORD * pExceptionRecord,     struct EXCEPTION_REGISTRATION * pRegistrationFrame,     struct _CONTEXT *pContextRecord,     void * pDispatcherContext ) {     LONG filterFuncRet     LONG trylevel     EXCEPTION_POINTERS exceptPtrs     PSCOPETABLE pScopeTable      CLD     // Clear the direction flag (make no assumptions!)      // if neither the EXCEPTION_UNWINDING nor EXCEPTION_EXIT_UNWIND bit     // is set...  This is true the first time through the handler (the     // non-unwinding case)      if ( ! (pExceptionRecord->ExceptionFlags             & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) )     {         // Build the EXCEPTION_POINTERS structure on the stack         exceptPtrs.ExceptionRecord = pExceptionRecord;         exceptPtrs.ContextRecord = pContextRecord;          // Put the pointer to the EXCEPTION_POINTERS 4 bytes below the         // establisher frame.  See ASM code for GetExceptionInformation         *(PDWORD)((PBYTE)pRegistrationFrame - 4) = &exceptPtrs;          // Get initial "trylevel" value         trylevel = pRegistrationFrame->trylevel           // Get a pointer to the scopetable array         scopeTable = pRegistrationFrame->scopetable;  search_for_handler:           if ( pRegistrationFrame->trylevel != TRYLEVEL_NONE )         {             if ( pRegistrationFrame->scopetable[trylevel].lpfnFilter )             {                 PUSH EBP                        // Save this frame EBP                  // !!!Very Important!!!  Switch to original EBP.  This is                 // what allows all locals in the frame to have the same                 // value as before the exception occurred.                 EBP = &pRegistrationFrame->_ebp                   // Call the filter function                 filterFuncRet = scopetable[trylevel].lpfnFilter();                  POP EBP                         // Restore handler frame EBP                  if ( filterFuncRet != EXCEPTION_CONTINUE_SEARCH )                 {                     if ( filterFuncRet < 0 ) // EXCEPTION_CONTINUE_EXECUTION                         return ExceptionContinueExecution;                      // If we get here, EXCEPTION_EXECUTE_HANDLER was specified                     scopetable == pRegistrationFrame->scopetable                      // Does the actual OS cleanup of registration frames                     // Causes this function to recurse                     __global_unwind2( pRegistrationFrame );                      // Once we get here, everything is all cleaned up, except                     // for the last frame, where we'll continue execution                     EBP = &pRegistrationFrame->_ebp                                          __local_unwind2( pRegistrationFrame, trylevel );                      // NLG == "non-local-goto" (setjmp/longjmp stuff)                     __NLG_Notify( 1 );  // EAX == scopetable->lpfnHandler                      // Set the current trylevel to whatever SCOPETABLE entry                     // was being used when a handler was found                     pRegistrationFrame->trylevel = scopetable->previousTryLevel;                      // Call the _except {} block.  Never returns.                     pRegistrationFrame->scopetable[trylevel].lpfnHandler();                 }             }              scopeTable = pRegistrationFrame->scopetable;             trylevel = scopeTable->previousTryLevel              goto search_for_handler;         }         else    // trylevel == TRYLEVEL_NONE         {             retvalue == DISPOSITION_CONTINUE_SEARCH;         }     }     else    // EXCEPTION_UNWINDING or EXCEPTION_EXIT_UNWIND flags are set     {         PUSH EBP    // Save EBP         EBP = pRegistrationFrame->_ebp  // Set EBP for __local_unwind2          __local_unwind2( pRegistrationFrame, TRYLEVEL_NONE )          POP EBP     // Restore EBP          retvalue == DISPOSITION_CONTINUE_SEARCH;     } }

雖然__except_handler3的程式碼看起來很多,但是記住一點:它只是一個我在文章開頭講過的異常處理回撥函式。它同MYSEH.EXE和 MYSEH2.EXE中的異常回撥函式都帶有同樣的四個引數。__except_handler3大體上可以由第一個if語句分為兩部分。這是由於這個函式可以在兩種情況下被呼叫,一次是正常呼叫,另一次是在展開階段。其中大部分是在非展開階段的回撥。
  __except_handler3一開始就在堆疊上建立了一個EXCEPTION_POINTERS結構,並用它的兩個引數來對這個結構進行初始化。我在虛擬碼中把這個結構稱為 exceptPrts,它的地址被放在[EBP-14h]處。你回憶一下前面我講的編譯器為 GetExceptionInformation和 GetExceptionCode 函式生成的彙編程式碼就會意識到,這實際上初始化了這兩個函式使用的指標。
  接著,__except_handler3從EXCEPTION_REGISTRATION幀中獲取當前的trylevel(在[EBP-04h]處)。 trylevel變數實際是scopetable陣列的索引,而正是這個陣列才使得一個函式中的多個__try塊和巢狀的__try塊能夠僅使用一個 EXCEPTION_REGISTRATION結構

View Code
typedef struct _SCOPETABLE {     DWORD       previousTryLevel;     DWORD       lpfnFilter     DWORD       lpfnHandler } SCOPETABLE, *PSCOPETABLE;

SCOPETABLE結構中的第二個成員和第三個成員比較容易理解。它們分別是過濾器表示式程式碼的地址和相應的__except塊的地址。但是prviousTryLevel成員有點複雜。總之一句話,它用於巢狀的__try塊。這裡的關鍵是函式中的每個__try塊都有一個相應的SCOPETABLE結構。
  正如我前面所說,當前的 trylevel 指定了要使用的scopetable陣列的哪一個元素,最終也就是指定了過濾器表示式和__except塊的地址。現在想像一下兩個__try塊巢狀的情形。如果內層__try塊的過濾器表示式不處理某個異常,那外層__try塊的過濾器表示式就必須處理它。那現在要問,__except_handler3是如何知道SCOPETABLE陣列的哪個元素相應於外層的__try塊的呢?答案是:外層__try塊的索引由 SCOPETABLE結構的previousTryLevel域給出。利用這種機制,你可以巢狀任意層的__try塊。previousTryLevel 域就好像是一個函式中所有可能的異常處理程式構成的線性連結串列中的結點一樣。如果trylevel的值為0xFFFFFFFF(實際上就是-1,這個值在 EXSUP.INC中被定義為TRYLEVEL_NONE),標誌著這個連結串列結束。
  回到__except_handler3的程式碼中。在獲取了當前的trylevel之後,它就呼叫相應的SCOPETABLE結構中的過濾器表示式程式碼。如果過濾器表示式返回EXCEPTION_CONTINUE_SEARCH,__exception_handler3 移向SCOPETABLE陣列中的下一個元素,這個元素的索引由previousTryLevel域給出。如果遍歷完整個線性連結串列(還記得嗎?這個連結串列是由於在一個函式內部巢狀使用__try塊而形成的)都沒有找到處理這個異常的程式碼,__except_handler3返回DISPOSITION_CONTINUE_SEARCH(原文如此,但根據_except_handler函式的定義,這個返回值應該為ExceptionContinueSearch。實際上這兩個常量的值是一樣的。我在虛擬碼中已經將其改正過來了),這導致系統移向下一個EXCEPTION_REGISTRATION幀(這個連結串列是由於函式巢狀呼叫而形成的)。
  如果過濾器表示式返回EXCEPTION_EXECUTE_HANDLER,這意味著異常應該由相應的__except塊處理。它同時也意味著所有前面的EXCEPTION_REGISTRATION幀都應該從連結串列中移除,並且相應的__except塊都應該被執行。第一個任務透過呼叫__global_unwind2來完成的,後面我會講到這個函式。跳過這中間的一些清理程式碼,流程離開__except_handler3轉向__except塊。令人奇怪的是,流程並不從__except塊中返回,雖然是 __except_handler3使用CALL指令呼叫了它。
  當前的trylevel值是如何被設定的呢?它實際上是由編譯器隱含處理的。編譯器非常機靈地修改這個擴充套件的EXCEPTION_REGISTRATION 結構中的trylevel域的值(實際上是生成修改這個域的值的程式碼)。如果你檢查編譯器為使用SEH的函式生成的彙編程式碼,就會在不同的地方都看到修改這個位於[EBP-04h]處的trylevel域的值的程式碼。
  __except_handler3是如何做到既透過CALL指令呼叫__except塊而又不讓執行流程返回呢?由於CALL指令要向堆疊中壓入了一個返回地址,你可以想象這有可能破壞堆疊。如果你檢查一下編譯器為__except塊生成的程式碼,你會發現它做的第一件事就是將EXCEPTION_REGISTRATION結構下面8個位元組處(即[EBP-18H]處)的一個DWORD值載入到ESP暫存器中(實際程式碼為MOV ESP,DWORD PTR [EBP-18H]),這個值是在函式的 prolog 程式碼中被儲存在這個位置的(實際程式碼為MOV DWORD PTR [EBP-18H],ESP)。

The ShowSEHFrames Program

如果你現在覺得已經被EXCEPTION_REGISTRATION、scopetable、trylevel、過濾器表示式以及展開等等之類的詞搞得暈頭轉向的話,那和我最初的感覺一樣。但是編譯器層面的結構化異常處理方面的知識並不適合一點一點的學。除非你從整體上理解它,否則有很多內容單獨看並沒有什麼意義。當面對大堆的理論時,我最自然的做法就是寫一些應用我學到的理論方面的程式。如果它能夠按照預料的那樣工作,我就知道我的理解(通常)是正確的。
  下面是ShowSEHFrame.EXE的原始碼。它使用__try/__except塊設定了好幾個 Visual C++ SEH 幀。然後它顯示每一個幀以及Visual C++為每個幀建立的scopetable的相關資訊。這個程式本身並不生成也不依賴任何異常。相反,我使用了多個__try塊以強制Visual C++生成多個 EXCEPTION_REGISTRATION 幀以及相應的 scopetable。

View Code
//================================================== // ShowSEHFrames - Matt Pietrek 1997 // Microsoft Systems Journal, February 1997 // FILE: ShowSEHFrames.CPP // To compile: CL ShowSehFrames.CPP //================================================== #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> #pragma hdrstop  //---------------------------------------------------------------------------- // !!! WARNING !!!  This program only works with Visual C++, as the data // structures being shown are specific to Visual C++. //----------------------------------------------------------------------------  #ifndef _MSC_VER #error Visual C++ Required (Visual C++ specific information is displayed) #endif  //---------------------------------------------------------------------------- // Structure Definitions //----------------------------------------------------------------------------  // The basic, OS defined exception frame  struct EXCEPTION_REGISTRATION {     EXCEPTION_REGISTRATION* prev;     FARPROC                 handler; };   // Data structure(s) pointed to by Visual C++ extended exception frame  struct scopetable_entry {     DWORD       previousTryLevel;     FARPROC     lpfnFilter;     FARPROC     lpfnHandler; };  // The extended exception frame used by Visual C++  struct VC_EXCEPTION_REGISTRATION : EXCEPTION_REGISTRATION {     scopetable_entry *  scopetable;     int                 trylevel;     int                 _ebp; };  //---------------------------------------------------------------------------- // Prototypes //----------------------------------------------------------------------------  // __except_handler3 is a Visual C++ RTL function.  We want to refer to // it in order to print it's address.  However, we need to prototype it since // it doesn't appear in any header file.  extern "C" int _except_handler3(PEXCEPTION_RECORD, EXCEPTION_REGISTRATION *,                                 PCONTEXT, PEXCEPTION_RECORD);   //---------------------------------------------------------------------------- // Code //----------------------------------------------------------------------------  // // Display the information in one exception frame, along with its scopetable //  void ShowSEHFrame( VC_EXCEPTION_REGISTRATION * pVCExcRec ) {     printf( "Frame: %08X  Handler: %08X  Prev: %08X  Scopetable: %08X\n",             pVCExcRec, pVCExcRec->handler, pVCExcRec->prev,             pVCExcRec->scopetable );      scopetable_entry * pScopeTableEntry = pVCExcRec->scopetable;      for ( unsigned i = 0; i <= pVCExcRec->trylevel; i++ )     {         printf( "    scopetable[%u] PrevTryLevel: %08X  "                 "filter: %08X  __except: %08X\n", i,                 pScopeTableEntry->previousTryLevel,                 pScopeTableEntry->lpfnFilter,                 pScopeTableEntry->lpfnHandler );          pScopeTableEntry++;     }      printf( "\n" ); }     // // Walk the linked list of frames, displaying each in turn //  void WalkSEHFrames( void ) {     VC_EXCEPTION_REGISTRATION * pVCExcRec;      // Print out the location of the __except_handler3 function     printf( "_except_handler3 is at address: %08X\n", _except_handler3 );     printf( "\n" );      // Get a pointer to the head of the chain at FS:[0]     __asm   mov eax, FS:[0]     __asm   mov [pVCExcRec], EAX      // Walk the linked list of frames.  0xFFFFFFFF indicates the end of list     while (  0xFFFFFFFF != (unsigned)pVCExcRec )     {         ShowSEHFrame( pVCExcRec );         pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev);     }        }  void Function1( void ) {     // Set up 3 nested _try levels (thereby forcing 3 scopetable entries)     _try     {         _try         {             _try             {                 WalkSEHFrames();    // Now show all the exception frames             }             _except( EXCEPTION_CONTINUE_SEARCH )             {             }         }         _except( EXCEPTION_CONTINUE_SEARCH )         {         }     }     _except( EXCEPTION_CONTINUE_SEARCH )     {     } }  int main() {     int i;      // Use two (non-nested) _try blocks.  This causes two scopetable entries     // to be generated for the function.      _try     {         i = 0x1234;     // Do nothing in particular     }     _except( EXCEPTION_CONTINUE_SEARCH )     {         i = 0x4321;     // Do nothing (in reverse)     }      _try     {         Function1();    // Call a function that sets up more exception frames     }     _except( EXCEPTION_EXECUTE_HANDLER )     {         // Should never get here, since we aren't expecting an exception         printf( "Caught Exception in main\n" );     }      return 0; }

ShowSEHFrames程式中比較重要的函式是WalkSEHFrames和ShowSEHFrame。WalkSEHFrames函式首選列印出 __except_handler3的地址,列印它的原因很快就清楚了。接著,它從FS:[0]處獲取異常連結串列的頭指標,然後遍歷該連結串列。此連結串列中每個結點都是一個VC_EXCEPTION_REGISTRATION型別的結構,它是我自己定義的,用於描述Visual C++的異常處理幀。對於這個連結串列中的每個結點,WalkSEHFrames都把指向這個結點的指標傳遞給ShowSEHFrame函式。
  ShowSEHFrame函式一開始就列印出異常處理幀的地址、異常處理回撥函式的地址、前一個異常處理幀的地址以及scopetable的地址。接著,對於每個 scopetable陣列中的元素,它都列印出其priviousTryLevel、過濾器表示式的地址以及相應的__except塊的地址。我是如何知道scopetable陣列中有多少個元素的呢?其實我並不知道。但是我假定VC_EXCEPTION_REGISTRATION結構中的當前trylevel域的值比scopetable陣列中的元素總數少1。
  圖十一是 ShowSEHFrames 的執行結果。首先檢查以“Frame:”開頭的每一行,你會發現它們顯示的異常處理幀在堆疊上的地址呈遞增趨勢,並且在前三個幀中,它們的異常處理程式的地址是一樣的(都是004012A8)。再看輸出的開始部分,你會發現這個004012A8不是別的,它正是 Visual C++執行時庫函式__except_handler3的地址。這證明了我前面所說的單個回撥函式處理所有異常這一點。

  你可能想知道為什麼明明 ShowSEHFrames 程式只有兩個函式使用SEH,但是卻有三個異常處理幀使用__except_handler3作為它們的異常回撥函式。實際上第三個幀來自 Visual C++ 執行時庫。Visual C++ 執行時庫原始碼中的 CRT0.C 檔案清楚地表明瞭對 main 或 WinMain 的呼叫也被一個__try/__except 塊封裝著。這個__try 塊的過濾器表示式程式碼可以在 WINXFLTR.C文 件中找到。   回到 ShowSEHFrames 程式,注意到最後一個幀的異常處理程式的地址是 77F3AB6C,這與其它三個不同。仔細觀察一下,你會發現這個地址在 KERNEL32.DLL 中。這個特別的幀就是由 KERNEL32.DLL 中的 BaseProcessStart 函式安裝的,這在前面

Unwinding

在挖掘展開(Unwinding)的實現程式碼之前讓我們先來搞清楚它的意思。我在前面已經講過所有可能的異常處理程式是如何被組織在一個由執行緒資訊塊的第一個DWORD(FS:[0])所指向的連結串列中的。由於針對某個特定異常的處理程式可能不在這個連結串列的開頭,因此就需要從連結串列中依次移除實際處理異常的那個異常處理程式之前的所有異常處理程式。

  正如你在Visual C++的__except_handler3函式中看到的那樣,展開是由__global_unwind2這個執行時庫(RTL)函式來完成的。這個函式只是對RtlUnwind這個未公開的API進行了非常簡單的封裝。(現在這個API已經被公開了,但給出的資訊極其簡單,詳細資訊可以參考最新的Platform SDK文件。)

__global_unwind2(void * pRegistFrame) {     _RtlUnwind( pRegistFrame,                 &__ret_label,                 0, 0 );     __ret_label: }

雖然從技術上講RtlUnwind是一個KERNEL32函式,但它只是轉發到了NTDLL.DLL中的同名函式上。下面是我為此函式寫的虛擬碼。

View Code
void _RtlUnwind( PEXCEPTION_REGISTRATION pRegistrationFrame,                  PVOID returnAddr,  // Not used! (At least on i386)                  PEXCEPTION_RECORD pExcptRec,                  DWORD _eax_value )  {     DWORD   stackUserBase;     DWORD   stackUserTop;     PEXCEPTION_RECORD pExcptRec;     EXCEPTION_RECORD  exceptRec;         CONTEXT context;      // Get stack boundaries from FS:[4] and FS:[8]     RtlpGetStackLimits( &stackUserBase, &stackUserTop );      if ( 0 == pExcptRec )   // The normal case     {         pExcptRec = &excptRec;          pExcptRec->ExceptionFlags = 0;         pExcptRec->ExceptionCode = STATUS_UNWIND;         pExcptRec->ExceptionRecord = 0;         // Get return address off the stack         pExcptRec->ExceptionAddress = RtlpGetReturnAddress();         pExcptRec->ExceptionInformation[0] = 0;     }      if ( pRegistrationFrame )         pExcptRec->ExceptionFlags |= EXCEPTION_UNWINDING;     else         pExcptRec->ExceptionFlags|=(EXCEPTION_UNWINDING|EXCEPTION_EXIT_UNWIND);      context.ContextFlags =         (CONTEXT_i486 | CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS);      RtlpCaptureContext( &context );      context.Esp += 0x10;     context.Eax = _eax_value;      PEXCEPTION_REGISTRATION pExcptRegHead;      pExcptRegHead = RtlpGetRegistrationHead();  // Retrieve FS:[0]      // Begin traversing the list of EXCEPTION_REGISTRATION     while ( -1 != pExcptRegHead )     {         EXCEPTION_RECORD excptRec2;          if ( pExcptRegHead == pRegistrationFrame )         {             _NtContinue( &context, 0 );         }         else         {             // If there's an exception frame, but it's lower on the stack             // then the head of the exception list, something's wrong!             if ( pRegistrationFrame && (pRegistrationFrame <= pExcptRegHead) )             {                 // Generate an exception to bail out                 excptRec2.ExceptionRecord = pExcptRec;                 excptRec2.NumberParameters = 0;                 excptRec2.ExceptionCode = STATUS_INVALID_UNWIND_TARGET;                 excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;                      _RtlRaiseException( &exceptRec2 );             }         }          PVOID pStack = pExcptRegHead + 8; // 8==sizeof(EXCEPTION_REGISTRATION)          if (    (stackUserBase <= pExcptRegHead )   // Make sure that             &&  (stackUserTop >= pStack )           // pExcptRegHead is in             &&  (0 == (pExcptRegHead & 3)) )        // range, and a multiple         {                                           // of 4 (i.e., sane)             DWORD pNewRegistHead;             DWORD retValue;              retValue = RtlpExecutehandlerForUnwind(                             pExcptRec, pExcptRegHead, &context,                             &pNewRegistHead, pExceptRegHead->handler );              if ( retValue != DISPOSITION_CONTINUE_SEARCH )             {                 if ( retValue != DISPOSITION_COLLIDED_UNWIND )                 {                     excptRec2.ExceptionRecord = pExcptRec;              excptRec2.NumberParameters = 0;                     excptRec2.ExceptionCode = STATUS_INVALID_DISPOSITION;                     excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;                          RtlRaiseException( &excptRec2 );                 }                 else                     pExcptRegHead = pNewRegistHead;             }              PEXCEPTION_REGISTRATION pCurrExcptReg = pExcptRegHead;             pExcptRegHead = pExcptRegHead->prev;              RtlpUnlinkHandler( pCurrExcptReg );         }         else    // The stack looks goofy!  Raise an exception to bail out         {             excptRec2.ExceptionRecord = pExcptRec;             excptRec2.NumberParameters = 0;             excptRec2.ExceptionCode = STATUS_BAD_STACK;             excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;                  RtlRaiseException( &excptRec2 );         }     }      // If we get here, we reached the end of the EXCEPTION_REGISTRATION list.     // This shouldn't happen normally.      if ( -1 == pRegistrationFrame )         NtContinue( &context, 0 );     else         NtRaiseException( pExcptRec, &context, 0 );  }  PEXCEPTION_REGISTRATION RtlpGetRegistrationHead( void ) {     return FS:[0]; }  _RtlpUnlinkHandler( PEXCEPTION_REGISTRATION pRegistrationFrame ) {     FS:[0] = pRegistrationFrame->prev; }  void _RtlpCaptureContext( CONTEXT * pContext ) {     pContext->Eax = 0;     pContext->Ecx = 0;     pContext->Edx = 0;     pContext->Ebx = 0;     pContext->Esi = 0;     pContext->Edi = 0;     pContext->SegCs = CS;     pContext->SegDs = DS;     pContext->SegEs = ES;     pContext->SegFs = FS;     pContext->SegGs = GS;     pContext->SegSs = SS;     pContext->EFlags = flags; // __asm{ PUSHFD / pop [xxxxxxxx] }     pContext->Eip = return address of the caller of the caller of this function     pContext->Ebp = EBP of the caller of the caller of this function      pContext->Esp = Context.Ebp + 8 }


雖然 RtlUnwind 函式的規模看起來很大,但是如果你按一定方法把它分開,其實並不難理解。它首先從FS:[4]和FS:[8]處獲取當前執行緒堆疊的界限。它們對於後面要進行的合法性檢查非常重要,以確保所有將要被展開的異常幀都在堆疊範圍內。

  RtlUnwind 接著在堆疊上建立了一個空的EXCEPTION_RECORD結構並把STATUS_UNWIND賦給它的ExceptionCode域,同時把 EXCEPTION_UNWINDING標誌賦給它的 ExceptionFlags 域。指向這個結構的指標作為其中一個引數被傳遞給每個異常回撥函式。然後,這個函式呼叫RtlCaptureContext函式來建立一個空的CONTEXT結構,這個結構也變成了在展開階段呼叫每個異常回撥函式時傳遞給它們的一個引數。

  RtlUnwind函式的其餘部分遍歷EXCEPTION_REGISTRATION結構連結串列。對於其中的每個幀,它都呼叫 RtlpExecuteHandlerForUnwind 函式,後面我會講到這個函式。正是這個函式帶 EXCEPTION_UNWINDING 標誌呼叫了異常處理回撥函式。每次回撥之後,它呼叫RtlpUnlinkHandler 移除相應的異常幀。

  RtlUnwind 函式的第一個引數是一個幀的地址,當它遍歷到這個幀時就停止展開異常幀。上面所說的這些程式碼之間還有一些安全性檢查程式碼,它們用來確保不出問題。如果出現任何問題,RtlUnwind 就引發一個異常,指示出了什麼問題,並且這個異常帶有EXCEPTION_NONCONTINUABLE 標誌。當一個程序被設定了這個標誌時,它就不允許再執行,必須終止。

Unhandled Exceptions

在文章的前面,我並沒有全面描述 UnhandledExceptionFilter 這個 API。通常情況下你並不直接呼叫它(儘管你可以這麼做)。大多數情況下它都是由 KERNEL32 中進行預設異常處理的過濾器表示式程式碼呼叫。前面 BaseProcessStart 函式的虛擬碼已經表明了這一點。

  圖十三是我為 UnhandledExceptionFilter 函式寫的虛擬碼。這個API有點奇怪(至少在我看來是這樣)。如果異常的型別是 EXCEPTION_ACCESS_VIOLATION,它就呼叫_BasepCheckForReadOnlyResource。雖然我沒有提供這個函式的虛擬碼,但可以簡要描述一下。如果是因為要對 EXE 或 DLL 的資源節(.rsrc)進行寫操作而導致的異常,_BasepCurrentTopLevelFilter 就改變出錯頁面正常的只讀屬性,以便允許進行寫操作。如果是這種特殊的情況,UnhandledExceptionFilter 返回 EXCEPTION_CONTINUE_EXECUTION,使系統重新執行出錯指令。

View Code
UnhandledExceptionFilter( STRUCT _EXCEPTION_POINTERS *pExceptionPtrs ) {     PEXCEPTION_RECORD pExcptRec;     DWORD currentESP;     DWORD retValue;     DWORD DEBUGPORT;     DWORD   dwTemp2;     DWORD   dwUseJustInTimeDebugger;     CHAR    szDbgCmdFmt[256];   // Template string retrieved from AeDebug key     CHAR    szDbgCmdLine[256];  // Actual debugger string after filling in     STARTUPINFO startupinfo;     PROCESS_INFORMATION pi;     HARDERR_STRUCT  harderr;    // ???     BOOL fAeDebugAuto;     TIB * pTib;                 // Thread information block      pExcptRec = pExceptionPtrs->ExceptionRecord;      if (   (pExcptRec->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)         && (pExcptRec->ExceptionInformation[0]) )     {         retValue =              _BasepCheckForReadOnlyResource(pExcptRec->ExceptionInformation[1]);          if ( EXCEPTION_CONTINUE_EXECUTION == retValue )             return EXCEPTION_CONTINUE_EXECUTION;      }      // See if this process is being run under a debugger...     retValue = NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort,                                          &debugPort, sizeof(debugPort), 0 );      if ( (retValue >= 0) && debugPort )     // Let debugger have it         return EXCEPTION_CONTINUE_SEARCH;      // Did the user call SetUnhandledExceptionFilter?  If so, call their     // installed proc now.      if ( _BasepCurrentTopLevelFilter )     {         retValue = _BasepCurrentTopLevelFilter( pExceptionPtrs );          if ( EXCEPTION_EXECUTE_HANDLER == retValue )             return EXCEPTION_EXECUTE_HANDLER;                  if ( EXCEPTION_CONTINUE_EXECUTION == retValue )             return EXCEPTION_CONTINUE_EXECUTION;          // Only EXCEPTION_CONTINUE_SEARCH goes on from here     }      // Has SetErrorMode(SEM_NOGPFAULTERRORBOX) been called?     if ( 0 == (GetErrorMode() & SEM_NOGPFAULTERRORBOX) )     {         harderr.elem0 = pExcptRec->ExceptionCode;         harderr.elem1 = pExcptRec->ExceptionAddress;          if ( EXCEPTION_IN_PAGE_ERROR == pExcptRec->ExceptionCode )             harderr.elem2 = pExcptRec->ExceptionInformation[2];         else             harderr.elem2 = pExcptRec->ExceptionInformation[0];          dwTemp2 = 1;         fAeDebugAuto = FALSE;          harderr.elem3 = pExcptRec->ExceptionInformation[1];          pTib = FS:[18h];          DWORD someVal = pTib->pProcess->0xC;          if ( pTib->threadID != someVal )         {             __try             {                 char szDbgCmdFmt[256]                 retValue = _GetProfileStringA( "AeDebug", "Debugger", 0,                                      szDbgCmdFmt, sizeof(szDbgCmdFmt)-1 );                  if ( retValue )                     dwTemp2 = 2;                  char szAuto[8]                  retValue = GetProfileStringA(   "AeDebug", "Auto", "0",                                                 szAuto, sizeof(szAuto)-1 );                 if ( retValue )                     if ( 0 == strcmp( szAuto, "1" ) )                         if ( 2 == dwTemp2 )                             fAeDebugAuto = TRUE;             }             __except( EXCEPTION_EXECUTE_HANDLER )             {                 ESP = currentESP;                 dwTemp2 = 1                 fAeDebugAuto = FALSE;             }         }          if ( FALSE == fAeDebugAuto )         {             retValue =  NtRaiseHardError(                                 STATUS_UNHANDLED_EXCEPTION | 0x10000000,                                 4, 0, &harderr,                                 _BasepAlreadyHadHardError ? 1 : dwTemp2,                                 &dwUseJustInTimeDebugger );         }         else         {             dwUseJustInTimeDebugger = 3;             retValue = 0;         }          if (    retValue >= 0              &&  ( dwUseJustInTimeDebugger == 3)             &&  ( !_BasepAlreadyHadHardError )             &&  ( !_BaseRunningInServerProcess ) )         {             _BasepAlreadyHadHardError = 1;              SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr), 0, TRUE };              HANDLE hEvent = CreateEventA( &secAttr, TRUE, 0, 0 );              memset( &startupinfo, 0, sizeof(startupinfo) );              sprintf(szDbgCmdLine, szDbgCmdFmt, GetCurrentProcessId(), hEvent);              startupinfo.cb = sizeof(startupinfo);             startupinfo.lpDesktop = "Winsta0\Default"              CsrIdentifyAlertableThread();   // ???              retValue = CreateProcessA(                             0,              // lpApplicationName                             szDbgCmdLine,   // Command line                             0, 0,           // process, thread security attrs                             1,              // bInheritHandles                             0, 0,           // creation flags, environment                             0,              // current directory.                             &statupinfo,    // STARTUPINFO                             &pi );          // PROCESS_INFORMATION              if ( retValue && hEvent )             {                 NtWaitForSingleObject( hEvent, 1, 0 );                 return EXCEPTION_CONTINUE_SEARCH;             }         }          if ( _BasepAlreadyHadHardError )             NtTerminateProcess(GetCurrentProcess(), pExcptRec->ExceptionCode);     }      return EXCEPTION_EXECUTE_HANDLER; }  LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(     LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter );    {      // _BasepCurrentTopLevelFilter is a KERNEL32.DLL global var     LPTOP_LEVEL_EXCEPTION_FILTER previous= _BasepCurrentTopLevelFilter;      // Set the new value     _BasepCurrentTopLevelFilter = lpTopLevelExceptionFilter;      return previous;    // return the old value }

UnhandledExceptionFilter接下來的任務是確定程序是否執行於Win32偵錯程式下。也就是程序的建立標誌中是否帶有標誌DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS。它使用NtQueryInformationProcess函式來確定程序是否正在被除錯,我在本月的Under the Hood專欄中講解了這個函式。如果正在被除錯,UnhandledExceptionFilter就返回 EXCEPTION_CONTINUE_SEARCH,這告訴系統去喚醒偵錯程式並告訴它在被除錯程式(debuggee)中產生了一個異常。

  UnhandledExceptionFilter接下來呼叫使用者安裝的未處理異常過濾器(如果存在的話)。通常情況下,使用者並沒有安裝回撥函式,但是使用者可以呼叫 SetUnhandledExceptionFilter這個API來安裝。上面我也提供了這個API的虛擬碼。這個函式只是簡單地用使用者安裝的回撥函式的地址來替換一個全域性變數,並返回替換前的值。

  有了初步的準備之後,UnhandledExceptionFilter就開始做它的主要工作:用一個時髦的應用程式錯誤對話方塊來通知你犯了低階的程式設計錯誤。有兩種方法可以避免出現這個對話方塊。第一種方法是呼叫SetErrorMode函式並指定SEM_NOGPFAULTERRORBOX標誌。另一種方法是將AeDebug子鍵下的Auto的值設為1。此時UnhandledExceptionFilter跳過應用程式錯誤對話方塊直接啟動AeDebug 子鍵下的Debugger的值所指定的偵錯程式。如果你熟悉“即時除錯(Just In Time Debugging,JIT)”的話,這就是作業系統支援它的地方。接下來我會詳細講。

  大多數情況下,上面的兩個條件都為假。這樣UnhandledExceptionFilter就呼叫NTDLL.DLL中的 NtRaiseHardError函式。正是這個函式產生了應用程式錯誤對話方塊。這個對話方塊等待你單擊“確定”按鈕來終止程序,或者單擊“取消”按鈕來除錯它。(單擊“取消”按鈕而不是“確定”按鈕來載入偵錯程式好像有點顛倒了,可能這只是我個人的感覺吧。)

  如果你單擊“確定”,UnhandledExceptionFilter就返回EXCEPTION_EXECUTE_HANDLER。呼叫UnhandledExceptionFilter 的程序通常透過終止自身來作為響應(正像你在BaseProcessStart的虛擬碼中看到的那樣)。這就產生了一個有趣的問題——大多數人都認為是系統終止了產生未處理異常的程序,而實際上更準確的說法應該是,系統進行了一些設定使得產生未處理異常的程序將自身終止掉了。

  UnhandledExceptionFilter執行時真正有意思的部分是當你單擊應用程式錯誤對話方塊中的“取消”按鈕,此時系統將偵錯程式附加(attach)到出錯程序上。這段程式碼首先呼叫 CreateEvent來建立一個事件核心物件,偵錯程式成功附加到出錯程序之後會將此事件物件變成有訊號狀態。這個事件控制代碼以及出錯程序的ID都被傳到 sprintf函式,由它將其格式化成一個命令列,用來啟動偵錯程式。一切就緒之後,UnhandledExceptionFilter就呼叫 CreateProcess來啟動偵錯程式。如果CreateProcess成功,它就呼叫NtWaitForSingleObject來等待前面建立的那個事件物件。此時這個呼叫被阻塞,直到偵錯程式程序將此事件變成有訊號狀態,以表明它已經成功附加到出錯程序上。UnhandledExceptionFilter函式中還有一些其它的程式碼,我在這裡只講重要的。


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

相關文章