SetUnhandledExceptionFilter

1. 前言 
幾 乎每個遊戲都或多或少地存在着缺陷,辛辛苦苦完成的遊戲要是最終在玩家那裏崩潰了,對開發人員來說可能是最不好的消息了。不僅如此,在遊戲發佈前都需要經 過大量的測試,杭州異地訂機,通常用於測試的電腦上並不會安裝調試環境,因此當遊戲崩潰時,往往只能得到一個錯誤提示。如果能夠在遊戲崩潰時提供更多的信息,就可以爲開 發人員對此進行再現或是進一步調試帶來很多方便。 
當然,最理想的情況就是在每個測試人員以及每個客戶的電腦上安裝調試環境,每次都讓遊戲在 調試模式下運行,這樣一旦出現錯誤,只需要把當時的函數調用棧、各種局部和全部變量的當前值以及其它系統信息記下來就行了,可惜這在現實情況下通常不可 行。因此,最終用戶看到的往往是這樣一個窗口 
而開發人員聽到的則是“我在走過一扇門的時候,程序突然退出了”之類的描述。 
2. 怎樣纔可以獲得更有用的崩潰信息 
其實如果在上面的那個崩潰提示窗口中,點擊“click here”進入的話,可以有機會看到一些技術信息(technical information): 
這 些信息包括了當前程序中包含哪些模塊以及每個線程棧在崩潰時的情況等。事實上這些信息已經足以告訴開發人員崩潰的大致環境了。有很多文章提到過怎樣根據這 些信息獲得當時的函數調用棧以及局部變量信息。譬如說,參考文獻1和參考文獻2。可惜的是,這個機制只有在XP以後的版本中才有,並且這份錯誤報告也不會 發送迴游戲開發商,而是直接被髮送到微軟。所以,我們必須找到一個機制,不僅可以在微軟之前獲得遊戲崩潰的第一手信息,而且也適用於各種常用的 Windows版本。 
3. 全局的try/catch 
很多遊戲採取的方法就是在應用程序的入口函數(通常是WinMain)中加入一對全局的try/catch,這樣任何未被捕獲到的異常都會被全局的異常處理代碼捕獲到,從而使得程序能夠有機會記錄一些系統信息並且發回給開發人員,至少也可以體面地結束遊戲。 
可 是這樣做並不能保證我們一定能夠在程序發生崩潰時獲得控制權。首先,不少遊戲中存在着大量的靜態初始化工作,這部分工作是在程序入口函數被調用之前由C+ +運行庫進行的。由於在進行靜態初始化時還沒有進入我們的全局try/catch,無論發生什麼情況,我們都不可能捕捉到。當然,旅遊商務,這可以通過不在應用程序 中使用任何可能拋出異常的靜態初始化來避免,把所有可能拋出異常的靜態初始化都修改爲局部靜態初始化,並且在入口函數中強制調用(不少大型程序使用這種方 法來控制靜態初始化順序)。不過,這並不是一個通用的方法,因爲使用靜態初始化畢竟是大多數C++程序員的習慣,上海到拉薩機票。 
其次,這不能捕捉到其它線 程中拋出的異常,因爲各個線程間的異常是獨立的,一個線程中的try/catch塊並不能夠捕獲到它所創建的子線程中拋出的異常。不過這個問題相對來說比 較好解決一點,稍具規模的程序大多會有自己的線程類,因此只需要把對線程函數進行調用的代碼包在一對try/catch之間就可以達到目的。 
再次,全局的try/catch會對調試工作帶來很大的麻煩,相信很多人都遇到過調試時某個異常被全局的異常處理程序catch到導致失去出錯上下文的情況。並且,對於大多數懶惰的程序員來說,需要編寫代碼(哪怕只有幾行)總是多少有些不適。 
本文下面將花費大量的篇幅來描述怎樣一勞永逸地解決這個問題。 
4. 異常的終點 
對 於一個Windows平臺上的C++程序來說,異常其實可以分爲兩種:Win32異常,也就是結構化異常(Structured Exception)以及C++異常。無論是Win32還是C++都爲我們提供了一些機制來處理未被正常捕獲的異常,讓我們在應用程序退出之前盡一些人 事。 
在Win32中,我們可以通過SetUnhandledExceptionFilter來設置一個SEH的過濾函數。這個函數的原型是: 
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( 
LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter 
); 
這個函數把全局SEH過濾函數設置爲lpTopLevelExceptionFilter,而返回值則是上一次設置的過濾函數。LPTOP_LEVEL_EXCEPTION_FILTER其實是一個函數指針: 
typedef LONG (WINAPI *LPTOP_LEVEL_EXCEPTION_FILTER)( 
struct _EXCEPTION_POINTERS *ExceptionInfo 
); 
ExceptionInfo包含了對異常的描述以及發生異常的線程在當時的處理器狀態,我們的過濾函數可以通過返回不同的值來讓系統在異常拋出點繼續運行、繼續搜索異常處理函數(可能會給調試程序一個機會)或是退出應用程序。 
每當有一個SEH發生時,如果系統找不到合適的處理代碼,就會調用我們所設置的過濾函數。由於這個函數是線程無關的,我們只需要設置這個過濾函數就可以對整個遊戲中未被捕獲的結構化異常進行處理。 
C++中也有一個類似的函數,叫做set_terminate,其原型如下: 
typedef void (__cdecl *terminate_function)(); 
terminate_function set_terminate( 
terminate_function term_func 
); 
它也可以裝載一個處理函數來處理任何未捕獲的C++異常,唯一的區別是它的處理函數沒有任何返回值,因此退出這個函數後C++運行庫就會調用abort退出應用程序,深圳到成都機票。 
5. 入口函數 
幾 乎每個Windows下的C++程序員都應該知道main和WinMain,前者是控制檯程序的入口函數,後者是Win32程序的入口函數。這些函數會在 靜態初始化完畢後被調用,並且開始應用程序的主要工作。但是這些函數只不過是C/C++的入口函數,而不是應用程序真正的入口函數,也就是說,操作系統裝 載應用程序後,它並不是直接調用我們的main/WinMain,上海到宜昌機票,而是調用另一個由CRT提供的函數,由那個函數進行一些初始化工作(包括靜態初始化)以 後再調用我們的入口函數,深圳到齊齊哈爾機票。下面的表格中列出了在通常的Windows應用程序中CRT所提供的入口函數: 
CRT入口函數 
相應的C/C++入口函數 
相應的應用程序類型 
WinMainCRTStartup WinMain Win32應用程序(非Unicode) 
wWinMainCRTStartup wWinMain Win32應用程序(Unicode) 
mainCRTStartup Main Win32控制檯程序(非Unicode) 
wmainCRTStartup wmain Win32控制檯程序(Unicode) 
(表一) 
僅以mainCRTStartup爲例,如果我們寫一個這樣的GEHmainCRTStartup: 
int GEHmainCRTStartup () 

