自制办公桌:调试经验谈-根据crash dump查找bug - videosender的专栏 - CSDN博客

来源:百度文库 编辑:九乡新闻网 时间:2024/04/29 16:09:28

很多时候,我们不知道如何重现一个crash问题,只有一些log或者dump,拿到一个这样的crash的问题,并不知道是什么原因,怎么样来慢慢分析,这篇文章就举了一个现实的例子,看看怎么查找一个crash问题的原因。

这个是我们软件发生的一次崩溃,只在客户的环境上发生,没有人知道在实验室里怎么重现,好在客户那边给出了windows的dump文件,这样就可以很容易的查看当时的案发现场了。

用windbg打开dump文件,打开汇编窗口,点击调用堆栈最上面的函数,就可以看到如下汇编代码

2e252013 51              push    ecx
2e252014 53              push    ebx
2e252015 57              push    edi
2e252016 8bf9            mov     edi,ecx
2e252018 33db            xor     ebx,ebx
2e25201a 395f04          cmp     dword ptr [edi+4],ebx
2e25201d 7454            je      nqp+0x22073 (2e252073)
2e25201f 66395f14        cmp     word ptr [edi+14h],bx
2e252023 763c            jbe     nqp+0x22061 (2e252061)
2e252025 895dfc          mov     dword ptr [ebp-4],ebx
2e252028 56              push    esi
2e252029 8da42400000000  lea     esp,[esp]
2e252030 8b7704          mov     esi,dword ptr [edi+4]
2e252033 0375fc          add     esi,dword ptr [ebp-4]
2e252036 837e0800        cmp     dword ptr [esi+8],0  ds:0023:613d634e=????????  <<-----软件在这里就崩溃了

通过调用堆栈,我们可以找到对应的函数的源码

void HTMLelement:: ResetPriv()
{
    if (i_pAttributeList) {
        for (int i = 0; i < i_totAttrs; i++) {
            GetAttributeEntry(i)->Reset();
        } 
    }   
}

可以看出来 是在cmp     dword ptr [esi+8],0 的时候出了异常,这个指令能出的异常,也就是 [esi+8]不能读,这说明 esi+8这个地址错了,但是为啥会错呢,只能是esi的值不对。先看看esi是多少,在命令窗口 输入 r esi

0:036> r esi
Last set context:
esi=613d6346

这个613d6346 显然是不可访问的内存。所以,我们就需要看看esi是哪里来的。一行行的往上看:

add     esi,dword ptr [ebp-4]

这句把ebp-4里面的数加到esi上,那我们需要看看ebp-4是多少了,在命令窗口里面输入 dd ebp-4

0:036> dd ebp-4
32e3cc10  00000000 32e3cc24 2e252966 32e47284
32e3cc20  32e47218 32e3cc44 2e252e24 32e47284
32e3cc30  00000000 00000001 32e47218 32e47218
32e3cc40  00000000 32e3e9fc 2e3abbd6 32e3e960
32e3cc50  00000000 00000000 00000001 32e3f8f0
32e3cc60  00000000 2e462e8c 00000000 32e3cc84
32e3cc70  00000001 016edda8 32e3cf5c 03434cf8
32e3cc80  034348f8 32e3cc90 60001780 016edda8

可见 ebp-4是0,这很正常,这个ebp-4是个局部变量i,其实就是个数组的下标,0不像是个越界的值,那么就需要继续往前看

mov     esi,dword ptr [edi+4]

esi的值是来自edi+4,那我们来看看edi+4是啥

0:036> dd edi
32e3e960  32257974 613d6346 00000075 00000042
32e3e970  00000002 00000002 00000000 00000000
32e3e980  32e3e9a0 00000000 00000000 00000019
32e3e990  00000002 00000000 00000000 00000000
32e3e9a0  32e3e960 00000000 00000000 0000006e
可见这个值就是613d6346,就是esi的值,那就说明 这个内存里面的值有问题,出错的可能有两个,一种可能是这个内存的值不对,另一种可能是是edi的值不对,导致取值取错了地方。跟踪edi还是容易一点,所以我们继续往上看edi的值是哪里来的。

2e252016 8bf9            mov     edi,ecx

这个是函数的开头,我们知道ecx放的就是对象的地址,也就是this,说明edi里面就是this,这就说明edi出错的可能比较小了,除非上面函数传值的时候就传错了,看了一下源码,这个HTMLelement的地址应该没啥太大机会出错。这里不是说完全排除,但是进一步跟踪的代价比较大了,所以我们先放一下,看另外一个可能。那就是这个对象里面的内容出错了。

越界的问题往往是数组越界,而字符串是最容易越界的,我们来看看这个会是啥字符,输入 dc edi 看看

