当前位置: 首页 > news >正文

使用Windbg分析dump文件去排查C++软件异常的一般步骤与要点分享

目录

1、概述

2、打开dump文件,查看发生异常的异常类型码

3、查看发生异常的那条汇编指令

3.1、汇编代码能最直接、最本真的反映出崩溃的原因

3.2、汇编指令中访问64KB小地址内存区,可能是访问了空指针

3.3、汇编指令中访问了很大的内核态的内存地址

3.4、汇编指令中访问了一个正常的内存地址,可能是野指针

4、查看发生崩溃时的函数调用堆栈

5、可以尝试在Windbg中查看相关变量的值,可能是排查问题的关键线索

6、有时可能需要尝试找到复现问题的方法和步骤,确定代码的排查范围和方向

7、有时可能需要查看二进制文件的汇编代码上下文去辅助分析问题

8、最后


C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/article/details/125529931C/C++实战专栏(专栏文章已更新460多篇,持续更新中...)icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/article/details/140824370C++ 软件开发从入门到精通(专栏文章,持续更新中...)icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/category_12695902.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/article/details/124272585C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/category_2276111.html       当程序发生异常崩溃时,程序中安装的异常捕获模块感知到了异常,自动生成了dump文件,事后用Windbg静态分析dump文件去排查异常,这是分析程序异常的常用方法。但技术群中很多朋友反映不知道该如何分析,表示无从下手,普遍缺乏分析问题的思路与方法。所以今天在这里总结一下使用Windbg分析dump文件去排查C++软件异常的一般步骤与方法,供大家借鉴或参考。

1、概述

​       使用Windbg静态分析dump文件,是排查C++软件异常与崩溃的常见手段与方法。使用Windbg分析dump文件的思路和流程大概是这样子的:

1)用Windbg打开dump文件后,先看一下发生的异常类型,可能能找到直接的线索。

2)紧接着查看发生异常的那条汇编指令,汇编指令中可能访问了不该访问的内存,可以初步地估计出引发该内存访问的可能原因,作为排查问题的方向和线索。

3)接下来就是查看发生异常的线程的函数调用堆栈,对照着C++源码去分析。

4)必要时,可能需要在Windbg中查看函数中相关变量的值,这些变量的值可能是关键线索。

5)此外,在分析不出问题时,可能要使用IDA反汇编工具查看二进制文件的汇编代码上下文去辅助分析

6)有时可能需要尝试找到复现问题的方法和步骤,确定代码的排查范围和方向。

2、打开dump文件,查看发生异常的异常类型码

​         取来dump文件,用Windbg打开,就能看到程序中发生的异常类型码,如下所示:

​通过这个异常类型码可以初步判断出异常的种类,比如Access violation(内存访问违例)、Integer divided by zero(除0异常)、Stack overflow(线程栈溢出)Stack buffer overrun(栈内存越界)等,通过这个异常类型码可能就能找到最直接的线索。

       比如Integer divided by zero(除0异常)异常,就是代码中出现除数为0的情况,查看异常发生时的函数调用堆栈就能快速定位。

       再比如Stack overflow(线程栈溢出)异常,就是发生异常的线程的函数调用堆栈中函数占用的栈空间达到了系统给线程分配的栈内存的上限,直接去查看异常发生时的函数调用堆栈,就能快速定位问题。之前在文章中总结过引发线程栈溢出异常的常见原因

1)函数递归调用的深度过深
因为一直在递归调用,在到达最底下的那层调用之前,递归函数一直没返回,栈空间一直没有释放,导致当前线程占用的栈空间越来越多,达到上限。
2)消息上触发函数的死循环调用
消息触发的函数死循环调用,因为死循环调用了,函数的栈空间一直没释放,导致当前线程占用的栈空间越来越多。这个问题我们在实际项目中遇到过两次。
3)定义了一个占用内存很大的局部变量
比如定义了一个很庞大的结构体,在一个函数中用该结构体定义了一个局部变量,假设该结构体接近或者大于1MB,则会直接导致线程栈溢出。
4)函数中使用switch...case语句,包含了大量的case分支
每个case分支中都定义了局部变量,导致当前函数占用了大量的栈空间。case分支中的局部变量的生命周期是在case分支中的,即代码运行到对应的case分支中时该分支中的局部变量才有“生命”,但其实这个局部变量的栈空间已经在函数入口处分配好栈空间了,并不是代码执行到case子句中才分配栈空间的。这点可以通过编写测试代码,查看函数入口处给当前函数分配栈空间的汇编代码就能看出来了,可以先顶一个变量查看汇编代码看看分配了多少栈空间,然后再增加一个变量,看看分配的栈空间是否变大。
5)多个if-else分支,每个分支中都有定义局部变量
引发问题的原因与多个case语句的场景是一样的,此处就不再赘述了。本问题案例的场景,就是与if-else分支有关系的。

       对于Access violation内存访问违例的异常,就是程序中访问了不该访问的内存,原因比较多,比如访问空指针或野指针、访问内核态内存地址、访问已经释放的内存等都会引发内存访问违例,内存访问违例就没有明确的问题指向性了,就要查看函数调用堆栈与源码进行详细分析了。

