如何定位导致Crash的代码位置

时间:2021-01-18 15:28:30
1. 在开发环境下定位Crash错误
  1.1 普通的crash
  1.2 较难定位的crash
  1.3 注意vc的输出日志
2. 定位发布在外的版本的Crash错误
3. 小技巧
  3.1 根据程序地址找到代码位置
  3.2 根据消息值查看对应的windows消息
  3.3 查看GetLastError返回值
  3.4 在代码中暂停程序
4. 编程小警示
  4.1 慎用IsBadPtr系列函数
  4.2 慎用catch(...)
5. 附录
  5.1 为什么程序crash时调用堆栈是乱的
  5.2 使用Debugging tools for windows查看.dmp文件(错误报告)

------------------------------------------------------------------------------------------------------------------------
1. 在开发环境下定位Crash错误
   1.1 普通的crash
    先来看看最普通的crash
    参见图1(c01.png)
    当你在debug模式下运行上面的程序就会弹出上面的框。vc就帮你定位到了错误的位置。是个对零指针的操作。非常简单,不是吗。

   1.2 较难定位的crash
    较难定位的crash往往是由于内存错误(参见5.1 为什么程序crash时调用堆栈是乱的)。例如以下代码:
代码

    char *p = new char[16];
    p[10] = 0xfd;
    delete[] p;
    printf(p);

    以上代码有两处错误,一是第2行的内存写越界,二是第4行使用被删除的指针。
    但以上代码在vc的release和debug下都不会报错。这使得这类错误很难定位。
    检测这一类问题可以使用BoundsChecker工具的FinalCheck模式(BoundsChecker)

    用BoundsChecker检测后可得到两个错误:Write overrun(写越界) 和 Dangling pointer(使用被删除的指针)而且都精确定位到了出错的位置。是个不错的工具。
    参见图2(c02.png)
    参见图3(c03.png)

   1.3 注意vc的输出日志
    由于一些目前未知的原因(有可能是程序的错误太严重或是BoundsChecker本身的bug),BoundsChekcer有时不能正常工作。
    这里vc的输出日志有时能提供一些有用的信息。
    在难找的crash中,有很大一部分是引用了非法的指针。

    有时在vc的输出日志里可以看到类似于这样的信息
    “emule.exe 中的 0x004277b7 处最可能的异常: 0xC0000005: 读取位置 0xfeeeff62 时发生访问冲突 。”
    在缺少BoundsChecker的支持时,这是一条很重要的信息。意思是说在“程序地址0x004277b7处”对“值为0xfeeeff62的指针”进行操作。
    (怎么通过“程序地址0x004277b7”找到对应的代码行可参照 3.1,)
    这条信息的重要性在于,这个操作只会触发一个警告,而不会导致crash,当crash真正发生时,很有可能不会在0x004277b7附近,
    甚至调用堆栈都已经被写乱,让你无从下手。(参见5.1 为什么程序crash时调用堆栈是乱的)

2. 定位发布在外的版本的Crash错误
  发布在外的软件crash了,往往不好调试,所以目前很多软件都有“发送错误报告”这一功能。
  实现这一功能一般分以下几步:

  a. 使用SetUnhandledExceptionFilter函数
    使用SetUnhandledExceptionFilter设置最高一级的异常处理函数,当程序出现任何未处理的异常,都会触发你设置的函数里。具体使用可参照msdn和emule源码。
  b. 使用MiniDumpWriteDump函数
    在你的异常处理函数里,使用MiniDumpWriteDump把错误信息存成特定格式的文件。具体使用可参照msdn和emule源码。
  c. 发送错误报告
    选用一种形式把第二步产生的错误报告(.dmp)文件发送给你指定的地方。
  d. 查看错误报告
    这里介绍用vc查看错误报告的方法,还可以用windows debug tools这个工具看,方法见5.2 使用windows debug tools查看.dmp文件(错误报告)
    
    查看错误报告需要有三样东西:对应release版的代码,当时编译release版所产生的.exe和.pdb文件。(这两个文件都在编译的输出目录里。)所以当程序发布时,要保留下这两个文件。
    把.dmp(错误报告文件), .pdb, .exe. 代码,在同一目录下,用vc打开.dmp 文件。
    按F5运行,程序即到达crash时的状态,可以对其进行相应的分析。
    

  一点补充:当没有“发送错误报告”的功能,或是此功能失效,以致弹出了windows的“发送错误报告”的对话框。这时其实也是有错误报告的,一般在C:Documents and Settings用户名Local SettingsTemp里的一个.dmp文件(一般只有一个.dmp)