:036> dc edi
32e3e960  32257974 613d6346 00000075 00000042  ty%2Fc=au...B...
32e3e970  00000002 00000002 00000000 00000000  ................
32e3e980  32e3e9a0 00000000 00000000 00000019  ...2............
32e3e990  00000002 00000000 00000000 00000000  ................
32e3e9a0  32e3e960 00000000 00000000 0000006e  `..2........n...
32e3e9b0  00000002 00000000 00000000 00000000  ................
32e3e9c0  2e46beb4 00000000 3607274c 00000091  ..F.....L'.6....
32e3e9d0  32e3e9a0 00000000 00000000 00000033  ...2........3...

很明显这里应该是是个指针的地方,居然看起来像个字符串了,往前看看完整的字符串是啥

0:036> dc edi-128 edi+16
32e3e838  00000000 00000000 00000000 00000000  ................
32e3e848  00000000 00000001 00000001 ffffffff  ................
32e3e858  2e2f2e2e 64242f2e 75616665 6976746c  ../../$defaultvi
32e3e868  372f7765 45314243 43443944 43463944  ew/7CB1ED9DCD9FC
32e3e878  38424145 36353235 30334337 32453630  EAB852567C3006E2
32e3e888  2f454244 65704f3f 636f446e 6e656d75  DBE/?OpenDocumen
32e3e898  72502674 74655365 6c656946 683d7364  t&PreSetFields=h
32e3e8a8  7465535f 64616552 6e656353 5f683b65  _SetReadScene;h_
32e3e8b8  75636553 79746972 626d654d 6e497265  SecurityMemberIn
32e3e8c8  682c6f66 6d654d5f 4e726562 3b656d61  fo,h_MemberName;
32e3e8d8  4a3d6e63 61696c75 3032256e 73726143  cn=Julian%20Cars
32e3e8e8  32256e6f 3d756f46 69737542 7373656e  on%2Fou=Business
32e3e8f8  50303225 6e6e616c 25676e69 6e613032  %20Planning%20an
32e3e908  30322564 6f736552 65637275 46322573  d%20Resources%2F
32e3e918  533d756f 65647574 3225746e 646e6130  ou=Student%20and
32e3e928  43303225 756d6d6f 7974696e 53303225  %20Community%20S
32e3e938  69767265 25736563 756f4632 6174533d  ervices%2Fou=Sta
32e3e948  32256666 4d3d6f46 73616e6f 30322568  ff%2Fo=Monash%20
32e3e958  76696e55 69737265 32257974 613d6346  University%2Fc=a
32e3e968  00000075 00000042 00000002 00000002  u...B...........

看来前面是个很长的字符串,数数看,从字符串开始的地方 32e3e858 算,有270多个字符,估计有某个地方定义了个256的缓存区,然后没做检查,就把后面的栈上的内存给覆盖了,继续看stack,看看这个字符数组是属于哪个函数的栈帧。k可以看函数调用栈

0:036> k
  *** Stack trace for last set context - .thread/.cxr resets it
ChildEBP RetAddr  
WARNING: Stack unwind information not available. Following frames may be wrong.
32e3cc14 2e252966 nqp+0x22036
32e3cc24 2e252e24 nqp+0x22966
32e3cc44 2e3abbd6 nqp+0x22e24
32e3e9fc 2e3ae8d7 nqp+0x17bbd6
32e3eb40 2e3ace45 nqp+0x17e8d7
32e3eb84 2e3c9823 nqp+0x17ce45
32e3ebbc 2e2fb73b nqp+0x199823
32e3ebcc 2e301e22 nqp+0xcb73b
32e3ffe4 2e318255 nqp+0xd1e22
32e402a8 2e271b68 nqp+0xe8255
32e42c78 2e27236a nqp+0x41b68
32e42cc0 600bbf1a nqp+0x4236a

因为我现在没有当时那个版本的pdb,所以显示不了具体的函数,有pdb的话,可以看到函数名字,我们可以看出来32e3e858 是属于下面这个函数调用的栈上的

32e3e9fc 2e3ae8d7 nqp+0x17bbd6

在有symbol的情况下,就可以知道是哪个函数了,在这个函数里面,找到了这样的语句。

                char memberDocURL[MAX_PATH+1] = "javascript:void(0);";

                ........

                URLencodedString.Copy(dn);
                huURL.URLencode(&URLencodedString);
                strcat(memberDocURL, URLencodedString);

在栈上开了个MAX_PATH+1的数组,这里MAX_PATH是260,然后一系列操作,都是靠strcat来做的,dn这个字符串如果很长,就像上面显示的那样,URLencodedString也会很长,就会导致写越界了。把后面的对象给覆盖了。

到这里,原因就找到了。典型的字符串操作不小心导致缓存区溢出。

总结一下,对于这种不可重现的crash问题,就是要从crash的地方入手,看看哪个数据不对,顺藤摸瓜,一点点往前面排查。windbg是个很强大的调试工具,特别是分析这种只有dump,没法重现的问题,非常有用。