目录
1、问题说明
2、使用Windbg动态调试去初步分析
3、使用Windbg详细分析
4、最后
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html开源组件及数据库技术(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_2276111.html 最近在联调程序的新功能时,更新了底层模块的库之后,出现了启动报错导致程序启动失败的问题。这个问题有一定的代表性,在这里给大家分享一下问题的排查过程。
1、问题说明
当前软件的新需求基本开发完成,目前正处于调试与bug修改的过程中,因为登录当前的平台出现注册失败的问题,软件重启了几次还是有问题,于是手动将代码中的dll都换成最新的版本(拷贝终端组件整体编译的库,更新加入到版本控制的底层库),并将头文件更新成最新的。更新完成后,在VS中重新编译代码,开启调试,结果程序一启动就报错了,弹出如下的提示框:
查看此时的函数调用堆栈,显示崩溃在medaisdk.dll中,但看不到中间的模块,如下:
这个崩溃是必现的,程序始终启动不起来,导致没法继续进行业务联调。
此外,还有个奇怪的现象,同一个release版本的软件,在一个测试同事的Win10系统上启动并没有问题,可以正常运行。但在另一个测试同事的Win7系统中一启动就崩溃,是必现的!
后来排查得知,是代码中访问了一个未初始化的指针变量(野指针)引发内存访问违例导致的。在release下,未初始化的变量值不会自动初始化,是分配内存时内存中残留的值,是随机值。
所以从Win7和Win10系统中的不同表现可以看出,两个系统的内存管理机制是有差异的,正是因为有差异,导致同一个版本的软件在两个系统中有不同的表现。
在这里,给大家重点推荐一下我的几个热门畅销专栏:
专栏1:(该专栏订阅量接近350个,有很强的实战参考价值,广受好评!)
C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931
本专栏根据近几年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的实战问题分析实例,带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!
专栏中的文章都是通过项目实战总结出来的(通过项目实战积累了大量的异常排查素材和案例),有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!
专栏2:
C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html
以多年的开发实战为基础,总结并讲解一些的C/C++基础与进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域的多个方面的内容,同时给出C/C++及网络方面的常见笔试面试题,并详细讲述Visual Studio常用调试手段与技巧!
专栏3:
开源组件及数据库技术https://blog.csdn.net/chenlycly/category_12458859.html
以多年的开发实战为基础,分享一些开源组件及数据库技术!
2、使用Windbg动态调试去初步分析
于是决定使用Windbg动态分析一下,看看到底是什么原因导致的。因为程序启动时就发生了崩溃,没法先启动程序再把Windbg附加上去,所以需要直接使用Windbg去启动程序,这样才能监测到程序启动过程中的异常。
用Windbg动态启动程序,感知到了异常,查看此时的函数调用堆栈,然后找来函数调用堆栈中相关模块的pdb文件,设置到Widnbg中,重新查看函数调用堆栈,堆栈中显示了详细的函数名称和代码行号:
从函数调用堆栈得知,在程序启动时调用了终端组件终端组件业务初始化接口,然后终端组件层在创建音视频组的编解码器时产生了内存访问违例,进而产生崩溃。
初步怀疑可能是终端组件库与音视频库mediasdk.dll版本不一致导致的,可能是mediasdk.dll的头文件修改了(修改了函数参数或者修改了结构体),只发布了dll库文件,没有发布头文件导致的。当时因为手头有很多事情要处理,没有去深究这个问题,于是尝试让音视频组重新发布mediasdk.dll库和头文件,看看发布后还没有问题。
3、使用Windbg详细分析
但mediasdk.dll库文件和头文件重新发布后,终端组件的相关模块重新编译,然后拷贝最新的终端组件及底层的库重新编译主程序,启动调试运行,结果启动时还是报错。看来并不是版本不对导致的问题。
于是使用Windbg启动程序,感知到异常中断,拿来pdb文件,查看详细的函数调用堆栈,和维护mediasdk.dll模块的同事一起对照代码进行详细分析。崩溃时的堆栈如下:
从堆栈中可以看出,代码崩溃在mediasdk!CVidEncWrapper::Id函数中,查看该函数的源码,该函数中只是简单地返回一个整型变量的值:
所以引发问题的点应该不在该函数中,需要沿着函数调用堆栈往上看,看调用mediasdk!CVidEncWrapper::Id接口的函数。
另外,查看发生崩溃的这条汇编指令,首先是访问了一个内核态地址0xcdcdf001引发的内存访问违例(用户态的代码不能访问内核态的内存地址)。然后查看这条汇编指令中用到的寄存器eax,崩溃时该寄存器的值为0xcdcdcdcd。以前我讲过一些C++程序中常见的异常值0xcdcdcdcd、0xdddddddd、0xfeeefeee等,这几个异常值的说明如下:
所以第一眼看到这个0xcdcdcdcd,根据上面的含义说明,就能大概判断代码中使用了未初始化的堆内存变量,可能是mediasdk!CVidEncWrapper::Id函数所在的类对象有问题,查看这条返回整型变量的汇编指令,按讲返回成员变量的值,当前类对象地址应该是存放在ecx寄存器中的,按讲返回成员变量的值,直接使用ecx就行,为啥会使用eax寄存器呢?
要确定这个问题很简单,直接查看汇编代码上下文就知道了。于是在菜单栏中点击 View -> Disassembly,查看汇编代码上下文:
从汇编代码就找到答案了。对于被调用函数CVidEncWrapper::Id,主调函数肯定是将CVidEncWrapper类对象的首地址通过ecx传进来的,汇编代码中先将ecx中的C++类对象首地址,拷贝到[ebp-4]栈内存上,然后又将[ebp-4]栈内存中的值拷贝到eax中,然后执行发生崩溃的这条指令,所以执行该条崩溃指令时,eax中存放的就是当前类对象的首地址。
所以给CVidEncWrapper::Id函数传入的CVidEncWrapper对象首地址为0xcdcdcdcd,肯定使用的是一个未初始化的指针变量导致的。所以沿着函数调用堆栈,查看调用CVidEncWrapper::Id的函数mediasdk!CKdvEncoder::CKdvEncoder,这是CKdvEncoder类的构造函数。根据函数调用堆栈中显示的cpp路径及代码行号,找到对应的源码位置,如下:
这行代码是一个打印日志的宏Mc_Enter,这就是个宏,并没有看到对CVidEncWrapper::Id函数调用啊,是不是Windbg中指示的行号有问题!
我不了解音视频组的代码,音视频组维护代码的也是一个刚接手的刚毕业小哥,对代码也不熟悉。于是以“Id()”为关键字搜索,看看是哪些地方调用了CVidEncWrapper::Id函数。结果刚才的那个打印日志的宏定义中调用了:
这就对上了,说明Windbg指示的行号是没问题的。对于宏,在代码编译时会被替换成定义的内容。
这个打印日志的宏是放置在CXXXEncoder类的构造函数的入口处,而对指针变量m pcXXXVideoEncoder的初始化放在宏的下一行,所以在宏定义中访问了没有初始化的指针变量m_pcXXXVideoEncoder,该指针变量在Debug下会被初始化为0xcdcdcdcd(指针变量的内存区域中会被填充成0xcdcdcdcd),所以将0xcdcdcdcd作为一个CVidEncWrapper类对象的首地址,调用CVidEncWrapper::Id接口去读取类中的成员变量m dwIndex的值,读成员变量m dwIndex的值,就是去读取该变量在内存中的内容,即访问该变量的内存。
类成员变量的内存位于所在CVidEncWrapper类对象中,是相对于类对象首地址的偏移,即eax+2234h = 0xcdcdcdcd + 2234h = 0xcdcdf001,所以要读取成员变量m dwIndex的值,就是去访问该变量的内存地址0xcdcdf001中的内容,但这个地址对于32为程序,是个内核态内存地址,当前代码是用户态的代码,是不能访问内核态地址的,所以产生了内存访问违例,程序进而发生崩溃。
解决办法是,在CXXXEncoder构造函数中将对指针变量m_pcXXXVideoEncoder初始化的代码调整到打印日志那句宏Mc_Enter代码前面去就好了。保证在使用前就被初始化。
4、最后
当前这个问题是必现的,为啥之前没有出过问题呢?查看音视频组代码的修改记录,在打印宏Mc_Enter的定义处,修改了宏的实现代码。当时修改代码后,只在release下做了测试,没有测试Debug版本的,这个问题在Debug下是必现的。
最开始我们说过,使用问题库的Relase软件版本(通过release安装包安装的),在Win10系统上可以正常运行的,没有暴露出问题。但这个版本在Win7上启动会直接报错的,这是Win7和Win10中的内存管理机制不同导致的。现在大部分人用的都是Win10,所以可能很难将问题暴露出来。所以有两点需要注意一下:
1)Release版本没问题,不代表Debug版本没问题;
2)Win10系统上运行没问题,不代表在其他系统(比如Win7)上运行没问题。
此外,还有两点值得注意一下:
1)通过异常值0xcdcdcdcd,初步推断出是变量未初始化引起的,然后以这个线索为切入点,快速定位问题;
2)在崩溃的那条汇编代码中,没有通过ecx去访问类中成员变量的内存,而是使用eax,查看一下CVidEncWrapper::Id函数的汇编代码就知道了。查看上下文便知道,当前类对象的首地址已经传给了eax了,所以在崩溃的额那条汇编指令中使用了eax。