C++软件异常分析概述

目录

1、软件异常的分类

2、使用常用分析工具分析软件问题

3、使用Windbg分析软件异常

4、除Windbg之外的常用异常排查方法

5、开源的异常捕获库CrashRpt

6、最后


VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_11931267.html       最近几年工作中很大一部分内容是排查软件运行过程中遇到的各种异常,无论是底层的网络模块、协议模块和组件模块,还是上层的UI模块,都处理过多次,见识了各式各样的C++异常或崩溃,积累了大量的实战经验,在此给大家做个分享。本文将详细讲述Windows系统中C++软件异常的分类以及常用的排查方法,给大家提供一个借鉴与参考。

考察一个软件开发人员的水平,一是看其编码与架构设计能力,二是看其软件调试能力!软件调试能力很重要,也很关键!

C++软件异常分析概述_第1张图片

1、软件异常的分类

       引发软件异常常见原因有变量未初始化内存越界内存访问违例stack overflow线程栈溢出空指针与野指针死循环多线程死锁、多线程未加锁内存泄露堆内存被破坏、程序抛出异常、GDI对象泄露、函数调用约定不一致导致的栈不平衡、调用abort强行退出进程、格式化符与参数不匹配、库与库之间版本不匹配、第三方库注入等。

C++软件异常分析概述_第2张图片

 引发C++软件异常的常见原因详细分析,可以参见下面的文章:

引发C++软件异常的常见原因分析与总结(实战经验分享)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124996473       有的异常会导致软件崩溃。有的异常会立即导致软件崩溃,比如内存访问违例;有的异常则在运行一段时间或者长时间运行后才会导致崩溃,比如内存泄露和GDI对象泄露。

       有的异常并不会导致崩溃,只会导致软件发生堵塞或卡死,比如死循环和死锁。

       还有一类问题会导致业务代码执行上的异常,这类问题不会导致软件崩溃,会导致业务代码没有按照正常逻辑或者分支被执行,导致业务逻辑出异常。比如函数抛出异常导致部分代码被跳过,即该执行的代码没有执行到,导致后续业务代码执行时出现逻辑异常。正常情况下,这些被跳过的代码会执行一些判断,会设置一些变量的值,会直接影响后续代码的判断与执行逻辑,所以会导致后续代码在运行时出现业务逻辑上的异常。这样的问题我们以前遇到过几次。

      再比如系统API函数的lasterror被覆盖,导致后续判断lasterror值的条件判断出现逻辑上的错误,即产生了误判。

这个问题我们以前也遇到过,在libjingle库的开源代码中添加了一个打印,因为开源代码的接口封装的层次比较深,导致我们在添加日志打印的代码时没看出会把lasterror值覆盖的问题。具体的是,添加的那句打印日志的代码中调用系统API函数,该行打印日志的代码执行完后就会把其前一句的开源库代码产生的lasterror值给覆盖了。而在打印日志下面的下一句开源代码中的接口内部会判断上一句开源代码执行后的lasterror值,因为该lasterror值被覆盖了,所以导致对lasterror的判断条件出现误判,导致后续的业务代码出现了逻辑上的异常。

       对于GDI对象泄露,是使用GDI对象去绘制窗口导致的,绘制操作执行完后没有去释放GDI对象。GDI对象包括Pen画笔、Brush画刷、BItmap位图、Font字体、DC设备上下文、Region区域等。程序中如果有GDI对象泄露,不会立即导致异常或崩溃,当程序进程的GDI对象总量达到10000个左右时就会出现异常,出现闪退崩溃。

在Windows系统中,单个进程的默认的GDI对象总数上限是10000个,当进程的GDI对象快要接近10000个时,就会出现GDI函数绘制异常,接着就会产生崩溃。

       其实对于GDI对象泄露的排查,相对于内存泄露,要简单的多,只需要使用GDIView软件工具找出发生泄露的是哪种GDI对象,结合代码可能很快就查出来了 。GDIView工具的界面如下所示:

C++软件异常分析概述_第3张图片

       排查C++软件异常的常用方法和手段,可以参见我之前的文章:

排查C++软件异常的常见思路与方法(实战经验总结)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/120629327

2、使用常用分析工具分析软件问题

       常用的C++软件辅助分析工具有PE工具、SPY++、Dependency Walker、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro等,使用这些工具可以高效地分析一些软件问题。

C++软件异常分析概述_第4张图片

        比如使用下面这些工具可以分析以下一些问题:

1)使用PE工具可以查看二进制文件的生成时间;

2)使用Dependency Walker工具,可以查看库与库的依赖关系,去排查程序启动失败或者启动报错问题;

3)使用Process Explorer工具,可以查看程序加载的库的详细信息,可以查看目标进程虚拟内存占用和GPU占用情况,可以去分析程序的高CPU占用问题;

4)使用Process Monitor工具,可以监测目标进程的文件操作活动和注册表操作活动;

5)使用API Monitor工具,可以监测目标进程对系统API函数以及对第三方库接口的调用情况;

6)使用Clumsy工具,可以模拟网络状况差或者网络堵塞的情况,去复现网络不好情况下软件出现的问题;

