標籤:

所有CLR開發人員都應該了解的關於運行時異常的知識(下)

直接使用SEH

有些情況里直接使用SEH會更合適一些。特別是,如果需要在第一次遍歷(first pass - SEH異常處理流程里的第一遍處理)時需要執行某些操作時,也就是在堆棧向上展開之前,SEH是唯一的選項。一個SEH里的 try/except 過濾代碼除了決定是否要處理某個異常以外,還能執行任何操作。調試器通知(Debugger notification)就是在第一次遍歷時需要考慮的領域。

過濾代碼的編寫需要極其小心。一般來講,過濾代碼需要考慮到任何隨機,而且很可能不一致的狀態。因為過濾代碼在第一次遍歷時執行,而析構函數(dtors)在第二次遍歷時執行,holders還沒有執行,而且也沒有恢復它們的狀態。

PAL_TRY / PAL_EXCEPT, PAL_EXCEPT_FILTER, PAL_FINALLY / PAL_ENDTRY

如果需要過濾代碼,PAL_TRY家族代碼是CLR里可移植的寫法。因為過濾代碼直接使用SEH,它不能在一個函數里與C++異常處理共存,因此在函數里不能使用holders。

再次強調,這種情況很少見。

try / except, __finally

在CLR里沒有充足理由來直接使用它們。

異常和GC模式

使用COMPlusThrowXXX()拋出異常不會影響GC模式,而且在所有模式里都是安全的。當異常將堆棧展開到EX_CATCH,在棧上的所有holders都會銷毀,釋放它們的資源和重置它們的狀態。當在EX_CATCH里恢復執行時,被holder保護的狀態會恢復到EX_TRY時的狀態。

轉換

在託管代碼,CLR,COM伺服器和其它原生代碼之間,有很多如調用規範(calling conventions),內存管理和異常處理機制之間的轉換。關於異常,對CLR開發者來說是幸運的,大部分轉換要麼完全在CLR之外,或者就是被自動處理了。CLR開發人員日常需要考慮的就是三種轉換。其它的都是高端話題。

託管代碼進入CLR

由「fcall」、"jit helper"等觸發。典型路徑是CLR通過一個託管異常向託管代碼報告錯誤。因此,如果是一個fcall函數直接或間接拋出一個託管異常,那沒什麼問題。一般的CLR託管異常實現會「做正確的事」而且會去找一個適合的託管異常處理代碼。

從另一面來說,如果fcall函數也可以包含拋出CLR內部異常的代碼(一個C++異常),這個異常不能泄漏到託管代碼那邊。為了處理這種情況,CLR提供了UnwindAndContinueHandler (UACH),它是捕捉C++異常並轉換成託管異常並拋出的一系列代碼。

任何從託管代碼端調用的CLR函數都有可能拋出C++異常,必須將拋出異常的代碼封裝在INSTALL_UNWIND_AND_CONTINUE_HANDLER / UNINSTALL_UNWIND_AND_CONTINUE_HANDLER里。安裝一個HELPER_METHOD_FRAME會自動安裝UACH。安裝UACH的性能代價很大,因此不能隨處使用。一種技術是,在執行關鍵代碼的時候不要使用UACH,但是在拋出異常之前安裝一個UACH。

當一個C++異常被拋出時,而少了一個UACH,典型錯誤就是一個在CPFH_RealFirstPassHandler里「GC_TRIGGERS在一個GC_NOTRIGGER區域里被調用」的合約違規(Contract Violation)。要修復這些錯誤,留意託管代碼到CLR的切換,並檢查INSTALL_UNWIND_AND_CONTINUE_HANDLER或HELPER_METHOD_FRAME_BEGIN_XXX。

CLR代碼到託管代碼

從CLR切換到託管代碼是跟平台很相關的。在32位Windows平台,CLR託管異常代碼要求在進入託管代碼之前已經安裝了「COMPlusFrameHandler」。這些切換被一些專用的輔助函數處理,它們來執行恰當的異常處理。一般對託管代碼的調用很少用其它方法。如果沒有COMPlusFrameHander,最可能的情況就是託管代碼端的異常處理代碼沒有被執行,finally塊和catch塊都不會被執行。

CLR代碼切換到外部原生代碼

從CLR調用其它原生代碼(操作系統、CRT和其它DLL)的過程要格外注意。這是因為外部代碼可能會觸發一個異常。由於EX_TRY宏的實現方式,這是一個問題,特別是它們將一個非異常錯誤翻譯或者封裝異常的方式。對於C++異常,只能通過放棄捕捉到的異常的所有信息,是可以捕捉任意或者所有異常(通過"catch(...)")。捕捉到一個異常後*,這個宏有異常對象可檢查,但如果捕捉到其它東西,那就沒什麼可檢查的了,宏只能猜異常是什麼。如果異常是從CLR外部來的,那宏總是會猜錯。

目前的解決方案是將對外部代碼的調用封裝到一個「callout filter」。這個過濾代碼會捕捉外部異常,並將它翻譯成一個CLR內部異常:SEHException。這個過濾代碼是預定義的,而且用起來也很簡單。然而,使用過濾代碼就意味著使用SEH,因此在同一個函數里不能使用C++異常。在使用C++異常的函數里加上「callout filter」要求將它分成兩個函數。

要使用callout filter,這樣的代碼:

length = SysStringLen(pBSTR);

要寫成:

BOOL OneShot = TRUE;nnPAL_TRYn{n length = SysStringLen(pBSTR);n}nPAL_EXCEPT_FILTER(CallOutFilter, &OneShot)n{n _ASSERTE(!"CallOutFilter returned EXECUTE_HANDLER.");n}nPAL_ENDTRY;n

如果函數拋出了一個異常,而沒有callout filter的話,後果就是CLR會報告一個錯誤的異常。而且錯誤報告的異常類型也不是確定的;如果系統里已經有託管異常,那麼就會報告成這個託管異常。如果沒有異常拋出,那麼就會報告成一個內存不足異常(OOM)。在調試版里,如果缺失callout filter,會觸發一個斷言。斷言消息會包含「The runtime may have lost track of the type of an exception(運行時可能無法確定異常的類型)」。

其它

在EX_TRY里實際上包含很多宏,這些宏永遠不能在宏的實現以外使用。

BEGIN_EXCEPTION_GLUE / END_EXCEPTION_GLUE需要特別提一下。它們本來是用做切換宏的,在VS 2008里應該被換成更恰當的宏。當然,它們現在工作的很好,因此也不用被換掉。理想情況下,是在一個「清理」里程碑里,將這些實例都換掉,然後刪掉這些宏。CLR開發人員如果要用它們的話,應該使用EX_TRY/EX_CATCH/EX_CATCH_END或 EX_CATCH_HRESULT。


推薦閱讀:

Stack-based 的虛擬機有什麼常用的優化策略?
如何開始學習CoreCLR源代碼?
.NET CLR怎麼保證執行正確的unsafe代碼不掛掉?
垃圾內存回收演算法
程序集什麼玩意?我知道其表現形式為dll和exe,但是exe不是直接執行的文件嗎?而dll只是類庫,供exe調用代碼?

TAG:NET | CLR |