3、查看发生异常的那条汇编指令

        打开dump文件,输入.ecxr命令,切换到发生异常的线程上下文,可以查看到发生异常崩溃的那条汇编指令以及各个寄存器的值,如下所示:

       查看发生异常的那条汇编指令,可能能找到初步的线索。比如汇编指令中访问了一个很小的内存地址,可能是访问了空指针导致的。再比如汇编指令中访问了一个很大的内核态的内存地址,可能是访问了未初始化的指针变量导致的,用户态的代码是不允许访问内核态内存地址,会触发内存访问违例。围绕着这些初步的估计线索,对照着函数调用堆栈去查看C++源码,这样排查的方向可能更明确、更有针对性,效率更高。

3.1、汇编代码能最直接、最本真的反映出崩溃的原因

       程序运行时执行的是二进制文件中的汇编代码(或者称为二进制机器码,二进制机器码和汇编代码是等价的),程序最终崩溃在某条汇编指令上,汇编指令能直观的反映出崩溃的原因。

       比如我们都知道访问空指针可能会引发崩溃,但为啥会崩溃呢?其实是汇编指令中访问了很小的内存地址,而系统禁止访问这个小地址内存区域,一旦访问就会触发内存访问违例,引发崩溃。

       再比如,访问野指针,对应的汇编指令中可能访问了之前已经释放的内存或者一个随机的未知内存,一定会触发内存访问违例吗?答案是不一定,系统允许你访问,你就能访问;系统不允许你访问,则会触发内存访问违例。

3.2、汇编指令中访问64KB小地址内存区,可能是访问了空指针

​       在Windows系统中,0到64KB以内的小内存地址区域是禁止访问的,该区域被称为空指针内存区,专门用来协助程序员去排查访问空指针问题的。如果程序访问这个范围内的内存,则会触发内存访问违例,系统会强行将进程终止掉。

      导致代码访问小地址内存区域可能有以下几个原因:

1)访问了空指针
程序中访问了空指针,通过空指针去访问类的成员变量,类的成员变量的地址就是相对所在类对象首地址的偏移,如果类对象地址是空指针,则访问成员变量的地址就很小,这样就产生了访问小内存地址的场景。
2)访问了未初始化的指针
还可能是访问了一个未初始化的指针引起的,指针未初始化,则指针值是给指针变量分配的内存中残留的随机值,这个随机值可能很小,所以也会触发访问小地址内存区域。
3)程序中发生内存越界,指针变量的值被篡改了
程序中发生了内存越界,指针变量的值被篡改了,可能被篡改成一个很小的值,该值对应着一个很小的内存地址。


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

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

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

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

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

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

专栏2:(本专栏涵盖了C++多方面的内容,是当前重点打造的专栏,订阅量已达160多个,专栏文章已经更新到400多篇,持续更新中...)

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

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

专栏3:  

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

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

专栏4:   

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

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

专栏5: 

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

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


3.3、汇编指令中访问了很大的内核态的内存地址

       对于32位程序,系统会给进程分配4GB的虚拟内存,一般情况下用户态和内核态会各占一半,即各占2GB,我们编写的代码基本都是运行在用户态的,用户态的代码时不能访问内核态内存地址的(内核态地址是供系统内核模块使用的)。

       如果汇编指令中访问了一个很大的内存地址,超过用户态的地址范围0-2GB,内存地址大于0x8000000,即访问了一个内核态的内存地址,则会触发内存违例,因为用户态的代码是禁止访问内核态内存地址的。

       为什么代码中会访问内核态内存地址呢?可能有两个原因:

1)访问了一个未初始化的指针
Release下,指针未初始化,则指针值是给指针变量分配内存时内存中残留的随机值,这个随机值可能很大,可能是内存态的内存地址。
2)程序中发生内存越界,指针变量的值被篡改了
程序中发生了内存越界,指针变量的值被篡改了,可能被篡改成一个很大的值,该值对应着内核态的内存地址。

3.4、汇编指令中访问了一个正常的内存地址,可能是野指针

       当前发生崩溃的这条汇编指令中访问了一个正常的内存地址,既不是很小的内存地址,也不是很大的内核态内存地址。很可能是访问的野指针,野指针主要包含以下两种情况:

1)该野指针可能是未初始化的指针变量。指针值是给指针变量分配内存时内存中残留的随机值。
2)可能是指针指向的内存已经被释放了(指针的值就是其指向的内存地址),而指针变量没有置为NULL,代码中又访问了该指针变量。

       之前在项目中就遇到这个场景,发生崩溃的汇编指令中访问了一个正常的内存地址,产生了内存访问违例,查看崩溃的函数调用堆栈,一直找不到引发问题的原因。后来又回到这个汇编指令,大概地思考了一下,从这个访问的内存地址入手,既然访问的是一个正常的内存地址(内存地址不小,也不大,在正常范围),有没有可能该内存地址空间被释放了呢?代码中在释放内存时,没有将指针变量置为NULL所以出现代码中访问已释放内存的野指针的场景。后来围绕这个可能的线索,根据函数调用堆栈去查看C++源码,然后就快速地定位了问题。相关的问题案例,之前已经写成了文章,可以查看:

根据发生异常的汇编指令以及函数调用堆栈,估计出问题的原因,确定排查方向,快速定位问题icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/article/details/141869579

4、查看发生崩溃时的函数调用堆栈

       查看异常崩溃时的函数调用堆栈,是我们分析问题的最重要的线索。根据函数调用堆栈去查看C++源码,引发问题的点可能就在当前的调用堆栈中的函数中,当然也可能在其他处的代码中(代码崩溃有时具有延后性,执行到引发问题的代码点时不会立即崩溃,在执行到后续代码时发生崩溃)。

       用Windbg打开dump文件,输入.ecxr命令切换到发生异常的那个线程,输入kn命令查看发生异常时的函数调用堆栈:

       要查看到详细的函数调用堆栈信息,比如具体的函数名以及代码的行号,需要根据模块的时间戳找到对应的pdb文件,然后将pdb的路径设置到Windbg中,然后重新输入.ecxr命令和kn命令,就可以查看到详细的函数调用堆栈了。

       这里需要注意一下,函数调用堆栈可能会显示代码崩溃在Windows系统库或者开源库(比如duilib)模块中,一般问题都不在系统库或开源库中,问题应该出在上层的模块中要到上层模块中找问题,很多时候要从内存的角度去分析。比如上层操作的类对象地址有问题,可能对象内存已经释放了,但上层还在使用,再次访问就会出现内存访问违例,导致程序崩溃。

5、可以尝试在Windbg中查看相关变量的值,可能是排查问题的关键线索

       我们看到了发生异常时的函数调用堆栈,然后根据堆栈去详细查看源码,有时分析问题原因可能需要查看相关变量的值,可以直接尝试在Windbg中查看,变量的值可能是关键的线索。在Windbg中查看变量值的一个示例截图如下所示:

​      之前写过相关的实战问题分析案例,可以查看我的文章:
通过查看Windbg中变量值去定位C++软件异常问题icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/article/details/125731044通过查看Windbg中变量值去定位C++软件异常的又一典型案例分享icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/article/details/125793532

6、有时可能需要尝试找到复现问题的方法和步骤,确定代码的排查范围和方向

       因为执行了引发问题的代码,其导致后续的崩溃会有延后性,通过崩溃时的代码没法找到引发问题的代码,可能需要尝试找到复现问题的方法和步骤,确定代码的排查范围和方向。