//進行我們自己的初始化工作 
return mainCRTStartup (); 

並把它指定爲程序的入口點,就可以讓我們自己的代碼在CRT入口函數之前執行。 
先試試看我們到目前爲止的成果,把下面這段代碼加入到一個Win32控制檯項目中,並且把linker選項中的入口點設爲GEHmainCRTStartup,我們可以看到靜態初始化中拋出的結構化異常已經被捕獲了。 
#include <windows.h> 
#include <stdio.h> 
extern "C" int mainCRTStartup(); 
LONG WINAPI GEHExceptionFilter( _EXCEPTION_POINTERS* ExceptionInfo ) 

printf( "caught ...\n" ); 
ExitProcess( 0 ); 
return EXCEPTION_CONTINUE_SEARCH; 

extern "C" int GEHmainCRTStartup() 

SetUnhandledExceptionFilter( GEHExceptionFilter ); 
return mainCRTStartup(); 

struct Bug() 

*( int* )0 = 0; 


bug; 
int main(){} 
6. C++異常 
把 上面程序中的*( int* )0 = 0;改成throw 1,然後編譯運行,就會發現我們目前所使用的機制並不能夠有效地捕獲C++異常。這是因爲VC中雖然使用SEH作爲實現C++異常的機制,但是對於未捕獲 的C++異常,它有自己的處理方式。這不還有一個set_terminate函數沒用嗎?正好用在這裏!於是我們寫一個C++異常處理函數: GEH_terminate,然後在GEH入口函數裏面加入如下的語句: 
set_terminate( GEH_terminate)。這樣做看上去似乎一切正常,甚至可以通過編譯,但是這個做法並不可行,深圳到海口機票,至少是不安全的,天津機票。因爲我們並不能像使用 SetUnhandledExceptionFilter一樣使用set_terminate,SetUnhandledExceptionFilter 是一個Win32函數,而set_terminate是一個C++函數。在GEH入口函數中,C++的初始化工作還沒有進行,因此過早地設置這個處理函數 很可能會導致錯誤,至少在VS 2002中,如果把項目的代碼生成方式設爲multithreaded的話,在GEH入口中調用這個函數會導致程序崩潰。 
如果我們的庫只能處理SEH而不能處理C++異常,那顯然是一個很大的缺憾。可是C++也的確沒有提供如此低級的處理措施(因爲對C++編譯器而言,入口代碼不是程序員應該操心的事情)。於是,我們只能通過一個更低級的方法來解決這個問題,上海到天津機票。 
在VC(VC 6/VC 2002/VC 2003)中,所有未被捕獲的C++異常最終都會調用__CxxUnhandledExceptionFilter來進行處理,如果可以讓程序每次調用這個函數時轉而調用我們提供的函數,一切問題就迎刃而解了。 
要 做到這點,最簡單的方法就是直接替換__CxxUnhandledExceptionFilter函數的前幾個字節,把它修改成一個far jmp,跳轉到我們提供的處理函數中。通常這樣做時必須確保被修改函數的函數體超過5個字節,幸好通常都是這樣的,而 __CxxUnhandledExceptionFilter也不例外。__CxxUnhandledExceptionFilter的原型是 
long __stdcall __CxxUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *); 
因 此我們可以借用一下GEHExceptionFilter來作爲它跳轉的歸宿。由於__CxxUnhandledExceptionFilter位於代碼 段中,而代碼段缺省情況下是不可寫的,因此我們必須先修改相應內存段的保護屬性。下面這段代碼會先把 __CxxUnhandledExceptionFilter所處的內存段改成可讀寫的,並且把前5個字節修改爲一個跳轉到 GEHExceptionFilter的far jmp指令。 
DWORD oldProtect; 
VirtualProtect( __CxxUnhandledExceptionFilter, 5, 
PAGE_EXECUTE_READWRITE, &oldProtect ); 
*(char*)__CxxUnhandledExceptionFilter = ‘\xe9’;// far jmp 
*(unsigned int*)( (char*)__CxxUnhandledExceptionFilter + 1 ) = 
(unsigned int)GEH_terminate - 
( (unsigned int)__CxxUnhandledExceptionFilter + 5 ); 
VirtualProtect( __CxxUnhandledExceptionFilter, 5, oldProtect, &oldProtect ); 
把這段代碼加入GEHmainCRTStartup,深圳到南昌機票,然後編譯運行。可以看到我們的程序終於能夠處理未捕獲的C++異常了。 
注:Visual C++ 2005對未捕獲C++異常的處理有所改變,只需要使用結構化異常過濾函數就可以捕獲到未捕獲的C++異常,因此不必動態修改執行代碼即可。並且,由於__CxxUnhandledExceptionFilter不復存在,因此我們必須刪除上述代碼,深圳到西雙版納機票,否則會導致連接錯誤。 
7. 怎樣不妨礙開發人員的正常調試 
前 面提到了一個過於“積極”的異常處理器通常會給開發人員的調試工作帶來很大的麻煩,因此當我們的程序正在被調試時,GEH最好不起作用。Windows Platform. SDK提供了一個函數IsDebuggerPresent來判斷當前是否有調試程序的存在。我們可以在GEH入口函數中使用這個函數進行判斷,如果程序正 在被調試,那麼就跳過我們的主函數體,直接調用CRT入口函數。 
8. 線程 
GEH也可以正確地捕獲其它線程中的未捕獲異常,無論這個線程是使用Win32函數還是CRT函數創建的,這可以通過一個簡單的測試程序來驗證。 
不 過對於線程來說,GEH有一個更爲有趣的作用。雖然大多數遊戲都不是典型的多線程程序,但是通常遊戲中往往會使用多個線程,除了主線程以外,其它線程往往 起到數據讀寫或是音樂播放等輔助作用,商務服務。如果一個音樂播放線程中出現了未捕獲的異常,我們或許希望遊戲可以繼續運行下去,畢竟沒有音樂的遊戲總比崩潰的遊戲 要好很多(上帝,請原諒我說的這句話)。因此,可以從邏輯上把遊戲中的線程分爲兩種:重要的和不重要的,前者遇到未捕獲異常時,我們保存當前的狀態以便於 開發人員進行調試,並且退出遊戲;後者遇到未捕獲異常時,我們也保存當前狀態,深圳到恩施機票,但是並不退出遊戲,只是中止當前線程的運行。因此,深圳到西寧機票,我們需要做的就是在程序 中創建線程時,深圳到宜昌機票,把那些不重要線程的標識保存下來。因爲我們的異常過濾函數是在發生異常線程的上下文中運行的,因此如果當前線程是一個不重要線程,我們可以 選擇永遠掛起這個線程來讓遊戲繼續運行下去。 
9. 使用lib 
前面提到過,重慶機票,我們希望儘量避免修改現有的代碼,因此下一步就是要把這些函數放入lib中,這樣每次把GEH加入新項目時只需要在linker選項裏面指定一下入口點並且加一個lib就可以了。 
因爲我們目前需要支持四個不同的入口函數(參見表一),我們就需要四個不同的GEH入口函數。下面的代碼就可以生成這樣一個lib: 
#include <windows.h> 
#include <,深圳到包頭機票;stdio.h> 
extern "C" int mainCRTStartup(); 
extern "C" int wmainCRTStartup(),上海到煙臺機票; 
extern "C" int WinMainCRTStartup(); 
extern "C" int wWinMainCRTStartup(); 
long __stdcall __CxxUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *); 
static LONG WINAPI GEHExceptionFilter( _EXCEPTION_POINTERS* ExceptionInfo ) 

