结构化异常处理

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

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

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

一個 \_\_try 語句不能既有 \_\_except，又有 \_\_finally。但 try-except 與 try-finally 語句可以嵌套使用。

## 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 複合語句中受保護的代碼以任何方式執行結束後，不論 \_\_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 語句更為有效，因為它不會導致堆疊展開。

## 例子

輸出結果為：

```
開始主程序
進入外層的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） 。如果不存在這樣的 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, 用於存放處理異常的信息。

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

如果 `/SAFESEH` 被使用，連結器生成的可執行映像文件（image）包含它的安全的異常處理函數的清單。

如果 `/SAFESEH` 未使用，連結器將檢查如果所有的模塊都兼容於安全的異常處理函數特性，則生成的可執行映像文件（image）包含它的安全的異常處理函數的清單；如果有模塊不兼容於安全的異常處理函數特性，則生成的可執行映像文件（image）不包含安全的異常處理函數的清單。

如果 `/SAFESEH:NO` 被使用，連結器生成的可執行映像文件（image）不包含安全的異常處理函數的清單。

## 相關的 Windows API 函數

## 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++ 對象。

## 參考文獻

## 外部連結

* Igor Skochinsky. Reversing Microsoft Visual C++ Part I: Exception Handling. OpenRCE. Monday, March 6, 2006 12:02.38 CST [2009-11-17]. （原始內容存檔於 2010-04-16）.
* Microsoft Corp. Structured Exception Handling. MSDN Library. 2009-11-12 [2009-11-17]. （原始內容存檔於 2009-12-08）.
* Matt Pietrek. A Crash Course on the Depths of Win32 Structured Exception Handling. MSJ. Jan 1997, **12** (1) [2013-06-30]. （原始內容存檔於 2015-09-29）.  Note that the examples given there do not work as-is on modern Windows systems (post XP SP2) due to the changes Microsoft made to address the security issues present in the early SEH design. The examples still work on later versions of Windows if compiled with `/link /safeseh:no`.
* win32: Safe Structured Exception Handling. Yasm manual. [2017-02-14]. （原始內容存檔於 2017-01-07）.
* US patent 7,480,919 - Safe exceptions
* Johannes Passing. Fun with low level SEH. May 20, 2008 [2017-02-14]. （原始內容存檔於 2017-02-15）.  Covers the obscure details needed to get low-level SEH (and particularly SafeSEH) code to work on more modern Windows.
* Igor Skochinsky. Reversing Microsoft Visual C++ Part I: Exception Handling. OpenRCE. Monday, March 6, 2006 12:02.38 CST [2009-11-17]. （原始內容存檔於 2010-04-16）.
* Matt Miller. Preventing the Exploitation of Structured Exception Handler (SEH) Overwrites with SEHOP. Technet. 2 Feb 2009 [2017-02-14]. （原始內容存檔於 2014-07-01）.
* Stéfan Le Berre, Damien Cauquil. Bypassing SEHOP (PDF). Sysdream. 22 Dec 2009. （原始內容 (PDF) 存檔於 2012-09-07）.
* Joshua J. Drake. Old Meets New: Microsoft Windows SafeSEH Incompatibility. 10 Jan 2012 [2017-02-14]. （原始內容存檔於 2017-01-09）.  An article explaining why Windows 7 SP1 ignores SafeSEH for some older binaries, while Windows XP SP3 honors it.
* MSDN:Structured Exception Handling Functions （頁面存檔備份，存於網際網路檔案館）