使用Windbg静态分析dump文件的完整过程介绍

news2025/5/21 14:49:29

目录

1、概述

2、静态分析dump文件的一般步骤

2.1、查看异常类型

2.2、使用.ecxr命令切换到发生异常的线程上下文,查看发生异常的那条汇编指令

3、问题分析实例说明

4、使用Windbg详细分析dump文件,展现完整分析过程

4.1、查看异常类型和发生崩溃的汇编指令,初步分析

4.2、使用kn/kv/kp命令查看函数调用堆栈

4.3、找到pdb文件,设置到windbg中,查看完整的函数调用堆栈

4.4、可以将C++源代码路径设置到Windbg中,Windbg会自动跳转到源代码行号上

4.5、有时需要查看函数调用堆栈中函数的局部变量或C++类对象中变量的值

4.6、有时可能需要使用IDA去查看二进制文件的汇编代码上下文去辅助分析


VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/category_11397492.htmlC++软件分析工具案例集锦(正在更新中...)https://blog.csdn.net/chenlycly/category_12279968.html       有很多正在学习C++软件调试技术的朋友或者刚入门的人,希望我能详细讲讲使用Windbg静态分析dump文件的一般步骤,他们好对照这个步骤去练习去实操。所以今天就来讲讲这个主题内容,先概要性讲述Windbg静态分析dump文件的一般步骤,然后再以一个问题分析实例详细展现分析dump文件的完整过程。

1、概述

       基本大部分软件都内置了异常捕获模块,在软件发生异常闪退崩溃时,会弹出相关的提示框,比如PC版的微信在崩溃时,其内置的异常捕获模块会捕获到异常并生成日志及dump文件,同时会弹出如下的发送错误报告的提示框:

提示框的下方会自动带上崩溃相关的文件,其中最后一个文件就是我们要讲的dump文件,点击确定则会将这些文件发送到腾讯远端的后台服务器上。腾讯后台的运维人员会收到通知,然后到服务器上将dump等文件下载下去分析。

       有些软件可能没有上传崩溃日志到服务器的功能,捕捉到异常时会自动将dump文件保存到指定的路径中,事后可以到该路径中取到对应的dump文件。如果客户机器上遇到崩溃,可以和客户联系,让客户帮忙从对应的路径中取来出dump文件发过去。

       使用Windbg分析包含异常上下文的dump文件,属于静态分析,下面就来详细讲一下拿到dump文件之后如何使用Windbg去静态分析dump文件。

      关于Windbg的下载安装及详细介绍,可以查看我之前写的文章:
Windbg使用详解https://blog.csdn.net/chenlycly/article/details/120631007关于Windbg的命令汇总,可以查看我之前写的文章:

Windbg调试命令汇总https://blog.csdn.net/chenlycly/article/details/51711212

2、静态分析dump文件的一般步骤

        可以先打开Windbg,然后将dump文件拖到Windbg中。

2.1、查看异常类型

        打开dump文件,就会显示发生异常的类型,如下所示:

根据显示的异常类型(ExceptionCode值对应的含义,比如0xC0000005对应的就是Access violation内存访问违例)。

       通过这个异常类型可以初步判断出异常的种类,比如Access violation(内存访问违例)、Integer divided by zero(除0异常)、Stack overflow(线程栈溢出)等。对于Access violation内存访问违例的异常,原因比较多,很难一步确定引发异常的原因。但对于Integer divided by zero除0异常、Stack overflow线程栈溢出异常,则能给出直接的线索,可以快速地定位问题。

2.2、使用.ecxr命令切换到发生异常的线程上下文,查看发生异常的那条汇编指令

        在查看异常类型之后,我们输入.ecxr命令切换到发生异常的线程上下文,查看发生异常的那条汇编指令及相关寄存器的值:

通过查看汇编指令及各寄存器的值,可以初步判断当前发生异常的初步原因,比如可能是访问C++类空指针、野指针或者用户态的代码访问了内核态内存地址。