printf( "caught ...\n" ); 
ExitProcess( 0 ); 
return EXCEPTION_CONTINUE_SEARCH; 

static void setupHandlers() 

SetUnhandledExceptionFilter( GEHExceptionFilter ); 
DWORD oldProtect; 
VirtualProtect( __CxxUnhandledExceptionFilter, 5, PAGE_EXECUTE_READWRITE,深圳到杭州機票, &oldProtect ); 
*(char*)__CxxUnhandledExceptionFilter = ‘\xe9’;// far jmp 
*(unsigned int*)( (char*)__CxxUnhandledExceptionFilter + 1 ) = 
(unsigned int)GEHExceptionFilter - ( (unsigned int)__CxxUnhandledExceptionFilter + 5 ); 
VirtualProtect( __CxxUnhandledExceptionFilter,上海到庫爾勒機票, 5, oldProtect, &oldProtect ); 

extern "C" int GEHmainCRTStartup() 

setupHandlers(); 
return mainCRTStartup(); 

extern "C" int GEHwmainCRTStartup() 

setupHandlers(); 
return wmainCRTStartup(); 

extern "C" int GEHWinMainCRTStartup() 

setupHandlers(); 
return WinMainCRTStartup(); 

extern "C" int GEHwWinMainCRTStartup() 

