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 最近在使用Visual Studio调试代码的过程中发生了异常崩溃,但在Visual Studio中看不到详细的函数调用堆栈,使用Windbg去动态调试目标进程则可以看到详细的堆栈,进而可以去详细分析这个崩溃问题。这个问题有一定的代表性,本文详细讲述一下这个问题的完整分析过程。
1、问题说明
在调试软件的新功能时,刚加入一个正在发送桌面共享的会议,软件就发生了崩溃:
且问题是必现的。此时,切换到函数调用堆栈窗口中查看函数调用堆栈,如下所示:
从堆栈中只能看到崩溃在Debug版本的运行时库ucrtbased.dll中,但看不到详细的函数调用堆栈。这种情形下,使用VS排查很不方便,还是使用Windbg排查崩溃问题要顺手很多。
在这里,给大家**重点推荐一下我的几个热门畅销专栏:**
专栏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查看崩溃时详细的函数调用堆栈
从崩溃时报错的提示框来看:
程序中访问了一个很小的内存地址0x00000003,产生了内存访问违例,第一感觉是不是访问了空指针导致的,但这也只是最开始时的感觉,还要结合后续分析才能找出最终问题的。
当前这个问题是在Debug下调试出现的,且是必现的,所以先直接启动Debug版本的程序,然后将Windbg附加到程序进程上,然后加入一个正在发送桌面共享的会议中,然后复现崩溃,Windbg中断下来,如下所示:
然后输入kn命令查看此时的函数调用堆栈:
因为没有加载相关模块的pdb文件,所以堆栈中看不到具体的函数名及代码的行号。然后根据堆栈中涉及到的模块,使用lm命令查看模块的时间戳,然后通过时间戳到文件服务器上找到模块对应的pdb文件。这个地方有两个细节需要注意一下:
1)不需要找出堆栈中所有模块的pdb文件,离崩溃点最近的1-2个模块即可,当需要更上层模块的详细函数信息时,再去获取对应模块的pdb文件。
2)加载pdb文件时,会严格校验二进制文件与pdb文件的时间戳,要完全一致才能加载成功,否则会加载失败。比如昨天编译的库和今天编译的库,即使代码没修改,二进制文件和pdb文件都不能交叉配对使用,因为严格校验时间戳。
拿来pdb文件后,将pdb文件的路径设置到Windbg中,然后重新输入kn命令查看加载pdb符号后的函数调用堆栈:
从函数调用堆栈中可以看到,业务库中调用C函数vsnprintf_s去格式化字符串,最终引发了异常。但调用堆栈最上面的几行是运行时库ucrtbased.dll中,显示的偏移地址较大,看不到系统库中具体的函数名,但有时可能需要查看系统库中具体接口,对分析问题可能很关键!那如何找到系统库的pdb符号文件呢?其实很简单!对于Windows系统库,只需要设置微软在线pdb下载地址就可以了,如下所示:
srvf:\mss0616http://msdl.microsoft.com/download/symbols
C:\Users\admin \Desktop\pdbdir;srvf:\mss0616http://msdl.microsoft.com/download/symbols
设置微软在线pdb下载地址后,就可以看到系统库的接口了,如下所示:
其实在本例中,不需要查看系统库中的具体接口就能定位问题了,此处我之所以加到加载系统库pdb文件,是为了说明有时可能会比较关键!
关于如何查看二进制文件的时间戳以及如何将pdb文件所在的路径设置到Windbg中,已经多次讲过,此处就不详细展开了,如果需要查看,可以参看我之前写的文章:
使用Windbg静态分析dump文件的一般步骤及要点详解https://blog.csdn.net/chenlycly/article/details/130873143
3、将Windbg中显示的函数调用堆栈对照着C++源码进一步分析
当我看到函数堆栈中因为调用vsnprintf_s接口去格式化字符串产生了崩溃,我第一反应可能是待格式化的参数与使用的格式化符不一致导致的,这是格式化字符串产生崩溃的一种最常见的原因。比如整型变量错误地是用字符串格式化符%s。
**还有一种隐蔽性较强的场景,带格式化的参数类型长度与格式化符的长度不一致**,比如64位整型参数(占用8字节)使用对应四字节的%d去格式化,这种场景有较强的隐蔽性,不熟悉的可能很难看出问题,即使能看出来,可能也搞不清楚引发异常的根本原因。对于这种隐蔽的场景,我前段时间在项目中遇到过,也发表了对应的博客文章:**(深度分析了因为格式化符与带格式化参数的类型不匹配引发崩溃的根本原因)**
UINT64整型数据在格式化时使用了不匹配的格式化符%d导致其他参数无法打印的问题排查https://blog.csdn.net/chenlycly/article/details/132549186感兴趣的可以去看看。
于是根据函数调用堆栈中显示的cpp名称和代码的行号,到C++源码中找到调用vsnprintf_s格式化接口的函数,此处为啥会有多行记录呢?因为我们对相关接口做了多层封装。我们只要找添加打印的那个函数即可,如下所示:
因为我最开始就怀疑可能是带格式化的参数类型与格式化符不一致导致的,所以一看到源码,我一眼看出了问题,一个枚举类型的整型值居然使用了字符串格式化符%s,这就是问题所在了。
经查看,这个枚举值就是3,格式化函数内部在解析格式化串时,当看到%s格式化符时,会把传入的对应参数值3,当成一个字符串的首地址,即会到0x00000003地址的内存中去读取字符串,而这个0x00000003地址,是很小的内存地址,小于64KB地址值的内存区域是禁止访问的,所以产生了内存访问违例,产生了崩溃。这个问题修改起来很简单,将%s换成%d就好了。
**关于格式化函数内部时如何解析格式化符,以及如何根据格式化符找到对应的带格式化参数的**,可以参见我的这篇文章:
UINT64整型数据在格式化时使用了不匹配的格式化符%d导致其他参数无法打印的问题排查https://blog.csdn.net/chenlycly/article/details/132549186 至于为什么会产生这个问题,应该是开发人员手误导致的,也有可能打印日志的这行代码是从其他地方直接拷贝过来的,没有检查格式化符是否合适,就直接提交代码了。写代码时还是要严谨一点比较好!
4、最后
大家在日常工作中要去主动排查问题,排查的问题多了,见过的问题场景就多了,经验就更丰富了,在后面遇到问题时思路就会更多,排查的更迅速了!特别是一些“难缠”问题,更锻炼人,能学到的更多!在问题中学习,在问题中成长,在问题中积累经验!
版权归原作者 dvlinker 所有, 如有侵权,请联系我们删除。