WinCE系統(tǒng)下應用崩潰原因的分析
做為嵌入式程序員,也是一樣的。一般來說嵌入式系統(tǒng)都提供了異常分析的方法,特別是強大的調(diào)試工具,這些工具使用在 PC 上編程使用的工具是一樣的,例如:Visual Studio 系列。但是一些專用的、或小的嵌入式系統(tǒng),可能會提供專用的調(diào)試工具。雖然從功能上來說,沒有微軟提供的 VS 功能強大,使用起來也不太方便,但也會提供類似的調(diào)試功能。這里我主要討論的還是微軟提供的工具。
目前,在車載與 PND 市場,使用 WinCE 系統(tǒng)的比較多。在 WinCE6.0 系統(tǒng)中,如果應用發(fā)生較嚴重的錯誤時,一般都會彈出系統(tǒng)標準的、令人十分討論的應用錯誤對話框。大概提示:XXX.exe出現(xiàn)嚴重錯誤,必須被關閉。
如何解決此類問題呢?
只要能接上調(diào)試串口,或與調(diào)試工具連接,如VS2008等,獲取出錯時的異常信息后,就可以來分析異??赡艿脑颉?br />但如果設備已經(jīng)處于量產(chǎn)狀態(tài),無法連接輸出 LOG 的串口和調(diào)試 USB 口時,如何能捕捉到異常信息呢?
在無法徹底解決此類問題的情況下,有人就想能不能不讓系統(tǒng)顯示那個錯誤對話框。為了能使應用“優(yōu)美”的退出(網(wǎng)絡上的說法),即程序退出時不出現(xiàn)述的錯誤對話框,有人曾試著去修改 WinCE 提供的內(nèi)核代碼,但這部分應該是屬于未開源的部分。所以此方法也行不通的!
解決此類問題的根本辦法當然是提高編碼的質(zhì)量,然后加強質(zhì)量保證(即測試),盡量將 Bug 消滅在研發(fā)階段。因為研發(fā)階段,有大量的調(diào)試工具可以使用,如下述的第一種方法。
在沒有調(diào)試工具可以依賴時,有沒有辦法獲取到異常信息呢?方法當然是有的,如下述第二種和第三種方法。為什么要說第一種方法呢,因為它提供的信息是最基礎的,是后面兩種方法都要用到的基礎。在這里,我重點推薦的是第三種方法。因為它的處理比較獨立、在 WinCE 系統(tǒng)中比較有效、且方便集成到已有代碼中,實現(xiàn)異常捕獲。
第一種方法:如果有輸出 LOG 串口可說時,串口輸出的異常信息,加 MAP 文件一起分析錯誤的出處,可以到函數(shù)一級。所以要求在調(diào)試時一定要將對應版本的 MAP 文件一起保留,用于后繼異常問題的分析。
對于如下的測試代碼:
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)用默認值 ??if(1?==?nIDEvent) ??{ ????KillTimer(1); ????CallCrashFunc(); ???? ????//?其它的功能 ??} ??CDialog::OnTimer(nIDEvent); }
在 WinCE6.0 和 WinCE7.0 下運行時,串口的輸出內(nèi)容基本上是相同的,但在 WinCE7.0 下沒有出錯的對話框。
串口中輸出的 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
從對應的 MAP 文件中查到是 TestCrashFunc 函數(shù)出錯(PC 指針 0x00001738 + MAP 文件中的 Preferred load address 偏移量),此例中出錯時位置為: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)對這種對空指針賦值等異常是做了一些處理的,至少不再彈出那個令人十分討厭的對話框,也不影響后繼其它功能的執(zhí)行。在 WinCE6.0 下如果出現(xiàn)類似的對話框,則應用就會退出。
第二種方法:使用 __try 和 __except。在開源的多媒體播放器 TCPMP 中,就有如下的用法。
先定義兩個宏,然后將重要的處理線程代碼包含在定義的這兩個宏中,以捕捉兩個宏之間代碼出現(xiàn)的異常。這樣做有一個缺點:但代碼量很大時,就需要增加很多對這兩個宏的調(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 中一個關鍵線程的異常處理代碼(TCPMP 線程的代碼,沒有完整的給出,有興趣的童鞋請自己去看 TCPMP 的源代碼),其中兩個定義的異常處理宏,將線程的所有代碼包含在內(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; }
此種實現(xiàn)方法,最最關鍵是 SafeException() 函數(shù)中分析與記錄異常信息的辦法。
但由于在 TCPMP 中,獲取異常的信息與 TCPMP 的軟件框架結合在一起。需要移植此部分代碼到其它工程時,需要將有用的代碼分離出來,其實這個也比較簡單。
只要將與 EXCEPTION_POINTERS 相關的代碼拿出來即可。
int?SafeException(void*?p) { ??EXCEPTION_POINTERS*?Data?=?(EXCEPTION_POINTERS*)p; ??//?刪除了無關的代碼?-?此部分代碼是?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"); ??????} ?????? ??????//......?關鍵是處理?EXCEPTION_POINTERS?結構體相關的成員 ??????//?其它一些相關的,如可執(zhí)行程序文件名等,根據(jù)需要來獲取 ??} }
第三種方法:使用函數(shù) AddVectoredExceptionHandler()。
在 WinCE 下使用此函數(shù),需要包含頭文件: TlHelp32.h 和庫文件: toolhelp.lib。由于此函數(shù)屬于 WinCE 示公開的 API,所以幫忙只要以 PC 上為準。
使用此函數(shù),是向 WinCE 系統(tǒng)注冊一個矢量異常處理程序,但有異常發(fā)生時會調(diào)用此處理程序。
函數(shù)的原型如下(MSDN),各參數(shù)具體的含義,請參考 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); ??/*?在使用時,必須包含一些頭文件。這些頭文件,需要從?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) ????{ ????} ??} ??//?獲取進程內(nèi)?dll?信息 ??MODULEENTRY32?CurrentModule; ??HANDLE?hSnapShot?=?CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,GetCurrentProcessId()); ??if((HANDLE)-1?!=?hSnapShot) ??{ ????//?調(diào)用??Module32First??和?Module32Next?完成?Module?枚舉 ??} }
是否還需要其它信息來分析程序出現(xiàn)異常的原因,可以參考 MSDN 中對結構 _EXCEPTION_POINTERS 中各成員的說明。然后將有用的信息,寫到 SD 卡等可永久存貯的設備中。這樣就不必為擔心找不到用于分析異常的資源,且此記錄的文件中的信息遠大于串口輸出的異常信息。同時,也可以根據(jù)需要輸入一些應用(進程)的相關信息。
可以將此異常捕獲功能的代碼,封裝成一個 LIB 來供使用程序調(diào)用。
相對于 PC Windows 下感知程序崩潰(其實就是運行時的嚴重錯誤)的方法,WinCE 還是比較少的,且真正被用的更是少之又少。
PC 下除了以上第二和第三種方法外,還有以下 3 個核心的函數(shù)可以感知程序的異常,分別是:
SetUnhandledExceptionFilter(HandleException),功能是確定出現(xiàn)沒有控制的異常發(fā)生時調(diào)用的函數(shù)為 HandleException;函數(shù)在 WinCE 上是不可用的,無100%替代函數(shù)。
_set_invalid_parameter_handler(HandleInvalidParameter),功能是確定出現(xiàn)無效參數(shù)調(diào)用發(fā)生時調(diào)用的函數(shù)為 HandleInvalidParameter;
_set_purecall_handler(HandlePureVirtualCall),功能是確定純虛函數(shù)調(diào)用發(fā)生時調(diào)用的函數(shù)為 HandlePureVirtualCall。
充分利用 Bug 的解決方法,是一個程序員成長的必由之路。因為程序員的工作不只是編碼,還包括前期設計與后期的產(chǎn)品問題的修復等。
好的應用,就應該像 TCPMP 一樣,在異常來臨時能正確的提示用戶,這樣的程序的崩潰也朝“優(yōu)美”邁進了一步。同時,也提供的開發(fā)人員分析異常的信息:記錄在文件中。