qt生成dump文件并定位异常

一、什么是Dump文件

对于程序崩溃,最快的解决方式是生成dump文件,通过生成dump文件使用调试工具进行调试,还原程序崩溃时的状态,能够起到快速定位排查问题的作用。dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中。Dump文件是用来给驱动程序编写人员调试驱动程序用的,这种文件必须用专用工具软件打开。

二、Dump文件的类型

2.1 内核模式Dump

Dump文件分为两大类,内核模式Dump和用户模式Dump。内核模式Dump是操作系统创建的崩溃转储,最经典的就是系统蓝屏,这时候会自动创建内核模式的Dump。

2.2 用户模式Dump

用户模式Dump进一步可以分为完整Full Dump和Mini dump。Full Dump包含了某个进程完整的地址空间数据,以及许多用于调试的信息,而Mini dump则有许多类型,根据需要可以包含不同的信息,有的可能只包含某个线程和部分模块的信息。

三、Dump文件的生成

Dump文件能够保存程序内部的内存、堆栈、句柄、线程等程序运行相关的信息,非常具有重要性。

3.1 通过调试工具生成

通过调试工具创建。调试工具如Visual Studio,Windbg以及微软提供的ADplus都可以创建DUMP,在Windbg中通过.dump命令来生成。

3.2 通过使用任务管理器生成

该方式可以生成.DMP文件,通过打开任务管理器,找到插件服务对应的进程,右击,选择创建转储文件

3.3 通过编程自动生成

当程序遇到未处理异常(主要指非指针造成)导致程序崩溃死,如果在异常发生之前调用了SetUnhandledExceptionFilter()函数,异常交给函数处理。因而,在程序开始处增加SetUnhandledExceptionFilter()函数,并在函数中利用适当的方法生成Dump文件,即可实现需要的功能。

在编程过程中,可以预期的异常都通过结构化异常(try/catch)进行了处理。此时,如果发生了未预期的异常,这些异常处理代码无法处理,则转由Windows提供的默认异常处理器来进行处理,这个特殊的异常处理函数为UnhandledExceptionFilter。该函数会显示一个消息框,提示发生了未处理的异常,同时,让用户选择结束或调试该进程。也就是如下界面:

因此,为了更友好的处理未预期的异常(主要是创建内存转储),可以覆盖默认的异常处理操作。这是通过函数SetUnhandledExceptionFilter完成的,函数原型如下

