两例典型的C++软件异常排查实例分享

时间:2022-10-24 13:17:25

目录

1、动态申请内存时抛出了bad_alloc异常,导致程序闪退

1.1、问题分析

1.2、动态申请内存失败可能原因分析

2、注入到进程中的输入法模块发生异常,导致进程崩溃

2.1、问题分析

2.2、第三方软件注入后引发软件异常的案例说明

3、最后


C++软件异常排查从入门到精通系列汇总两例典型的C++软件异常排查实例分享https://blog.csdn.net/chenlycly/article/details/125529931       前段时间的某一天接连遇到两个典型的C++软件异常崩溃问题,很有代表性,今天把这两个问题及排查过程做个详细的分享,以供参考。

1、动态申请内存时抛出了bad_alloc异常,导致程序闪退

1.1、问题分析

       软件在执行某个比较消耗CPU和内存资源的操作时,发生了闪退崩溃,软件的异常捕获模块捕获到异常,并生成了dump文件。取来dump文件并用windbg打开,输入.ecxr命令切换到异常上下文,输入kn命令查看函数调用堆栈,如下:

两例典型的C++软件异常排查实例分享

从上图可以看出,代码抛出了异常,沿着函数调用堆栈向下看,异常应该发生在mediaxxx.dll中

       于是使用lm vm mediaxxx*命令查看mediaxxx.dll的时间戳:

两例典型的C++软件异常排查实例分享

找来对应时间点的pdb文件,将pdb文件的路径设置到windbg中。然后再次输入.ecxr命令切换到异常上下文,输入kn命令查看详细的函数调用堆栈,如下:

两例典型的C++软件异常排查实例分享

从调用堆栈可以看出,抛出了__scrt_throw_std_bad_alloc异常应该是C++运行时库抛出的标准bad_alloc动态申请内存失败的异常。

       继续沿着函数调用堆栈向下看,应该是mediaxxx!CDShowDraw::Create函数中的594行触发的,于是到对应cpp文件中查看该函数的594行代码,如下:

两例典型的C++软件异常排查实例分享

该行代码中对应的宏的定义为:

两例典型的C++软件异常排查实例分享

这行代码使用new去申请一段堆内存,其申请的堆内存的大小为:3840*2160*2/1024/1024 = 15.8MB,按讲申请15.8MB的堆内存倒不算很大,为啥会出现申请失败的问题。可能是进程的虚拟内存即将达到2GB的上限(32位程序系统会分配4GB的虚拟内存,其中用户态内存大小为2GB),再申请内存可能就申请不到了。那为啥进程的用户态的内存会接近2GB的上限,可能是因为代码中有内存泄漏导致的。

1.2、动态申请内存失败可能原因分析

       下面我们就来讨论一下什么情况会导致动态申请内存失败。动态申请堆内存失败可能是以下几个原因引起的:

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

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

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

      有可能是虚拟内存占用太多,也有可能代码中有内存泄露,导致用户态的内存快被消耗完了。对于一个32程序,系统会给对应的进程分配4GB的虚拟地址空间,而用户态和内核态内存各占一半,即用户态的内存只有2GB,如果程序占用的虚拟内存比较大,比如接近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;
}

       使用C语言中的malloc函数去申请堆内存是不会抛出异常的,申请失败时会返回NULL。对于代码中出现申请堆内存的失败的问题,一般的做法是终止进程的运行,很多开源库中就是这么处理的,比如我们在WebRTC开源库中就看到过。因为内存申请失败,正常的代码逻辑和业务也就没法正常继续下去了,让程序继续运行可能就没多大意义了。

2、注入到进程中的输入法模块发生异常,导致进程崩溃

2.1、问题分析

       有用户反馈其在执行某个操作时软件出现了闪退崩溃,其将崩溃信息文件发给了我们。打开后看到一个log文件和一个dump文件,打开log文件看到了异常概要信息,但是正要打开dump文件去查看详细的异常上下文信息,结果发现dump文件居然是空的

两例典型的C++软件异常排查实例分享

       估计可能是生成dump文件时产生了二次崩溃,导致dump文件中的内容是空的。这样只能到log文件中找线索了。打开log文件,看到了发生异常时的异常上下文信息及函数调用堆栈,如下所示:

两例典型的C++软件异常排查实例分享

两例典型的C++软件异常排查实例分享

上图显示异常发生在SogouTSF.ime模块中,这是搜狗输入法的模块文件,为啥会出现在我们的软件中呢?搜狗输入法会注入到每个带UI文字输入的软件中,以实现文字的输入。

       这个SogouTSF.ime文件应该是搜狗输入法的注入模块,直接注入到我们的软件进程中(进驻到软件的进程空间中)。此处正是SogouTSF.ime注入模块产生了异常,因为其注入到了我们的进程空间了,所以导致我们软件进程的崩溃。因为没有有效的dump文件,没法做深入的分析,所以此处初步判定应该是搜狗输入法注入模块的问题。

2.2、第三方软件注入后引发软件异常的案例说明

       因为搜索输入法的注入引发的问题,我们之前遇到过几次。除了输入法注入,我们还遇到过第三方安全软件注入到我们软件中引发异常的案例。其中,一个案例是注入后引发了软件进程的内存泄漏,另一个案例是注入后导致我们的某处代码调用socket套接字API函数recvfrom产生了崩溃。在这两个案例中,问题都出在第三方软件的注入模块中。

3、最后

       上面简单地讲述了两个具有一定代表性的C++软件异常,一个是申请堆内存失败时抛出异常,一个是第三方软件注入引发的异常,都具有一定的参考价值。希望大家在后面在遇到类似的问题时,本文能提供一些分析思路与参考。