• 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
    ",pNullPoint));  
     6   *pNullPoint = 0;  
     7   RETAILMSG(1,(L"-----------------------------%d,%d
    ",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 一样,在异常来临时能正确的提示用户,这样的程序的崩溃也朝“优美”迈进了一步。同时,也提供的开发人员分析异常的信息:记录在文件中。

  • 相关阅读:
    JVM虚拟机
    antd Table排序问题
    关于element-ui中el-container布满全局的问题!
    vue 安装css预处理器LESS
    mybatis官网
    Lombok安装及使用介绍
    thymeleaf中th:each的使用,遍历数组
    thymeleaf中th:text和th:utext的使用与区别
    SprongBoot项目的打包与启动
    SpringBoot简单学习
  • 原文地址:https://www.cnblogs.com/91program/p/5201536.html
Copyright © 2020-2023  润新知