将Windbg附加到软件进程上排查异常闪退的问题

目录

1、问题背景

2、将Windbg附加到进程上,发现软件发生异常时中断在DebugBreak接口上

3、根据Windbg中显示的函数调用堆栈,查看Webrtc库的开源代码,发现是new失败了 

4、malloc或new失败的可能原因分析

5、发生内存泄漏导致进程的用户态虚拟内存即将被用完,导致malloc失败

6、最后


       近日业务组件组的同事在联调webrtc集成硬编硬解库功能时,发现我们的C++软件会频繁出现闪退的问题,邀请我过去帮忙分析一下。我们将Windbg工具附加到软件进程上调试运行,最终找出了原因,今天就来分享这一问题的排查过程。

1、问题背景

       我们的软件在入会后会进行繁重的视频编解码操作,一方面要执行本端图像的采集与编码,另一方面要将会议中接收的多路视频码流进行解码显示。大家都知道,音视频编解码操作会消耗大量的CPU资源,目前我们的视频编解码库使用的都是CPU软解,没有使用到GPU硬解。

       在一些散热比较差的电脑上,比如微软Surface平板电脑上,入会后一直在进行视频编解码操作,会占用大量的CPU资源,会导致CPU超频,超频后机器会产生更多的热量,而Surface平板散热很差,导致热量不能及时散发出去,温度会持续升高,当温度达到上限时就会降频(这是CPU防止温度过高的自我保护策略),比如如下的截图:

图片[1]-将Windbg附加到软件进程上排查异常闪退的问题-卡核

当前CPU的基准主频是2.5GHz,实际主频已经降频降到了1.22GHz。 降频后,CPU的处理能力会严重下降,会导致CPU跑满,导致系统严重卡顿,导致整个系统都没法操作了。所以我们想使用GPU进行硬编硬解,减少对CPU的占用,以解决使用CPU软编软解导致的系统卡顿问题。

       我们当前使用的是开源的webrtc库,视频的编解码操作都是在库中完成的。webrtc移动版本是自带硬编硬解的编解码库的,在移动平台上默认情况下也是启用硬编硬解的。但webrtc Windows平台的版本是不支持硬编硬解的,需要开发者自己去开发硬编硬解的编解码库,以插件的方式集成到webrtc库中的。

       开源组件组研究了硬编硬解的编解码算法,提交到业务组件这边进行集成联调。运行软件,加入到会议中,结果一会之后软件就闪退了,软件的异常捕获模块CrashReport没捕获到异常,很是奇怪!这个问题是必现的!

2、将Windbg附加到进程上,发现软件发生异常时中断在DebugBreak接口上

       既然异常捕获模块在软件发生异常时没有捕获到异常,那我们可以尝试将Windbg附加到进程上进行调试运行,看看Windbg动态调试时能否感知到异常。

根据多年来排查C++软件异常的经验,有些异常,异常捕获模块是捕获不到的,所以也就不会生成包含异常上下文的dump文件的。此时可以尝试直接用Visaul Studio调试Release版本的代码,或者是将Windbg附加到目标进程上,看看Windbg能否感知到异常。

将Windbg附加到进程上的方式,基本都能捕获到异常捕获模块捕获不到的异常。当然,这个还要看bug是否好复现。如果问题不是必现的,需要去研究问题复现的规律,尽量找到问题的复现办法。但有些问题是没有规律的,是很难复现的。

       加入会议后跑一小会,Windbg就感知到了,中断了下来。于是使用kn命令查看此时的函数函数调用堆栈,发现是DebugBreak接口触发Windbg中断下来的,如下所示:

图片[2]-将Windbg附加到软件进程上排查异常闪退的问题-卡核

DebugBreak接口是Windows系统API接口,目的是为了让当前的调试器中断下来,DebugBreak的调用者一般是为了提示软件发生了问题。

       其实,DebugBreak接口内部的实现类似下面的代码:

#ifdef _X86_
#define DebugBreak()    _asm { int 3 }
#endif

内部类似一个INT 3软中断,目的是让调试器中断下来。

x86系列处理器从其第一代产品英特尔8086开始就提供了一条专门用来支持调试的指令,即INT 3。简单地说,这条指令的目的就是使CPU中断(break)到调试器,以供调试者对执行现场进行各种分析。当我们调试程序时,可以在可能有问题的地方插 入一条INT 3指令,使CPU执行到这一点时停下来。这便是软件调试中经常用到的断点(breakpoint)功能,因此INT 3指令又被称为断点指令。

3、根据Windbg中显示的函数调用堆栈,查看Webrtc库的开源代码,发现是malloc失败了 

       根据windbg中显示的函数调用堆栈,调用DebugBreak接口的代码位于webrtc开源库中。于是查看webrtc的开源代码,发现是执行malloc操作时失败了,webrtc框架中认为malloc失败是致命的问题,先是调用DebugBreak,然后紧接着调用abort将进程强制结束掉。这就对了,这就和软件的运行现象一致的。

       webrtc库中的相关代码如下所示:

图片[3]-将Windbg附加到软件进程上排查异常闪退的问题-卡核

上面调用malloc申请内存失败,返回NULL空指针,进入RTC_CHECK宏中:

图片[4]-将Windbg附加到软件进程上排查异常闪退的问题-卡核

       紧接着进入到rtc_FatalMessage函数中: 

图片[5]-将Windbg附加到软件进程上排查异常闪退的问题-卡核

 然后又进入FatalLog接口中:

图片[6]-将Windbg附加到软件进程上排查异常闪退的问题-卡核

图片[7]-将Windbg附加到软件进程上排查异常闪退的问题-卡核

最终在该接口中先是调用了DebugBreak接口,尝试让当前正在调试的调试器中断下来,然后紧接着调用abort接口将进程强行终止掉。 

4、malloc或new失败的可能原因分析

       那为啥会出现malloc或new操作失败的问题呢?之前我们总结过,一般new操作失败可能是有四种可能的原因。

1)申请的内存过大,进程中没有这么大内存可用了

       可能受一些异常数据的影响,申请了很大尺寸的内存。比如前段时间排查一个崩溃问题,当时因为数据有异常,一次性申请了9999*9999*4*2 = 762MB的堆内存,进程中没有这么大可用的堆内存了,所以申请失败了。

2)用户态的内存已经达到了上限,申请不到内存了       

       有可能是虚拟内存占用太多,也有可能代码中有内存泄露,导致用户态的虚拟内存被消耗完了。对于一个32程序,一个进程分配了4GB的虚拟地址空间,而用户态和内核态内存各占一半,即用户态的虚拟内存只有2GB,如果程序占用的虚拟内存比较大,比如接近2GB的用户虚拟内存了,再申请大的内存就会申请失败了。或者程序中有内存泄露,快要把用户态的2GB的虚拟内存给占用完了,再申请内存可能会申请失败的。

3)进程中的内存碎片过多    

       如果进程中在大量的new和delete,产生了大量的小块内存碎片,可用的内存被切割成一小块一小块的小内存块,如果要申请一块长度很长的内存,因为到处是内存碎片,没有这么一大块连续的可用内存,可能会导致内存申请失败的。

4)发生堆内存越界

       堆内存被破坏,导致new操作产生异常(此时new不会返回NULL,会抛出异常)。我们可以在出问题的地方,对该处的new添加一个保护(但不可能对代码中所有new的地方都加这样的保护),我们通过添加try…catch去捕获new抛出的异常,并将异常码打印出来,如下所示:(下面的代码在循环申请内存,直到内存申请失败为止,主要用来测试用)

​#include <iostream>
using namespace std;
 
int main(){
    char *p;
    int i = 0;
    try
    {
        do{
            p = new char[10*1024*1024];
            i++;
            
            Sleep(5);
        }
        while(p);
    }
    catch(const std::exception& e)
    {
        std::cout << e.what() << "\n"
                    << "分配了" << i*10 << "M" << std::endl;
 
    }
    
    return 0;   
}

       还有一种方式,在new时传如一个std::nothrow参数,让new在申请不到内存时不要抛出异常,直接返回为NULL,这样我们就可以通过返回的地址是否为NULL(空),判断是否是内存申请失败了,示例代码如下:

#include <iostream>
 
int main(){
    char *p = NULL;
    int i = 0;
    do{
        p = new(std::nothrow) char[10*1024*1024]; // 每次申请10MB
        i++;
        
        Sleep(5);
    }
    while(p);
 
    if(NULL == p){
        std::cout << "分配了 " << (i-1)*10 << " M内存"         //分配了 1890 Mn内存第 1891 次内存分配失败           
                  << "第 " << i << " 次内存分配失败";
 
    }
    return 0;
}

5、发生内存泄漏导致进程的用户态虚拟内存即将被用完,导致malloc失败

       对于本问题,我们初步怀疑,可能是第二种情况引起的,即用户态的内存已经用完了。我们程序是32位的,所以用户态的虚拟内存只有2GB,如果程序的虚拟内存已经接近或者达到2GB,再去malloc申请内存可能就会失败。于是再将windbg附加上去,入会后一会windbg就中断了下来,此时去查看我们软件进程占用的内存大小。正好Windbg中断时会将进程挂起,我们正好有机会使用工具其查看进程的虚拟内存占用情况。在崩溃的情况,都没有机会去查看进程占用的内存,程序直接闪退了。

       Windows任务管理器中是看不到进程总的虚拟内存占用的,只能看到其他类型的内存占用,必须要使用Process Explorer工具去查看。

       又轮到Process Explorer上场了,Process Explorer工具默认是不显示虚拟内存占用大小的,需要在列表栏中右键点击,在弹出的右键菜单中点击“Select Columns”菜单项,在打开的窗口中点击“Process Memory”标签页,在页面中勾选“Virtual Size”选项:

图片[8]-将Windbg附加到软件进程上排查异常闪退的问题-卡核

该选项就是进程的虚拟内存了。勾选后,在Process Explorer的主窗口的列表中就能看到虚拟内存列了:

图片[9]-将Windbg附加到软件进程上排查异常闪退的问题-卡核

查看到我们的进程确实快达到2GB了,所以出现malloc失败了。

       后来经webrtc开源组同事的确认,他们添加的代码中有内存泄漏,内存泄漏导致用户态的虚拟内存逐步被耗尽,导致接近2GB的情况,导致malloc内存失败的。

6、最后

       webrtc库在malloc申请内存失败后,先是调用了DebugBreak接口,然后是调用abort强制将进程终止了。所以在没挂windbg时就直接闪退了,是主动终止,不是异常崩溃,所以异常捕获模块CrashReport没有捕获到异常,没有生成相应的dump文件。

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

昵称

取消
昵称表情代码图片

    暂无评论内容