WinCE系统下应用崩溃原因的分析方法

做为程序员,最怕什么?Bug?大家都清楚,调试期的 Bug 并不可怕,那怕是那些神龙见首不见尾的 INT(随机、没有规律) Bug。

做为嵌入式程序员,也是一样的。一般来说嵌入式系统都提供了异常分析的方法,特别是强大的调试工具,这些工具使用在 PC 上编程使用的工具是一样的,例如:Visual Studio 系列。但是一些专用的、或小的嵌入式系统,可能会提供专用的调试工具。虽然从功能上来说,没有微软提供的 VS 功能强大,使用起来也不太方便,但也会提供类似的调试功能。这里我主要讨论的还是微软提供的工具。
目前,在车载与 PND 市场,使用 WinCE 系统的比较多。在 WinCE6.0 系统中,如果应用发生较严重的错误时,一般都会弹出系统标准的、令人十分讨论的应用错误对话框。大概提示:XXX.exe出现严重错误,必须被关闭。
如何解决此类问题呢?
只要能接上调试串口,或与调试工具连接,如VS2008等,获取出错时的异常信息后,就可以来分析异常可能的原因。
但如果设备已经处于量产状态,无法连接输出 LOG 的串口和调试 USB 口时,如何能捕捉到异常信息呢?
在无法彻底解决此类问题的情况下,有人就想能不能不让系统显示那个错误对话框。为了能使应用“优美”的退出(网络上的说法),即程序退出时不出现述的错误对话框,有人曾试着去修改 WinCE 提供的内核代码,但这部分应该是属于未开源的部分。所以此方法也行不通的!


解决此类问题的根本办法当然是提高编码的质量,然后加强质量保证(即测试),尽量将 Bug 消灭在研发阶段。因为研发阶段,有大量的调试工具可以使用,如下述的第一种方法。
在没有调试工具可以依赖时,有没有办法获取到异常信息呢?方法当然是有的,如下述第二种和第三种方法。为什么要说第一种方法呢,因为它提供的信息是最基础的,是后面两种方法都要用到的基础。在这里,我重点推荐的是第三种方法。因为它的处理比较独立、在 WinCE 系统中比较有效、且方便集成到已有代码中,实现异常捕获。


第一种方法:如果有输出 LOG 串口可说时,串口输出的异常信息,加 MAP 文件一起分析错误的出处,可以到函数一级。所以要求在调试时一定要将对应版本的 MAP 文件一起保留,用于后继异常问题的分析。

对于如下的测试代码:

 1 void TestCrashFunc(void)  
 2 {  
 3   int *pNullPoint = NULL;  
 4   
 5   RETAILMSG(1,(L"-----------------------------%d\r\n",pNullPoint));  
 6   *pNullPoint = 0;  
 7   RETAILMSG(1,(L"-----------------------------%d,%d\r\n",pNullPoint,*pNullPoint));  
 8 }  
 9   
10 void CallCrashFunc(void)  
11 {  
12   TestCrashFunc();  
13 }  
14   
15 void CSmartDeviceMFCDlg::OnTimer(UINT_PTR nIDEvent)  
16 {  
17   // TODO: 在此添加消息处理程序代码和/或调用默认值  
18   if(1 == nIDEvent)  
19   {  
20     KillTimer(1);  
21     CallCrashFunc();  
22       
23     // 其它的功能  
24   }  
25   CDialog::OnTimer(nIDEvent);  
26 }  

