Bootstrap

【C++软件调试技术】C++软件开发维护过程中典型软件异常问题的排查与总结

目录

1、引发C++软件异常的常见原因有哪些?

2、排查C++软件异常的常用方法有哪些?

3、为什么要熟悉常见的异常内存地址?

4、调试时遇到调用IsBadReadPtr或者IsBadWritePtr引发的异常,该如何处理?

5、如何排查GDI对象泄露问题?

6、如何排查内存泄露问题?

7、如何排查死循环(高CPU占用)问题?

8、如何排查数据格式化时的崩溃问题?

9、如何排查堆内存被破坏问题?

10、如何排查线程堵塞或卡死问题?

11、如何排查程序中的资源泄露问题?

12、如何排查第三方库注入引发的异常问题?

13、如何排查库与库之间不匹配问题?

14、如何排查程序启动异常(启动报错、崩溃、卡死)问题?

15、使用Windbg分析软件异常有哪些方式?

16、为什么有的异常捕获不到?

17、常用的开源异常捕获库有哪些?

18、异常捕获库捕获不到异常时,该怎么办?

19、生成dump文件的方式有哪些?

20、为什么要加载pdb文件?为什么要加载系统库的pdb文件?


C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931C/C++实战进阶(专栏文章已更新400多篇,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_11931267.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124272585C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_2276111.html       最近在C++软件调试交流群中,很多人问了开发维护过程中遇到的多个调试问题,这两天正好有时间,对这些常见的问题进行一个梳理和解答。本文以问答的方式进行展开,罗列了C++软件日常开发和维护中遇到的多个软件调试问题及有代表性的场景,给出详细的处置思路和处理办法,以供大家借鉴和参考!希望大家在了解这些内容以后,能从容地应对开发维护过程中遇到的各种问题。

考察一个开发人员的水平,一是要看其编码与设计能力,二是要看其软件调试能力(软件异常排查能力)!所以,软件调试能力非常重要,我们需要具备高效解决软件运行过程中出现的多种问题的能力!

为什么要学习C++软件调试技术?学习调试技术有哪些好处?,可以参看我之前的写的文章:

为什么要学习C++软件调试技术?掌握调试技术都有哪些好处?icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/130856385学习C++软件调试技术的学习路线路线,可以参见我的文章:

C++软件调试与异常排查技术从入门到精通学习路线分享icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/135048954

1、引发C++软件异常的常见原因有哪些?

       引发C++软件异常的常见原因有:访问空指针与野指针、内存越界(栈内存越界、堆内存越界和全局内存越界)、内存访问违例、线程栈溢出、堆内存被破坏、内存泄露、死循环、多线程死锁、待格式化参数与格式化符号不匹配、库与库之间的不匹配等。

       我之前根据多年的C++软件异常排查实践,对这些引发C++软件异常的常见原因进行了详细总结,可查看文章:

引发C++软件异常的常见原因分析与总结(实战经验分享)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124996473了解这些常见原因之后,我们在排查问题时可能会快速找到可疑点和排查方向,能有限地提高我们排查问题的效率!

2、排查C++软件异常的常用方法有哪些?

       了解引发C++软件异常的常见原因之后,我们还要掌握排查C++软件异常的常用手段与方法。

       排查问题的常用方法有:使用常用工具去辅助分析(比如程序启动失败、启动报错、库加载失败、死循环与高CPU占用问题等)、使用IDE开发工具调试(Debug调试、Release调试和附加到进程调试)、添加打印日志、分块注释代码、数据断点监测内存、历史版本比对法、Windbg分析(静态分析dump文件、动态调试目标进程)、使用反汇编工具IDA查看汇编代码辅助分析等。

       我之前根据多年的C++软件异常排查实践,对这些排查手段和方法进行了详细的总结,可查看文章:

排查C++软件异常的常见思路与方法(实战经验总结)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/120629327掌握这些排查软件异常的常用手段与方法之后,在遇到新的问题时我们的排查思路会更开阔、更高效,必要时需要将多种排查方法结合起来使用。


        在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)

专栏1:(该精品技术专栏的订阅量已达到510多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到200篇以上!欢迎订阅!)

C++软件调试与异常排查从入门到精通系列文章汇总icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931