setupHandlers(); 
return wWinMainCRTStartup(); 

然後再創建一個簡單的測試程序: 
int main() 

throw 1; 

這 看上去非常簡單,可惜在修改入口點設置並且加入前面生成的GEH.lib以後,在連接時會發生錯誤。這是因爲我們的代碼在連接時,通常是以obj文件爲單 位被連入最終應用程序的,因此位於同一個obj文件(也就是同一個cpp文件)中的所有全局函數/變量只要有一個被使用到,整個obj文件都會被連接到應 用程序中,這意味着連接程序會試圖找出該obj中所引用的所有外部符號,但是這裏我們只會定義一個主函數,因此連接程序必然無法找到其餘三個,從而導致連 接錯誤。這可以通過把這四個GEH入口函數放入不同的cpp文件來解決。 
10. 再懶一點 
現在只需要把 GEH.lib加入項目,並且把入口點設爲GEH入口函數就可以截獲所有的未捕獲異常了。可是要點擊那麼多次鼠標另加按下數十次按鍵總是讓人覺得十分費 力。好在微軟總是那麼善解人意,我們可以方便地用宏來把這幾十次的肢體運動變爲一次鼠標點擊或是一個快捷鍵(使用聲控或是意念或許會更加省力)。下面這段 宏在執行時可以自動地爲所有選中的項目進行參數設置使之自動享有GEH的一切保障: 
Imports EnvDTE 
Imports System.Diagnostics 
Imports Microsoft.VisualStudio.VCProjectEngine 
Public Module GEH 
Sub GEHSetProjectProperty() 
Dim project As Project 
Dim vcproject As VCProject 
Dim configure As VCConfiguration 
Dim linker As VCLinkerTool 
Dim patchNum As Integer = 0 
For Each project In DTE.ActiveSolutionProjects() 
vcproject = project.Object 
For Each configure In vcproject.Configurations 
If configure.ConfigurationType = ConfigurationTypes.typeApplication Then 
linker = configure.Tools("VCLinkerTool" 
If (configure.CharacterSet = charSet.charSetUnicode) Then 
If (linker.SubSystem = subSystemOption.subSystemConsole) Then 
linker.EntryPoi ... 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章