結構化異常處理

結構化異常處理，是 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 複合語句之後的程式碼。如果try 複合語句中的受保護執行的程式碼發生了異常，或受保護執行的程式碼呼叫的函式內部發生了異常並要求呼叫者來處理該異常，except 語句的過濾表示式（filter expression）被求值，根據其結果來決定如何處理異常：

EXCEPTIONCONTINUEEXECUTION (–1) : 導致異常的問題已經解決，在異常出現的現場重新執行操作。
EXCEPTIONCONTINUESEARCH (0) ：當前except 語句不能處理該異常，通知作業系統繼續搜尋該執行緒其他的異常處理程式。
EXCEPTIONEXECUTEHANDLER (1)：當前except 語句識別該異常，透過執行except 的複合語句來處理該異常。然後執行except 複合語句之後的程式碼。

內在函式 GetExceptionCode 返回一個 32 位整型值，表示異常的型別。內在函式 GetExceptionInformation 返回異常的詳細資訊及現場資訊（如 CPU 暫存器的值） 。這兩個函式可用於異常表示式中來判斷是否處理該異常。這裡說的內在函式（intrinsic function），是指編譯器提供內聯（inline）實現的函式。

Windows 作業系統的應用程式的 main 函式受到結構化異常的 try-except 語句保護，因此程式的未被處理的異常都會被捕獲。這是因為，Windows 作業系統在載入使用者程序時，Kernel32.dll 中的 BaseProcessStart 函式在try 塊中呼叫了使用者程序的入口函式 mainCRTStartup，因此使用者程式的所有異常都會被捕獲、得到處理。

可以使用 C++ 執行時庫中的setse_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 語句的過濾函式結果值為 EXCEPTIONEXECUTEHANDLER。這時，再從包含異常出現的執行點的最內層 try-finally 語句開始由內向外執行每個 finally 塊中的程式碼，直至回退到那個過濾函式結果值為 EXCEPTIONEXECUTEHANDLER 的 try-except 語句執行其 except 塊，最後執行該 try-except 語句後面的其他程式碼。

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

例子

include
include //for EXCEPTIONACCESSVIOLATION
include

int filter (unsigned int code, struct EXCEPTIONPOINTERS *ep)

{printf ("在異常表示式中 \n");

if (code == EXCEPTIONACCESSVIOLATION) {printf ("接受處理訪問違例異常 \n");

return EXCEPTIONEXECUTEHANDLER;

}

else {printf ("其他異常都不處理 \n");

return EXCEPTIONCONTINUESEARCH;

};

}

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 起），對每個使用者執行緒，都設立一個異常處理幀連結串列來處理異常事件。該連結串列的每個異常處理幀由兩個成員組成，分別是連結串列上一項地址、當前異常處理器地址，組成了結構EXCEPTIONREGISTRATION_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 語句，則在函式的入口與結尾處，編譯器插入了 EHprolog 與 EHepilog 程式碼，把函式內所有在 try 塊中被保護的程式碼包了起來。 EHprolog 在呼叫棧上建立一個EXCEPTIONREGISTRATIONRECORD，作為異常處理連結串列的新的表頭，其中包含了 Visual C++ 的執行時庫 msvcrt.dll 的excepthandler4 函式地址。在函式塊的結尾處，EHepilog 把這項EXCEPTIONREGISTRATIONRECORD 從連結串列頭移除，恢復其原來的表頭。except 語句中的過濾表示式，由掛在連結串列中的異常處理回撥函式 MSVCR100D!excepthandler4 來呼叫執行，返回值即為過濾表示式的求值結果。

實際上，編譯器實現結構化異常時，把連結串列每項的資料結構由 2 個成員擴充套件為 5 個成員，即在高地址方向追加了一個 scopetableentries 型別結構體陣列的指標、一個整型項表示執行點位於當前函式的哪個 try 塊中、一個儲存暫存器 EBP 的整數項。此後（低地址方向）緊接著是一個指向 EXCEPTIONPOINTERS 結構的指標（前述的內在函式 GetExceptionInformation 即返回這個指標值）。

異常發生時，作業系統的異常處理機制的 ntdll!RtlDispatchException 函式會從 FS:0 指向的連結串列表頭依次呼叫異常幀連結串列的每個節點所包含的異常處理回撥函式 MSVCR100D!excepthandler4，根據該回調函式的返回值來確定異常是否已經被處理，可以根據異常上下文（Exception context）恢復執行緒的執行。excepthandler4 回撥函式實際上只是呼叫了 MSVCR100D!excepthandler4common 函式。excepthandler4common 函式是實際的 workhorse，負責在當前異常幀所在的函式中查詢那個 try-except 語句能夠處理該異常（即過濾表示式的結果為 1） 。如果不存在這樣的 try-except 塊，excepthandler4common 函式返回 ExceptionContinueExecution（值 0），由 RtlDispatchException 繼續訪問異常幀連結串列的下一個節點。如果找到了能處理當前異常的 try-except 塊，excepthandler4common 函式首先呼叫全域性展開函式EH4GlobalUnwind，把從異常幀連結串列表頭所在的函式，直到能處理當前異常的函式的下一層函式，都做棧展開（unwinding）；然後，對能處理當前異常的函式，從包含執行點的最記憶體try 語句，直到能處理異常的 try-except 塊，呼叫區域性展開函式EH4LocalUnwind；再執行能處理異常的 try-except 塊的異常處理程式碼；最後，繼續執行 try-except 之後的其他程式碼。

全域性展開，是由EH4GlobalUnwind 呼叫 ntdll!RtlUnwind，RtlUnwind 遍歷訪問異常幀連結串列，把從表頭幀到目標幀（不含）的所有異常處理回撥函式用異常碼（STATUSUNWIND 即 0C0000027H）、異常標誌（EXCEPTIONUNWINDING 即值 2）呼叫。異常處理回撥函式根據當前的異常碼與異常標誌，對當前異常幀所在函式，從包含了產生異常的執行點的最內層try 語句開始，直至函式內的最外層 try 塊，依次呼叫 try-finally 的清理用途程式碼。這一步實際上是呼叫EH4LocalUnwind 函式來完成。

區域性展開，由EH4LocalUnwind 函式實現，是從包含了產生異常的執行點的最內層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++ 物件。