本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的项目问题实战分析实例(很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!

考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!

专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!

专栏2:  

C++常用软件分析工具从入门到精通案例集锦汇总(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131405795

常用的C++软件辅助分析工具有PE工具、Dependency Walker、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro等,本专栏详细介绍如何使用这些工具去巧妙地分析和解决日常工作中遇到的问题,很有实战参考价值!

专栏3:(本专栏涵盖了多方面的内容,是当前重点打造的专栏,专栏文章已经更新到400多篇,持续更新中...)

C/C++实战进阶(专栏文章已更新380多篇,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_11931267.html

以多年的开发实战为基础,总结并讲解一些的C/C++基础与项目实战进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域多个方面的内容,包括C++基础及编程要点(模版泛型编程、STL容器及算法函数的使用等)、C++11及以上新特性(不仅看开源代码会用到,日常编码中也会用到部分新特性,面试时也会涉及到)、常用C++开源库的介绍与使用、代码分享(调用系统API、使用开源库)、常用编程技术(动态库、多线程、多进程、数据库及网络编程等)、软件UI编程(Win32/duilib/QT/MFC)、C++软件调试技术(排查软件异常的手段与方法、分析C++软件异常的基础知识、常用软件分析工具使用、实战问题分析案例等)、设计模式、网络基础知识与网络问题分析进阶内容等。

专栏4:   

VC++常用功能开发汇总(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124272585

将10多年C++开发实践中常用的功能,以高质量的代码展现出来。这些常用的高质量规范代码,可以直接拿到项目中使用,能有效地解决软件开发过程中遇到的问题。

专栏5: 

Windows C++ 软件开发从入门到精通(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_12695902.html

根据多年C++软件开发实践,详细地总结了Windows C++ 应用软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。


3、为什么要熟悉常见的异常内存地址?

       一般我们只需要了解0xcccccccc、0xcdcdcdcd、0xfeeefeee和0xdddddddd这4个内存地址异常值:

当我们调试代码时遇到这些内存地址异常值后,可以大概知道是什么原因引起的,以这个为线索,就可以迅速地分析并定位问题。

        对于0xcccccccc和0xcdcdcdcd,在 debug 模式下,Visual Studio会把未初始化的栈内存全部填充成0xcccccccc,当成字符串看就是“烫烫烫烫……”;Visual Studio会把未初始化的堆内存全部填充成 0xcdcdcdcd,当成字符串看就是 “屯屯屯屯……”。这两类特殊的字符串,很多人应该都见到过。所以在debug调试时遇到有变量的值为0xcccccccc 或0xcdcdcdcd,一般是变量没有初始化引起的。

        对于0xfeeefeee,是Debug下用来标记堆上已经释放掉的内存,即已经释放的堆内存中会被填充成0xfeeefeee。注意,如果指针指向的内存被释放了,指针变量本身的地址是没做改变的,还是其之前指向的内存的地址,只是其指向的堆内存中被填充成0xfeeefeee。

        对于0xdddddddd,是Debug下用来标记堆上已经释放掉的内存,即已经释放的堆内存会被填充成0xdddddddd。0xfeeefeee也是Debug下用来填充已经释放的堆内存,但0xdddddddd和0xfeeefeee的使用场景应该是有区别的,具体区别我也不太清楚。

之前在项目问题中看到的基本都是0xfeeefeee,没见到过0xdddddddd,但前段时间在Debug下调试代码时遇到了,代码中访问了已经释放的内存,内存中都被置为0xdddddddd,这还是第一次遇到0xdddddddd异常值!正是通过0xdddddddd的说明,得知这个0xdddddddd是用来填充已经释放的堆内存,以这个为线索,快速地定位了问题!

        关于上述异常地址的详细说明,可以参见文章:

C++ 中常见异常内存地址的说明(0xcccccccc、0xcdcdcdcd、0xfeeefeee 和 0xdddddddd 等)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/128285918

4、调试时遇到调用IsBadReadPtr或者IsBadWritePtr引发的异常,该如何处理?

       IsBadReadPtr和IsBadWritePtr最开始是为了判断内存地址是否是可读可写的,但这两个API函数已经被官方废弃了,已经不再具有最开始那种判断作用了,强烈建议大家不要再使用这两个API函数!

       一些老的代码中可能还会调用IsBadReadPtr和IsBadWritePtr,比如我们项目中调用了一个第三方的dll库,这个库是好几年前的版本。我是怎么知道第三方库中调用了IsBadReadPtr和IsBadWritePtr函数的呢?看了我接下来说的内容就知道了!

       当代码执行到IsBadReadPtr和IsBadWritePtr函数调用的地方会产生一个异常,如果当前正在调试代码,则会让调试器中断下来。比如我们在用Visual Studio调试代码或者正在使用Windbg动态调试程序,会让调试器Visual Studio或Windbg中断下来。

        程序产生异常,如果在调试状态下,都会让调试器中断下来,怎么知道是调用IsBadReadPtr和IsBadWritePtr的呢?其实很简单,调试器中断下来后直接查看此时的函数调用堆栈就知道了,堆栈中能看到这两个函数的调用。

        当我们使用Visual Studio或者Windbg调试时,代码执行到IsBadReadPtr和IsBadWritePtr调用处,会抛出一个异常,调试器中断下来,没有经验的人以为程序产生崩溃了,其实不然,这个异常是非致命性的,可以跳过去,程序还可以继续运行。

异常根据严重程度可分为致命性异常和非致命性异常。致命性异常是不可恢复的,会导致程序发生崩溃闪退。非致命性的异常,则是可以跳过去的,程序可以继续运行,不会导致程序的崩溃和闪退。

       如果在用Visual Studio调试代码遇到这个异常中断,按下F5可以将异常中断跳过去。如果是在用Windbg调试目标进程,则输入命令g就可以将异常中断跳过去。注意,如果代码中多次调用了IsBadReadPtr和IsBadWritePtr,则需要多次跳过去。

       之前我写过关于IsBadReadPtr和IsBadWritePtr相关案例的文章,可以去查看:

使用Windbg排查C++程序调用IsBadReadPtr或IsBadWritePtr引发内存访问违例问题icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/129892952

5、如何排查GDI对象泄露问题?

       所谓的GDI对象泄露,就是GDI对象使用完成后没有释放,导致泄露。在Windows系统中,一个进程的GDI对象总数为1万个,当进程的GDI对象总数接近或达到这个上限,则会导致GDI绘图失败,甚至引发程序崩溃。

       GDI对象泄露在UI程序编程中经常遇到,如果存在GDI对象泄露,在Windows任务管理器中可以看到GDI对象总数在持续的上涨。但任务管理器中看不到具体是哪一种GDI对象在不断增长。可以使用GDIView工具,可以看到给类GDI对象的数目,这样就能看到具体是哪个对象在增长,这样结合代码的修改记录,就能很快定位泄露的地方了。

       之前写过使用GDIView排查GDI对象泄露的分析实例,可以去查看文章:

使用GDIView工具排查GDI对象泄漏导致程序UI界面绘制异常的问题icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/128625868使用GDIView工具排查GDI对象泄漏案例的若干细节总结icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/141526436

6、如何排查内存泄露问题?

       所谓的内存泄露就是动态申请的内存,在使用完后没有去释放,导致泄露。内存泄露在C++程序中比较常见。

       对于大型项目,代码量比较大,业务复杂,内存泄露排查起来会比较费劲!有多个工具和方法去排查,比如可以使用Windbg中的umdh. exe工具、使用Windbg中的堆内存信息查看命令、使用Visual Leak Detector工具、甚至使用Visual Studio 2019开始集成的google出品的专用内存分析工具AddressSanitizer。此外像BoundsChecker这种老的工具,若干年前就已经停止维护了,已经不支持新版本的Visual Studio了,也就没法使用了。

       有时有的内存泄露问题,很难排查,用一个工具和方法分析不出来,可以多尝试几个工具和方法,直到分析出来为止!

       使用Visual Leak Detector(简称VLD)排查内存泄露的案例,可以查看我的文章:

​ 使用Visual Leak Detector排查内存泄漏 icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/135472681%20%20%E2%80%8B       使用Windbg检测内存泄漏的案例,可以查看我的文章:

使用Windbg排查C++程序内存泄漏问题icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/121295720使用 Windbg 的 !heap 命令分析内存泄漏icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131576063       使用历史版本比对法排查内存泄漏的案例,可以查看我的文章:

使用历史版本比对法排查C++程序中的内存泄漏问题icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/141002375       使用其他工具分析内存泄露的案例,可以查看内存泄露专栏中的文章:
​ C++内存泄漏排查专题icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_12370029.html%20%20%E2%80%8B

7、如何排查死循环(高CPU占用)问题?

       代码中发生了死循环,一直在执行循环体中的代码,会有一个典型的现象,进程的CPU占用会比较高。

       为什么死循环会导致CPU占用高的问题呢?其实很好理解,因为代码一直在不停歇地执行循环体中代码,一直在占用CPU时间片,所以导致CPU占用高。其实是表现为进程中的某个线程占用的CPU高,因为死循环的代码是执行在某个线程中的,所以这个线程占用的CPU高。

        这种死循环问题排查起来很简单,先用Process Explorer查看哪个线程占用CPU高。然后将Windbg附加到进程上,然后通过占用CPU高的那个线程的id,在windbg中切换到该线程中,输入kn命令查看该线程当前的函数调用堆栈,然后对照着代码就能确定发生死循环的代码位置了。在Process Explorer中也可以查看函数调用堆栈,但看的不准确,并且有时要看相关变量的值去确定为什么发生死循环,这些只能在Windbg中查看,在Process Explorer中是查看不到的。

       我之前写过使用Process Explorer和Windbg排查死循环的案例,可以查看文章:

使用Process Explorer/Process Hacker和Windbg高效排查软件高CPU占用问题icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/135822428

8、如何排查数据格式化时的崩溃问题?

        一般我们在打印日志时需要将相关变量的值格式化到字符串中,然后将字符串打印出来。在格式化时,时常会用到遇到异常崩溃问题。

        一种最简单的场景是,待格式化参数与对应的格式化符不匹配导致了异常比如一个int类型的参数,却使用了%s的格式化符,比如:

int i = 1;
char szBuf[256] = { 0 };
sprintf( szBuf, "%s", i );

格式化函数内部会去解析出设置的格式化符,然后到栈上传过来的对应位置的待格式化参数。格式化函数解析到%s,到栈上拿到对应的参数,然后把参数值与%s对应,即把参数值当成字符串首地址来处理,在此示例代码中,把参数i的值1,当成字符串首地址,即到 0x00000001地址的内存中把字符串读出来去格式化,而这个0x00000001地址值很小,属于64KB的小地址内存区(空指针地址内存区),这个区域的内存是禁止访问的,所以产生了内存违例,引发崩溃。

        这种场景下问题排查很简单,只要看看格式化符与待格式化参数是否匹配即可发现问题。一般这类问题,可能是写代码时手误,讲格式化符号用错了。

       还有一类场景隐藏的比较深,本质上也是格式化符与待格式化参数类型不匹配的问题。比如一个int64的参数,却使用了32位整型对应的%d,导致格式化函数内部再从栈上取对应位置的数据(传入被调用函数的参数是压到栈上传递过来的)时取的数据长度有问题,导致格式化符取数据错位问题,可能会引发崩溃。这类问题一般很难一眼看出来,之前我就帮同事排查过一个类似的问题,详细分析排查过程可以见文章:

UINT64整型数据在格式化时使用了不匹配的格式化符%d导致其他参数无法打印的问题排查icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/132549186

9、如何排查堆内存被破坏问题?

       堆内存被越界破坏,是最难查的一类内存问题。当堆内存被破坏时,程序会到处“乱崩溃”,一会崩溃在这里,一会崩溃在那里,有时崩溃在new一个对象或一段内存地方,有时崩溃在delete对象或内存地方。

       为啥会出现胡乱崩溃的问题呢?因为越界将堆内存头部或尾部区域给篡改了,而系统管理堆内存就是通过头部和尾部的信息来管理的,一旦被篡改,导致堆内存管理出问题,导致后续申请或释放堆内存产生异常。

       堆内存被破坏,排查起来比较困难,Windows平台上没有专用的内存检测工具,只能通过注释代码逐步缩小排查范围去查。而Linux平台上则有专用的内存检测工具Valgrind和AddressSanitizer。好在Visual Studio 2019 19.6以后的版本引入了google的AddressSanitizer,大家后面遇到问题可以试用一下。因为我们项目组从上到下统一使用的是Visual Studio 2017,所以不能使用。

       以前项目中遇到一个内存越界篡改堆内存头信息导致delete去释放堆内存时产生了异常,相关文章如下:

C++堆内存错误:C运行时库检测到向堆内存头部写入了内容icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/121800292

10、如何排查线程堵塞或卡死问题?

       一般线程堵塞或卡死可能是两个原因导致的,一个是多线程之间发生了死锁,一个是线程中发生了死循环。一旦线程发生堵塞,线程相关的业务没法顺利执行,导致软件业务出现异常。

       对于死循环问题的排查,上面已经讲过了。对于多线程死锁,如果是临界区死锁,也可以在Windbg中查看发生死锁的临界区锁信息,可以快速地定位。如果是非临界区锁,排查起来比较麻烦,可以将所有线程的函数调用堆栈都打印出来,然后结合代码,判断出哪个线程发生了死锁。一般添加打印日志,也能辅助定位死锁发生的位置。

       对于使用Windbg分析临界区死锁问题,可以查看我以前写的文章:

使用Windbg分析多线程临界区死锁问题分享icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/128532743

11、如何排查程序中的资源泄露问题?

        程序中的GDI对象泄露、内存泄露和句柄泄露,都属于资源泄露。GDI对象泄露和内存泄露比较常见,我上面已经详细的讲解了。对于句柄泄露,我们在项目中遇到过,代码中频繁创建线程,任务很快执行完线程退出,但没有关闭线程句柄,导致后续创建线程失败,业务出现异常。

        对于资源泄露,之前写了专题文章,可以查看:

深入探究 C++ 程序中的资源泄漏问题icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/133631728

12、如何排查第三方库注入引发的异常问题?

       有些第三方程序(比如输入法和安全软件)会有模块注入到我们的程序进程中,如果注入模块发生异常,可能会导致我们的程序出现异常。因为注入模块是运行在我们程序的进程空间的。我们在项目中遇到过几次。

       对于输入法注入,输入法之所以能支持在所有程序中输入文字,是因为他有专门的注入模块,会注入到所有的进程中。到这种注入可能会引发异常,比如会导致软件时不时的卡顿,导致软件发生死锁,我们在项目中遇到过。对于死锁,我们在Windbg显示的发生死锁的那个线程的函数调用堆栈中看到了输入法的模块,正因为输入法接口调用触发的!

        对于第三方安全软件注入,第三方安全软件就是通过注入到进程中对进程进行监控的。如果注入模块发生内存泄露或者崩溃,会直接影响到我们的软件进程,因为它位于我们的进程空间中!这个我们在项目中都遇到过,相关文章可以查看:

第三方模块远程注入到软件中引发软件异常的若干实战案例分享icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/134545223

如果问题出在客户的环境中,且问题是第三方软件触发的,则一般要拿出充分的证据,否则客户是不认可的!客户会说,为什么机器上其他软件运行都没问题,就你们的软件有问题!我们在项目中遇到过好几个这样的问题了!

其实不同的软件,因为其代码实现与业务逻辑完全不同,表现出不同的问题现象,也是能讲的通的!但要证明是第三方软件的问题,必须要拿出确凿的证据才行!

13、如何排查库与库之间不匹配问题?

        库与库之间的不匹配,一般是两个库的版本不一致。可以从头文件的角度去看(底层发布库到我们的软件中),比如只更新了API头文件但库没更新库文件,再比如只更新了库文件却没有更新API头文件。API头文件中可能有相关结构体的定义,比如新版本中修改了结构体成员(删除或新增了成员),库的版本不匹配也可能会出问题。

        比如A库依赖B库,A库中调用了B库的接口,A库编译时用到B库的API头文件b. h。编译A库时,因为调用 B库的接口,所以要依赖B库的API头文件b. h。而B库本身编译时,一会依赖其API头文件b. h。如果A库使用的B库与B库的头文件b. h版本不一致,也可能会出问题,编译可能没问题,但运行时可能会出问题。

       当软件出现崩溃时,分析函数调用堆栈时也看不出问题,这时可能是库与库不匹配导致的。根据函数调用堆栈中模块,查看这些模块及头文件在svn上的发布记录(假设库是底层模块开发组打不过来的),并与模块的维护人员确认是否是库的版本问题。

14、如何排查程序启动异常(启动报错、崩溃、卡死)问题?

        程序启动时,系统会先把exe程序依赖的所有dll库加载到程序的进程空间中,最后才会进入exe主程序的main函数,程序才能运行起来。

        如果依赖的库,在电脑系统中找不到,则程序启动时会报缺少库的提示,一般是因为程序打包(生成安装包)忘记把缺少的库打包进安装包导致的。把缺少的库打包进去即可。

       如果程序调用的接口在对应的库找不到,程序启动时会报在某个库找不到接口(可能接口名称边了,也可能是接口参数变了),此时可以用Dependency Walker打开exe主程序文件,看看具体是哪个模块调用了这个接口,确认一下是主调模块有问题,还是被调模块有问题。

       如果是启动过程中发生崩溃,可能是加载某个库时库的初始化代码有问题引发的,程序发生崩溃闪退。这时没有机会将Windbg附加到进程上去分析,因为程序已经崩溃闪退了。此时可以使用Windbg将程序启动起来,一旦程序发生崩溃,Windbg就会感知到,就会中断下来,就可以去分析了。

      《C++软件调试与异常排查从入门到精通》专栏中有专门的章节讲述程序启动异常排查的专题,感兴趣的朋友可以去查看:

C++软件调试与异常排查从入门到精通系列文章汇总icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931

15、使用Windbg分析软件异常有哪些方式?

       Windbg排查软件异常主要有两种方式,即静态分析dump和动态调试目标进程。当我们有dump文件时,则使用静态分析dump文件的方式。如果没有生成dump文件(可能没捕获到异常),或者从现有的dump文件中分析不出问题时,可以将Windbg附加到目标进程上进行动态调试。

       使用Windbg静态分析dump文件的一般步骤,见文章:

使用Windbg静态分析dump文件的一般步骤详解icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/135484682       使用Windbg动态调试目标进程的一般步骤,见文章:

使用Windbg动态调试目标进程的一般步骤详解icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/135484906

16、为什么有的异常捕获不到?

       应该是程序中只是简单使用API函数SetUnhandleExceptFilter设置异常回调函数去捕获异常,但这种做法是与线程关联的,需要每个线程都设置回调。只在上层模块中调用设置,只作用所在的线程中的,一般无法捕获其他线程中的异常。而进程中包含了多个模块多个线程,所以其他线程中的异常捕获不到。所以会遇到很多时候捕获不到异常的问题。

       只是简单地调用SetUnhandleExceptFilter设置异常回调去捕获异常,是不够的!一般我们使用开源的异常捕获库去捕获异常,比如CrashRpt、BreadPad和CrashPad。

17、常用的开源异常捕获库有哪些?

       常见的开源捕获库CrashRpt、BreadPad和CrashPad。其中CrashRpt是Google早期开源的,但通过项目实战发现该库有些时捕获不到发生的异常,才看代码研究其实现机制发现,她是通过将加载模块导入表中创建线程的CreateThread函数hook成自定义的函数,在该函数中给当前线程设置异常回调函数去实现该线程的异常捕获。但只对在异常捕获模块之前加载的模块进行hook,后加载到进程空间中的模块没有hook,所以后加载模块如果发生异常,是捕获不到的。

       后来我们使用了微软开源的detours项目对CrashRpt进行改进,有效地提高了捕获异常的几率。BreadPad是Google开源的Chrome浏览器中的异常捕获模块,最新版本浏览器中已经改为CrashPad。CrashPad默认是支持Windows的,如果用在linux中,则需要使用Linux版本。

在应用程序中安装一个异常捕获模块,已经成为一种标配了。通过这个异常捕获模块,去感知(捕获)应用程序中发生的异常或崩溃,自动生成包含异常上下文的dump文件。事后使用Windbg去分析dump文件,去分析软件异常!这一策略和方式,对于一些很难复现的问题,或者只在客户环境中才能出现的问题,非常有用!
当前深度改造的异常捕获库,极大地提高了异常捕获的效率,几乎可以捕获到所有的异常:
1)原生的开源库CrashRpt,对多线程支持的不好,且其实现机制(通过HOOK模块导入表中的CreateThread函数)导致其只能捕获在该库之前加载dll模块中的异常,之后加载的dll库发生异常是捕获不到的。
2)改进后的库,提供了对多线程的支持。
3)通过微软开源的Detours技术,彻底解决了CrashRpt很多异常捕获不到的问题,极大地提升了捕获的效率。
4)具体如何使用本异常捕获库,压缩包中提供了一个demo程序,可以参照demo中的做法。

18、异常捕获库捕获不到异常时,该怎么办?

       大家在实际项目中会发现,即使使用了开源的异常捕获库(或者经过升级改造的捕获库),有少部分异常还是捕获不到的

       对于捕获不到的异常,则只能将Windbg附加到目标进程上,和目标进程一起跑,一旦进程发生异常,会投递给正在附加调试的Windbg,Windbg就会感知到异常并中断下来,就可以直接在Windbg中查看此时的函数调用堆栈进行分析了。如果一时半会看不出来,可以使用. dump命令将异常上下文导出到dump文件中,事后再去静态分析。

       在正在调试的Windbg中使用命令导出的dump文件,属于全dump文件,文件大小接近进程的虚拟内存的大小。Windbg动态调试相对于静态分析,可以多次go多次动态查看函数调用堆栈,还可以添加断点进行断点调试。

19、生成dump文件的方式有哪些?

       生成dump文件的方式主要有三种。

       最常用的方式是通过异常捕获库去自动生成,即异常捕获库捕获到了软件中发生的异常,自动调用系统API函数MiniDumpWriteDump将异常上下文信息保存到dump文件中。

       也可以从Windows任务管理器中导出进程的dump文件。它用在什么场合下呢?比如程序发生异常弹出保存提示框,此时进程还在的,可以导出dump文件。再比如程序发生死循环或死锁时,进程还在的,也可以从任务管理器中导出dump文件。

       还可以从正在调试的Windbg中使用. dump命令导出dump文件。比如我们在使用Windbg动态调试目标进程时,如果一时半会分析不出问题或者出问题的电脑别人还要用(我们不能长时间占用别人的电脑去分析问题),可以导出dump文件,然后事后去分析dump文件即可。

        此外,程序中安装的异常捕获模块没法捕获所有的异常(只能捕获大部分场景下的异常),对于程序崩溃时没有生成dump文件的场景,可以尝试到Windows应用程序日志中看看系统有没有自动生成dump文件

这点对于一些很难复现的崩溃很重要,是个排查问题的重要途径!使用系统自动生成的dump文件去排查问题,我们在项目中用过好几次了! 

       关于生成dump文件方式的详细说明,可以参见文章:

dump文件类型与dump文件生成方法详解icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/127991002

20、为什么要加载pdb文件?为什么要加载系统库的pdb文件?

       pdb文件中包含函数及变量的符号信息,加载pdb后在函数调用堆栈中才能看到具体的函数名和代码行号。有时我们需要在Windbg中查看函数中相关变量的值,变量的值可能是关键线索,有pdb中的变量符号才能识别变量,才能看到变量的值。

       pdb文件分软件业务模块的pdb文件和Windows系统库pdb文件。业务模块的pdb需要加载,有时为了搞清楚问题,也要加载系统库pdb的,加载后可以看到系统库中具体调用了哪些接口,可能能找到更为直接的线索。加载pdb后,可能能看到更多行的函数调用,更便于分析问题。

       对于Windows系统库的pdb文件,不需要下载,微软提供了一个在线pdb符号服务器,只需要将这个在线地址设置给Windbg,Windbg在需要时会自动去下载。

       此外,不仅仅Windbg会用到pdb文件,有些分析工具也会用到。比如Process Explorer/Process Hacker、Process Monitor,使用这些工具分析问题时有时需要看函数调用堆栈,需要pdb中的函数符号。再比如使用反汇编工具IDA查看二进制文件的汇编代码,也需要使用到二进制文件的pdb文件中的函数与变量符号。

       关于pdb符号文件的详细说明,可以参见文章:

pdb符号文件详解icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125508858

;