汇编代码能最直接、最本真的反映出崩溃的原因,程序运行时执行的都是二进制文件中的汇编代码(或者称为二进制机器码,二进制机器码和汇编代码是等价的)。

       下面讲两个典型的汇编指令中访问了不该访问的内存异常场景。

2.2.1、汇编指令中访问64KB小地址内存区,引发访问违例,导致崩溃

       在Windows系统中,64KB以内的内存地址是禁止访问的,如果程序访问这个范围内的内存,则会触发内存访问违例,系统会强行将进程终止掉。

2.2.2、汇编指令中访问了很大的内核态的内存地址,引发访问违例,导致崩溃

       对于32位程序,系统会给进程分配4GB的虚拟内存,一般情况下用户态和内核态会各占一半,即各占2GB,我们编写的代码基本都是运行在用户态的,用户态的代码时不能访问内核态内存地址的(内核态地址是供系统内核模块使用的),如果崩溃指令中访问了一个很大的内存地址,超过用户态的地址范围0-2GB,内存地址大于0x8000000,则会触发内存违例,因为用户态的代码是禁止访问内核态内存地址的。

2.3、使用kn/kv/kp命令查看异常发生时的函数调用堆栈

       接着,使用kn/kv/kp命令(三个命令,可以使用其中任何命令)去查看异常发生时的函数调用堆栈,看看是调用了什么函数触发的异常。根据函数调用堆栈,对照着源码去进行分析。    

       一般此时没有加载pdb文件,调用堆栈中不会显示具体的函数名和代码的行号:

因为Windbg没有加载包含函数及变量符号信息的pdb文件。要函数调用堆栈中显示具体的函数名和代码行号,则需要拿到pdb文件,然后将pdb文件的路径设置到Windbg中。然后Windbg记载到pdb符号库文件之后,重新输出函数调用堆栈,堆栈中就会显示出具体的函数名和代码行号了。

2.4、使用lm命令查看模块的时间戳,找到对应的pdb文件,设置到Windbg中

       函数调用堆栈在没加载pdb文件时,会显示代码所在的模块名称,我们使用lm命令查看这些模块编译生成的时间戳:

然后通过时间戳找来这些模块对应的pdb文件,然后将pdb文件的路径设置到windbg中。如何将pdb文件的路径设置到Windbg中,下面讲实例时会详细说明,此处就不再赘述了。

       Windbg加载pdb文件之后,使用kn/kv/kp命令再次输出函数调用堆栈,堆栈中就会显示具体的函数名及行号,这样对照着C++源代码去详细分析引发问题的最终原因了。

       有时候为了搞清楚发生异常的本质,我们还需要使用IDA查看相关二进制文件的汇编代码,查看一下发生异常的那条汇编指令的上下文,对照着C++源码,看看那条汇编指令为啥会出现异常。

3、问题分析实例说明

        我们为了方便展开讲解,我们特意使用VisualStudio创建了一个基于MFC对话框的exe测试程序,对话框中有个名为button1的按钮,如下所示:

我们在此按钮的响应中添加了一段引发崩溃的测试代码,故意让程序产生崩溃,测试然后拿到崩溃时的dump文件。

       具体的测试代码如下所示:

// 添加的一段测试代码
SHELLEXECUTEINFO *pShExeInfo = NULL;
int nVal = pShExeInfo->cbSize; // 通过空指针访问结构体成员,导致崩溃
 
CString strTip;
strTip.Format( _T("nVal=%d."), nVal );
AfxMessageBox( strTip );

代码中使用到的结构体SHELLEXECUTEINFO 定义如下:

typedef struct _SHELLEXECUTEINFOW
{
    DWORD cbSize;               // in, required, sizeof of this structure
    ULONG fMask;                // in, SEE_MASK_XXX values
    HWND hwnd;                  // in, optional
    LPCWSTR  lpVerb;            // in, optional when unspecified the default verb is choosen
    LPCWSTR  lpFile;            // in, either this value or lpIDList must be specified
    LPCWSTR  lpParameters;      // in, optional
    LPCWSTR  lpDirectory;       // in, optional
    int nShow;                  // in, required
    HINSTANCE hInstApp;         // out when SEE_MASK_NOCLOSEPROCESS is specified
    void *lpIDList;             // in, valid when SEE_MASK_IDLIST is specified, PCIDLIST_ABSOLUTE, for use with SEE_MASK_IDLIST & SEE_MASK_INVOKEIDLIST
    LPCWSTR  lpClass;           // in, valid when SEE_MASK_CLASSNAME is specified
    HKEY hkeyClass;             // in, valid when SEE_MASK_CLASSKEY is specified
    DWORD dwHotKey;             // in, valid when SEE_MASK_HOTKEY is specified
    union                       
    {                           
        HANDLE hIcon;           // not used
#if (NTDDI_VERSION >= NTDDI_WIN2K)
        HANDLE hMonitor;        // in, valid when SEE_MASK_HMONITOR specified
#endif // (NTDDI_VERSION >= NTDDI_WIN2K)
    } DUMMYUNIONNAME;           
    HANDLE hProcess;            // out, valid when SEE_MASK_NOCLOSEPROCESS specified
} SHELLEXECUTEINFOW, *LPSHELLEXECUTEINFOW;
 
 
#ifdef UNICODE
typedef SHELLEXECUTEINFOW SHELLEXECUTEINFO;
typedef LPSHELLEXECUTEINFOW LPSHELLEXECUTEINFO;
#else
typedef SHELLEXECUTEINFOA SHELLEXECUTEINFO;
typedef LPSHELLEXECUTEINFOA LPSHELLEXECUTEINFO;
#endif // UNICODE

       在测试代码中定义了SHELLEXECUTEINFO结构体指针pShExeInfo,并初始化为NULL,然后并没有给该指针赋一个有效的结构体对象地址,然后使用pShExeInfo访问结构体的cbSize成员的内存,因为pShExeInfo中的值为NULL,所以结构体cbSize成员的内存地址是结构体对象起始地址的偏移,因为结构体对象地址为NULL,cbSize成员位于结构体的首位,所以cbSize成员就是结构体对象的首地址,就是NULL,所以就访问64KB小地址内存块的异常,引发内存访问违例,导致程序发生崩溃闪退。

       至于如何在程序中设置异常捕获模块去捕获异常、自动生成dump文件,可以尝试使用google开源的CrashRpt库,我们产品很早就有了。大家如果想集成异常捕获库,可以使用开源CrashRpt(开源版本有一些缺陷,我们进行了一些优化和改进),也可以使用Google浏览器开源代码中的BreakPad,网上有很多版本,可以去研究一下。

4、使用Windbg详细分析dump文件,展现完整分析过程

4.1、查看异常类型和发生崩溃的汇编指令,初步分析

       使用Windbg打开dump文件(直接将dump文件拖入到Windbg),首先我们查看一下发生的异常类型,如下:

       然后使用.excr命令切换到发生异常的线程上下文,会自动将发生异常的那条汇编指令显示出来,如下:

       我们可以先看一下这条汇编指令以及崩溃时的各个寄存器的值,可能能初步估计出发生异常的原因。如果指令中访问了一个很小的地址或者访问了一个很大的地址,都会触发内存访问违例。
从崩溃的这条汇编指令来看,是访问了小地址0x00000000地址,这是访问了小于64KB小地址内存区,这个范围的地址是禁止访问的,所以引发了内存访问违例。这个异常很可能是访问了空指针引起的。

4.2、使用kn/kv/kp命令查看函数调用堆栈

       接着输入了kn命令,将函数调用堆栈打印出来:

函数调用堆栈是从下往上看的,最上面一行就是最后调用的一个函数,也是崩溃的那条汇编指令所在的函数。从函数调用堆栈的最后一帧调用的函数来看,程序的崩溃是发生在TestDlg.exe文件模块中,不是其他的dll模块。显示的函数地址是相对TestDlg.exe文件模块起始地址的偏移,为啥看不到模块中具体函数名称呢?那是因为Windbg找不到TestDlg.exe对应的pdb文件,pdb文件中包含对应的二进制文件中的函数名称及变量等信息,Windbg加载到pdb文件才能显示完整的函数名。

       查看函数调用堆栈的命令,除了kn,还有kv和kp命令,其中kv还可以看到函数调用堆栈中调用函数时传递的参数,如下所示:

我们需要取来pdb符号库文件,去查看具体的函数名及行号的,这样才好找到直接的线索的。下面就来看看如何获取到TestDlg.exe模块的pdb文件。

4.3、找到pdb文件,设置到windbg中,查看完整的函数调用堆栈

       如何才能找到TestDlg.exe文件对应的dpb文件?我们可以通过查看TestDlg.exe文件的时间戳找到文件的编译时间,通过编译时间找到文件对应的pdb文件。在Windbg中输入lm vm TestDlg*命令,可以查看到TestDlg.exe文件的详细信息,其中就包含文件的时间戳:(当前的lm命令中使用m通配符参数,所以在TestDlg后面加上了*号)

可以看到文件是2022年6月25日8点26分23秒生成的,就可以找到对应时间点的pdb文件了。

        一般在公司正式的项目中,通过自动化软件编译系统,每天都会自动编译软件版本,并将软件的安装包及相关模块的pdb文件保存到文件服务器中,如下所示:

这样我们就可以根据模块的编译时间找到对应版本的pdb文件了。

        我们找到了TestDlg.exe对应的pdb文件TestDlg.pdb,将其所在的路径设置到Windbg中。点击Windbg菜单栏中的File->Symbol File Path...,打开设置pdb文件路径的窗口,将pdb文件的路径设置进去,如下所示:

点击OK按钮之前,最好勾选上Reload选项,这样Windbg就会去自动加载pdb文件了。但有时勾选了该选项,好像不会自动去加载,我们就需要使用.reload /f TestDlg.exe命令去让Windbg强制去加载pdb文件(命令中必须是包含文件后缀的文件全名)。

        设置完成后,我们可以再次运行lm vm TestDlg*命令去看看pdb文件有没有加载进来:

如果已经加载进来,则会在上图中的位置显示出已经加载进来的pdb文件的完整路径,如上所示。

       关于pdb符号库文件的说明,可以查看我之前写的文章:
pdb符号库文件详解https://blog.csdn.net/chenlycly/article/details/125508858       加载到TestDlg.exe文件对应的pdb文件之后,我们再次执行kn命令就可以包含具体的函数名及及代码的行号信息了,如下:

 我们看到了具体的函数名CTestDlgDlg::OnBnClickedButton1,还看到了对应的代码行号312。通过这些信息,我们就能到源代码中找到对应的位置了,如下所示:

是访问了空指针产生的异常。当然上面的代码是我们故意这样写的,目的是为了构造一个异常来详细讲解如何使用Windbg进行动态调试跟踪的。

4.4、可以将C++源代码路径设置到Windbg中,Windbg会自动跳转到源代码行号上

       为了方便查看,我们可以直接在Windbg中设置C++源码路径,这样Windbg会自动跳转到源码对应的位置。点击Windbg菜单栏的File->Source File Path...,将源码路径设置进去:

然后在Windbg点击函数调用堆栈中的每一行记录前面的数字超链接,会自动跳转到对应的函数及行号上,如下所示:

上图中的函数调用堆栈中很多模块是系统库中的,比如mfc100u、User32等,这些库是系统库,是没有源码的。我们可以点击最下面的第23个链接,其位于我们应用程序的模块中,会自动跳转到对应的代码中,如下:

4.5、有时需要查看函数调用堆栈中函数的局部变量或C++类对象中变量的值

        有时我们通过查看变量的值,找到排查问题的线索,比如变量中值为0或者很大的异常值。这点我们在多次问题排查中使用到,确实能找到一些线索。可以查看函数中局部变量的值,也可以查看函数所在类对象的this指针指向的类对象中变量的值。我们要查看哪个函数,就点击函数调用堆栈中每一行前面的数字超链接,如下所示:

我们看到了局部变量pShExeInfo 的值:

struct _SHELLEXECUTEINFOW * pShExeInfo = 0x00000000

       我们可以点击this对象的超链接:

就能查看当前函数对应的C++类对象中成员变量的值,如下:

       但有时不一定能查看变量的值,因为当前通过异常捕获模块自动生成的dump文件一般是minidump文件,文件也就几MB左右,不可能包含所有变量的值。所以要在minidump文件中查看变量的值,要看运气的,有时能查看到,有时是看不到的。这里要讲一下dump文件的分类,主要分为minidump文件和全dump文件。

      关于dump文件的分类与dump文件的生成方式,可以查看我之前写的文章:
dump文件类型与dump文件生成方法详解https://blog.csdn.net/chenlycly/article/details/127991002

4.6、有时可能需要使用IDA去查看二进制文件的汇编代码上下文去辅助分析

       有时通过函数调用堆栈和源码很难定位问题时,可能需要使用IDA打开二进制文件去查看汇编代码的上下文去辅助定位问题。关于如何使用IDA去辅助排查问题,可以参见我之前写的文章:

使用IDA查看汇编代码上下文去辅助排查C++软件异常问题https://blog.csdn.net/chenlycly/article/details/128942626

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/568660.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FreeRTOS:队列

目录 前言一、队列简介1.1数据存储1.2多任务访问1.3出队阻塞1.4入队阻塞1.5队列操作过程图示1.5.1创建队列1.5.2向队列发送第一个消息1.5.3向队列发送第二个消息1.5.4从队列中读取消息 二、队列结构体三、队列创建3.1创建函数3.2函数xQueueCreateStatic()3.3函数xQueueCreate()…

隐私计算论文合集「联邦学习系列」第2期

前言: 隐语awesome-PETs(PETs即Privacy-Enhancing Technologies ,隐私增强技术)精选业内优秀论文,按技术类型进行整理分类,旨在为隐私计算领域的学习研究者提供一个高质量的学习交流社区。awesome-pets包含…

集权设施攻防兵法:实战攻防之AD篇

一、黑客眼中的AD AD域是攻击者经常攻击的目标,因为AD域作为企业的核心身份验证和授权系统,攻击AD域可以使攻击者获得系统内所有计算机和用户的权限,从而轻松获取敏感信息和控制企业系统。 另外,AD域内存在众多的计算机资产&…

使用git在Github上创建自己的项目及一些基础操作

使用git在Github上创建自己的项目及一些基础操作 一、什么是git Git 是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。 Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。 Git 与常用的版本控制工具…

Doris安装

Apache Doris 由百度大数据部研发(之前叫百度 Palo,2018 年贡献到 Apache 社区后, 更名为 Doris ),在百度内部,有超过 200 个产品线在使用,部署机器超过 1000 台,单一 业务最大可达…

Mac app文件签名与公证

一、Mac app文件签名与公证 在钥匙串中创建要公证app的profile(公证的时候会用到) xcrun notarytool store-credentials "mac_app" --apple-id "xxxxxxxx163.com" --team-id "S24Z9326XX" --password "fnjx-qjoe-l…

PYTHON制作前后端分离的图书信息管理系统(flask+vue)

前言 嗨喽,大家好呀~这里是爱看美女的茜茜呐 本次案例,使用 flask、vue、elements-plus、axios 制作一个基于 restful api 的前后端分离的图书信息管理案例 效果展示 👇 👇 👇 更多精彩机密、教程,尽在下…

Python 的 type 函数和 isinstance 函数

type()、isinstance()都是对象类型操作函数,用于判定对象类型,用哪个函数更好哩? 【学习的细节是欢悦的历程】 Python 官网:https://www.python.org/ Free:大咖免费“圣经”教程《 python 完全自学教程》,…

数字档案馆建设指南

