結構化異常處理

結構化異常處理,是Windows操作系統上,Microsoft對C/C++程序語言做的語法擴展,用於處理異常事件的程序控制結構。

異常事件是打斷程序正常執行流程的不在期望之中的硬件、軟件事件。硬件異常是CPU拋出的如「除0」、數值溢出等;軟件異常是操作系統與程序通過RaiseException語句拋出的異常。

Microsoft擴展了C語言的語法,用 try-except與try-finally語句來處理異常。[1]異常處理程序可以釋放已經獲取的資源、顯示出錯信息與程序內部狀態供調試、從錯誤中恢復、嘗試重新執行出錯的代碼或者關閉程序等等。

一個__try語句不能既有__except,又有__finally。但try-except與try-finally語句可以嵌套使用。

try-except

編輯
__try 
{
   // 受保护执行的代码
}
__except ( 过滤表达式 )
{
   // 异常处理代码
}

首先,__try複合語句中的受保護的代碼被執行。如果沒有異常發生,則繼續執行__except複合語句之後的代碼。如果__try複合語句中的受保護執行的代碼發生了異常,或受保護執行的代碼調用的函數內部發生了異常並要求調用者來處理該異常,__except語句的過濾表達式(filter expression)被求值,根據其結果來決定如何處理異常:

  • EXCEPTION_CONTINUE_EXECUTION (–1)  : 導致異常的問題已經解決,在異常出現的現場重新執行操作。
  • EXCEPTION_CONTINUE_SEARCH (0) :當前__except語句不能處理該異常,通知操作系統繼續搜尋該線程其他的異常處理程序。
  • EXCEPTION_EXECUTE_HANDLER (1):當前__except語句識別該異常,通過執行__except的複合語句來處理該異常。然後執行__except複合語句之後的代碼。

內在函數GetExceptionCode返回一個32位整型值,表示異常的類型。內在函數GetExceptionInformation返回異常的詳細信息及現場信息(如CPU寄存器的值) 。這兩個函數可用於異常表達式中來判斷是否處理該異常。這裡說的內在函數(intrinsic function),是指編譯器提供內聯(inline)實現的函數。

Windows操作系統的應用程序的main()函數受到結構化異常的try-except語句保護,因此程序的未被處理的異常都會被捕獲。這是因為,Windows操作系統在加載用戶進程時,Kernel32.dll中的BaseProcessStart函數在__try塊中調用了用戶進程的入口函數mainCRTStartup,因此用戶程序的所有異常都會被捕獲、得到處理。

可以使用C++運行時庫中的_set_se_translator()函數把結構化異常轉為拋出一個C++對象的C++異常。此外,C++異常的catch(...)語句也能直接捕捉結構化異常。

可以使用操作系統運行時庫kernel32中的SetUnhandledExceptionFilter()函數設置頂層未處理異常過濾器(top-level unhandled exception filter),捕獲進程的各個線程中一切未被處理的結構化異常。該函數一般用於從特定的錯誤中恢復,如無效的調用棧(invalid stack)。

try-finally語句

編輯
__try {
     // 受保护执行的代码
 }
__finally {
     // 清理用途的代码
}

__try複合語句中受保護的代碼以任何方式執行結束後,不論__try複合語句是因為出現異常而非正常結束,還是沒有出現異常而正常結束,__finally複合語句中的代碼都會被執行。

如果__try複合語句中受保護的代碼的執行沒有出現異常,包括用goto語句或longjump系統函數跳出__try複合語句等情形,這時將執行__finally複合語句,然後執行try-finally語句之後的其他代碼。

在__finally複合語句中使用內在函數AbnormalTermination判斷是正常結束還是非正常結束__try複合語句。

執行順序

編輯

如果__try複合語句中受保護的代碼執行中出現了異常,首先按函數調用順序從新向舊搜索所有包含了出現異常的執行點的try-except語句,執行每個except的過濾函數,直到某個try-except語句的過濾函數結果值為EXCEPTION_EXECUTE_HANDLER。這時,再從包含異常出現的執行點的最內層try-finally語句開始由內向外執行每個finally塊中的代碼,直至回退到那個過濾函數結果值為EXCEPTION_EXECUTE_HANDLER的try-except語句執行其except塊,最後執行該try-except語句後面的其他代碼。

__leave關鍵字用在 try-finally 語句的受保護節(try塊)中,其效果是跳轉到受保護節的結尾處。 與goto語句跳出受保護節不同,__leave語句更為有效,因為它不會導致堆棧展開。

例子

編輯
 
#include <stdio.h>
#include <windows.h> // for EXCEPTION_ACCESS_VIOLATION
#include <excpt.h>