在 WinCE6.0 和 WinCE7.0 下运行时,串口的输出内容基本上是相同的,但在 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 函数出错(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 系统对这种对空指针赋值等异常是做了一些处理的,至少不再弹出那个令人十分讨厌的对话框,也不影响后继其它功能的执行。在 WinCE6.0 下如果出现类似的对话框,则应用就会退出。


第二种方法:使用 __try 和 __except。在开源的多媒体播放器 TCPMP 中,就有如下的用法。
先定义两个宏,然后将重要的处理线程代码包含在定义的这两个宏中,以捕捉两个宏之间代码出现的异常。这样做有一个缺点:但代码量很大时,就需要增加很多对这两个宏的调用。

1 #define SAFE_BEGIN __try {  
2 #define SAFE_END ;} __except (SafeException(_exception_info())) {}  

可以看到 WinCE 下的使用方法,与 PC 上 SEH(Structured Exception Handling)是一样的。如下所示:

1 __try   
2 {  
3    // guarded code  
4 }  
5 __except ( expression )  
6 {  
7    // exception handler code  
8 }  

以下是 TCPMP 中一个关键线程的异常处理代码(TCPMP 线程的代码,没有完整的给出,有兴趣的童鞋请自己去看 TCPMP 的源代码),其中两个定义的异常处理宏,将线程的所有代码包含在内。

 1 static int ProcessThread(player_base* p)  
 2 {  
 3   int Result = ERR_NONE;  
 4   
 5 #ifdef MULTITHREAD  
 6   SAFE_BEGIN  
 7   
 8   while (p->Wnd)  
 9   {  
10     ......  
11     if (p->RunProcess)  
12     {  
13       processstate State;  
14       State.Fill = p->Fill;  
15   
16       p->Timer->Get(p->Timer,TIMER_TIME,&State.Time,sizeof(tick_t));  
17   
18       //DEBUG_MSG1(DEBUG_PLAYER,T("Process Time:%d"),State.Time);  
19   
20       Result = p->Format->Process(p->Format,&State);  
21   
22       if (Result == ERR_SYNCED)  
23       {  
24         ......  
25       }  
26       else if (p->Fill && (Result == ERR_END_OF_FILE || Result == ERR_BUFFER_FULL  
27         || (Result == ERR_NEED_MORE_DATA && (p->NoMoreInput || State.BufferUsedAfter >= p->CurrBufferSize2-2))))  
28       {  
29         ......  
30       }  
31   
32       ......  
33     }  
34     ......  
35   }  
36   
37   SAFE_END  
38   return 0;  
39 }  

此种实现方法,最最关键是 SafeException() 函数中分析与记录异常信息的办法。
但由于在 TCPMP 中,获取异常的信息与 TCPMP 的软件框架结合在一起。需要移植此部分代码到其它工程时,需要将有用的代码分离出来,其实这个也比较简单。
只要将与 EXCEPTION_POINTERS 相关的代码拿出来即可。

 1 int SafeException(void* p)  
 2 {  
 3   EXCEPTION_POINTERS* Data = (EXCEPTION_POINTERS*)p;  
 4   
 5   // 删除了无关的代码 - 此部分代码是 TCPMP 中的代码,所以未做排版。  
 6   {  
 7     {  
 8       const uint8_t* ContextRecord = (const uint8_t*) Data->ContextRecord;  
 9       EXCEPTION_RECORD* Record = Data->ExceptionRecord;  
10   
11       switch (Record->ExceptionCode)  
12       {  
13       case STATUS_ACCESS_VIOLATION:   Name = T("Access violation"); break;  
14       case STATUS_BREAKPOINT:       Name = T("Breakpoint"); break;  
15       case STATUS_DATATYPE_MISALIGNMENT:  Name = T("Datatype misalignment"); break;  
16       case STATUS_ILLEGAL_INSTRUCTION:  Name = T("Illegal instruction"); break;  
17       case STATUS_INTEGER_DIVIDE_BY_ZERO: Name = T("Int divide by zero"); break;  
18       case STATUS_INTEGER_OVERFLOW:   Name = T("Int overflow"); break;  
19       case STATUS_PRIVILEGED_INSTRUCTION: Name = T("Priv instruction"); break;  
20       case STATUS_STACK_OVERFLOW:     Name = T("Stack overflow"); break;  
21       default:              Name = T("Unknown"); break;  
22       }  
23   
24       if (Record->ExceptionCode == STATUS_ACCESS_VIOLATION)  
25       {  
26         if (Record->ExceptionInformation[0])  
27           Name = T("Write to");  
28         else  
29           Name = T("Read from");  
30       }  
31         
32       //...... 关键是处理 EXCEPTION_POINTERS 结构体相关的成员  
33       // 其它一些相关的,如可执行程序文件名等,根据需要来获取  
34   }  
35 }  

第三种方法:使用函数 AddVectoredExceptionHandler()。
在 WinCE 下使用此函数,需要包含头文件: TlHelp32.h 和库文件: toolhelp.lib。由于此函数属于 WinCE 示公开的 API,所以帮忙只要以 PC 上为准。
使用此函数,是向 WinCE 系统注册一个矢量异常处理程序,但有异常发生时会调用此处理程序。
函数的原型如下(MSDN),各参数具体的含义,请参考 MSDN。这里就不做翻译了。

1 PVOID WINAPI AddVectoredExceptionHandler(__in ULONG FirstHandler, __in PVECTORED_EXCEPTION_HANDLER VectoredHandler); 

以下代码,演示了如何使用 AddVectoredExceptionHandler() 函数:
(1) AddVectoredExceptionHandler(1,MyVectoredExceptionHandler);

 

(2) 定义异常处理程序

 1 LONG WINAPI MyVectoredExceptionHandler(struct _EXCEPTION_POINTERS *pExceptionInfo)  
 2 {  
 3   typedef ULONG (WINAPI *lpGetThreadCallStack)(HANDLE,ULONG,LPVOID,DWORD,DWORD);  
 4   /* 在使用时,必须包含一些头文件。这些头文件,需要从 WinCE 的安装目录中获得。 
 5   OS Versions: Windows CE 5.0 and later. 
 6   Header: Pkfuncs.h. 
 7   */  
 8   typedef struct _CallSnapshotEx  
 9   {  
10     DWORD dwReturnAddr;  
11     DWORD dwFramePtr;  
12     DWORD dwCurProc;  
13     DWORD dwParams[4];  
14   }CallSnapshotEx;  
15   // 打印 Dump 信息   
16   ......  
17   // 打印 SP 堆栈  
18   ......  
19   ULONG *punSp = (ULONG *)pExceptionInfo->ContextRecord->Sp;  
20   // 获取线程堆栈调用  
21   HMODULE hCore = LoadLibrary(L"coredll.dll");  
22   if(NULL != hCore)  
23   {  
24     lpGetThreadCallStack pGetThreadCallStack = (lpGetThreadCallStack)GetProcAddress(hCore,L"GetThreadCallStack");  
25     if(NULL != pGetThreadCallStack)  
26     {  
27     }  
28   }  
29   // 获取进程内 dll 信息  
30   MODULEENTRY32 CurrentModule;  
31   HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,GetCurrentProcessId());  
32   if((HANDLE)-1 != hSnapShot)  
33   {  
34     // 调用  Module32First  和 Module32Next 完成 Module 枚举  
35   }  
36 }  

是否还需要其它信息来分析程序出现异常的原因,可以参考 MSDN 中对结构 _EXCEPTION_POINTERS 中各成员的说明。然后将有用的信息,写到 SD 卡等可永久存贮的设备中。这样就不必为担心找不到用于分析异常的资源,且此记录的文件中的信息远大于串口输出的异常信息。同时,也可以根据需要输入一些应用(进程)的相关信息。
可以将此异常捕获功能的代码,封装成一个 LIB 来供使用程序调用。


相对于 PC Windows 下感知程序崩溃(其实就是运行时的严重错误)的方法,WinCE 还是比较少的,且真正被用的更是少之又少。
PC 下除了以上第二和第三种方法外,还有以下 3 个核心的函数可以感知程序的异常,分别是:
SetUnhandledExceptionFilter(HandleException),功能是确定出现没有控制的异常发生时调用的函数为 HandleException;函数在 WinCE 上是不可用的,无100%替代函数。
_set_invalid_parameter_handler(HandleInvalidParameter),功能是确定出现无效参数调用发生时调用的函数为 HandleInvalidParameter;
_set_purecall_handler(HandlePureVirtualCall),功能是确定纯虚函数调用发生时调用的函数为 HandlePureVirtualCall。


充分利用 Bug 的解决方法,是一个程序员成长的必由之路。因为程序员的工作不只是编码,还包括前期设计与后期的产品问题的修复等。
好的应用,就应该像 TCPMP 一样,在异常来临时能正确的提示用户,这样的程序的崩溃也朝“优美”迈进了一步。同时,也提供的开发人员分析异常的信息:记录在文件中。

posted @ 2016-02-19 16:49  91program  阅读(1086)  评论(0编辑  收藏  举报