cpp 复制代码
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(
        _In_ LPTOP_LEVEL_EXCEPTION_FILTER  lpTopLevelExceptionFilter

lpTopLevelExceptionFilter即异常处理函数指针,如果设置为NULL,则默认使用UnhandledExceptionFilter。因此我们可以对照lpTopLevelExceptionFilter自定义一个异常处理函数。我们需要创建内存转储。这通过函数MiniDumpWriteDump来实现。

下述代码是一个通过MiniDumpWriteDump函数来实现转储文件创建

cpp 复制代码
LONG WINAPI MyUnhandledExceptionFilter( struct _EXCEPTION_POINTERS *ExceptionInfo )
{
    HANDLE hFile = CreateFile("mini.dmp", GENERIC_READ|GENERIC_WRITE,
        FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
 
    if( hFile == INVALID_HANDLE_VALUE )
        return EXCEPTION_EXECUTE_HANDLER;
 
    MINIDUMP_EXCEPTION_INFORMATION mdei;
    mdei.ThreadId = GetCurrentThreadId();
    mdei.ExceptionPointers = ExceptionInfo;
    mdei.ClientPointers = NULL;
    MINIDUMP_CALLBACK_INFORMATION mci;  
    mci.CallbackRoutine     = NULL;  
    mci.CallbackParam       = 0;  
 
    MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, MiniDumpNormal, &mdei, NULL, &mci);  
 
    CloseHandle(hFile);
 
    AfxMessageBox("已成功创建崩溃转储!");
 
     return EXCEPTION_EXECUTE_HANDLER;
}

原文链接:https://blog.csdn.net/qq_39543984/article/details/123968834

3.4.代码实现:

3.4.1声明函数

DumpHelper.h

cpp 复制代码
//#ifndef DUMPHELPER_H
//#define DUMPHELPER_H

#pragma once

#include <QSystemSemaphore>
#include <QDir>
#include <QDateTime>
#include <QDebug>

#include <tchar.h>
#include <Windows.h>
#include <DbgHelp.h>

#pragma comment(lib, "user32.lib")
#pragma comment(lib, "DbgHelp.Lib")

int GenerateMiniDump(PEXCEPTION_POINTERS pExceptionPointers)
{
    // 定义函数指针
    typedef BOOL(WINAPI * MiniDumpWriteDumpT)(
        HANDLE,
        DWORD,
        HANDLE,
        MINIDUMP_TYPE,
        PMINIDUMP_EXCEPTION_INFORMATION,
        PMINIDUMP_USER_STREAM_INFORMATION,
        PMINIDUMP_CALLBACK_INFORMATION
        );
    // 从 "DbgHelp.dll" 库中获取 "MiniDumpWriteDump" 函数
    MiniDumpWriteDumpT pfnMiniDumpWriteDump = NULL;
    HMODULE hDbgHelp = LoadLibrary(_T("DbgHelp.dll"));
    if (NULL == hDbgHelp)
    {
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    pfnMiniDumpWriteDump = (MiniDumpWriteDumpT)GetProcAddress(hDbgHelp, "MiniDumpWriteDump");

    if (NULL == pfnMiniDumpWriteDump)
    {
        FreeLibrary(hDbgHelp);
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    // 创建 dmp 文件件
    TCHAR szFileName[MAX_PATH] = { 0 };
    TCHAR szVersion[] = L"DumpFile";
    SYSTEMTIME stLocalTime;
    GetLocalTime(&stLocalTime);
    wsprintf(szFileName, L"%s-%04d%02d%02d-%02d%02d%02d.dmp",
        szVersion, stLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay,
        stLocalTime.wHour, stLocalTime.wMinute, stLocalTime.wSecond);
    HANDLE hDumpFile = CreateFile(szFileName, GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_WRITE | FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0);
    if (INVALID_HANDLE_VALUE == hDumpFile)
    {
        FreeLibrary(hDbgHelp);
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    // 写入 dmp 文件
    MINIDUMP_EXCEPTION_INFORMATION expParam;
    expParam.ThreadId = GetCurrentThreadId();
    expParam.ExceptionPointers = pExceptionPointers;
    expParam.ClientPointers = FALSE;
    pfnMiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
        hDumpFile, MiniDumpWithDataSegs, (pExceptionPointers ? &expParam : NULL), NULL, NULL);
    // 释放文件
    CloseHandle(hDumpFile);
    FreeLibrary(hDbgHelp);
    return EXCEPTION_EXECUTE_HANDLER;
}

LONG WINAPI ExceptionFilter(LPEXCEPTION_POINTERS lpExceptionInfo)
{
    // 这里做一些异常的过滤或提示
    if (IsDebuggerPresent()) {
        return EXCEPTION_CONTINUE_SEARCH;
    }
    return GenerateMiniDump(lpExceptionInfo);
}

long __stdcall errCallback(_EXCEPTION_POINTERS*  pException)
{
 
//     //用于崩溃重启
//    // 信号量的意义,把操作共享内存的代码锁住。因为有可能同时启动, 防止并发
//    QSystemSemaphore sema("DyError", 1, QSystemSemaphore::Open);
//    sema.acquire();


    QDir dir;
    dir.mkdir("./dumps");
    dir.cd("./dumps");

    /*
      ***保存数据代码***
    */

    QString fileName = dir.path() + "/" +
                       QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss.zzz") + ".dmp";

    LPCWSTR pFileName = (LPCWSTR)fileName.unicode();

    //创建 Dump 文件
    HANDLE hDumpFile = CreateFile(pFileName,
                                  GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    qDebug() << "create dumpFile:" << hDumpFile << INVALID_HANDLE_VALUE;
    if(hDumpFile != INVALID_HANDLE_VALUE)
    {
        //Dump信息
        MINIDUMP_EXCEPTION_INFORMATION dumpInfo;
        dumpInfo.ExceptionPointers = pException;
        dumpInfo.ThreadId = GetCurrentThreadId();
        dumpInfo.ClientPointers = TRUE;
        
        // ::全局作用域符号
        // 写入Dump文件内容
        // DumpType这里仅仅保存普通信息。假如需要保存变量值,可以加上【MiniDumpWithFullMemory】
        // 参考https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ne-minidumpapiset-minidump_type
        ::MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, MiniDumpNormal, &dumpInfo, NULL, NULL);
//        ::MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, MiniDumpNormal | MiniDumpWithFullMemory, &dumpInfo, NULL, NULL);
    }

//    delete  unimem;

//    qDebug() << "start application:" << QProcess::startDetached(qApp->applicationFilePath(), QStringList());//重启

//    qApp->quit();
    qApp->exit(-1);

    return EXCEPTION_EXECUTE_HANDLER;
}


//#endif // DUMPHELPER_H

3.4.2.调用函数

在main函数中注册

main.cpp

cpp 复制代码
#include "mainwindow.h"

#include <QApplication>

#include "DumpHelper.h"

int main(int argc, char *argv[])
{
//    SetUnhandledExceptionFilter(ExceptionFilter); // dmp文件比较大
    SetUnhandledExceptionFilter(errCallback); // dmp文件比较小

    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

2、版本发布

方式1:选择release编译模式,勾选Generate separate debug info复选框,生成符号文件pdb。

方式2:选择Profile编译模式,生成符号文件pdb。

(注:发布版本后需要将生成的pdb文件、exe文件以及源码备份)

需补充:为什么要使用Profile编译模式,有什么优点?以上两种方式有没有区别?

3、使用VS分析dump文件,定位崩溃位置

可参考:Qt下生成pdb文件,并在exe崩溃时生成dmp文件,且由dmp查询崩溃原因_qt生成pdb文件-CSDN博客

3.1程序崩溃后,使用vs将生成的.dmp文件打开。

3.2手动设置符号路径

1、点击窗口右上侧的"设置符号路径";

2、选择QT源码路径;

3、选择备份的pdb和exe所在路径;

4、选择缓存符号路径;

3.3手动设置源码路径

1、选择"视图"->"解决方案资源管理器",打开"解决方案资源管理器"窗口;

1、

2、右击"解决方案solution1(0项目)",选择"属性",打开"解决方案Solution1属性页"窗口;

3、在"调试源文件"页面添加源码所在路径;

3.4进行调试

1、点击窗口右上侧的"使用仅限本机进行调试",在VS加载系统pdb的时候,点击"取消"按钮,如下图,VS会自动把源代码打开,并指出异常崩溃的地方。(防止VS将异常定位到系统模块的符号,而非源代码上)

2、VS分析完后,会自动将异常定位到源代码位置,如下图:

3.5不取消加载系统pdb的结果

如下图。出现未加载 wntdll.pdb 报错大概率是你的指针使用错误 ,比如使用野指针、越界访问、或者堆区空间释放方式错误等。

Qt开发 之 抓取崩溃信息(读这一篇就够了)_wx635f8a025188b的技术博客_51CTO博客

4、使用windbg分析dmp文件,定位崩溃位置

4.1打开windbg,添加符号路径(1)、源码路径(2)、加载dmp文件(3):

4.2 加载符号路径

符号路径下文件内容:

4.3加载源码路径

多路径使用分号(该项目使用插件化架构),如下图

4.4 使用命令"!analyze -v"查看堆栈信息

1、获取堆栈信息如图,图中编号1处为有效的代码错误信息

4.5 使用命令"u BaseProject!CModuleString::createException+0x1a"查看源码错误行号

代码结构如下图所示,baseProject为项目中的一个插件,CommonIO为调用该插件的子项目。

注意事项:

windbg版本应该与生成项目的版本一致,如下图所示,生成项目使用的为msvc 2015 32bit,则windgb使用的为x86版本。

总结:

1、使用windgb分析dmp文件比VS分析 更有优势,windgb总能定位到具体的源码位置,VS成功概率并非100%。

5.windbg常用命令

5.1 基础调试命令

  1. 寄存器操作

  2. r:显示/修改寄存器和标志位状态2

  3. rm:控制寄存器显示掩码,筛选特定寄存器2

  4. 执行控制

  5. p/t:单步执行(p跳过函数调用,t进入函数)

  6. .excr:重置当前线程上下文

5.2内存分析命令

  1. 堆内存分析

    • !heap:显示堆内存分配详情(对象列表、类型统计等)4
    • !address -summary:查看进程内存分布(含托管/非托管堆)5
  2. 内存泄漏检测

    • !analyze -v:自动分析崩溃转储文件,优先执行此命令获取错误上下文
    • 导出自动分析结果
bash 复制代码
##导出自动分析结果到D盘的Analysis_Report.txt文件中
.logopen D:\Analysis_Report.txt
!analyze -v
.logclose

WinDbg中!analyze -v命令分析DMP文件详解

一、核心字段解析

  1. BUG_CHECK_CODE

表示系统崩溃时的停止代码(Stop Code),例如 0x124(硬件错误)、 0x3B(系统服务异常)等。代码对应具体错误类型,是诊断的起点1。

  1. PROCESS_NAME

崩溃时正在运行的进程名称,如svchost.exe或第三方软件进程。若进程属于用户程序,可能指向应用程序兼容性问题2。

  1. MODULE_NAME

导致崩溃的驱动或模块名称,例如nvlddmkm.sys(NVIDIA显卡驱动)。此字段直接关联故障源,需优先检查对应驱动或软件的更新/兼容性1。

  1. TRAP_FRAME

包含崩溃时的寄存器状态和堆栈信息,例如rax=00000000表示寄存器值。通过分析寄存器可定位异常触发点,如空指针访问或内存越界3。

  1. STACK_TEXT

堆栈调用跟踪,显示崩溃前函数调用链。

5.2.1.windbg STACK_TEXT(堆栈信息)含义

WinDbg 显示的堆栈信息通常由多列表示,每列都有特定的意义。以下是对常见几列含义的具体解释:

1.地址(Address)

第一列为当前帧在内存中的起始地址。每个地址代表了一个函数调用的位置或者是某个返回指令前的状态快照。点击某一行即可将该处设为活动环境,方便进一步检查局部变量等内容。

2.模块名(Module Name)

这是包含此地址代码段所属动态链接库(.dll)或可执行文件(exe)的名字标识符。了解哪部分程序正在被执行有助于快速锁定问题来源范围。

3.函数原型(Function Prototype)

它给出了准确无误的功能入口描述形式------即包括完整命名空间、类别成员资格以及参数清单在内的规范表达式。对于识别递归深度或者追踪复杂控制流而言至关重要。

4.源码位置(Source Location)如有

若已加载适当符号并且路径指向有效的原始文本,则会附加显示实际所在脚本里的确切地方(如行数)。这对于从机器级回溯至高级语言层非常有价值。

5.偏移量Offset

指的是相对于函数头部的实际位移数值,可以帮助确定究竟是在哪一个内部步骤产生了故障等情况。

5.2.2如何进一步利用windbg显示堆栈信息里的偏移量分析问题,如获取源码位置

在WinDbg调试过程中,堆栈信息中的偏移量可以帮助我们定位程序崩溃的位置、函数调用顺序以及更深入地理解代码运行状态。以下是关于如何分析堆栈信息中的偏移量的一些关键点:

1.偏移量的基本含义

堆栈信息通常会包含类似这样的内容:ntdll!R

tlAllocateHeap+0x1234

ntd1l!RtlAllocateHeap表示当前指令所在的模块(ntdll.dl1)和具体函数名称(RtlAllocateHeap )。

+0x1234则表示从该函数入口地址开始计算的偏移量,即当前执行位置距离函数起始地址的具体字节数。

分析步骤:

1.确定模块与函数名

模块(如 ntdll)提供了上下文环境。

函数名指出了代码的大致作用区域。

2.查看反汇编代码

使用命令.disasm /f <address>i

或直接输入 u <module>!<function>+<offs et>查看对应地址处的汇编代码片段。例

如:

u ntdll!RtlAllocateHeap+0x1234

这样可以清楚看到当前偏移量指向的具体机

器码及其解释。

3.检查源文件及行号(如果有符号支持)

如果加载了正确的PDB符号表,则除了偏移量外还会有对应的源文件路径和行数提示。这极大地方便了对高层语言代码的理解。

4.结合其他调试工具进一步探索

当前断点附近的寄存器值、内存布局等也可能影响最终结论,因此需要综合考虑所有可用数据来完成全面诊断。

示例解析

假设你遇到了下面这条堆栈记录:

eax=00000000 ebx=7ffdf000 ecx=Obadf00 eip=77b9abc6 esp=0018f9bc ebp=0018fac CS=001b SS=0023 ds=0023 es=0023 fs=00

ntdll!RtlAllocateHeap+0x1a2:

77b9abc6 c706feffffff mov dwor

通过上面提到的技术手段逐步展开剖析过程.....

首牛阳确这早发生在 RtlAllocateHean 内部的一

个动作;然后利用disassembly功能获取确切的行为模式,并且评估是否合理合法------比如这里尝试向某个特定位置写入固定数值FFFFFFFE。如果发现异常则需回溯到更高层呼叫者那里去挖掘根本原因。

5.3 断点管理

  1. 断点设置

  2. bp <地址>:在指定地址设置软件断点3

  3. bm <模式>:通过通配符批量设置断点(如bm module!func*)3

  4. 断点列表

  5. bl:列出所有活动断点及状态

5.4 系统级操作

  1. 模块管理

    • .reload:强制重新加载符号文件
    • .chain:列出所有已加载扩展模块
  2. 转储文件

    • .dump /ma <文件名>:生成包含完整内存信息的转储文件

5.5 线程控制

• ~*k:显示所有线程调用栈

• ~<线程号>s:切换到指定线程上下文2

5、windbg常用内存分析指令

  1. 堆内存分析

    • !heap:显示堆内存分配详情(对象列表、类型统计等)4
    • !address -summary:查看进程内存分布(含托管/非托管堆)5
  2. 内存泄漏检测

    • !analyze -v:自动分析崩溃转储文件,优先执行此命令获取错误上下文

5.1.ecxr命令

在Windbg中,.ecxr命令用于显示当前线程的异常上下文信息,常用于分析崩溃转储文件(如.dmp文件)时定位异常发生时的处理器状态。以下是.ecxr输出信息的详细解释:

5.1.1.寄存器状态

输出会显示异常发生时CPU寄存器的值,例如:

  • eax=00000000:通用寄存器,可能存储函数返回值或临时数据。
  • eip=77f0b6c2:指令指针,指向触发异常的指令地址。
  • esp=0019f4b4:堆栈指针,指向当前线程的堆栈顶部。
  • ebp=0019f4d0:基址指针,用于函数调用堆栈的展开。
  • 段寄存器cs=001b, ds=0023, es=0023, fs=003b, gs=0000):用于内存分段,现代系统中fs通常指向线程环境块(TEB)。

5.1.2. 异常记录(Exception Record)

描述异常类型和详细信息:

  • ExceptionCode: c0000005 :异常代码,如 0xC0000005表示访问违规(Access Violation)。
  • ExceptionFlags: 00000000:异常标志,通常为0。
  • ExceptionAddress: 77f0b6c2 :触发异常的指令地址,与eip一致。
  • Parameters :附加参数,例如:
    • 访问违规时,参数1( 0)表示读操作,参数2( 00420000)表示访问的无效内存地址。

5.1.3. 上下文记录(Context Record)

包含完整的线程上下文,如:

  • ContextFlags :标识哪些寄存器/状态被捕获(如CONTEXT_INTEGER表示通用寄存器有效)。
  • SegGs/SegFs:段寄存器值,与线程执行环境相关。
  • Dr0-Dr7:调试寄存器,用于硬件断点。

5.1.4. 关键调试步骤

  1. 定位异常代码 :通过ExceptionCode确定异常类型(如除零、非法指令)。
  2. 分析eip地址 :使用u eip反汇编触发异常的指令。
  3. 检查内存访问 :若为访问违规,用dc 参数2查看目标内存是否有效。
  4. 展开堆栈 :输入k查看调用堆栈,结合符号路径分析函数调用链。
bash 复制代码
0:000> .ecxr
eax=00000000 ebx=00000000 eip=77f0b6c2 esp=0019f4b4 ... 
ExceptionCode: c0000005 (Access violation)
ExceptionAddress: 77f0b6c2 (ntdll!RtlFreeHeap+0x00000052)
Parameters: 00000000 (Read) 00420000 (Invalid address)

0:000> u eip
ntdll!RtlFreeHeap+0x52:
77f0b6c2 8b39            mov     edi,dword ptr [ecx]  ; 尝试读取ecx指向的内存

0:000> dc 00420000
00420000  ???????? ???????? ...             ; 无效地址,说明ecx未正确初始化
相关推荐
fpcc1 小时前
跟我学C++中级篇—内存流
开发语言·c++
Cicada1281 小时前
ccvt:一个用 Rust 写的中国地图坐标系互转命令行工具
开发语言·后端·rust
1001101_QIA1 小时前
工控机网络配置
开发语言·数据库·php
程序员雷欧2 小时前
ThreadPoolExecutor 深度解析:从核心参数到源码实现的全面剖析
java·开发语言·jvm
catchadmin2 小时前
对标 npx 的 CPX PHP 的 Composer 包执行器
开发语言·php·composer
一位狮子座的程序员2 小时前
如何用RAG解决AI智能体的知识盲区?
开发语言·c#
吹什么轩3 小时前
c++复习:模拟实现list来更好的理解和应用list
开发语言·c++
小白学大数据3 小时前
Python 爬虫实战:抓取汽车之家二手车成交价格与里程数据
开发语言·爬虫·python·汽车