WinCE系統(tǒng)下應(yīng)用崩潰原因的分析
做為嵌入式程序員,也是一樣的。一般來說嵌入式系統(tǒng)都提供了異常分析的方法,特別是強(qiáng)大的調(diào)試工具,這些工具使用在 PC 上編程使用的工具是一樣的,例如:Visual Studio 系列。但是一些專用的、或小的嵌入式系統(tǒng),可能會(huì)提供專用的調(diào)試工具。雖然從功能上來說,沒有微軟提供的 VS 功能強(qiáng)大,使用起來也不太方便,但也會(huì)提供類似的調(diào)試功能。這里我主要討論的還是微軟提供的工具。
目前,在車載與 PND 市場,使用 WinCE 系統(tǒng)的比較多。在 WinCE6.0 系統(tǒng)中,如果應(yīng)用發(fā)生較嚴(yán)重的錯(cuò)誤時(shí),一般都會(huì)彈出系統(tǒng)標(biāo)準(zhǔn)的、令人十分討論的應(yīng)用錯(cuò)誤對(duì)話框。大概提示:XXX.exe出現(xiàn)嚴(yán)重錯(cuò)誤,必須被關(guān)閉。
如何解決此類問題呢?
只要能接上調(diào)試串口,或與調(diào)試工具連接,如VS2008等,獲取出錯(cuò)時(shí)的異常信息后,就可以來分析異??赡艿脑颉?br />但如果設(shè)備已經(jīng)處于量產(chǎn)狀態(tài),無法連接輸出 LOG 的串口和調(diào)試 USB 口時(shí),如何能捕捉到異常信息呢?
在無法徹底解決此類問題的情況下,有人就想能不能不讓系統(tǒng)顯示那個(gè)錯(cuò)誤對(duì)話框。為了能使應(yīng)用“優(yōu)美”的退出(網(wǎng)絡(luò)上的說法),即程序退出時(shí)不出現(xiàn)述的錯(cuò)誤對(duì)話框,有人曾試著去修改 WinCE 提供的內(nèi)核代碼,但這部分應(yīng)該是屬于未開源的部分。所以此方法也行不通的!
解決此類問題的根本辦法當(dāng)然是提高編碼的質(zhì)量,然后加強(qiáng)質(zhì)量保證(即測試),盡量將 Bug 消滅在研發(fā)階段。因?yàn)檠邪l(fā)階段,有大量的調(diào)試工具可以使用,如下述的第一種方法。
在沒有調(diào)試工具可以依賴時(shí),有沒有辦法獲取到異常信息呢?方法當(dāng)然是有的,如下述第二種和第三種方法。為什么要說第一種方法呢,因?yàn)樗峁┑男畔⑹亲罨A(chǔ)的,是后面兩種方法都要用到的基礎(chǔ)。在這里,我重點(diǎn)推薦的是第三種方法。因?yàn)樗奶幚肀容^獨(dú)立、在 WinCE 系統(tǒng)中比較有效、且方便集成到已有代碼中,實(shí)現(xiàn)異常捕獲。
第一種方法:如果有輸出 LOG 串口可說時(shí),串口輸出的異常信息,加 MAP 文件一起分析錯(cuò)誤的出處,可以到函數(shù)一級(jí)。所以要求在調(diào)試時(shí)一定要將對(duì)應(yīng)版本的 MAP 文件一起保留,用于后繼異常問題的分析。
對(duì)于如下的測試代碼:
void?TestCrashFunc(void) { ??int?*pNullPoint?=?NULL; ??RETAILMSG(1,(L"-----------------------------%drn",pNullPoint)); ??*pNullPoint?=?0; ??RETAILMSG(1,(L"-----------------------------%d,%drn",pNullPoint,*pNullPoint)); } void?CallCrashFunc(void) { ??TestCrashFunc(); } void?CSmartDeviceMFCDlg::OnTimer(UINT_PTR?nIDEvent) { ??//?TODO:?在此添加消息處理程序代碼和/或調(diào)用默認(rèn)值 ??if(1?==?nIDEvent) ??{ ????KillTimer(1); ????CallCrashFunc(); ???? ????//?其它的功能 ??} ??CDialog::OnTimer(nIDEvent); }
在 WinCE6.0 和 WinCE7.0 下運(yùn)行時(shí),串口的輸出內(nèi)容基本上是相同的,但在 WinCE7.0 下沒有出錯(cuò)的對(duì)話框。
串口中輸出的 Crash 信息如下:
Exception?'Data?Abort'?(0x4):?Thread-Id=0780000a(pth=c08e24e0),?Proc-Id=077e000a(pprc=c088da7c)?'SmartDeviceMFC.exe',?VM-active=077e000a(pprc=c088da7c)?'SmartDeviceMFC.exe' PC=00011738(SmartDeviceMFC.exe+0x00001738)?RA=4002ac4c(coredll.dll+0x0001ac4c)?SP=0004f6a8,?BVA=00000000 Exception?'Raised?Exception'?(0x116):?Thread-Id=0780000a(pth=c08e24e0),?Proc-Id=00400002(pprc=8360b5e0)?'NK.EXE',?VM-active=077e000a(pprc=c088da7c)?'SmartDeviceMFC.exe' PC=eff6ed60(k.coredll.dll+0x0001ed60)?RA=8052a62c(kernel.dll+0x0000e62c)?SP=d9bbf3b4,?BVA=ffffffff
從對(duì)應(yīng)的 MAP 文件中查到是 TestCrashFunc 函數(shù)出錯(cuò)(PC 指針 0x00001738 + MAP 文件中的 Preferred load address 偏移量),此例中出錯(cuò)時(shí)位置為:0x00001738 + 00010000 = 00011738:
SmartDeviceMFC ?Timestamp?is?539fa9e3?(Tue?Jun?17?10:37:23?2014) ?Preferred?load?address?is?00010000 ...... ?0001:000006a8????????InitInstance@CSmartDeviceMFCApp@@UAAHXZ?000116a8?f???SmartDeviceMFC.obj ?0001:00000708????????OnCbnDropdownCombo1@CSmartDeviceMFCDlg@@QAAXXZ?00011708?f???SmartDeviceMFCDlg.obj ?0001:00000714????????TestCrashFunc@@YAXXZ??????00011714?f???SmartDeviceMFCDlg.obj ?0001:0000075c????????BeginModalState@CWnd@@UAAXXZ?0001175c?f?i?SmartDeviceMFCDlg.obj ?0001:00000768????????EndModalState@CWnd@@UAAXXZ?00011768?f?i?SmartDeviceMFCDlg.obj ?0001:00000774?????????_GCComboBox@@UAAPAXI@Z???00011774?f?i?SmartDeviceMFCDlg.obj
由此可見 WinCE7.0 系統(tǒng)對(duì)這種對(duì)空指針賦值等異常是做了一些處理的,至少不再彈出那個(gè)令人十分討厭的對(duì)話框,也不影響后繼其它功能的執(zhí)行。在 WinCE6.0 下如果出現(xiàn)類似的對(duì)話框,則應(yīng)用就會(huì)退出。
第二種方法:使用 __try 和 __except。在開源的多媒體播放器 TCPMP 中,就有如下的用法。
先定義兩個(gè)宏,然后將重要的處理線程代碼包含在定義的這兩個(gè)宏中,以捕捉兩個(gè)宏之間代碼出現(xiàn)的異常。這樣做有一個(gè)缺點(diǎn):但代碼量很大時(shí),就需要增加很多對(duì)這兩個(gè)宏的調(diào)用。
#define?SAFE_BEGIN?__try?{ #define?SAFE_END?;}?__except?(SafeException(_exception_info()))?{}
可以看到 WinCE 下的使用方法,與 PC 上 SEH(Structured Exception Handling)是一樣的。如下所示:
__try? { ???//?guarded?code } __except?(?expression?) { ???//?exception?handler?code }
以下是 TCPMP 中一個(gè)關(guān)鍵線程的異常處理代碼(TCPMP 線程的代碼,沒有完整的給出,有興趣的童鞋請(qǐng)自己去看 TCPMP 的源代碼),其中兩個(gè)定義的異常處理宏,將線程的所有代碼包含在內(nèi)。
static?int?ProcessThread(player_base*?p) { ??int?Result?=?ERR_NONE; #ifdef?MULTITHREAD ??SAFE_BEGIN ??while?(p->Wnd) ??{ ????...... ????if?(p->RunProcess) ????{ ??????processstate?State; ??????State.Fill?=?p->Fill; ??????p->Timer->Get(p->Timer,TIMER_TIME,&State.Time,sizeof(tick_t)); ??????//DEBUG_MSG1(DEBUG_PLAYER,T("Process?Time:%d"),State.Time); ??????Result?=?p->Format->Process(p->Format,&State); ??????if?(Result?==?ERR_SYNCED) ??????{ ????????...... ??????} ??????else?if?(p->Fill?&&?(Result?==?ERR_END_OF_FILE?||?Result?==?ERR_BUFFER_FULL ????????||?(Result?==?ERR_NEED_MORE_DATA?&&?(p->NoMoreInput?||?State.BufferUsedAfter?>=?p->CurrBufferSize2-2)))) ??????{ ????????...... ??????} ??????...... ????} ????...... ??} ??SAFE_END ??return?0; }
此種實(shí)現(xiàn)方法,最最關(guān)鍵是 SafeException() 函數(shù)中分析與記錄異常信息的辦法。
但由于在 TCPMP 中,獲取異常的信息與 TCPMP 的軟件框架結(jié)合在一起。需要移植此部分代碼到其它工程時(shí),需要將有用的代碼分離出來,其實(shí)這個(gè)也比較簡單。
只要將與 EXCEPTION_POINTERS 相關(guān)的代碼拿出來即可。
int?SafeException(void*?p) { ??EXCEPTION_POINTERS*?Data?=?(EXCEPTION_POINTERS*)p; ??//?刪除了無關(guān)的代碼?-?此部分代碼是?TCPMP?中的代碼,所以未做排版。 ??{ ????{ ??????const?uint8_t*?ContextRecord?=?(const?uint8_t*)?Data->ContextRecord; ??????EXCEPTION_RECORD*?Record?=?Data->ExceptionRecord; ??????switch?(Record->ExceptionCode) ??????{ ??????case?STATUS_ACCESS_VIOLATION:???Name?=?T("Access?violation");?break; ??????case?STATUS_BREAKPOINT:???????Name?=?T("Breakpoint");?break; ??????case?STATUS_DATATYPE_MISALIGNMENT:??Name?=?T("Datatype?misalignment");?break; ??????case?STATUS_ILLEGAL_INSTRUCTION:??Name?=?T("Illegal?instruction");?break; ??????case?STATUS_INTEGER_DIVIDE_BY_ZERO:?Name?=?T("Int?divide?by?zero");?break; ??????case?STATUS_INTEGER_OVERFLOW:???Name?=?T("Int?overflow");?break; ??????case?STATUS_PRIVILEGED_INSTRUCTION:?Name?=?T("Priv?instruction");?break; ??????case?STATUS_STACK_OVERFLOW:?????Name?=?T("Stack?overflow");?break; ??????default:??????????????Name?=?T("Unknown");?break; ??????} ??????if?(Record->ExceptionCode?==?STATUS_ACCESS_VIOLATION) ??????{ ????????if?(Record->ExceptionInformation[0]) ??????????Name?=?T("Write?to"); ????????else ??????????Name?=?T("Read?from"); ??????} ?????? ??????//......?關(guān)鍵是處理?EXCEPTION_POINTERS?結(jié)構(gòu)體相關(guān)的成員 ??????//?其它一些相關(guān)的,如可執(zhí)行程序文件名等,根據(jù)需要來獲取 ??} }
第三種方法:使用函數(shù) AddVectoredExceptionHandler()。
在 WinCE 下使用此函數(shù),需要包含頭文件: TlHelp32.h 和庫文件: toolhelp.lib。由于此函數(shù)屬于 WinCE 示公開的 API,所以幫忙只要以 PC 上為準(zhǔn)。
使用此函數(shù),是向 WinCE 系統(tǒng)注冊(cè)一個(gè)矢量異常處理程序,但有異常發(fā)生時(shí)會(huì)調(diào)用此處理程序。
函數(shù)的原型如下(MSDN),各參數(shù)具體的含義,請(qǐng)參考 MSDN。這里就不做翻譯了。
PVOID?WINAPI?AddVectoredExceptionHandler(__in?ULONG?FirstHandler,?__in?PVECTORED_EXCEPTION_HANDLER?VectoredHandler);
以下代碼,演示了如何使用 AddVectoredExceptionHandler() 函數(shù):
(1) AddVectoredExceptionHandler(1,MyVectoredExceptionHandler);
(2) 定義異常處理程序
LONG?WINAPI?MyVectoredExceptionHandler(struct?_EXCEPTION_POINTERS?*pExceptionInfo) { ??typedef?ULONG?(WINAPI?*lpGetThreadCallStack)(HANDLE,ULONG,LPVOID,DWORD,DWORD); ??/*?在使用時(shí),必須包含一些頭文件。這些頭文件,需要從?WinCE?的安裝目錄中獲得。 ??OS?Versions:?Windows?CE?5.0?and?later. ??Header:?Pkfuncs.h. ??*/ ??typedef?struct?_CallSnapshotEx ??{ ????DWORD?dwReturnAddr; ????DWORD?dwFramePtr; ????DWORD?dwCurProc; ????DWORD?dwParams[4]; ??}CallSnapshotEx; ??//?打印?Dump?信息? ??...... ??//?打印?SP?堆棧 ??...... ??ULONG?*punSp?=?(ULONG?*)pExceptionInfo->ContextRecord->Sp; ??//?獲取線程堆棧調(diào)用 ??HMODULE?hCore?=?LoadLibrary(L"coredll.dll"); ??if(NULL?!=?hCore) ??{ ????lpGetThreadCallStack?pGetThreadCallStack?=?(lpGetThreadCallStack)GetProcAddress(hCore,L"GetThreadCallStack"); ????if(NULL?!=?pGetThreadCallStack) ????{ ????} ??} ??//?獲取進(jìn)程內(nèi)?dll?信息 ??MODULEENTRY32?CurrentModule; ??HANDLE?hSnapShot?=?CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,GetCurrentProcessId()); ??if((HANDLE)-1?!=?hSnapShot) ??{ ????//?調(diào)用??Module32First??和?Module32Next?完成?Module?枚舉 ??} }
是否還需要其它信息來分析程序出現(xiàn)異常的原因,可以參考 MSDN 中對(duì)結(jié)構(gòu) _EXCEPTION_POINTERS 中各成員的說明。然后將有用的信息,寫到 SD 卡等可永久存貯的設(shè)備中。這樣就不必為擔(dān)心找不到用于分析異常的資源,且此記錄的文件中的信息遠(yuǎn)大于串口輸出的異常信息。同時(shí),也可以根據(jù)需要輸入一些應(yīng)用(進(jìn)程)的相關(guān)信息。
可以將此異常捕獲功能的代碼,封裝成一個(gè) LIB 來供使用程序調(diào)用。
相對(duì)于 PC Windows 下感知程序崩潰(其實(shí)就是運(yùn)行時(shí)的嚴(yán)重錯(cuò)誤)的方法,WinCE 還是比較少的,且真正被用的更是少之又少。
PC 下除了以上第二和第三種方法外,還有以下 3 個(gè)核心的函數(shù)可以感知程序的異常,分別是:
SetUnhandledExceptionFilter(HandleException),功能是確定出現(xiàn)沒有控制的異常發(fā)生時(shí)調(diào)用的函數(shù)為 HandleException;函數(shù)在 WinCE 上是不可用的,無100%替代函數(shù)。
_set_invalid_parameter_handler(HandleInvalidParameter),功能是確定出現(xiàn)無效參數(shù)調(diào)用發(fā)生時(shí)調(diào)用的函數(shù)為 HandleInvalidParameter;
_set_purecall_handler(HandlePureVirtualCall),功能是確定純虛函數(shù)調(diào)用發(fā)生時(shí)調(diào)用的函數(shù)為 HandlePureVirtualCall。
充分利用 Bug 的解決方法,是一個(gè)程序員成長的必由之路。因?yàn)槌绦騿T的工作不只是編碼,還包括前期設(shè)計(jì)與后期的產(chǎn)品問題的修復(fù)等。
好的應(yīng)用,就應(yīng)該像 TCPMP 一樣,在異常來臨時(shí)能正確的提示用戶,這樣的程序的崩潰也朝“優(yōu)美”邁進(jìn)了一步。同時(shí),也提供的開發(fā)人員分析異常的信息:記錄在文件中。