排查软件异常的常见思路与方法
Posted IT老张
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排查软件异常的常见思路与方法相关的知识,希望对你有一定的参考价值。
目录
上一篇我们简单讲述了引发软件异常的常见原因,本篇我们将详细介绍一下排查软件异常的常用思路与方法。
1、概述
总的来说,要分析软件发生异常的具体原因及找到解决办法,我们需要想办法获取到发生异常时的函数调用堆栈,通过函数调用堆栈,我们可以知道都调用了哪些函数。我们还需要知道发生异常时异常上下文中相关变量在内存中的值,确定相关变量是否出现了非法的异常值。通过调用的函数和变量无法确定问题时,我们需要推测出发生异常的可能原因,然后使用一些方法去验证,有时甚至需要去查看相关的汇编代码才能搞清楚问题的具体原因。
2、常见的异常排查思路与方法
我们有以下几种常用的分析思路与方法,具体使用哪种方法就要看具体的情况了。
2.1、直接调试
这是最直接,也是较有效的方法。主要优下列三类调试方法,具体使用那种方法还要看问题是否是必现的,问题发生在哪些模块中等。通过直接调试,我们可以看到发生异常
1) Debug和Release下的调试
如果问题是必现的,最直接的方法就是直接调试源代码了。可以先尝试在Debug下调试,但有的bug可能在Release下才会出现,这时就需要在Release下进行调试了。之所以Debug版本程序和Release版本程序在实际运行时有差异,这个两个编译版本下的特性有关系的,比如Debug下编译器会对一些未初始化的变量进行自动的初始化,再比如debug和release版本在内存管理机制上是有所不同的。
需要注意的是,在release下调试需要事先将工程选项中的优化关闭掉,比如Windows平台下Visual Studio(本文中的实例均以Windows平台下使用Visual Studio开发C++程序为例,Visual Studio在下文中将简称VS)中相关的设置:
2)VS附加到进程调试
对于底层的组件库、协议库等调试,需要依附上层的exe才能运行,可以让底层库的开发人员先安装release版本的安装包,然后只需要编译要调试的模块库,编译完成后将库文件覆盖到安装路径下。然后启动exe程序,用VS的附加到进程的调试功能:
去直接调试这些底层库。当然有些问题可能是release下才会有的,所以可能也需要进行release下的调试。
这个地方需要注意一下,如果要调试程序启动时初始化那部分的代码,则需要在代码中添加阻塞程序执行的代码,比如UI程序中可以人为地添加一个MessageBox,这样去等待VS去设置附加调试,这样就有机会去调试初始化的代码了。
3)Windbg附加到进程调试
Windbg是Windows平台下软件异常静态分析与动态调试的利器,是我们分析Windows程序异常最常用的工具。
对于有些崩溃(比如Stack overflow栈溢出)问题,即便是使用Visual Studio进行调试时检测到了,但进程可能已经退出了,VS随即也退出了调试状态,所以此时是无法查看到崩溃那一刻的函数调用堆栈的,更是没法看到发生异常时变量在内存中的值的。此时可以考虑将Windbg附加到目标进程中:
按操作步骤将崩溃复现后,Windbg中是可以查看到崩溃时的函数调用堆栈的。
2.2、添加日志打印
这应该是我们这边用的比较多的方法了。
很多时候根据出问题时的现象,很难判断出到底是哪一块出的问题,并且问题是很难复现的,所以也是没办法去调试代码的。这时我们可以在代码中添加打印日志,将可能出现问题的地方都添加上打印,将运行到的点以及相关的内存中数据打印到目标窗口或文件中,然后根据运行时产生的打印看数据及流程是否有异常,从日志打印中去找线索。比如我们某个模块的打印日志如下:
这种方法对于频繁执行、不方便直接调试的代码块,也是比较实用的。
对于大型软件系统,很有必要去构建一个完备的日志系统,不仅可以检测到系统运行过程中用户的行为,还可以通过打印来定位软件运行过程中遇到的问题。就像编译程序时编译器输出的异常提示信息一样,我们也可以对打印出的日志类型进行分类,比如有error错误日志、warning警告日志、debug调试日志、info日常运行日志等。
2.3、分块注释代码
在问题始终排查不出来的时候,可以尝试采取分块注释代码的方法来定位问题。这个方法,我们也经常使用。
我们可以将某一行或者某些行的代码注释掉后,问题可能就没有了,那基本上可以确定问题就出在被注释的代码上了,甚至能确定问题出在哪一行代码上。然后排查这些被注释的代码,也许就能找到问题的症结了。
有次遇到这样的问题,A库依赖B库,B库定义了结构体Struct1,A库调用了B库的GetData接口, GetData接口是以Struct1结构体作为参数的,GetData中进行了数据的memcpy操作。因为库发布的问题,导致两个库不匹配。B库中在Struct1结构体中增加了字段,但是A库中使用的还是老的结构体,这样在调用GetData传入结构体地址或引用,由于GetData中进行的是memcpy的操作,在调用完该接口后,导致主调函数中栈内存越界,直接篡改了主调函数中其他栈变量的内存,导致代码出现不可解释的异常运行行为。这个问题当时查了很久,就是通过分块注释代码才查出来的。
2.4、数据断点
对于运行过程中内存数据被无故修改,或者我们通过排查现有代码始终找不到修改变量值的代码行号时,我们此时可以尝试使用Visual Studio自带的数据断点功能,通过添加数据断点,监测出修改目标内存的代码行。
数据断点本质上是监控某段内存中的数据是否被修改,如果我们对某段内存设置了数据断点,一旦内存中的数据被修改,IDE的调试器就会命中该断点,此时切换到函数调用窗口中去查看此时的函数调用堆栈,就知道是哪块的代码修改目标变量的值了。
设置数据断点是有技巧的,即在目标变量初始化的地方设置普通断点,然后启动VS调试,当VS命中该普通断点时,目标变量就分配了内存,此时通过变量名就可以取到目标变量的内存地址了,然后给这个内存地址设置数据断点即可:
当有代码修改了该变量的值时,就会命中该数据断点,查看此时的函数调用堆栈就知道是什么代码修改了变量的值了。
数据断点确实非常好用,我们已经用过很多次了。
2.5、历史版本比对法
这个方法有点笨,有时会有点耗时,但有时却很有效果。
比如某天我们软件突然出现了一个之前未曾出现的问题,实在是找不到线索,我们可以尝试安装不同时间的历史版本:
然后看看这个问题是从哪天开始出现的,然后查看这个时间点附近提交的代码,问题基本上就出在这个时间点提交的代码中。具体操作时,我们可以在时间上使用二分法缩小排查的范围。
举个之前遇到的例子,测试同事发现最近我们的软件在运行时窗口老是有明显的卡顿问题,很是困惑,但最近代码并没有做太大的改动,前一段时间运行还好好的。查找了最近代码的修改记录,似乎也没找到问题。
于是尝试了这个方法,让测试同事多安装几个时间点的版本,看看是哪个时间点的版本开始出现问题的。找到了这个时间点,在svn上查找前一天提交的代码,仔细地看了一下,是从网上拷贝的一段参考代码有问题,不看不知道,一看吓一跳!在代码中启动了一个UI界面的定时器,在定时器消息的处理代码中竟然有sleep操作,这个是运行在UI界面线程中的,当执行到sleep操作时,系统会将UI线程挂起,这就直接导致UI窗口卡住了,虽然sleep时间很短,但是定时器消息会一直触发,这个代码会频繁地执行,所以就出现了界面频繁卡顿的问题了。
使用该办法找出第一次问题的版本后,就基本能确定是前一次提交的代码引起的,于是我们就找到问题的突破口,进一步分析可能就能很快定位问题了。
2.6、Windbg静态分析dump文件
对于Widnows程序,dump文件包含了软件发生异常时异常上下文信息,包含目标进程的所有线程的信息(比如寄存器信息)、线程中的函数调用堆栈信息,甚至包含有部分或全部的变量值。那dump文件时如何产生的呢?主要有以下三个途径生成dump文件:
1)从任务管理器中导出
可以在WIndows系统的任务管理器中右键点击目标进程,在弹出的右键菜单中点击“创建转储文件”菜单项导出dump文件。
2)安装异常捕获模块,由该模块自动生成dump文件
一般Windows程序中都会安装异常捕获模块,一旦软件发生异常崩溃,该异常捕获模块就能感知到,就会将发生异常时的异常上下文信息导出到dump文件中。比如QQ、钉钉等常用的软件中都安装了异常捕获模块,当软件发生异常时就会弹出一个提示框,提示软件发生了崩溃,询问是否将崩溃信息上报给后台,然后后台拿到这个dump文件之后好久可以进行分析了。
3)windbg动态调试时使用.dump命令动态导出
直接将windbg挂载到正在运行的目标进程上,或者直接使用windbg启动目标进程上,一旦软件发生崩溃,windbg就会捕获到,windbg会中断下来,此时可以使用.dump命令将异常上下文信息导出到dump文件中。windbg捕获到异常后会立即中断下来,按讲此时就可以直接分析了,为啥还要手动将异常信息导出到dump文件中呢?可能当时不能立即查出问题,亦或是出问题的电脑是其他同事的或者是客户的,我们需要导出到dump文件中去详细研究一下。
建议大家在代码中安装捕获软件异常的模块或代码,比如常用的是google开源的CrashReport异常捕获库(很多软件比如QQ、钉钉在都使用类似的库),在软件发生崩溃时能实时捕获到异常信息,自动生成dump文件。然后测试人员将取到的dump文件发送给开发人员,开发人员使用Windbg打开dump文件进行分析:
在Windbg打开dump文件,先用.ecxr命令切换到异常发生时的上下文,然后使用kn、kv或kp命令查看异常发生时的函数调用堆栈,然后就可以对照着代码去详细分析了。这种情况属于Windbg的静态分析,不是动态调试。
另外,dump文件还分全dump文件和小dump文件。小dump文件则比较小(大概几MB的大小),只包含了少量或部分变量的内存信息,一般是异常捕获模块自动生成的。异常捕获模块自动生成的dump文件可能会比较多,比较频繁,不能生成太大的文件,否则会消耗用户大量的磁盘空间,甚至将用户的磁盘空间占满。而全dump文件包含了所有变量的内存信息,文件大小一般会达到好几百MB,一般是通过任务管理器或者windbg动态调试时导出的。
关于Windbg的大致使用方法,后续的文章会详细的介绍。
2.7、Windbg动态调试
我们可以通过Windbg直接启动目标程序,也可以将Windbg附加到已经启动的程序进程上进行动态调试:
比如程序发生“卡死”或者死循环时,我们可以将Windbg附加到程序进程中进行动态调试,查看此时线程的函数调用堆栈,以确定发生问题的线程以及线程中当前的函数调用堆栈信息。
甚至在系统已经弹出报错提示框时:
将Windbg附加到出问题的进程上也能查看到异常信息的。
另外,我们在遇到一些VS捕获不到异常的问题时,比如遇到Stack overflow线程栈溢出崩溃时,VS中是看不到异常发生时函数调用信息的,但我们可以通过Windbg的动态调试能及时捕获到线程栈溢出异常,然后就可以查看到崩溃时的函数调用堆栈了。
此外,我们在软件中安装的异常捕获模块并不能捕获所有场景下的异常,特别是有些闪退是捕获不到的,此时我们可以尝试将Windbg挂载到目标进程上去动态调试,然后对软件进行拷机测试(可以使用自动化测试工具自动进行拷机测试)。当软件发生异常时,Windbg是能捕获到的,能能第一时间中断下来。
2.8、使用IDA反汇编工具查看汇编代码
有时我们查看C++源代码很难搞清楚问题到底处在什么地方,此时我们可能就需要查看对应的汇编代码了。软件最终是崩溃在某一条汇编代码上,通过查看汇编代码才能最直观的看出为什么会发生异常崩溃。
在Windows平台下我们主要使用IDA Pro反汇编工具打开二进制文件,去查看二进制文件中的汇编代码。我们一般对汇编代码不是很精通,我们需要将C++源代码和汇编代码对照看,才能看懂汇编代码的上下文,但release下编译器在编译时会对代码进行优化,这样会导致汇编代码有时很难和C++源代码对应起来。
至于IDA Pro反汇编工具时如何使用的,我们也会有详细的介绍,这里就不再展开了。
3、最后
在实际的问题排查过程中,我们可能需要将多种方法结合起来使用,还是要具体问题具体分析吧。
以上是关于排查软件异常的常见思路与方法的主要内容,如果未能解决你的问题,请参考以下文章