做为程序员,最怕什么?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 一样,在异常来临时能正确的提示用户,这样的程序的崩溃也朝“优美”迈进了一步。同时,也提供的开发人员分析异常的信息:记录在文件中。