使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃

目录

1、引言

2、问题描述

3、异常上下文及对应的C++代码

4、在IDA中查看异常汇编指令的上下文

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

6、再回到C++代码找出问题

7、解决办法

8、结束语


1、引言

        Windbg是在Windows平台下,强大的用户态和内核态调试工具,给我们分析Windows平台软件的异常提供了极大的便利和强有力的支持,比原始的直接查看代码分析异常的效率要来的高的多。Windbg支持动态调试,也支持对dump文件的静态分析。我们日常使用的主要是Windbg的静态分析功能,通过对软件异常时crashrpt抓取到的dump文件的分析,定位异常发生的原因和位置,从而提高我们的软件质量和稳定性。

       除了Windbg之外,我们可能还需要使用到反汇编工具IDA,通过IDA,我们可以查看到目标文件的汇编代码。通过windbg可以确定出我们的C++代码崩溃在哪一句上,但是要搞清楚崩溃的具体诱因,还是要从汇编代码入手,从上下文中找出具体原因和异常触发的位置。本文就最近遇到的两个典型的异常崩溃实例,详细介绍了使用Windbg及IDA分析问题和解决问题的过程。另外,本文主要讲解dump的静态分析,至于将Windbg动态挂载到目标进程中去动态调试的内容,不是本文讲解的范畴,如需了解,可查阅本专栏中相关的文章。

2、问题描述

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

3、异常上下文及对应的C++代码

       这个崩溃发生在发送一个很长的聊天内容时。对于长的聊天内容,我们的会自动分成一个一个小段发送出去,结果在这个分段发送的过程中产生了崩溃。于是取到dump文件,拿来对应版本的pdb文件,用windbg打开dump文件,使用.excr命令切换到异常上下文,找到崩溃时执行的最后一条汇编指令,也就是崩溃时执行异常的汇编指令:

图片[1]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核

      上图中的汇编指令,是将以eax寄存器中的值(0x00000000)作为内存地址的内存中的一个字节内容,读出来放到dl寄存器中(32位寄存器edx -> 16位寄存器dx  -> 8位高位寄存器dh和8位低位寄存器dl),即上述汇编指令访问了0x00000000H内存地址,这个地址属于64KB NULL指针内存区域(关于64KB NULL指针内存区域的具体说明,可以参看Windows核心编程的相关章节),是禁止访问的,即出现内存访问违例,系统会自动将进程强制结束掉。那为什么会出现访问0x00000000内存地址的呢?于是使用kn命令查看堆栈,点开堆栈中的第一帧,查看一下C++对象及函数局部变量内存中值:

图片[2]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核

也看不出来变量的值有什么明显的异常,只是知道是崩溃在终端组件的libjingledll库的CLibJingleIns::OnSaveRoomTalks函数中了。因为我机器上有下载终端组件的代码,在windbg中配置了源码路径,所以单击堆栈的第一帧时,会自动打开堆栈第一帧所在的函数,显示崩溃在SetBodyText函数调用的地方:
图片[3]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核

仅仅从C++代码,很难看出为什么会发生崩溃。表面上看,是崩溃在C++代码的某一行,其实是崩溃在某一条汇编指令上。而一句C++代码可能对应多条汇编指令,最终是崩溃在其中的一条汇编指令上。所以,有时往往看C++代码,不能直观的看出为什么会引起崩溃,但是看汇编代码则会一目了然。

4、在IDA中查看异常汇编指令的上下文

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

图片[4]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核
在上述页面中,可以根据执行异常的汇编指令的地址,到上述页面中搜索对应的位置:
 图片[5]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核
       上面的页面中只显示了部分汇编代码,也没有相关注释,再加上release下编译器做的优化处理,很难和我们的C++代码对应起来,很难看懂。于是想到,可以拿到libjingledll库对应pdb文件,使用IDA打开libjingledll库文件,因为有pdb文件,所以IDA中显示的汇编是有注解的,这样看起来就比较方便,通过上下文,很容易和C++代码对应起来。

       不过要找到异常崩溃的汇编指令在IDA反汇编出来的指令位置,需要做个计算,具体的做法是:拿到windbg中崩溃的那条汇编指令的地址,然后使用lm vm命令获取所在库libjingledll的起始地址:

图片[6]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核

       然后计算出汇编指令相对起始地址的偏移:0x659f63d0– 0x659b0000 = 0x000063d0,然后到IDA中,使用这个偏移加上IDA中库的静态默认加载地址0x1000000:

图片[7]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核

得到在IDA中的指令地址位置:0x100063d0,然后在ida中跳到这个地址(Jump to address)就可以找到对应的汇编指令了,如下:
 

图片[8]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核

 

图片[9]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核

这样依据汇编指令的上下文,结合windbg中异常所在的函数C++代码的上下文,从崩溃的这条汇编指令出发,阅读汇编指令上下文,对照代码的上下文,看看能不能定位出问题的地方。

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

        异常所在的函数CLibJingleIns::OnSaveRoomTalks比较长,对应的汇编代码也比较长,整个函数的汇编代码看下来比较吃力,只能挑选异常汇编指令前后较近的汇编指令看一下,看看能否找到问题。

       将pdb文件放到libjingledll库文件所在的目录中,IDA打开libjingledll库时,能自动识别出来,这样可以将pdb中函数及变量符号作为注解,显示在IDA反汇编出来的汇编代码中,比如:
图片[10]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核

对于call的函数,也有包含参数在内的详细的注解:
图片[11]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核
这个参数很重要,也是本问题分析的突破口。

       根据崩溃时的堆栈中给出的行号: 图片[12]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核
图片[13]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核

找到C++代码如下:

void  CLibJinglePro::OnSaveRoomTalks()
{
    char buf[MAX_BUFF_SIZE];

    // 如果保存路径没有设置
    if ( 0 == g_cJingleData.m_szConfigPath[0] )
    {
        return;
    }

    // 如果client为0
    if ( 0 == g_cJingleData.m_pXmppApp->pump()->client() )
    {
        return;
    }

    if ( g_cJingleData.m_chatroom_history )
    {
        delete  g_cJingleData.m_chatroom_history;
        g_cJingleData.m_chatroom_history = 0;
    }

    g_cJingleData.m_chatroom_history = new buzz::XmlElement(buzz::QN_CHATROOM_HISTORY, true);
    SNPRINTF( buf, MAX_BUFF_SIZE, "%lu", g_cJingleData.m_dwChatroomHistoryVersion );
    g_cJingleData.m_chatroom_history->SetAttr( buzz::QN_VERSION, buf );

    std::vector< CRoomReadMsgs *>::iterator it;
    for ( it = g_cJingleData.m_vRoomReadMsgs.begin(); it != g_cJingleData.m_vRoomReadMsgs.end(); it++ )
    {
        CRoomReadMsgs * p = *it;

        // 查看是否有对应固定群
        vPersistentRoom::iterator it_p = find_if( g_cJingleData.m_vPersistentRooms.begin(), g_cJingleData.m_vPersistentRooms.end(),
            FindPersistentRoom( p->m_szRoomId, p->m_szGroupChatServiceName ) );
        // 如果不存在持久性房间
        if ( it_p == g_cJingleData.m_vPersistentRooms.end() )
        {
            continue;
        }
        // END


        buzz::XmlElement * groupchat = new buzz::XmlElement( buzz::QN_GROUPCHAT, true );
        g_cJingleData.m_chatroom_history->AddElement( groupchat );

        groupchat->SetAttr( buzz::QN_ID, p->m_szRoomId );
        groupchat->SetAttr( buzz::QN_CHATROOM_SERVICE, p->m_szGroupChatServiceName );


        unsigned long i;
        unsigned long cnt = p->m_room.GetCount();

        unsigned long start = cnt > MAX_MUC_HISTORY_STANZAS ? (cnt - MAX_MUC_HISTORY_STANZAS) : 0;

        for ( i = start; i < cnt; i++ )
        {
            char * pMsgId = p->m_room[i];
            assert( pMsgId );

            buzz::XmlElement * item = new buzz::XmlElement( buzz::QN_ITEM, true );
            groupchat->AddElement( item );

            item->SetBodyText( pMsgId );
        }
    }

    std::string  sPath = g_cJingleData.m_szConfigPath;
    std::string  sDirSlash;
    char         chSlash;

#ifdef WIN32
    sDirSlash = "\\";
    chSlash   = '\\';
#else
    sDirSlash = "/";
    chSlash   = '/';
#endif

    if ( sPath.length() > 0 && sPath.at(sPath.length()-1) != chSlash  )
    {
        sPath += sDirSlash;
    }

    
    sPath += g_cJingleData.m_pXmppApp->pump()->client()->jid().BareJid().Str();
    sPath += ".chatroom_history.xml";

    WriteXmlFile( g_cJingleData.m_chatroom_history, sPath.c_str() );

    delete g_cJingleData.m_chatroom_history;
    g_cJingleData.m_chatroom_history = 0;
}

最开始是想,既然是崩溃在下面的汇编指令上:

图片[14]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核

那我就从异常指令的上面找找看,看看eax寄存器的值是什么时候赋值的,看看赋进来的是什么值(什么变量值),但是不幸的是,异常汇编指令上面的汇编指令比较繁多,再加上release下编译器做的优化,很难和原始C++代码对应起来,很难确定传递给eax寄存器是什么值,是何时设置的。

       既然是异常指令的向上的汇编代码不好看,那我们就从异常汇编指令向下看,看看能不能从下向上推导出来呢?对照C++代码:有调用AddElement和SetBodyText函数,在IDA汇编代码中能清楚的看到对着两个函数的call操作。再看SetBodyText函数的定义:

void XmlElement::SetBodyText(const std::string & text) {
  if (text == STR_EMPTY) {
    ClearChildren();
  } else if (pFirstChild_ == NULL) {
    AddText(text);
  } else if (pFirstChild_->IsText() && pLastChild_ == pFirstChild_) {
    pFirstChild_->AsText()->SetText(text);
  } else {
    ClearChildren();
    AddText(text);
  }
}

