我們?cè)诔绦虬l(fā)布后總會(huì)面臨崩潰的情況杨蛋,這個(gè)時(shí)候一般很難重現(xiàn)或者很難定位到程序崩潰的位置,之前有方法在程序崩潰的時(shí)候記錄dump文件然后通過(guò)windbg來(lái)分析映砖。那種方法對(duì)開(kāi)發(fā)人員的要求較高忍法,它需要程序員理解內(nèi)存、寄存器等等一系列概念還需要手動(dòng)加載對(duì)應(yīng)的符號(hào)表毅臊。Java理茎、Python等等語(yǔ)言在崩潰的時(shí)候都會(huì)打印一條異常的堆棧信息并告訴用戶那塊出錯(cuò)了,根據(jù)這個(gè)信息程序員可以很容易找到對(duì)應(yīng)的代碼位置并進(jìn)行處理管嬉,而C/C++則會(huì)彈出一個(gè)框告訴用戶程序崩潰了皂林,二者對(duì)比來(lái)看,C++似乎對(duì)用戶太不友好了蚯撩,而且根據(jù)它的彈框很難找到對(duì)應(yīng)的問(wèn)題础倍,那么有沒(méi)有可能使c++像Java那樣打印異常的堆棧呢?這個(gè)自然是可能的胎挎,本文就是要討論如何在Windows上實(shí)現(xiàn)類(lèi)似的功能
異常處理
一般當(dāng)程序發(fā)生異常時(shí)沟启,用戶代碼停止執(zhí)行,并將CPU的控制權(quán)轉(zhuǎn)交給操作系統(tǒng)犹菇,操作系統(tǒng)接到控制權(quán)后德迹,將當(dāng)前線程的環(huán)境保存到結(jié)構(gòu)體CONTEXT中,然后查找針對(duì)此異常的處理函數(shù)项栏。系統(tǒng)利用結(jié)構(gòu)EXCEPTION_RECORD保存了異常描述信息浦辨,它與CONTEXT一同構(gòu)成了結(jié)構(gòu)體EXCEPTION_POINTERS,一般在異常處理中經(jīng)常使用這個(gè)結(jié)構(gòu)體。
異常信息EXCEPTION_RECORD的定義如下:
typedef struct _EXCEPTION_RECORD
{
DWORD ExceptionCode; //異常碼
DWORD ExceptionFlags; //標(biāo)志異常是否繼續(xù)流酬,標(biāo)志異常處理完成后是否接著之前有問(wèn)題的代碼
struct _EXCEPTION_RECORD* ExceptionRecord; //指向下一個(gè)異常節(jié)點(diǎn)的指針币厕,這是一個(gè)鏈表結(jié)構(gòu)
PVOID ExceptionAddress; //異常發(fā)生的地址
DWORD NumberParameters; //異常附加信息
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //異常的字符串
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;
Windows平臺(tái)提供的這一套異常處理的機(jī)制,我們叫它結(jié)構(gòu)化異常處理(SEH)芽腾,它的處理過(guò)程一般如下:
- 如果程序是被調(diào)試運(yùn)行的(比如我們?cè)赩S編譯器中調(diào)試運(yùn)行程序)旦装,當(dāng)異常發(fā)生時(shí),系統(tǒng)首先將異常信息交給調(diào)試程序摊滔,如果調(diào)試程序處理了那么程序繼續(xù)運(yùn)行阴绢,否則系統(tǒng)便在發(fā)生異常的線程棧中查找可能的處理代碼。若找到則處理異常艰躺,并繼續(xù)運(yùn)行程序
- 如果在線程棧中沒(méi)有找到呻袭,則再次通知調(diào)試程序,如果這個(gè)時(shí)候仍然不能處理這個(gè)異常腺兴,那么操作系統(tǒng)會(huì)對(duì)異常進(jìn)程默認(rèn)處理左电,這個(gè)時(shí)候一般都是直接彈出一個(gè)錯(cuò)誤的對(duì)話框然后終止程序。
系統(tǒng)在每個(gè)線程的堆棧環(huán)境中都維護(hù)了一個(gè)SEH表页响,表中是用戶注冊(cè)的異常類(lèi)型以及它對(duì)應(yīng)的處理函數(shù)篓足,每當(dāng)用戶在函數(shù)中注冊(cè)新的異常處理函數(shù),那么這個(gè)信息會(huì)被保存在鏈表的頭部闰蚕,也就是說(shuō)它是采用頭插法來(lái)插入新的處理函數(shù)栈拖,從這個(gè)角度上來(lái)說(shuō),我們可以很容易理解為什么在一般的高級(jí)語(yǔ)言中一般會(huì)先找與try塊最近的catch塊没陡,然后在找它的上層catch涩哟,由里到外依次查找。與try塊最近的catch是最后注冊(cè)的诗鸭,由于采用的是頭插法染簇,自然它會(huì)被首先處理。
在Windows中針對(duì)異常處理强岸,擴(kuò)展了__try
和 __except
兩個(gè)操作符,這兩個(gè)操作符與c++中的try和catch非常相似,作用也基本類(lèi)似砾赔,它的一般的語(yǔ)法結(jié)構(gòu)如下:
__try
{
//do something
}
__except(filter)
{
//handle
}
使用 __try
和 __except
的時(shí)候它主要分為3個(gè)部分蝌箍,分別為:保護(hù)代碼體、過(guò)濾表達(dá)式暴心、異常處理塊
- 保護(hù)代碼體一般是try中的語(yǔ)句妓盲,它值被保護(hù)的代碼,也就是說(shuō)我們希望處理那個(gè)代碼塊產(chǎn)生的異常
- 過(guò)濾表達(dá)式是 except后面擴(kuò)號(hào)中的值专普,它只能是3個(gè)值中的一個(gè)悯衬,EXCEPTION_CONTINUE_SEARCH繼續(xù)向下查找異常處理,也就是說(shuō)這里的異常處理塊不處理這種異常檀夹,EXCEPTION_CONTINUE_EXECUTION表示異常已被處理筋粗,這個(gè)時(shí)候可以繼續(xù)執(zhí)行直線產(chǎn)生異常的代碼策橘,EXCEPTION_EXECUTE_HANDLER表示異常已被處理,此時(shí)直接跳轉(zhuǎn)到except里面的代碼塊中娜亿,這種方式下它的執(zhí)行流程與一般的異常處理的流程類(lèi)似.
- 異常處理塊丽已,指的是except下面的擴(kuò)號(hào)中的代碼塊.
注意:我們說(shuō)過(guò)濾表達(dá)式只能是這三個(gè)值中的一個(gè),但是沒(méi)有說(shuō)這里一定得填這三個(gè)值买决,它還支持函數(shù)或者其他的表達(dá)式類(lèi)型沛婴,只要函數(shù)或者表達(dá)式的返回值是這三個(gè)值中的一個(gè)即可。
上述的方式也有他的局限性督赤,也就是說(shuō)它只能保護(hù)我們指定的代碼嘁灯,如果是在 __try
塊之外的代碼發(fā)生了崩潰,可能還是會(huì)造成程序被kill掉躲舌,而且每個(gè)位置都需要寫(xiě)上這么些代碼實(shí)在是太麻煩了丑婿。其實(shí)處理異常還有一種方式,那就是采用 SetUnhandledExceptionFilter
來(lái)注冊(cè)一個(gè)全局的異常處理函數(shù)來(lái)處理所有未被處理的異常孽糖,其實(shí)它的主要工作原理就是往異常處理的鏈表頭上添加一個(gè)處理函數(shù)枯冈,函數(shù)的原型如下:
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(__in LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter);
它需要傳入一個(gè)函數(shù),以便發(fā)生異常的時(shí)候調(diào)用這個(gè)函數(shù)办悟,這個(gè)回調(diào)函數(shù)的原型如下:
LONG WINAPI UnhandledExceptionFilter(
__in struct _EXCEPTION_POINTERS* ExceptionInfo
);
回調(diào)函數(shù)會(huì)傳入一個(gè)表示當(dāng)前堆棧和異常信息的結(jié)構(gòu)體的指針尘奏,結(jié)構(gòu)的具體信息請(qǐng)參考MSDN, 函數(shù)會(huì)返回一個(gè)long型的數(shù)值,這個(gè)數(shù)值為上述3個(gè)值中的一個(gè)病蛉,表示當(dāng)系統(tǒng)調(diào)用了這個(gè)異常處理函數(shù)處理異常之后該如何繼續(xù)執(zhí)行用戶代碼炫加。
SetUnhandledExceptionFilter
函數(shù)返回一個(gè)函數(shù)指針,這個(gè)指針指向鏈表的頭部铺然,如果插入處理函數(shù)失敗那么它將指向原來(lái)的鏈表頭俗孝,否則指向新的鏈表頭(也就是注冊(cè)的這個(gè)回調(diào)函數(shù)的地址)
而這次要實(shí)現(xiàn)這么一個(gè)能打印異常信息和調(diào)用堆棧的功能就是要使用這個(gè)方法。
打印函數(shù)調(diào)用堆棧
關(guān)于打印堆棧的內(nèi)容魄健,這里不再多說(shuō)了赋铝,請(qǐng)參考本人之前寫(xiě)的博客
windows平臺(tái)調(diào)用函數(shù)堆棧的追蹤方法
這里的主要思路是使用StackWalker來(lái)根據(jù)當(dāng)前的堆棧環(huán)境來(lái)獲取對(duì)應(yīng)的函數(shù)信息,這個(gè)信息需要根據(jù)符號(hào)表來(lái)生成沽瘦,因此我們需要首先加載符號(hào)表革骨,而獲取當(dāng)前線程的環(huán)境,我們可以像我博客中寫(xiě)的那樣使用GetThreadContext來(lái)獲取析恋,但是在異常中就簡(jiǎn)單的多了良哲,還記得異常處理函數(shù)的原型嗎?異常處理函數(shù)本身會(huì)帶入一個(gè)EXCEPTION_POINTERS結(jié)構(gòu)的指針助隧,而這個(gè)結(jié)構(gòu)中就包含了異常堆棧的信息筑凫。
還有一些需要注意的問(wèn)題,我把它放到實(shí)現(xiàn)那塊了,請(qǐng)小心的往下看_
實(shí)現(xiàn)
實(shí)現(xiàn)部分的源碼我放到了github上巍实,地址
這個(gè)項(xiàng)目中主要分為兩個(gè)類(lèi)CBaseException滓技,主要是對(duì)異常的一個(gè)簡(jiǎn)單的封裝,提供了我們需要的一些功能蔫浆,比如獲取加載的模塊的信息殖属,獲取調(diào)用的堆棧,以及解析發(fā)生異常時(shí)的相關(guān)信息瓦盛。而這些的基礎(chǔ)都在CStackWalker中洗显。
使用上,我把CBaseException中的大部分函數(shù)都定義成了virtual 允許進(jìn)行重寫(xiě)原环。因?yàn)榫唧w我還沒(méi)想好這塊后續(xù)會(huì)需要進(jìn)行哪些擴(kuò)展挠唆。但是里面最主要的功能是OutputString函數(shù),這個(gè)函數(shù)是用來(lái)進(jìn)行信息輸出的嘱吗,默認(rèn)CBaseException是將信息輸出到控制臺(tái)上玄组,后續(xù)可以重載這個(gè)函數(shù)把數(shù)據(jù)輸出到日志中。
CBaseException 類(lèi)
CBaseException 主要是用來(lái)處理異常谒麦,在代碼里面我提供了兩種方式來(lái)進(jìn)行異常處理俄讹,第一種是通過(guò) SetUnhandledExceptionFilter
來(lái)注冊(cè)一個(gè)全局的處理函數(shù),這個(gè)函數(shù)是類(lèi)中的靜態(tài)函數(shù)UnhandledExceptionFilter绕德,在這個(gè)函數(shù)中我主要根據(jù)異常的堆棧環(huán)境來(lái)初始化了一個(gè)CBaseException類(lèi)患膛,然后簡(jiǎn)單的調(diào)用類(lèi)的方法顯示異常與堆棧的相關(guān)信息。第二種是通過(guò) _set_se_translator
來(lái)注冊(cè)一個(gè)將SEH轉(zhuǎn)化為C++異常的方法耻蛇,在對(duì)應(yīng)的回調(diào)中我簡(jiǎn)單的拋出了一個(gè)CBaseException的異常踪蹬,在具體的代碼中只要簡(jiǎn)單的用c++的異常處理捕獲這么一個(gè)異常即可
CBaseException 類(lèi)中主要用來(lái)解析異常的信息,里面提供這樣功能的函數(shù)主要有3個(gè)
- ShowExceptionResoult: 這個(gè)函數(shù)主要是根據(jù)異常碼來(lái)獲取到異常的具體字符串信息臣咖,比如非法內(nèi)存訪問(wèn)跃捣、除0異常等等
- GetLogicalAddress:根據(jù)發(fā)生異常的代碼的地址來(lái)獲取對(duì)應(yīng)的模塊信息,比如它在PE文件中屬于第幾個(gè)節(jié)夺蛇,節(jié)的地址范圍等等疚漆,它在實(shí)現(xiàn)上首先使用 VirtualQuery來(lái)獲取對(duì)應(yīng)的虛擬內(nèi)存信息,主要是這個(gè)模塊的首地址信息刁赦,然后解析PE文件獲取節(jié)表的信息愿卸,我們循環(huán)節(jié)表中的每一項(xiàng),根據(jù)節(jié)表中的地址范圍來(lái)判斷它屬于第幾個(gè)節(jié)截型,注意這里我們根據(jù)它在內(nèi)存中的偏移計(jì)算了它在PE文件中的偏移,具體的計(jì)算方式請(qǐng)參考PE文件的相關(guān)內(nèi)容.
3.ShowRegistorInformation:獲取各個(gè)寄存器的值儒溉,這個(gè)值保存在CONTEXT結(jié)構(gòu)中宦焦,我們只需要簡(jiǎn)單打印它就好
CStackWalker類(lèi)
這個(gè)類(lèi)主要實(shí)現(xiàn)一些基礎(chǔ)的功能,它主要提供了初始化符號(hào)表環(huán)境、獲取對(duì)應(yīng)的調(diào)用堆棧信息波闹、獲取加載的模塊信息
在初始化符號(hào)表的時(shí)候盡可以多的遍歷了常見(jiàn)的幾種符號(hào)表的位置并將這些位置中的符號(hào)表加載進(jìn)來(lái)酝豪,以便能更好的獲取到堆棧調(diào)用的情況。在獲取到對(duì)應(yīng)的符號(hào)表位置后有這樣的代碼
if (NULL != m_lpszSymbolPath)
{
m_bSymbolLoaded = SymInitialize(m_hProcess, T2A(m_lpszSymbolPath), TRUE); //這里設(shè)置為T(mén)RUE精堕,讓它在初始化符號(hào)表的同時(shí)加載符號(hào)表
}
DWORD symOptions = SymGetOptions();
symOptions |= SYMOPT_LOAD_LINES;
symOptions |= SYMOPT_FAIL_CRITICAL_ERRORS;
symOptions |= SYMOPT_DEBUG;
SymSetOptions(symOptions);
return m_bSymbolLoaded;
這里將 SymInitialize的最后一個(gè)函數(shù)置為T(mén)RUE孵淘,這個(gè)參數(shù)的意思是是否枚舉加載的模塊并加載對(duì)應(yīng)的符號(hào)表,直接在開(kāi)始的時(shí)候加載上可能會(huì)比較浪費(fèi)內(nèi)存歹篓,這個(gè)時(shí)候我們可以采用動(dòng)態(tài)加載的方式瘫证,在初始化的時(shí)候先填入FALSE,然后在需要的時(shí)候自己枚舉所有的模塊庄撮,然后手動(dòng)加載所有模塊的符號(hào)表背捌,手動(dòng)加載需要調(diào)用SymLoadModuleEx。這里需要提醒各位的是洞斯,這里如果填的是FALSE的話毡庆,后續(xù)一定得自己加載模塊的符號(hào)表,否則在后續(xù)調(diào)用SymGetSymFromAddr64的時(shí)候會(huì)得到一堆的487錯(cuò)誤(也就是地址無(wú)效)
我之前就是這個(gè)問(wèn)題困擾了我很久的時(shí)間烙如。
在獲取模塊的信息時(shí)主要提供了兩種方式么抗,一種是使用CreateToolhelp32Snapshot 函數(shù)來(lái)獲取進(jìn)程中模塊信息的快照然后調(diào)用Module32Next 和 Module32First來(lái)枚舉模塊信息,還有一種是使用EnumProcessModules來(lái)獲取所有模塊的句柄亚铁,然后根據(jù)句柄來(lái)獲取模塊的信息蝇刀,當(dāng)然還有另外的方式,其他的方式可以參考我的這篇博客 枚舉進(jìn)程中的模塊
在枚舉加載的模塊的同時(shí)還針對(duì)每個(gè)模塊調(diào)用了 GetModuleInformation
函數(shù)刀闷,這個(gè)函數(shù)主要有兩個(gè)功能熊泵,獲取模塊文件的版本號(hào)和獲取加載的符號(hào)表信息。
接下來(lái)就是重頭戲了——獲取調(diào)用堆棧甸昏。獲取調(diào)用堆棧首先得獲取當(dāng)前的環(huán)境顽分,在代碼中進(jìn)行了相應(yīng)的判斷,如果當(dāng)前傳入的CONTEXT為NULL施蜜,則函數(shù)自己獲取當(dāng)前的堆棧信息卒蘸。在獲取堆棧信息的時(shí)候首先判斷是否為當(dāng)前線程,如果不是那么為了結(jié)果準(zhǔn)確翻默,需要先停止目標(biāo)線程缸沃,然后獲取,否則直接使用宏來(lái)獲取修械,對(duì)應(yīng)的宏定義如下:
#define GET_CURRENT_THREAD_CONTEXT(c, contextFlags) \
do\
{\
memset(&c, 0, sizeof(CONTEXT));\
c.ContextFlags = contextFlags;\
__asm call $+5\
__asm pop eax\
__asm mov c.Eip, eax\
__asm mov c.Ebp, ebp\
__asm mov c.Esp, esp\
} while (0)
在調(diào)用StackWalker時(shí)只需要關(guān)注esp ebp eip的信息趾牧,所以這里我們也只簡(jiǎn)單的獲取這些寄存器的環(huán)境,而其他的就不管了肯污。這樣有一個(gè)問(wèn)題翘单,就是我們是在CStackWalker類(lèi)中的函數(shù)中獲取的這個(gè)線程環(huán)境吨枉,那么這個(gè)環(huán)境里面會(huì)包含CStackWalker::StackWalker,結(jié)果自然與我們想要的不太一樣(我們想要的是隱藏這個(gè)庫(kù)中的相關(guān)信息哄芜,而只保留調(diào)用者的相關(guān)堆棧信息)貌亭。這個(gè)問(wèn)題我還沒(méi)有什么好的解決方案。
在獲取到線程環(huán)境后就是簡(jiǎn)單的調(diào)用StackWalker以及那堆Sym開(kāi)頭的函數(shù)來(lái)獲取各種信息了认臊,這里就不再詳細(xì)說(shuō)明了圃庭。
至此這個(gè)功能已經(jīng)實(shí)現(xiàn)的差不多了。庫(kù)的具體使用請(qǐng)參考main.cpp這個(gè)文件失晴,相信有這篇博文以及源碼各位應(yīng)該很容易就能夠使用它剧腻。
據(jù)說(shuō)這些函數(shù)不是多線程安全的,我自己沒(méi)有在多線程環(huán)境下進(jìn)行測(cè)試师坎,所以具體它在多線程環(huán)境下表現(xiàn)如何還是個(gè)未知數(shù)恕酸,如果后續(xù)我有興趣繼續(xù)完善它的話,可能會(huì)加入多線程的支持胯陋。
<hr />