目录
1、概述
2、全面了解引发C++软件异常的常见原因
3、熟练掌握排查C++软件异常的常见手段与方法
3.1、IDE调试
3.2、添加打印日志
3.3、分块注释代码
3.4、数据断点
3.5、历史版本比对法
3.6、Windbg静态分析与动态调试
3.7、使用IDA查看汇编代码
3.8、使用常用工具分析
4、使用常用的软件分析工具分析
5、掌握异常排查的一些基础知识
6、了解基础的汇编知识,必要时可以对照着C++源码阅读汇编代码上下文
7、学会使用异常排查利器Windbg
8、全面学习项目问题分析实例
9、要主动去实践,实践后进行积极的思考与总结
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、概述
考察一个软件开发人员的水平,一是看其编码与设计能力,二是看其软件调试能力,所以软件调试能力非常重要。学习软件调试技术,就是为了快速高效地解决项目中遇到的各种问题,这既包括开发、联调及测试阶段的问题,也包括产品发布后的问题。高效地排查和解决问题,既能有效地体现对团队的贡献,也能快速地提升个人能力与价值。
这里讲的软件调试技术,是广义上的调试技术,不仅仅是使用IDE去直接调试代码,还包括使用一些进程与内存分析工具以及调试器(比如Windows上的Windbg、Linux上的gdb)去动态或静态分析软件运行过程中遇到的各种问题。
开发、联调与测试是在公司内部环境进行的,排查起来要方便很多,时间也要宽裕很多。而产品发布后的问题,排查起来可能更加困难,可能是非必现的或者很难复现的。有些问题可能在我们软件开发商内部环境很难复现,可能和客户机器的软硬件环境有关(软件方面,比如客户系统中安装了安全软件;硬件方面,客户系统中使用了不同厂商的硬件外设或芯片),也有可能和客户网络系统中复杂的网络环境有关,这些问题我们在项目中都遇到过。在遇到问题时,特别是客户现场反馈的问题,能否有比较多的分析思路与排查手段,能否快速高效地定位与解决问题,显得尤为关键!这需要相关的开发人员有较强的软件分析与调试能力,有较多的问题排查思路与手段,这也是学习软件调试技术的价值所在!
分析和解决问题,是提升个人能力的一个重要途径,在问题中进步,在问题中成长,在问题中收获心得、积累经验。
通过线上交流和线下培训来看,很多开发人员在软件调试技能上都很欠缺,遇到问题时分析问题的方法和手段单一有限,效率低下,甚至会直接影响到项目推进的进度。我也对这种C++开发人员普遍缺少软件调试技能的现状进行了思考,结合对身边环境的观察,导致这种现状的原因主要有以下两方面原因:
1)环境原因:项目团队中很少有人使用分析工具和调试器去分析软件运行时遇到的问题,当前工作环境中没有机会接触到软件调试技术相关的内容。环境对个人技术的提升有着较大的影响。
2)个人原因:项目团队中有人专门研究软件调试技术,并进行了技术分享与培训,大家也积极参与了培训和讨论,很多人都觉得软件调试技能很有价值,能快速高效地解决问题,纷纷表达学习这门技能的热情。但实际上我观察下来发现,很多人都是三分钟热度,培训时很积极热情,但事后并没有动手去捣鼓去实践,没有主动将相关分析工具和技能应用到实际的项目中去。技术这个东西,需要不断去捣鼓去实践,实践后才能真正的掌握,才会获取有价值的心得。只要真正用到项目问题的实战排查中,才能感受到解决问题的快速与便捷。
本文我们就来详细讲讲学习软件调试技术需要从哪些方面入手,要掌握哪些内容,循序渐进地逐步掌握这门技能。本文适合真心想学习的人,只有三分钟热度的人则可以忽略之。
其实,学习C++软件调试技术并不难,并没有多高的门槛,关键是要一步一个脚印去学,然后积极主动地应用到实际工作中,这是个循序渐进的积累过程。一定要动手实践,只有实践才能收获不一样的东西,才能有更进一步的理解和认知。其实不管学习什么技术,都要用到实践中去,都要把它倒腾起来、用起来!
在这里,给大家重点推荐一下我的几个热门畅销专栏:
专栏1:(该专栏订阅量接近350个,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到200篇以上!)
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、全面了解引发C++软件异常的常见原因
先系统地了解一下引发C++软件异常的常见原因,比如变量未初始化、空指针、野指针、内存越界、内存泄露、GDI对象泄露、线程栈溢出、堆内存被破坏、死循环(引发搞CPU)、多线程死锁、函数调用约定不一致、库与库版本不一致、第三方程序注入等原因,如下所示:
了解这些引发软件异常的常见原因之后,在遇到问题时可以根据现有的信息大概估计出引发问题的可能原因,可以确定问题排查的方向,这样排查起来会更有针对性,更高效!
之前我根据项目实践,详细地总结了引发C++软件异常的常见原因,可以查看文章:
引发C++软件异常的常见原因分析与总结(实战经验分享)https://blog.csdn.net/chenlycly/article/details/124996473在十多年的开发生涯中,参与开发了多款软件,排查了成百上千个C++软件异常问题,见识了多个C++软件异常的场景,积累了大量的案例素材和丰富的排查经验!排查过的问题涉及到软件的多个模块,从UI层到底层的业务组件层、协议层、音视频编解码层、网络组件层、开源组件层等。这篇文章是在积累的多个问题场景的基础上进行的归纳与总结,很有实战参考价值。
3、熟练掌握排查C++软件异常的常见手段与方法
需要熟悉并掌握排查C++软件异常的常用方法,比如IDE调试(Debug调试、Release调试与附加到进程调试)、添加打印日志、分块注释代码、数据断点、历史版本比对法、Windbg静态分析与动态调试、使用IDA查看汇编代码、使用常用工具分析等。
在遇到问题时,具体使用哪种方法,要具体情况具体分析,有时可能要将多种方法结合起来一起去分析。
之前我根据多年的项目实践也系统地总结了排查C++软件异常的常用思路与方法,可以查看文章:
排查C++软件异常的常见思路与方法(实战经验总结)https://blog.csdn.net/chenlycly/article/details/120629327上述方法在文章中进行了详细的阐述,很有实战参考价值。
3.1、IDE调试
IDE调试主要分Debug调试、Release调试与附加到进程调试,其中,最常用的是Debug调试,程序开发联调阶段主要使用Debug调试。有些问题可能会因为Debug版本与Release版本有差异,只在Release下有问题(Debug下没问题),需要在Release下进行调试(Release下进行调试前需要关闭优化)。
对于底层的dll模块,是不同开发组维护的,因为dll模块不能独自运行,需要依附exe主程序才能运行起来,如果要调试底层的dll库,则需要先将exe主程序运行起来,然后使用IDE的附加到进程调试的方式进行调试。
3.2、添加打印日志
添加打印日志,是排查软件问题的最重要、最常用的方法之一。通过查看打印出来的日志,可以查看到代码的运行路径和执行逻辑,也可以将代码中相关变量的值打印出来,有时这些变量值可能是分析问题的重要或关键线索。对于业务上的异常,该方法非常有用;对于异常崩溃,查看日志的方法相对要有限一些,通过日志一般很难确定为什么会崩溃,更是没法看到崩溃时的函数调用堆栈。
3.3、分块注释代码
分块注释代码是一种缩小排查范围直至定位到具体的代码行的方法,一般是一小片一小片注释代码,或者一行一行注释代码,如果注释掉若干代码后就没问题了,那问题就出在注释的代码中。
3.4、数据断点
数据断点则是排查因为内存越界导致内存被篡改的利器,可以将数据断点设置在被越界篡改的内存上,一旦内存越界篡改被监控的内存,则调试器就会中断下来,此时查看函数调用堆栈就能找到是发生内存越界的代码点了。可以在Visual Studio中设置数据断点,也可以在Windbg中设置数据断点。
3.5、历史版本比对法
该方法是多次安装不同时间点的软件(一般选择一个时间范围,采用二分法缩小排查范围),看看从哪天开始出现问题。确定出问题的那天,然后查看前一天的代码修改记录以及底层业务库的发布记录,大概率就是前一天的修改导致的。这个方法略显笨拙,但这个方法非常好用,我们在项目中多次使用到了该方法。
这个方法需要有一个完善的版本管理系统,比如我们这边有一个完善的自动化编译系统,每天定时去检测代码有没有修改或者有没有库发布记录,如果有修改,则会自动编译版本,并将版本(包含二进制文件、安装包、pdb文件等)文件拷贝到文件服务器上保存下来。如果每天都有修改,每天都会自动编译版本,如下所示:
这样精细到天的版本颗粒度,对于实施历史版本比对法是非常重要的。很多公司在发布版本时都是开发人员手动编译的,没有自动化编译系统,没有将历史版本维护起来,也就不太好实施历史版本比对法。
3.6、Windbg静态分析与动态调试
Windbg是微软提供的调试器工具,是分析软件异常的最常用的工具。Windbg可以事后静态分析dump文件,也可以附加到目标进程上进行实时的动态调试。
一般我们会在软件中安装异常捕获模块,当软件发生异常崩溃时,异常捕获模块会感知到,并自动生成dump文件。事后我们拿到dump文件就可以分析软件异常了。但异常捕获模块不能捕获到所有的异常,没有生成dump文件时,可以尝试将Windbg附加到进程上,然后复现异常,一旦软件发生异常,Windbg就会立即感知到并中断下来,这样就可以去分析了。
此外,有些问题不是崩溃,比如死循环(会导致高CPU占用)、死锁等问题,也可以将Windbg附加到进程上进行分析。
3.7、使用IDA查看汇编代码
有时为了辅助分析软件异常,我们需要去查看汇编代码上下文,IDA反汇编工具则是查看二进制文件汇编代码的工具。程序一般是崩溃在某一条汇编指令上(执行该条汇编指令时产生了异常),要搞清楚崩溃的根本原因,一般要看汇编指令或者汇编代码的上下文。
崩溃的那条汇编指令能直观的看到发生崩溃的直接原因,比如汇编指令中访问了不该访问的内存地址,比如小内存地址或者内核态的内存地址,导致内存访问违例,引发崩溃。有时要搞清楚具体是什么原因引发的这条汇编指令崩溃,可能还需要对照C++源码查看发生崩溃的那条汇编指令的上下文。
3.8、使用常用工具分析
除了使用上述排查方法,还可以使用一些常用的分析工具去辅助分析。此处就不展开了,下面会单独捻出来详细讲到。
4、使用常用的软件分析工具分析
在遇到问题时,可以优先考虑使用一些简单的分析工具先分析一下。常用的软件分析工具有SPY++、Dependecy Walker、GDIView、Process Explorer、Process Monitor、API Monitor等。这些工具的大概用途如下:
1)SPY++:可以去查看窗口类、窗口句柄值、窗口坐标、窗口风格等信息,可以用于窗口相关问题的分析。
2)Dependency Walker:可以查看库与库之间的依赖关系,查看调用了dll库中的哪些接口。这个工具在分析软件因为缺少库或者库中接口版本不一致导致程序启动失败的问题时会用到。
3)GDIView:可以查看UI程序对GDI对象的占用数,主要用于分析UI程序的GDI对象泄漏问题。
4)Process Explorer:可以查看程序加成加载了哪些dll模块、查看动态加载的dll模块有没有加载起来、可以查看线程信息(在排查死循环引发程序高CPU占用时会用到)、查看进程的虚拟内存占用(排查内存泄漏问题时会用到)等。
5)Process Monitor:可以监测目标程序的文件活动和注册表活动。比如监测某个文件是哪个模块生成的、监测执行某个操作时都读取或修改了哪些注册表项。
6)API Monitor:可以监测程序对系统API接口或者对第三方库接口的调用情况,这个在窥探其他软件的功能实现时很有用,可以窥探到软件在实现某一功能时调用了哪些系统接口,我们在项目中多次使用过。
这些工具在分析一些小问题或者细节问题时,非常好用,效率也比较高。关于这些工具的详细介绍,可以查看我的文章:
C++软件开发值得推荐的十大高效软件分析工具https://blog.csdn.net/chenlycly/article/details/127608247
关于使用这些分析工具排查项目问题的实战案例,可以查看我的文章:
C++常用软件分析工具从入门到精通案例集锦汇总https://blog.csdn.net/chenlycly/article/details/131405795
5、掌握异常排查的一些基础知识
首先需要了解一下C++程序的五大内存分区,如下所示:
不同场合下的变量占用的内存类型是不同的,要搞清楚堆内存与栈内存的区别。不少人分不清代码段内存与数据段内存,程序启动时会加载其依赖的二进制文件,会将二进制文件中的代码存放到代码段内存中。程序中变量的内存,则是属于数据段内存。二进制文件中的汇编代码,每条汇编指令都有对应的代码段地址:(当然下图中是静态默认的代码段地址,程序运行时实际的代码段地址是相对所在exe或dll模块的起始地址加上offset偏移值)
了解函数调用的栈分布,如下所示:
了解这个栈分布,对于阅读函数调用时的汇编代码很有好处。对于理解一些格式化函数的内部实现机制也有好处。对理解线程的函数调用堆栈的回溯非常重要。
可以了解一下函数调用堆栈的栈回溯原理,这点在面试软件调试相关主题问题时可能会被问到。
了解常用的函数调用约定:
函数调用约定决定了传递参数时参数压到栈中的先后顺序。如果函数调用约定不一致,在函数调用完成返回时会出现栈不平衡,会引发软件发生异常。
这些内容在我的专栏《C++软件调试与异常排查从入门到精通教程》中都有对应的专题文章做详细的介绍!如果想了解这些内容,可以去订阅这篇高质量专栏!
6、了解基础的汇编知识,必要时可以对照着C++源码阅读汇编代码上下文
了解汇编有诸多的好处,不仅可以辅助排查C++软件异常,还可以从汇编角度去理解很多高级语言不好理解的代码执行细节,特别是理解多线程执行细节。
程序运行时,CPU中执行的是程序二进制文件中的一条一条汇编指令,汇编指令可以最本真地反映代码的执行细节。程序发生异常崩溃时,最终是崩溃在某条汇编指令上,汇编指令可以最直接地反映出崩溃的直接原因。
很多人觉得学汇编很难很枯燥,不知道从何学起,经常有人问我汇编该从何处入门以及学到什么程度。其实汇编学习入门并不难,当然要学的很深,比如搞反汇编和逆向工程,要求要高很多,但一般我们只需要掌握一些基础的内容,能满足日常工作需要即可。
学习汇编之所以枯燥,可能是因为只是单纯的学习汇编,没有将汇编和高级语言编写的源码结合起来,没有将汇编用到实际工作中去,没有做到学以致用。
学习汇编,目的是为了能阅读汇编代码上下文,相对于深层次的汇编技能,一般我们只需要能对照C++源码阅读汇编上下文就可以了。对照着C++源码阅读汇编上下文,可能会出现C++源码与汇编“对不上”的问题,因为代码在release下编译时编译器会对代码进行优化。比如在C++源码中有个函数调用,但在汇编并没有看到call这个函数的汇编指令,而是直接用几句汇编代替了函数调用,这样就避免了函数调用的开销(调用函数时会涉及到函数参数的入栈出栈、保护现场与恢复现场,这点可以通过查看汇编代码看出来),提高代码的执行效率。
下面以排查软件异常所需要掌握的汇编基础知识为例来讲述如何学习基础汇编知识。一般我们为了辅助排查C++软件异常查看二进制文件的汇编代码上下文时,我们需要将汇编代码与C++源码对照起来看,查看汇编代码是为了定位C++源码中的问题的(汇编代码反应了C++源码的执行细节)。我们需要将汇编代码上下文与C++源码对应起来(一行C++源码对应哪几行汇编代码),一方面利用汇编代码上下文中注释,另一方面还是要参照着C++源码去阅读汇编代码上下文。阅读汇编代码也需要如下的汇编基础知识:
1)了解常用的寄存器及在C++代码中部分寄存器的常见用途(比如eax用于返回函数的返回值;ecx用于传递C++类对象地址;ecx也用于存放循环执行的次数;esi和edi是可变址寄存器,在进行内存拷贝时会用到,比如memcpy内存数据时);
2)了解常用的汇编指令,比如mov指令,push/pop指令、cmp指令、call指令和ret指令等。不需要了解所有的汇编指令,可以在查看汇编时遇到不了解的汇编指令,直接到网上搜索相关指令的含义。
3)了解常见代码块的汇编代码实现。比如memcpy内存拷贝的汇编代码实现、函数调用的汇编代码实现、虚函数调用的汇编代码实现等。因为了解这些常见汇编代码片的功能,一看到这些汇编代码片就知道他们是做什么的,就能快速地看出其对应的C++源码块。如果事先对这些常见汇编代码片不了解,阅读上下文时可能会费劲一些。
4)最后,遇到问题时有必要的话,可以尝试去看看与问题相关的汇编代码,汇编代码看的多了,就熟悉了,经常接触就慢慢上手了。
关于需要掌握的基础汇编知识的详细说明,可以参看我之前写的专题文章:
分析C++软件异常需要掌握的汇编知识汇总https://blog.csdn.net/chenlycly/article/details/124758670
7、学会使用异常排查利器Windbg
Windbg是微软提供的调试器工具,是分析软件异常的最常用的工具。Windbg可以事后静态分析dump文件,也可以附加到目标进程上进行动态调试。
一般我们会在软件中安装异常捕获模块,当软件发生异常崩溃时,异常捕获模块会感知到,并自动生成dump文件。事后我们拿到dump文件就可以分析软件异常了。但异常捕获模块不能捕获到所有的异常,没有生成dump文件时,可以尝试将Windbg附加到进程上,然后复现异常,一旦软件发生异常,Windbg就会立即感知到并中断下来,这样就可以去分析了。
用静态分析dump文件,就要了解dump文件的相关内容,比如pdb符号文件、dump是如何生成的、生成dump文件的方式有哪些等。这些内容在我的专栏《C++软件调试与异常排查从入门到精通教程》中都有对应的专题文章做详细的介绍!如果想了解这些内容,可以去订阅这篇高质量专栏!
使用Windbg分析问题,就是要用Windbg命令去查看我们想看的信息,比如使用.ecxr命令切换到发生异常的线程中、使用kn/kv/kp命令查看线程的函数调用堆栈、使用lm命令查看模块的信息等。要会用Windbg,则需要掌握Windbg的一些常用命令,常用的Windbg命令可以查看这篇文章:
Windbg常用命令详解https://blog.csdn.net/chenlycly/article/details/125508027 有时可能需要查看一些不怎么常用的命令,可以查看这篇文章中的Windbg命令汇总:
Windbg调试命令汇总https://blog.csdn.net/chenlycly/article/details/51711212 刚学习时,还需要掌握使用Windbg分析dump文件的完整步骤、使用Windbg动态调试目标进程的一般步骤,这些一般流程对于初学者很重要,可以按照这些流程去分析自己项目中遇到的问题,可以参看我的专题文章:
使用Windbg静态分析dump文件的一般步骤及要点详解https://blog.csdn.net/chenlycly/article/details/130873143使用Windbg动态调试目标进程的一般步骤及要点详解https://blog.csdn.net/chenlycly/article/details/131029795
8、全面学习项目问题分析实例
《C++软件调试与异常排查从入门到精通教程》专栏中的文章已经达到了100多篇,其中一半以上的文章都是我在项目中遇到的实战问题分析案例:
每个案例都以图文并茂的方式详细讲述问题的完整分析过程,力求讲到所有的分析细节,有很强的实战参考价值。
当你们在项目中遇到类似的问题时,完全可以参考文章中的分析步骤和思路去分析。
9、要主动去实践,实践后进行积极的思考与总结
在学习之后,一定要努力将所学的东西运用到自己的项目与工作中去,一定要多捣鼓多实践,实践了才有价值,实践了才会有更进一步的理解和认知。
通过排查问题,去见识更多的问题场景,积累更多的排查经验。也可以将问题相关的文件(dump文件、pdb文件、问题代码截图等)保存下来,方便后面去查看,也可以为后面的技术分享积累案例和素材。
实践之后,要进行积极的思考与总结,去有效地完善和扩充自己的知识体系。学到了,用上了,别人的技能与知识就是自己的了!
《C++软件调试与异常排查从入门到精通教程》专栏中所有的文章都是通过实战总结出来的(不是从哪里copy过来的,都是通过实战总结出来的),都是实战经验总结,特别是引发C++软件异常的常见原因 和 排查C++软件异常的常用方法,都是通过大量的项目实战排查案例与场景总结出来的,有很强的实战参考价值。