即参数类型是stl的string类,所以在调用之前是要构造string对象的,在IDA的汇编上下文中能看到,并且根据异常指令下面的若干条汇编指令能看出,异常指令中操作的eax就 是为构造string对象服务的:

图片[15]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核

结合注释中的参数类型:

图片[16]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核

第一个参数为字符串的首地址,第二个参数应该是字符串中的字符个数,结合汇编指令call之前要将参数push进来,所以push eax时,eax中的内容就对应函数的第二个参数(从右向左将参数依次压栈),即eax中的值就是字符串的字符个数,汇编代码再向上看,看这个字符个数是如何计算出来的:

图片[17]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核

根据汇编代码,执行到mov dl, [eax]是,eax应该是字符串的首地址,然后一个循环,取出字符串的每个字符看是否等于0,等于0则表示到达字符串的结尾了(\0是结尾符),循环结束后用eax减去this的值就是字符串的字符长度,这正好和下面的push传参相吻合。这样再从异常指令mov dl, [eax]向上看,就知道为什么要eax+1赋值给this变量了:

图片[18]-使用Windbg和IDA分析给被调用函数的std::string类型参数传递了空指针引发的崩溃-卡核

之所以要给将eax+1赋值给this变量,因为下面的循环计算出来的个数是包含\0结尾符的,所以是为了执行sub eax, this得到的eax中字符串字符个数是去掉\0结尾符的。

6、再回到C++代码找出问题

        C++代码片如下:

        unsigned long start = cnt > MAX_MUC_HISTORY_STANZAS ? (cnt - MAX_MUC_HISTORY_STANZAS) : 0;

        for ( i = start; i < cnt; i++ )
        {
            char * pMsgId = p->m_room[i];
            assert( pMsgId );

            buzz::XmlElement * item = new buzz::XmlElement( buzz::QN_ITEM, true );
            groupchat->AddElement( item );

            item->SetBodyText( pMsgId );
        }

从上面汇编代码分析得知,异常指令mov dll, [eax]中的eax肯定是上面代码中的pMsgId指针值,根据崩溃时指令mov dll, [eax]中的eax值为0,所以可以确定,出现异常时传递给SetBodyText函数pMsgId值为NULL,所以要看看这个pMsgId值是从何处返回的。根据p->m_room找到:

class CRoomReadMsgs
{
public:
    CRoomReadMsgs( const char * szRoomId, const char * szService )
    {
        strcpy( m_szRoomId, szRoomId );
        strcpy( m_szGroupChatServiceName, szService );
    }

    ~CRoomReadMsgs()
    {

    }

public:
    UniqueRoomId                       m_szRoomId;                                              // 房间id
    char                               m_szGroupChatServiceName[MAX_GROUPCHAT_SERVICE_NAME_LEN];// 聊天服务(conference.kedacom.com) 暂时用在OnInvite的情况
    CHistoryRoom                       m_room;
};

然后go到CHistoryRoom类的实现,看到其中重载了下标操作符:

    char * operator[] ( unsigned long dwIndex )
    {
        unsigned long dwCnt = GetCount();
        if ( dwIndex >= dwCnt )
        {
            return 0;
        }

        dwIndex += m_dwBeginPos;
        if ( dwIndex > MAX_MUC_HISTORY_STANZAS * 2 )
        {
            dwIndex -= (MAX_MUC_HISTORY_STANZAS * 2 + 1);
        }

        return m_aszMessageIds[dwIndex];
    }

确实有返回NULL的分支。下面的分支返回的数组的地址,是不可能出现返回NULL的问题的,因为数组是预先定义好的,对象一初始化,数组就有内存了。肯定是上面的分支返回的NULL引起了本案例中的崩溃的了。肯定是业务代码出问题了,走到了返回NULL分支。

7、解决办法

       考虑到目前维护libjingle的人员之前没接触过libjingle代码,再加上libjingle代码本身也比较复杂,排查这个问题需要先熟悉libjingle内部的逻辑和业务,需要大量的时间和精力,所以可以考虑暂时使用规避的办法:判断获取的pMsgId是否为空,为空则continue,如下所示:

        unsigned long start = cnt > MAX_MUC_HISTORY_STANZAS ? (cnt - MAX_MUC_HISTORY_STANZAS) : 0;

        for ( i = start; i < cnt; i++ )
        {
            char * pMsgId = p->m_room[i];
            assert( pMsgId );
			// 这个地方以前出现过崩溃,已经添加空指针保护,所以release下不需要assert了
			// 按照assert宏定义,按讲release下是不会生效的,为啥release版本的libjingle还会包assert断言错误呢?
			// 查看libjingle工程属性得知,release下没有添加NDEBUG宏,加上就好了
            if (NULL == pMsgId)
            {
                continue;
            }

            buzz::XmlElement * item = new buzz::XmlElement( buzz::QN_ITEM, true );
            groupchat->AddElement( item );

            item->SetBodyText( pMsgId );
        }

但是需要强调的是,规避不是最终的办法,最终还是找模块维护人员详细排查一下业务代码,看看为什么会出现获取的pMsgId为NULL的情况。

8、结束语

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

© 版权声明
THE END
喜欢就支持一下吧
点赞1.2W+赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容