int filter(unsigned int code, struct _EXCEPTION_POINTERS *ep) 
{
   printf("在異常表達式中\n");
   if (code == EXCEPTION_ACCESS_VIOLATION) {
      printf("接受處理訪問違例異常\n");
      return EXCEPTION_EXECUTE_HANDLER;
   }
   else {
      printf("其他異常都不處理\n");
      return EXCEPTION_CONTINUE_SEARCH;
   };
}

int main()
{
   int* p = 0x00000000;   // pointer to NULL

   printf("開始主程序\n");
   __try{
      printf("進入外層的try\n");
      __try{
         printf("進入內層的try\n");
         int *p=0; // 空指針
         *p = 13;    // 導致訪問衝突異常
      }__finally{
         printf("在finally內部。");
         printf(AbnormalTermination() ? "非正常終止\n" : "正常終止\n");
      }
   }__except(filter(GetExceptionCode(), GetExceptionInformation())){
      printf("在except內部\n");
   }
   printf("主函數結束\n");
}

輸出結果為:

開始主程序
進入外層的try
進入內層的try
在異常表達式中
接受處理訪問違例異常
在finally內部。非正常終止
在except內部
主函數結束

與Windows異常處理機制的關係

編輯

Windows操作系統(自Windows95起),對每個用戶線程,都設立一個異常處理幀鍊表來處理異常事件。該鍊表的每個異常處理幀由兩個成員組成,分別是鍊表上一項地址、當前異常處理器地址,組成了結構_EXCEPTION_REGISTRATION_RECORD。異常處理器是指一個處理異常的回調函數(callback function)。線程信息塊(thread information block)的開始處(即FS:[0]指向的內存,FS是CPU的一個段寄存器)保存了異常處理幀鍊表的表頭項的地址。程序執行遇到異常事件而中斷時,操作系統的RtlDispatchException函數會從FS:[0]指向的鍊表表頭依次調用每個節點包含異常處理回調函數,直到某個異常處理回調函數的返回值為0表示已經處理該異常,該線程可以恢復執行。鍊表最末一項是操作系統在裝入線程時設置的指向kernel32!UnhandledExceptionFilter函數,該函數總是向用戶顯示「Application error」對話框。

上述異常處理器程序及鍊表,是由用戶程序自己安裝的。鍊表各節點保存在程序調用棧(call stack)上。

Windows異常處理機制支持嵌套異常的處理,即在執行異常處理回調函數時再次發生異常。這種情況下仍遵照普通異常處理機制,操作系統RtlDispatchException函數再入處理新出現的嵌套的異常。嵌套的異常的處理函數得到的DispatcherContext參數值即為在執行時發生了新異常的異常幀的地址。

各種編程語言基於上述Windows異常處理機制,設計了各自的異常處理語句控制結構。Microsoft擴展了C語言語法,設計了結構化異常處理的try-except與try-finally語句。一個函數的所有在函數的的try-except與try-finally形成了一個基於包含(enclosing)關係的森林 (數據結構)。一個函數內如果有__try語句,則在函數的入口與結尾處,編譯器插入了EH_prolog與EH_epilog代碼,把函數內所有在try塊中被保護的代碼包了起來。 EH_prolog在調用棧上創建一個_EXCEPTION_REGISTRATION_RECORD,作為異常處理鍊表的新的表頭,其中包含了Visual C++ 的運行時庫msvcrt.dll的__except_handler4函數地址。在函數塊的結尾處,EH_epilog把這項_EXCEPTION_REGISTRATION_RECORD從鍊表頭移除,恢復其原來的表頭。__except語句中的過濾表達式,由掛在鍊表中的異常處理回調函數MSVCR100D!__except_handler4來調用執行,返回值即為過濾表達式的求值結果。

實際上,編譯器實現結構化異常時,把鍊表每項的數據結構由2個成員擴展為5個成員,即在高地址方向追加了一個scopetable_entries類型結構體數組的指針、一個整型項表示執行點位於當前函數的哪個try塊中、一個保存寄存器EBP的整數項。此後(低地址方向)緊接着是一個指向EXCEPTION_POINTERS結構的指針(前述的內在函數GetExceptionInformation即返回這個指針值)。

