windows崩溃调试经验谈

我是一个对各种语言特性来之不拒的人,我不会因为一个人写了各种goto而抨击这个人的代码质量,同样我也不会因为一个人在c++代码里写了c风格的代码而批评他的c++水平。语言特性本无罪,人为制定规范只是为了降低问题发生的概率,然而bug是恒存在的,因为谁也不能阻止人类犯二。
特别说明一下在C++代码里使用本该被C++替换的C特性的行为。首先必须强调现在的软件开发过程中用到C语言实现的(开源)第三方组件几乎是必然发生的,在发生于这类第三方库内部崩溃的情况下,C++的异常系统是不可用的。所以对我来说,看崩溃dump的时候,C++和C没啥区别。
1.只有栈没有堆的minidump
通常见到的都是minidump。运行于windows上的程序通常都是客户端程序,没有时间和空间来dump整个内存。所以你手上拿到的都是只有栈没有堆的信息,只能通过栈上的临时变量来反推导致崩溃的逻辑。dump会告诉你是怎样的内存访问导致了崩溃,比如下面这一行:
Unhandled exception at 0x0063e8fe (xxx.exe) in crashdump.dmp: 0xC0000005: Access violation reading location 0x2933b000.
意思就是越界读到了0x2933b000这个非法地址。需要注意是reading还是writing,因为这是判断哪个变量出错的关键信息。
2.令人困惑的变量信息
visual studio开了最高的编译优化(ox)之后,visual studio很多时候就不认识自己编译优化后的代码了。无论是debug过程中,还是看dump的过程中,这种情况几乎每次都会出现。它的调用栈是对的,但显示的变量信息是错的。大概是vs不认识自己的栈指针了(
关于这个问题,stackoverflow上有过讨论:

遇到这种情况,只能开汇编代码看了。运气好的话,能看到刚刚从栈上读到寄存器里的出错变量,运气不好的话只能根据栈指针往后查。
3.令人困惑的调用栈
这里讨论的是函数上一层调用和这一层调用完全对不上的情况——也就是说,从一个莫名其妙的函数call到了另一个跟它看似毫无关系的函数。
首先可能的原因就是call的目标地址不对,也就是说函数地址错了。这种情况并不多见,很可能你传入非法地址的时候就读越界崩溃了。
其次是编译优化导致的问题。
第一种情况是inline导致的。这里说的是inline这种编译优化行为,而不是C++里那个关键字。(事实上并不是inline这个关键字就会触发inline优化。)这种情况可以通过反汇编代码轻易判断出来。
第二种情况是编译器把两个函数实现当成了一个实现处理。比如两个不同的类,它们都有一个getter,返回的同样是offset为16的成员变量的值,它们生成的函数实现可能就是一样的。这时候,编译器可能就把这两个实现当成一个函数,于是从这一个类里的函数call到了另一个类的成员函数。这种情况对照一下两个函数的实现也能轻易看出来。
再次就是栈被破坏了。
这种就是典型的写缓冲区越界导致的,写坏了栈,连同之前入栈的返回地址。这种情况下,没人能保证栈上有多少正确的信息。如果后面有幸在当前函数内崩溃的话,你看到的当前函数是正确的,但上层调用很可能已经被抹去了。
PS:一个很容易就破坏栈的方法是用sprintf,第一个参数是一个栈上的临时数组,然后打印出的文本大小超出了数组的范围,就会写到栈上之前的数据。新版本的visual studio默认的编译选项下喜欢把sprintf这种不安全的函数当编译错误处理,你可能会使用sprintf_s来替代它。然而sprintf_s充其量就是不会把栈写坏而已,它会判断字符串大小是否超出了范围,如果超出了范围就会触发断言——然后在用户看来程序还是崩溃了。
4.