3. 小技巧
   3.1 根据程序地址找到代码位置
    可按如下步骤:
    a. 使程序处于停止状态。(比如程序运行时,在vc里按Ctrl+Alt+Break,或设断点使程序停下)
    b. 切换到汇编状态。(Ctrl+F11)
    c. 在地址栏输入程序地址,回车。 
    d. 可按Ctrl+F11切回代码模式。
   3.2 根据消息值查看对应的windows消息
    在vc的监视窗口里输入“消息值,wm”即可看到对应的消息。
   3.3 查看GetLastError返回值
    在vc的监视窗口里输入“@err,hr”,即可看到LastError及其解释。
   3.4 在代码中暂停程序
    在debug版中可以在代码中加上“AfxDebugBreak();”以暂停程序。release版可使用 “_asm int 3;”
4. 编程小警示
  4.1 慎用IsBadPtr系列函数

    当使用IsBadReadPtr, IsBadWritePtr, IsBadCodePtr一系列函数时要注意,这一类函数可能并不能达到你所想要的意图。
    比如下面的代码,两个返回都是false。
代码
    
    char *p = new char[10];
    bool b;
    b = IsBadReadPtr(p+10, 1);
    delete[] p;
    char *q = new char[10];
    b = IsBadReadPtr(p, 1);

    所以切忌在程序中以IsBadPtr函数来判断是否可以对这个指针进行操作。这些函数只能在调试中使用,或是你确切的知道这些函数的返回值表示的是什么意义的时候。

   4.2 慎用catch(...)
    为了防止程序crash或是解决一个不明白的crash时,大家很容易想到一个 try{}catch(...){}来解决问题。
    的确大部分时间这样不会出问题了,但这个try-catch很有可能隐藏掉在try里面的错误,而当由此错误引起其他错误时,就很难追踪到这个问题了。
    所以建议尽量少用catch(...),如果知道某一块代码会抛出异常,应该用确切的写法。比如catch(CFileException *e), catch(int e)等等。

5. 附录
  5.1 为什么程序crash时调用堆栈是乱的

    当内存被写乱时程序很有可能出现很难定位的crash,比如调用堆栈是乱的,或走到不存在的代码里。这里举一例