7)使用Windbg工具,可以去静态分析dump文件,也可以去动态调试目标进程。Windbg是分析C++软件异常或崩溃的最重要的工具;

8)使用IDA工具,可以查看二进制文件的汇编代码,去辅助分析软件异常问题。

关于这些软件工具的详细说明,可以查看如下文章:

C++软件开发值得推荐的十大高效软件分析工具icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/127608247

3、使用Windbg分析软件异常

C++软件异常分析概述_第5张图片

       Windbg是Windows平台一个最强大、最通用的软件调试分析工具,Windows平台主要使用它来分析各种软件异常。Windbg分析软件异常问题,主要有两种方式,一种是使用Windbg静态分析包含异常上下文的dump文件,一种是将Windbg附加到目标进程上进行动态调试

C++软件异常分析概述_第6张图片

       大部分异常崩溃问题,软件中的异常捕获模块(很多都在使用开源的CrashRpt异常捕获库)都能捕获到,并将发生异常时的上下文保存到dump文件中,事后可以使用Windbg去静态分析这些dump文件。

       对于有些不会导致软件崩溃的异常,比如死锁、死循环和内存泄露,需要我们可以将Windbg挂载到目标进程上进行动态分析。

       对于异常捕获模块捕获不到的少数异常,比如程序在运行过程中发生闪退(异常捕获模块没法捕获到100%的运行异常),这时就需要将Windbg附加到目标进程上跑,即Windbg和其附加的目标进程绑定在一起运行,一旦目标进程出现异常,Windbg就能立即感知到并中断下来。

将Windbg附加到目标进程上后,我们需要想办法去复现异常,异常产生后Windbg会捕获到并中断下来,此时可以直接去使用Windbg命令去分析了,也可以将使用.dump命令将异常上下文导出到dump文件中供事后分析。可能分析问题会比较耗时,可能出问题的电脑是同事的或者是客户的,不能一直占着别人的电脑,此时可以选择导出dump文件供事后分析。

       对于一些弹框报错或者软件发生卡死的异常,软件一直卡在这个点上(目标进程还在),此时可以直接将Windbg挂上去,这个时间点已经出现了异常,但挂载Windbg的时机也不晚,也是可以获取到异常的上下文信息的。对于这类问题,不要点击报错的确定按钮,或者不要急于通过资源管理器去强杀目标进程,将目标进程保留住,此时将Windbg挂上去正是时候,也能获取到完整的异常上下文信息。这些异常可能是事后很难复现的,必须要逮住这个机会,将Windbg直接挂载到出问题的进程上进行分析。错过这个机会,下次可能就很难复现了,这样软件中就留下了很大的隐患。

       使用Windbg静态分析dump文件的一般步骤:

使用Windbg静态分析dump文件的一般步骤及要点详解icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/130873143      使用Windbg动态调试目标进程的一般步骤:

使用Windbg动态调试目标进程的一般步骤及要点详解icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131029795      何时使用Windbg进行静态分析?何时使用Windbg进行动态调试?:

何时使用Windbg静态分析?何时使用Windbg动态调试?icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131806819

4、除Windbg之外的常用异常排查方法

       除了Windbg的静态分析与动态调试之外,还有一些其他的常用方法,这些方法也很重要,也需要掌握,比如使用VS直接调试(Debug或Release调试)、附加到进程中调试添加打印日志历史版本比对法(找出开始出问题的那个时间点)、分块注释代码设置数据断点(对内存进行实时监控)等。有时,我们需要将多种方法结合起来使用。对于排查异常方法的详细介绍,可以参见如下的文章:

排查C++软件异常的常见思路与方法(实战经验总结)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/120629327       对于业务上的一些逻辑异常,一般需要通过添加日志打印来排查。还有一种典型的异常是软件运行过程中遇到了错误,软件自己判断出来了,并认为是致命性错误,会直接调用abort或者exit强行终止进程的。比如在开源的jsoncpp库中,在解析到异常的json节点时出错,会直接调用abort强行终止整个进程。再比如在开源的WebRTC库中,在用new申请堆内存失败时,WebRTC库内部会认为发生了致命性的错误,也会调用abort强行终止进程。这类主动强行终止进程的,异常捕获模块是捕获不到的异常,因此是不会生成dump文件的。

       这类情况可以将Windbg挂载到目标进程上跑,一旦调用abort接口,Windbg就会中断下来,此时查看函数的调用堆栈就能看到是何处代码触发的问题了。为啥Windbg能感知到,软件中安装的异常捕获模块捕获不到呢?因为软件本身并没有RaiseException,是软件主动终止进程的。那为啥Windbg能感知到呢?因为abort接口中会产生一个SIGABRT终止信号通知,这个调试器能感知到,于是Windbg就产生了中断。此时使用kn命令查看函数调用堆栈,就可以看到是哪些接口触发的问题了。可以直接在Visual Studio中go到abort函数实现处,查看abort函数的内部实现了,如下:

/***
*void abort() - abort the current program by raising SIGABRT
*
*Purpose:
*   print out an abort message and raise the SIGABRT signal.  If the user
*   hasn't defined an abort handler routine, terminate the program
*   with exit status of 3 without cleaning up.
*
*   Multi-thread version does not raise SIGABRT -- this isn't supported
*   under multi-thread.
*
*Entry:
*   None.
*
*Exit:
*   Does not return.
*
*Uses:
*
*Exceptions:
*
*******************************************************************************/

void __cdecl abort (
        void
        )
{
    _PHNDLR sigabrt_act = SIG_DFL;

#ifdef _DEBUG
    if (__abort_behavior & _WRITE_ABORT_MSG)
    {
        /* write the abort message */
        _NMSG_WRITE(_RT_ABORT);
    }
#endif  /* _DEBUG */


    /* Check if the user installed a handler for SIGABRT.
     * We need to read the user handler atomically in the case
     * another thread is aborting while we change the signal
     * handler.
     */
    sigabrt_act = __get_sigabrt();
    if (sigabrt_act != SIG_DFL)
    {
        raise(SIGABRT);
    }

    /* If there is no user handler for SIGABRT or if the user
     * handler returns, then exit from the program anyway
     */

    if (__abort_behavior & _CALL_REPORTFAULT)
    {
        _call_reportfault(_CRT_DEBUGGER_ABORT, STATUS_FATAL_APP_EXIT, EXCEPTION_NONCONTINUABLE);
    }


    /* If we don't want to call ReportFault, then we call _exit(3), which is the
     * same as invoking the default handler for SIGABRT
     */


    _exit(3);
}

从代码中可以看出,产生了一个SIGABRT终止信号通知。 

       有的模块在检测到异常时,还会调用DebugBreak API函数,会让调试器中断下来。关于DebugBreak函数的说明如下:

C++软件异常分析概述_第7张图片

比如有一次在排查开源WebRTC库中申请内存失败导致闪退问题时,WebRTC库内部认为内存申请失败是致命错误,会调用abort函数强行终止进程。在调用abort函数之前,会先调用DebugBreak函数,如果当前Windbg正挂在问题进程上,DebugBreak函数的调用会让Windbg中断下来,这样就能让调试人员感知到问题了,就可以直接在Windbg中直接查看此时的函数调用堆栈了,就能确定是什么操作触发的问题了。 

5、开源的异常捕获库CrashRpt

C++软件异常分析概述_第8张图片

       很多厂商都在使用开源的CrashRpt异常捕获库,但原生的CrashRpt库是有缺陷的。很多大厂使用的应该是经过深度改进的CrashRpt库。当然也可以使用Chrome浏览器开源代码中的Breakpad、开源的qBreakpad和最新的Crashpad等库。

     开源的CrashRpt异常捕获库是动态地将已加载的库的导入表中创建线程的API函数CreateThread HOOK成我们自定义的MyCreateThread函数(不管调用哪个创建线程的接口,最终都会走到CreateThread接口中的),这样就能在MyCreateThread中调用系统API函数SetUnhandledExceptionFilter给每个创建的线程挂载异常处理函数了。

      但这种机制是有缺陷的,不能给软件的所有模块的线程挂载异常处理函数,只能给在CrashRpt库之前加载的库挂载异常处理函数,在CrashRpt之后加载的库就没法去HOOK了,这样就会导致没进行HOOK操作的那些库中发生的异常都捕获不到了。在exe启动时,会把所有依赖的库加载到进程空间中,我们没法控制所有的库都在CrashRpt库之前被加载的,这也导致了有些异常崩溃CrashRpt时捕获不到的。

       后来我们对CrashRpt库进行了改进,使用微软开源的detours项目中的代码将Windows系统库中的UnhandledExceptionFilter接口给HOOK掉。因为基本所有的异常都会最终进入到该函数中,我们将UnhandledExceptionFilter接口HOOK成我们自定义的接口,我们就能在该自定义的接口中感知到几乎所有的异常了。感知到异常后,就可以生成包含异常上下文的dump文件了。这样就能很好的解决老版本CrashRpt不能hook后加载的库的问题,新版本的CrashRpt就可以作用于当前进程的所有模块了,基本可以捕获到进程的所有异常了。

       当然改进后的CrashRpt也不是100%的异常都能捕获到,但可以捕获大概90%以上的异常。对于捕获不到的场景,就需要将Windbg挂载到目标进程上,让Windbg去捕获了。

6、最后

        排查C++软件异常问题,不仅需要了解引发C++软件异常的常见原因以及排查C++软件异常的常用方法,还要熟练使用各种常用的软件分析工具。另外还需要了解C++程序的五大内存分区、C++函数调用调用的栈分布、函数调用堆栈的栈回溯原理、C++虚函数调用的汇编代码解读、函数调用约定等与异常排查相关的基础内容。此外,还需要了解一些汇编方面的基础知识,因为有时需要借助汇编代码上下文去辅助分析软件异常问题。上述所有内容,都收录在我的《C++软件调试与异常排查从入门到精通》这篇专栏中,欢迎订阅:

C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931

你可能感兴趣的:(软件异常,排查方法,Windbg,dump文件,静态分析,动态调试)