发布时间:2022-11-09 09:30
目录
1、概述
2、如何在windbg中查看变量的值
3、mini dump文件与全dump文件
4、使用windbg初步分析
5、查看变量的内存找到了线索,排查出问题
6、解决办法
7、为啥不同的PC上会有不同的表现
8、最后
C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...) https://blog.csdn.net/chenlycly/article/details/125529931 我们在用windbg分析异常时,可以查看到发生崩溃的那条汇编指令以及崩溃时各个寄存器的值,可以查看崩溃时的函数调用堆栈,去查找排查问题的线索。但有时通过这些信息无法定位问题时,我们可以去查看函数调用堆栈中部分函数中的局部变量和C++类的数据成员的值,说不定可以从变量值中找到一些线索。本文就来讲一个通过查看变量值去排查异常问题的实例,以供大家参考。
我们使用Windbg分析C++软件异常时,一般会先去看发生崩溃的那条汇编指令及崩溃时的个寄存器的值,然后接着去查看崩溃时的函数调用堆栈,一般从函数调用堆栈中显示的函数调用关系及代码的行号就能找到线索。
在拿到相关模块的pdb文件后,函数调用堆栈中会显示详细的函数名和返回地址代码的行号。然后我们通过函数名称和代码的行号,去查看C++源码,可能就能定位出问题了。但有时通过这些信息很难确定为啥会出现崩溃,我们可以尝试去看一下函数调用堆栈中的函数中的局部变量或者C++类对象的数据成员的值,说不定可以找到一些线索。
我们之前排查的一些异常案例,正是通过变量的值找到线索,快速地定位了问题。下面我们以项目中遇到的一个异常排查实例,来讲述如何通过变量值来定位问题的。
如何才能查看到函数调用堆栈中某个函数中的变量在内存中的值呢?前提是要能拿到函数调用堆栈中对应模块的pdb文件,因为只有pdb文件中才会包含函数及变量变量信息,拿到pdb文件后将pdb设置到windbg中。
加载了pdb文件后,查看函数中变量的值就很简单了,只要点击函数调用堆栈中前面的序号超链接:
就会展开那一行对应的函数,可以查看到函数中局部变量的值:
如果该变量是C++类的成员函数,还可以查看到所属类对象的数据成员的值或内存。至于要查看函数中局部变量的值,还是查看函数所属类对象的数据成员的值,就要根据具体的需要了。
为啥函数中能查看到C++类对象呢?因为对于C++非静态成员函数,在使用C++类对象来调用该成员函数时都要给该成员函数传递对应的C++对象的首地址,因为成员函数中需要访问C++类的数据成员,因为非静态的数据成员是属于某个C++类对象的,要访问非静态的数据成员必须要使用对应的C++类对象去访问的,所以在使用C++对象调用非静态成员函数时必须要给成员函数传递C++对象地址的。如上图所示,点击04号栈帧时,展开的是CAudioDeviceCheck::InitWin7OrUpSpk函数,展开的变量中,除了有局部变量,还有指向当前函数所对应的类对象的首地址的this指针,点击该this指针就会将对应的类对象展开,如下所示:
在C++汇编代码中,是通过ecx寄存器给C++类成员函数传递C++类对象地址的。
通过内嵌在软件的异常捕获模块CrashRpt捕获到异常时生成的dump文件,一般是只有几MB的、精简版的mini dump文件,只能查看到部分变量的值,没法查看到所有变量的值。能否查看到我们想要看的目标变量的值,是要看运气的,有时能看到,有时则看不到。
只有全dump文件才能看到所有变量的值,或者是使用Windbg动态调试时,能看到所有变量的值。那么哪种情况下生成dump文件才是全dump文件呢?有两个场景下导出的dump文件是全dump文件:
1)在使用Windbg动态调试时使用.dump命令导出的dump文件,比如使用这样的命令:.dump /ma D:\0628.dmp。
2)在Windows系统的任务管理器中导出的转储文件。具体的操作是,在任务管理器的进程列表中右键单击目标进程,在弹出的右键菜单中,点击创建转储文件,导出的转储文件就是全dump文件,如下所示:
全dump文件保存了进程的所有内存信息,所以全dump文件一般都比较大,大概在几百MB左右,甚至会高达1GB以上,基本和进程占用的虚拟内存大小差不多。
在本文要讲的问题范例中,有人反馈其在家里安装我们的C++软件后,一登录就会出现崩溃,是必现的。同样的软件版本,安装其公司的电脑上跑都是正常的。于是让测试同事和客户联系,取来了软件中内置的CrashRpt异常捕获模块捕获到dump文件。用Windbg打开dump文件,输入.ecxr命令切换到异常上下文,看到了发生异常的汇编指令,但看汇编指令看不出来问题。
于是输入kn命令查看函数调用堆栈,如上所示。
从最上面的几行函数调用来看,调用了msvcr100.dll模块的_invalid_parameter接口,应该是触发了无效参数的异常,是调用_vswprintf_s触发的。_invalid_parameter和_vswprintf_s是系统运行时库msvcr100.dll库中接口,我们要往上层函数看,看看是哪个函数调用了_vswprintf_s接口。
但因为没有pdb文件,上层模块中看不到具体的函数名,于是没有pdb符号库,没法看出具体崩溃在哪个函数中,上层函数位于xxxxxx.exe模块中,于是使用lm命令,查看一下xxxxxx.exe二进制文件的时间戳(编译生成时间),看到是2020/02/10 22:59:43生成的:
于是根据软件的版本号及xxxxxx.exe二进制文件的时间戳,到文件服务器上找到对应版本路径,找到xxxxxx.exe对应的pdb文件。
然后将pdb符号库的路径设置到windbg中,使用.ecxr重新切换到异常时的上下文,使用kn重新查看函数调用堆栈:
看到是CAudioDeviceCheck::InitWin7OrUpSpk函数调用了swprintf_s,然后swprintf_s函数内部调用了msvcr100.dll库中的_vswprintf_s函数,然后引发了崩溃,系统库本身是没有问题的,肯定是上层函数的问题,要看上层的函数CAudioDeviceCheck::InitWin7OrUpSpk的内部实现代码。
在我的工作电脑上找到版本对应的源代码路径:E:\svn_dir\20191126_xxxxxx,设置到windbg中。点击函数调用堆栈中CAudioDeviceCheck::InitWin7OrUpSpk函数那一行,这样windbg就会跳到出问题的代码上:
其中,CAudioDeviceCheck::InitWin7OrUpSpk函数中调用的是_stprintf_s,_stprintf_s是个宏,其定义如下:
#ifdef _UNICODE
#define _stprintf_s swprintf_s
#else
#define _stprintf_s sprintf_s
#endif //_UNICODE
当前程序时_UNICODE版本的,所以_stprintf_s对应swprintf_s,这样和windbg中显示的函数就对上了。
这句swprintf_s函数的调用,是将字符串格式化到目标buffer中的,看到这个代码,第一反应是可能是目标buffer长度不够了,引发了内存越界问题。但如何才能进一步确认呢?
查看调用swprintf_s函数时目标buffer长度为64字节:
//音频设备信息结构体
typedef struct tagAUDIODEV
{
TCHAR sName[64];
UINT nVersion;
WORD nProId;
tagAUDIODEV()
{
memset(this, 0, sizeof(tagAUDIODEV));
}
}AudioDevice;
于是回到windbg中,看看能不能看到CAudioDeviceCheck::InitWin7OrUpSpk函数中的局部变量的值(因为此处的dump文件是精简版的,不是全dump(全dump文件会包含所有的内存信息,会达到几百MB)),不一定能看到有效的变量内存数据。结果在windbg中看到了待格式化的字符串变量在内存中的内容:
字符串为:"NVIDIA HDMI Out (NVIDIA Virtual Audio Device (Wave Extensible) (WDM))",从字面上看这是一个虚拟的音频设备,视频会议开始时,我们会去检测系统中的音频设备。于是将字符串拷贝到notepad++中查看字符串的长度,选中字符串内容后显示长度为69字节:
超过了格式化时目标buffer的长度(64字节),所以导致了内存溢出,引发了这个崩溃。
搞清楚了这个崩溃,该如何修改呢?直接将上层的buffer长度放大?还是要看看代码的上下文,看完后再找一个稳妥的办法,不要盲目片面的去放大buffer的长度。
查看了一下代码的上下文,这个音频设备的名称是要上报给网管服务器的,组件层的定义的结构体中对应字段buffer的长度就是64,版本比较紧急,让组件的同事扩大buffer的长度是来不及了,只能先规避一下这个崩溃,对现有业务没有什么影响。调用我们之前编写的一个公用的字符串安全拷贝函数SafeTCharBufCopy,不再使用_stprintf_s函数,超过目标buffer长度时就将原字符串将会被截断,如下所示:
关于安全拷贝函数的实现如下:
/*=============================================================================
函 数 名: SafeTCharBufCopy(处理TCHAR,支持Unicode)
功 能: 实现将源buf中的内容安全地拷贝到目标buf中,解决因目标buf较大而源buf内容较短时copy的异常
参 数: TCHAR* pchDest [in] 目标buf
const TCHAR* pchSrc [in] 源字符串
size_t destLen [in] 目标buf长度(注意:以字节为单位,不是以字符个数为单位)
注 意: 无
返 回 值: 无
=============================================================================*/
void SafeTCharBufCopy( TCHAR* pchDest, const TCHAR* pchSrc, size_t destLen )
{
if ( pchDest == NULL || pchSrc == NULL )
{
return;
}
// 此处计算的是字节数,方便与destLen比较
size_t srcLen = _tcslen( pchSrc )*sizeof(TCHAR);
size_t cmpLen = srcLen + 1*sizeof(TCHAR); // 1 - 预留给结尾符
if( cmpLen > destLen )
{
_tcsncpy( pchDest, pchSrc, destLen/sizeof(TCHAR)-1 ); // Unicode版本的字符串操作,用的是字符个数
pchDest[destLen/sizeof(TCHAR)-1] = 0;
}
else
{
_tcscpy( pchDest, pchSrc );
}
}
那为啥软件在公司的电脑上运行没问题,在家里的笔记本上运行就有问题呢?下面我们就来大概地说明一下。我们软件在登陆成功后,会去获取一下电脑上的音视频设备信息(包括扬声器和麦克风设备),然后将音频设备信息上报给服务器。
崩溃就出现在拷贝音视频设备信息时。因为公司电脑使用的音频设备和家里笔记本用的音频设备是不同的,公司电脑的音频设备的名称长度相对较短,将之格式化到目标buffer中时不会导致目标buffer溢出。而在家里笔记本的音频设备名称比较长,所以在格式化到目标buffer发生了buffer溢出,发生了崩溃。
产品发布出去以后,会有格式各样的软硬件运行环境,软件在其开发商的公司进行的测试是有限的,测试环境也是有限的,所以在开发商公司内部测试没问题了,不代表到客户环境中后就没问题。从本例的问题看,还是编写代码质量不够,在编写代码时没有意识到代码可能存在的潜在问题。
如果对软件异常的常见原因有足够的了解,在排查软件异常方面有足够的经验,那么在编写代码时潜意识中就会考虑的比较周全,会提前预知到多种潜在的问题,这样编写出来的代码质量就会比较高一些。另外,很多公司会使用代码静态分析工具(比如pclint、TScanCode)对代码进行静态扫描,也能发现一些潜在的问题。
所以,在我们找不到问题的线索时,可以尝试去看看函数调用堆栈中的函数中变量的值,结合C++源代码的逻辑,去进行一些有针对性的分析,说不定可以找到一些能快速定位问题的线索。
docker(alpine+golang) 中 hosts 不生效问题解决方法
Linux企业运维——Redis部署+redis主从复制+redis主从自动切换(基于sentinel哨兵模式)+集群+redis+mysql 的缓存服务器+配置 gearman 实现数据同步
深度卷积神经网络(AlexNet)与卷积神经网络(LeNet)的区别及其代码实现(可直接复制运行)
引领生物计算新浪潮 | 2021全球抗体亲和力预测大赛圆满结束
Django学习笔记:简单Django+Mysql实现学生信息管理系统实现(注册、登录、增删改查)
【vue3项目】解决 “TypeError: Cannot read properties of undefined (reading ‘xxx‘)“