異常發生時,操作系統的異常處理機制的ntdll!RtlDispatchException函數會從FS:[0]指向的鍊表表頭依次調用異常幀鍊表的每個節點所包含的異常處理回調函數MSVCR100D!__except_handler4,根據該回調函數的返回值來確定異常是否已經被處理,可以根據異常上下文(Exception context)恢復線程的執行。__except_handler4回調函數實際上只是調用了MSVCR100D!__except_handler4_common函數。__except_handler4_common函數是實際的workhorse,負責在當前異常幀所在的函數中查找那個try-except語句能夠處理該異常(即過濾表達式的結果為1) 。[2]如果不存在這樣的try-except塊,__except_handler4_common函數返回ExceptionContinueExecution(值0),由RtlDispatchException繼續訪問異常幀鍊表的下一個節點。如果找到了能處理當前異常的try-except塊,__except_handler4_common函數首先調用全局展開函數_EH4_GlobalUnwind,把從異常幀鍊表表頭所在的函數,直到能處理當前異常的函數的下一層函數,都做棧展開(unwinding);然後,對能處理當前異常的函數,從包含執行點的最內存__try語句,直到能處理異常的try-except塊,調用局部展開函數_EH4_LocalUnwind;再執行能處理異常的try-except塊的異常處理代碼;最後,繼續執行try-except之後的其他代碼。

全局展開,是由_EH4_GlobalUnwind調用ntdll!RtlUnwind,RtlUnwind遍歷訪問異常幀鍊表,把從表頭幀到目標幀(不含)的所有異常處理回調函數用異常碼(STATUS_UNWIND 即0C0000027H)、異常標誌(EXCEPTION_UNWINDING即值2)調用。異常處理回調函數根據當前的異常碼與異常標誌,對當前異常幀所在函數,從包含了產生異常的執行點的最內層__try語句開始,直至函數內的最外層try塊,依次調用try-finally的清理用途代碼。這一步實際上是調用_EH4_LocalUnwind函數來完成。

局部展開,由_EH4_LocalUnwind函數實現,是從包含了產生異常的執行點的最內層__try語句開始,按着代碼的包含關係向外直至目標try-except語句為止,依次調用try-finally的清理用途代碼。

SAFESEH

編輯

為了防止篡改異常處理函數的地址,自Windows XP SP2起引入了檢查異常處理函數的地址是否有效。但這僅用於x86。而x64或ARM已經把所有異常處理函數放在了PDATA節中;該節叫做「.pdata」或者Exception Directory,用於存放處理異常的信息。[3]

鏈接器選項/SAFESEH[:NO]用於設置是否需要開啟。

如果/SAFESEH被使用,鏈接器生成的可執行映像文件(image)包含它的安全的異常處理函數的清單。

如果/SAFESEH未使用,鏈接器將檢查如果所有的模塊都兼容於安全的異常處理函數特性,則生成的可執行映像文件(image)包含它的安全的異常處理函數的清單;如果有模塊不兼容於安全的異常處理函數特性,則生成的可執行映像文件(image)不包含安全的異常處理函數的清單。

如果/SAFESEH:NO被使用,鏈接器生成的可執行映像文件(image)不包含安全的異常處理函數的清單。

相關的Windows API函數

編輯
函數 描述
ExRaiseStatus 用指定狀態代碼觸發異常
ExRaiseAccessViolation 觸發STATUS_ACCESS_VIOLATION異常
ExRaiseDatatypeMisalignment 觸發STATUS_DATATYPE_MISALIGNMENT異常

Visual C++編譯選項

編輯

Visual C++編譯器的編譯選項/EH設置異常處理模型 (Exception Handling Model):

  • a 捕捉異步(asynchronous或structured)與同步(synchronous或C++)異常;C++的catch(...)語句也可捕捉異步異常。不論同步異常還是異步異常,所有局部變量對象都被自動析構。 /clr隱式包含了/EHa 。
  • s 只捕捉同步(synchronous或C++)異常,並假定使用extern "C" 聲明的函數可能拋出異常。 C++的catch(...)語句不捕捉異步異常。異步異常不會導致C++局部對象被自動析構。
  • c
    • 對於/EHsc, 只捕捉同步(synchronous或C++)異常,並假定使用extern "C" 聲明的函數不會拋出C++異常。
    • 對於/EHca, 等價於/EHa.
  • r 編譯器對所有noexcept函數總是產生運行時終止檢查代碼。
  • 如果未指定/EH,則編譯器將同時捕獲異步結構化異常和 C++ 異常,但不會銷毀由於異步異常超出範圍的 C++ 對象。

參考文獻

編輯
  1. ^ Microsoft Corp. Structured Exception Handling Functions. MSDN Library. 11/12/2009 [2009-11-17]. (原始內容存檔於2010-03-06). 
  2. ^ 《Microsoft Journal》上的__except_handler3函数的伪代码. [2013-07-01]. (原始內容存檔於2012-12-14). 
  3. ^ MSDN:/SAFESEH (Image has Safe Exception Handlers). [2017-02-14]. (原始內容存檔於2017-02-14). 

外部連結

編輯