数字档案馆建设指南 目 录 1.总体要求 2.管理系统功能要求 3.应用系统开发和服务平台构建 4.数字档案资源建设 5.保障体系建设 1.总体要求 1.1概述 数字档案馆是指各级各类档案馆为适应信息社会日益增长的对档案信息资源管理、利用需求,运用现代信息技术对数字…

一只羊的奥秘,您可知羊是有四个胃的噢

很多人知道反刍动物,但是对于反刍动物的四个胃怎样消化,不太了解,今天小编就介绍一下,反刍动物为何需要四个胃消化食物。 反刍俗称倒嚼,是指某些动物进食经过一段时间以后将半消化的食物从胃里返回嘴里再次咀嚼。反刍主…

有效项目进度管理的 10 条规则

项目进度管理是项目中比较关键的方面之一,因为它将决定事情的进展方式、进展速度以及是否会取得进展。换句话说,它可以让你较好地控制项目,帮助你预测不可预测的情况,并使所有相关团队能够高效地协同工作。 以下是有效项目进度管…

如何在华为OD机试中获得满分?Java实现【寻找两个正序数组的中位数】一文详解!

✅创作者:陈书予 🎉个人主页:陈书予的个人主页 🍁陈书予的个人社区,欢迎你的加入: 陈书予的社区 🌟专栏地址: Java华为OD机试真题(2022&2023) 文章目录 1. 题目描述2. 输入描述3. 输出描述…

3.数据结构期末复习之栈和队列

1.栈的应用 1.括号匹配问题(还有确定他们的符号优先级) 如 2((32) * 3) /3 扫描到左括号入栈,右括号入另外一个栈,如果两个栈数量相同,则是匹配的,不保存,不然去找最少的栈,出来提示报错2.十进制转2进制 如 23转二进制10111,需要栈辅助 每次除2的余数倒过来写3.mai…

Vue3(一):创建vue3工程、setup、vue3响应式原理、computed和watch

Vue3:第一章 一、创建Vue3.0工程1.使用vue-cli创建2.使用vite创建 二、Vue3中的响应式1.拉开序幕的setup2.ref函数3.reactive函数4.vue3中响应式的原理(1)vue2中响应式原理(2)Vue3中的Proxy 5.reactive和ref的对比6.se…

性能测试如何做?超详细性能测试-测试策略总结,新人进阶之路...

目录:导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜) 前言 性能测试出现的初…

YOLOv5【训练train.py逐行源码及参数调参解析】超详细解读!!!建议收藏✨✨!

之前的文章介绍了YOLOv5的网络结构🚀与目录结构源码🚀以及detect.py🚀的详细解读,今天带来的是YOLOv5的 train.py 代码参数逐行解读以及注释,废话不多说,让我们一起学习YOLOv5的 train.py 源码吧&#xff0…

功能测试和自动化测试的差距在哪里?

一直以来,软件的测试主要是以手工测试为主,但是随着现代软件的复杂程度的加深,人们对使用手工方式来完成软件测试感到的越来越力不从心,同时因为在软件测试中存在着大量的重复性工作,而这种工作是比较适合机器而不是人…

rsync

配置rsync源服务器: #建立/etc/rsyncd.conf 配置文件 vim /etc/rsyncd.conf #添加以下配置项 uid root gid root use chroot yes #禁锢在源目录 address 192.168.80.10 …

​Kali-linux无线网络嗅探工具Kismet​

如果要进行无线网络渗透测试,则必须先扫描所有有效的无线接入点。刚好在Kali Linux中,提供了一款嗅探无线网络工具Kismet。使用该工具可以测量周围的无线信号,并查看所有可用的无线接入点。本节将介绍使用Kismet工具嗅探无线网络。 &#xf…

MySQL_6 自连接和外连接

目录 一、自连接 1.概述 : 2.语法 : 3.演示 : 二、外连接 1.为什么需要外连接? 2.外连接的定义 : 3.外连接的演示 : 1 左外连接 2 右外连接 3 对部门表问题的解决 一、自连接 1.概述 : 自连接是指在同一张表上的连接查询(将同一张看做两张表)&a…