复现问题,找到复现问题的方法,也是排查问题的一种有效的辅助手段与方法。

       有时我们需要让测试人员去复现问题,找到复现问题的方法与步骤,通过复现的步骤确定问题与哪些操作有关,这样就能大概确定问题与哪块代码有关,这样就确定了代码的排查范围和方向,然后在查阅对应代码的过程中结合用户执行的操作(复现问题的步骤),查找代码中存在的逻辑控制错误或业务异常。

       对于软件UI异常或者业务异常,我们可能会尝试去找问题的复现办法,或者是我们在使用一些方法和手段去排查但很难定位问题时,也希望测试人员去尝试复现问题。 再比如,有些问题是在客户环境中出现的,在公司测试环境中没出现过,问题很难排查定位,我们可能需要尝试在公司环境中复现,如果能复现,在公司环境中排查起来也比较方便。

7、有时可能需要查看二进制文件的汇编代码上下文去辅助分析问题

       有时我们查看C++源代码很难搞清楚问题到底出在那句代码上,或者无法排查出问题时,我们可能就需要查看汇编代码上下文去分析了。软件最终是崩溃在某一条汇编指令上,通过查看汇编代码才能最直观的看出为什么会发生异常崩溃。

​       在Windows平台下我们主要使用IDA Pro反汇编工具打开二进制文件,去查看二进制文件中的汇编代码。我们一般对汇编代码不是很精通,我们需要将C++源代码和汇编代码对照看,找到汇编代码与C++源码的对应关系,同时依靠汇编代码中的注释,才能大概地看懂汇编代码的上下文。

​有一点需要注意,release下编译器在编译时会对代码进行优化,这样可能会导致汇编代码有时较难和C++源代码完全对应起来,比如有变量被优化掉、有函数调用被优化掉。

       要阅读汇编代码上下文,需要掌握一些基础的汇编知识,这些基础汇编知识可以查看我之前写的文章:

分析C++软件异常需要掌握的汇编知识汇总icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/article/details/124758670       至于如何使用IDA Pro反汇编工具以及实战问题分析案例,可以参看我的文章:

反汇编工具IDA使用详解(使用IDA查看二进制文件的汇编代码以及使用IDA分析崩溃问题实例分享)icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/article/details/120635120使用IDA查看汇编代码,结合安卓系统生成的Tombstone文件,分析安卓app程序崩溃问题icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/article/details/132283582       此外,C++开发人员要了解汇编,了解汇编后有诸多的好处。不仅可以辅助排查C++软件异常问题,还可以理解很多高级语言不好理解的代码执行细节(从汇编代码的角度可以看到代码的完整执行细节),特别是多线程编程中的问题。关于为什么要学习汇编以及学习汇编有哪些好处,可以查看我的文章:
C/C++程序员为什么要了解汇编?了解汇编有哪些好处?如何学习汇编?icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/article/details/142795872

8、最后

       本文详细讲述了使用Windbg分析dump文件去排查程序异常的一般方法与思路,并给出了一些实战问题分析实例(以文章链接的方式提供),有一定的实战参考价值,希望能帮到大家。本文讲到了Windbg分析dump文件的要点,至于使用Windbg分析dump文件的详细操作步骤及相关问题分析实例,可以参考我的文章:

使用Windbg分析dump文件的一般步骤详解icon-default.png?t=O83Ahttps://blog.csdn.net/chenlycly/article/details/135484682


http://www.mrgr.cn/news/51188.html

相关文章:

  • Android 自定义Toast显示View
  • WRF ungrib.exe 出错 ERROR: Data not found 的原因分析
  • “长三角档案数字资源长期保存与数据安全治理”专题培训内容抢先看
  • 正态分布拟合时,柱状图数据是怎么计算的
  • 数据治理基础
  • 防范数据泄露,守护安全存储新时代
  • 从零开始学习OMNeT++系列第三弹——新建一个使用INET框架的工程
  • 视频网站开发:Spring Boot框架的深入探索
  • 【分布式事务-03】分布式事务seata的AT模式
  • GESP CCF 图形化编程二级认证真题 2024年9月
  • 用comfyui复现remini爆火的黏土风格转绘,实现图片自由
  • 雷达中的时间尺度
  • 图像中的数值计算
  • SQL Server-导入和导出excel数据-注意事项
  • 就是这个样的粗爆,手搓一个计算器:科学计算器
  • 猫头虎 分享:Python库 aiohttp 的简介、安装、用法详解入门教程
  • vue综合指南(二)
  • 【数据结构】1.顺序表
  • 如何优化批处理策略,最大限度地“压榨”GPU性能
  • aosp14分屏分割线区域部分深入剖析-framework实战干货