代码

    class CA
    {
    public:
        CA(){}
        ~CA(){}
        virtual f(){}
    };

    void show()
    {
        printf("shown");
    }

    int main()
    {
        // 对像被创建删除
        CA *p = new CA;
        delete p;

        // 一些正常的操作
        int *q = new int;
        int codeAddress = (int)show;
        *q = (int)&codeAddress;

        // 调用被删除的对像,程序有可能执行到任何地方。
        p->f();
    }


    上面代码的结果会走到show()里显示出show(这跟编译环境有关,vc2003下测试结果是这样)。如果你了解c++的vtable机制就明白这是怎么回事。
    如果不明白也没关系,我下面说个大概。

    首先CA对象被创建又被删除。如果此时调用p->f()多半会crash。
    第二块代码可视为一些正常的操作,new了一个q,然后对它进行了一些赋值。
    如果你不明白vtable机制,那你只要知道,最后一行的 “p->f();”会执行变量q所指向的变量所指向的变量所标示的地址。(这里没打错字,是两个“所指向的变量”如何定位导致Crash的代码位置
    这里我“心地善良”的给这个值赋上了一个合法的函数地址show。但实际程序中q所指向的变量有可能是任意值,它再指向的变量就更是任意值了。
    那程序就不知道跑哪里去了。如果这个值过于离谱,那算你运气好,程序会立即crash,而你就知道错误位置在哪了。但讨人厌的是,它有可能是一个合法地址,那程序就继续走下去,
    但迟早会crash,并且调用堆栈面目全非(原因牵涉到“调用堆栈的推导”的问题这里就不多说了),
    到时就根本无从知道原来是调用了被删除的p对象而导致的。

  5.2 使用Debugging tools for windows查看.dmp文件(错误报告)
    a. 准备好程序对应的代码,exe文件,pdb文件(编译时在编译输出目录里)
    b. 安装WinDbg
    c. 在winDbg里把Symbol目录设在.pdb所在目录,Image目录设在.exe所在目录,code目录设到代码目录。
    d. 打开.dmp文件
    e. 输入命令.ecxr。(此命令使环境回到崩溃时的状态)
    f. 打开调用堆栈(ALT + F6)查看Crash的位置
    g. 进行分析

简介
  (FinalCheck能检测出的错误列表见附录1
  BoundsChecker是一个很强大的调试工具。这里只简单介绍如何用它的FinalCheck模式定位比较难定位的错误。
FinalCheck模式简单来说就是BoundsChecker在你的代码里加一些诊断代码来检查平时比较难查出的内存越界,错误的指针使用等。
不过付出的代价就是程序跑起来会比较慢,所以在不用时最好是把FinalCheck模式关掉。特别是发布前。

BoundsChecker下载地址
ed2k://|file|BoundsChecker.v7.2.rar|62579029|6032ED8CA789C23D1CC1553946F814A0|h=D3OR5R3FZXJSV7I5A7HWTFHSRZPTLN4N|/


启用FinalCheck模式(基于Visual Studio 2003)
  1. 在VC的菜单里的“工具->BoundsChecker”
    a. 选中“Error Detection” (选中此项让你在调试运行时让BoundsChecker同时检测程序的错误,不选中就是普通的调试程序)
    b. 选中“Log Event”
    c. 去掉“Display error and pause”(出现错误时是否立即提示,可以试试选中它看看是什么效果)
    d. 选中FinalCheck(编译时加入BoundsChecker的诊断代码,不再需要此功能时,要把这个选项去掉再把工程重新编译一遍)
  2. 在VC的菜单里打开“工具->BoundsChecker->Options”确认里面的“Memory Tracking->Enable FinalCheck”被选中。
  3. 重新编译你的工程,这时BoundsChecker会在编译的过程中插入些诊断代码用于之后的监测。(如果编译不通过,参看附录2
  4. 按F5调试运行你的程序
  -这时你的程序就在BoundsChecker的监测下运行起来了。

查看错误信息
  此时你的解决方案里会多出一个 DevPartner Sessions->BoundsChecker->BoundsChecker-Active Session
  双击它可以看到目前出现的错误。
  我们关注Errors那个页签,其他的可以自行研究。这里有很多错误。有的会有源码。
  不明的这个错误说什么的可以右键点击这个错误,点击explain里面有很详细的解释。

性能及相关设置
  FinalCheck是很耗cpu和内存资源的,所以如果机器不好,可能会非常慢。这里可以做想应设置先去掉一些检测功能来加快速度。
  打开BoundsChecker的选项“工具->BoundsChecker->Options”
  1. Resource Tracking里的Enable resource tracking可以先去掉,因为暂时不需要对资源的检测
  2. Memory Tracking中是对不同的情况进行监测,可以先去掉一些你不关心的。或是一次只监测一部分。

其他问题:
  1. 如果你的解决方案里含有多个项目,那要注意FinalCheck是对于项目的,要注意哪个是当前项目。
  2. 遇到其他问题可以查看BoundsChecker的帮助,或在网上搜索。帮助在安装目录下的help里的bc7.chm
  3. 如果不使用集成到VC里的BoundsChecker,也可以使用安装目录下的BC7.exe去打开你的程序exe运行。
   但编译还是要按上面所说的编译。另注意BC7.exe的"setting->Memory Tracking->Enable FinalCheck"要被选上。
  4. 如果过程中你遇到问题欢迎跟贴。

附录1: BoundsChecker的FinalCheck模式能检测出的错误列表

 Pointer Errors - 指针错误
  Array index out of range - 使用越界的数组索引
  Assigning pointer out of range - 使用越界的指针
  Expression uses dangling pointer - 使用野指针
  Expression uses unrelated pointer - 不相关指针相互比较
  Function pointer is not a function - 函数指针指向的不是函数地址

 Memory Errors - 内存错误
  Reading overflows memory - 越界读内存
  Reading uninitialized memory - 读未初始化的内存
  Writing overflows memory - 越界写内存

 Leak Errors - 泄漏
  Memory leaked due to free - 未释放内嵌指针导致的内存泄漏
  Memory leaked due to reassignment - 指针重赋值导致的内存泄漏
  Memory leaked leaving scope - 离开作用域导致的内存泄漏
  Returning pointer to local variable - 返回局部变量的指针

附录2:与FinalCheck冲突的编译参数

 在使用FinalCheck重新编译工程的过程中可能会出现一些编译错误,因为FinalCheck跟一些编译选项有冲突,
 目前所知的有:
 a. 关掉“常规->全程序优化”
 b. “C/C++ -> 优化 -> 内联函数展开”设成默认。
 遇到其他问题对照它给出的信息做相应设置修改就行了