Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)

1、引言

        Windbg是在Windows平台下,强大的用户态和内核态调试工具,给我们分析Windows平台软件的异常提供了极大的便利和强有力的支持,比原始的直接查看代码分析异常的效率要来的高的多。Windbg支持动态调试,也支持对dump文件的静态分析。我们日常使用的主要是Windbg的静态分析功能,通过对软件异常时crashreport抓取到的dump文件的分析,定位异常发生的原因和位置,从而提高我们的软件质量和稳定性。除了Windbg之外,我们可能还需要使用到反汇编工具IDA,通过IDA,我们可以查看到目标文件的汇编代码。通过windbg可以确定出我们的C++代码崩溃在哪一句上,但是要搞清楚崩溃的具体诱因,还是要从汇编代码入手,从上下文中找出具体原因和异常触发的位置。本文就最近TrueLink遇到的两个典型的异常崩溃实例,详细介绍了使用Windbg及IDA分析问题和解决问题的过程。另外,本文主要讲解dump的静态分析,至于将Windbg动态挂载到目标进程中去动态调试的内容,不是本文讲解的范畴,如需了解,可另行寻找资料。


2、问题描述

        最近项目组新来了一位刚毕业的测试小哥,很是了得,因为其独特的视角(没有现有测试人员的思维定势),发现了TrueLink中掩藏很深的几个bug。这不,可能是又发现了新的问题了,喊我过去看看是咋回事。结果在测试小哥手动点击复现问题的过程中,TrueLink出现了崩溃。一时兴起(因为异常分析不是我负责的,有时候只是偶尔看一下),就想分析一下dump文件,看看到底是怎么回事。于是乎,取来了crashreport捕捉到的dump文件,大概的分析了一把。下面就来详细讲述一下整个分析过程。


3、异常上下文及对应的C++代码
        这个崩溃发生在发送一个很长的聊天内容时。对于长的聊天内容,TrueLink会自动分成一个一个小段发送出去,结果在这个分段发送的过程中产生了崩溃。于是取到dump文件,拿来对应版本的pdb文件,用windbg打开dump文件,使用.excr命令切换到异常上下文,找到崩溃时执行的最后一条汇编指令,也就是崩溃时执行异常的汇编指令:
Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
        上图中的汇编指令,是将以eax寄存器中的值(0x00000000)作为内存地址的内存中的一个字节内容,读出来放到dl寄存器中(32位寄存器edx -> 16位寄存器dx  -> 8位高位寄存器dh和8位低位寄存器dl),即上述汇编指令访问了0x00000000H内存地址,这个地址属于64KB NULL指针内存区域(关于64KB NULL指针内存区域的具体说明,可以参看Windows核心编程的相关章节),是禁止访问的,即出现内存访问违例,系统会自动将进程强制结束掉。那为什么会出现访问0x00000000内存地址的呢?于是使用kn命令查看堆栈,点开堆栈中的第一帧,查看一下C++对象及函数局部变量内存中值:
Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
也看不出来变量的值有什么明显的异常,只是知道是崩溃在终端组件的libjingledll库的CLibJingleIns::OnSaveRoomTalks函数中了。因为我机器上有下载终端组件的代码,在windbg中配置了源码路径,所以单击堆栈的第一帧时,会自动打开堆栈第一帧所在的函数,显示崩溃在SetBodyText函数调用的地方:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
仅仅从C++代码,很难看出为什么会发生崩溃。表面上看,是崩溃在C++代码的某一行,其实是崩溃在某一条汇编指令上。而一句C++代码可能对应多条汇编指令,最终是崩溃在其中的一条汇编指令上。所以,有时往往看C++代码,不能直观的看出为什么会引起崩溃,但是看汇编代码则会一目了然。


4、在IDA中查看异常汇编指令的上下文
        从C++代码的上下文很难看出问题出在什么地方,于是尝试查看汇编代码的上下文,看看能不能找到线索,从而找到原因。可以直接在windbg中通过菜单,查看汇编代码的页面,如下所示:

Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
在上述页面中,可以根据执行异常的汇编指令的地址,到上述页面中搜索对应的位置:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
       上面的页面中只显示了部分汇编代码,也没有相关注释,再加上release下编译器做的优化处理,很难和我们的C++代码对应起来,很难看懂。于是想到,可以拿到libjingledll库对应pdb文件,使用IDA打开libjingledll库文件,因为有pdb文件,所以IDA中显示的汇编是有注解的,这样看起来就比较方便,通过上下文,很容易和C++代码对应起来。
        不过要找到异常崩溃的汇编指令在IDA反汇编出来的指令位置,需要做个计算,具体的做法是:拿到windbg中崩溃的那条汇编指令的地址,然后使用lm vm命令获取所在库libjingledll的起始地址:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
        然后计算出汇编指令相对起始地址的偏移:0x659f63d0– 0x659b0000 = 0x000063d0,然后到IDA中,使用这个偏移加上IDA中库的静态默认加载地址0x1000000:

Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)

得到在IDA中的指令地址位置:0x100063d0,然后在ida中跳到这个地址(Jump to address)就可以找到对应的汇编指令了,如下:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
这样依据汇编指令的上下文,结合windbg中异常所在的函数C++代码的上下文,从崩溃的这条汇编指令出发,阅读汇编指令上下文,对照代码的上下文,看看能不能定位出问题的地方。


5、在IDA中对照C++代码,分析异常汇编指令的上下文,定位问题

        异常所在的函数CLibJingleIns::OnSaveRoomTalks比较长,对应的汇编代码也比较长,整个函数的汇编代码看下来比较吃力,只能挑选异常汇编指令前后较近的汇编指令看一下,看看能否找到问题。
        将pdb文件放到libjingledll库文件所在的目录中,IDA打开libjingledll库时,能自动识别出来,这样可以将pdb中函数及变量符号作为注解,显示在IDA反汇编出来的汇编代码中,比如:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
对于call的函数,也有包含参数在内的详细的注解:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
这个参数很重要,也是本问题分析的突破口。
        根据崩溃时的堆栈中给出的行号:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)

找到C++代码如下:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)

最开始是想,既然是崩溃在下面的汇编指令上:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
那我就从异常指令的上面找找看,看看eax寄存器的值是什么时候赋值的,看看赋进来的是什么值(什么变量值),但是不幸的是,异常汇编指令上面的汇编指令比较繁多,再加上release下编译器做的优化,很难和原始C++代码对应起来,很难确定传递给eax寄存器是什么值,是何时设置的。
        既然是异常指令的向上的汇编代码不好看,那我们就从异常汇编指令向下看,看看能不能从下向上推导出来呢?对照C++代码:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
有调用AddElement和SetBodyText函数,在IDA汇编代码中能清楚的看到对着两个函数的call操作。再看SetBodyText函数的定义:
Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)

即参数类型是stl的string类,所以在调用之前是要构造string对象的,在IDA的汇编上下文中能看到,并且根据异常指令下面的若干条汇编指令能看出,异常指令中操作的eax就 是为构造string对象服务的:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)

结合注释中的参数类型:

Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
第一个参数为字符串的首地址,第二个参数应该是字符串中的字符个数,结合汇编指令call之前要将参数push进来,所以push eax时,eax中的内容就对应函数的第二个参数(从右向左将参数依次压栈),即eax中的值就是字符串的字符个数,汇编代码再向上看,看这个字符个数是如何计算出来的:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
根据汇编代码,执行到mov dl, [eax]是,eax应该是字符串的首地址,然后一个循环,取出字符串的每个字符看是否等于0,等于0则表示到达字符串的结尾了(\0是结尾符),循环结束后用eax减去this的值就是字符串的字符长度,这正好和下面的push传参相吻合。这样再从异常指令mov dl, [eax]向上看,就知道为什么要eax+1赋值给this变量了:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
之所以要给将eax+1赋值给this变量,因为下面的循环计算出来的个数是包含\0结尾符的,所以是为了执行sub eax, this得到的eax中字符串字符个数是去掉\0结尾符的。


6、再回到C++代码找出问题
        C++代码片如下:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
从上面汇编代码分析得知,异常指令mov dll, [eax]中的eax肯定是上面代码中的pMsgId指针值,根据崩溃时指令mov dll, [eax]中的eax值为0,所以可以确定,出现异常时传递给SetBodyText函数pMsgId值为NULL,所以要看看这个pMsgId值是从何处返回的。根据p->m_room找到:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
然后go到CHistoryRoom类的实现,看到其中重载了下标操作符:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
确实有返回NULL的分支。下面的分支返回的数组的地址,是不可能出现返回NULL的问题的,因为数组是预先定义好的,对象一初始化,数组就有内存了。肯定是上面的分支返回的NULL引起了本案例中的崩溃的了。肯定是业务代码出问题了,走到了返回NULL分支。


7、解决办法

       考虑到目前维护libjingle的人员之前没接触过libjingle代码,再加上libjingle代码本身也比较复杂,排查这个问题需要先熟悉libjingle内部的逻辑和业务,需要大量的时间和精力,所以可以考虑暂时使用规避的办法:判断获取的pMsgId是否为空,为空则continue,如下所示:
 Windbg定位异常系列 - 给被调用函数的stl string类型参数传递了空指针引发的崩溃(windbg结合IDA一起分析)
但是需要强调的是,规避不是最终的办法,最终还是找模块维护人员详细排查一下业务代码,看看为什么会出现获取的pMsgId为NULL的情况。


8、结束语

       Windbg是我们分析异常崩溃问题的一大利器,作为Windows开发人员,是很有必要去学习并使用的。本文结合软件维护过程中遇到的具体异常崩溃实例,详细讲解了使用windbg分析的过程,希望能给相关人员提供一个借鉴或参考。