Qt调试之Dump文件调试

前言

qt开发遇到问题的时候,我们肯定需要一些调试手段。除了qt自带的gdb调试,还有一种dump的调试方式,这个之前我有看别人用过,但自己一直没有亲自尝试过,所以现在刚好学会了,想要记录一下。
先记录一下转载的文章:
Qt调试技巧之QtCreator调试

Qt调试技巧之使用Dump文件调试

一、生成dump文件

想要使用dump,我们需要用到相关的lib。
dbghelp.lib 是 Windows SDK 的一部分,通常已经安装在你的系统中,所以我们无需特意去找第三方库,这样使用起来还是很方便的。
注意,MinGW 无法使用 dbghelp.lib!它是 MSVC 专用的库。
在写相关函数代码之前,需要先关注它Qt 项目中正确链接方法:
方法一:CMake 方式(推荐 for Qt6)
cmake
在 target_link_libraries 中直接添加

target_link_libraries(CrashTest PRIVATE 
    Qt6::Widgets
    dbghelp  # 添加这行,无需 -l 前缀
)

方法二:qmake 方式(.pro 文件)
pro

注意:Windows 上库名是区分大小写的!
win32: LIBS += -lDbgHelp  # 推荐这种写法
或
win32: LIBS += -ldbghelp  # 小写也可以

然后,我们开始添加代码,这段代码是我让ai写的,我觉得无需太过关注里面具体是怎么写的,只知道程序发生崩溃的时候,会生成dump文件即可。
这是一个minidumper.h:

#ifndef MINIDUMPER_H
#define MINIDUMPER_H

#include <QtGlobal>
#ifdef Q_OS_WIN
#include <windows.h>
#include <DbgHelp.h>
#include <QString>
#include <QDateTime>
#include <QMessageBox>

class MiniDumper
{
public:
    static MiniDumper& instance()
    {
        static MiniDumper dumper;
        return dumper;
    }

    void install(const QString& dumpFilePath = QString())
    {
        m_dumpPath = dumpFilePath.isEmpty() ?
                         generateDefaultDumpName() : dumpFilePath;
        ::SetUnhandledExceptionFilter(exceptionFilter);
    }

private:
    MiniDumper() = default;
    ~MiniDumper() = default;
    MiniDumper(const MiniDumper&) = delete;
    MiniDumper& operator=(const MiniDumper&) = delete;

    static QString generateDefaultDumpName()
    {
        // 生成带时间戳的文件名: Crash_20241127_153000.dmp
        return QString("Crash_%1.dmp")
            .arg(QDateTime::currentDateTime()
                     .toString("yyyyMMdd_hhmmss"));
    }

    static LONG WINAPI exceptionFilter(EXCEPTION_POINTERS* exceptionInfo)
    {
        // 避免在调试器下运行时生成 dump
        if (::IsDebuggerPresent()) {
            return EXCEPTION_CONTINUE_SEARCH;
        }

        auto& dumper = MiniDumper::instance();

        // 创建 dump 文件
        HANDLE hFile = ::CreateFile(
            reinterpret_cast<LPCWSTR>(dumper.m_dumpPath.utf16()),
            GENERIC_WRITE,
            0,
            nullptr,
            CREATE_ALWAYS,
            FILE_ATTRIBUTE_NORMAL,
            nullptr
            );

        if (hFile != INVALID_HANDLE_VALUE) {
            MINIDUMP_EXCEPTION_INFORMATION mei;
            mei.ThreadId = ::GetCurrentThreadId();
            mei.ExceptionPointers = exceptionInfo;
            mei.ClientPointers = FALSE;

            // 写入 dump 文件(包含内存和调用栈)
            ::MiniDumpWriteDump(
                ::GetCurrentProcess(),
                ::GetCurrentProcessId(),
                hFile,
                MiniDumpWithFullMemory,  // 完整内存信息
                exceptionInfo ? &mei : nullptr,
                nullptr,
                nullptr
                );

            ::CloseHandle(hFile);

            qDebug() << "崩溃!Dump 文件已生成:" << dumper.m_dumpPath;
        }

        // 显示错误对话框(可选)
        QMessageBox::critical(nullptr, "程序崩溃",
                              QString("程序遇到未处理的异常并已崩溃。\nDump 文件已生成:\n%1")
                                  .arg(dumper.m_dumpPath));

        return EXCEPTION_EXECUTE_HANDLER;
    }

    QString m_dumpPath;
};

#endif // Q_OS_WIN
#endif // MINIDUMPER_H

然后在main中调用:

#include <QApplication>
#include "mainwindow.h"
#include "minidumper.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

#ifdef Q_OS_WIN
    // Debug 模式下不安装(调试器会直接捕获)
#ifdef QT_NO_DEBUG
    MiniDumper::instance().install();
#endif
#endif

    MainWindow w;
    w.show();
    return a.exec();
}

然后程序运行,发生崩溃的时候就会生成dmp文件了。
在这里插入图片描述
这里还有一个pdb文件,之后会用到。
有关测试代码,我这里也放上来吧。

#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include <QDebug>
#include <QMessageBox>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 创建示例用的已删除对象
    deletedWidget = new QWidget();
    delete deletedWidget;  // 立即删除,制造悬挂指针
}

MainWindow::~MainWindow()
{
    delete ui;
}

// ========== 示例1:空指针解引用 ==========
void MainWindow::on_nullPtrButton_clicked()
{
    qDebug() << "触发空指针解引用...";

    int* p = nullptr;
    // 崩溃点:对空指针进行写操作
    *p = 42;  // SIGSEGV here

    // 这行代码永远不会执行
    qDebug() << "值:" << *p;
}

/*
崩溃原因分析:
- p是一个空指针(地址为0)
- 试图向地址0写入数据42
- 操作系统阻止对地址0的访问,触发SIGSEGV

GDB调试方法:
gdb ./SegmentFaultDemo
(gdb) run
点击"空指针"按钮后:
(gdb) backtrace
#0  0x000055555555d8b6 in MainWindow::on_nullPtrButton_clicked() at mainwindow.cpp:29
#1  ... Qt信号槽调用栈

(gdb) frame 0
(gdb) info locals
p = 0x0  <-- 确认指针为空
(gdb) p p
$1 = (int *) 0x0

解决方案:
*/
// 正确代码:
void fixed_nullPtrExample()
{
    int* p = new int(0);  // 分配有效内存
    if (p) {              // 检查指针有效性
        *p = 42;
        qDebug() << "值:" << *p;
        delete p;         // 释放内存
    }
}

// ---

   // ========== 示例2:数组越界访问 ==========
   void MainWindow::on_arrayOutOfBoundsButton_clicked()
{
    qDebug() << "触发数组越界...";

    int arr[5] = {1, 2, 3, 4, 5};

    // 崩溃点:访问越界内存
    int value = arr[100];  // SIGSEGV here (或读取到垃圾数据)
    arr[100] = 999;        // 更可能立即崩溃

    qDebug() << "越界值:" << value;
}

/*
崩溃原因分析:
- 数组只有5个元素(索引0-4)
- 访问索引100越过了数组边界
- 访问了未分配或保护的内存页

GDB调试方法:
(gdb) run
点击"数组越界"按钮后:
(gdb) backtrace
#0  0x000055555555d9e8 in MainWindow::on_arrayOutOfBoundsButton_clicked() at mainwindow.cpp:44

(gdb) info locals
arr = {1, 2, 3, 4, 5}
value = 32767  <-- 垃圾值

解决方案:
*/
// 正确代码:
void fixed_arrayExample()
{
    int arr[5] = {1, 2, 3, 4, 5};
    const size_t size = std::size(arr);  // C++17获取数组大小

    for (size_t i = 0; i < size; ++i) {
        qDebug() << "arr[" << i << "] = " << arr[i];
    }

    // 或者使用Qt的容器
    QVector<int> vector = {1, 2, 3, 4, 5};
    int safeIndex = 2;  // 添加这行:声明一个安全的索引
    if (safeIndex >= 0 && safeIndex < vector.size()) {  // 完整的边界检查
        vector[safeIndex] = 999;
    }
}

// ---

   // ========== 示例3:使用已删除的对象(悬挂指针) ==========
   void MainWindow::on_danglingPtrButton_clicked()
{
    qDebug() << "触发悬挂指针...";

    // deletedWidget已经在构造函数中被删除
    if (deletedWidget) {  // 指针非空,但对象已失效
        // 崩溃点:访问已删除对象的成员
        deletedWidget->setObjectName("Crash");  // SIGSEGV here
        deletedWidget->show();
    }
}

/*
崩溃原因分析:
- deletedWidget指向的内存已被释放
- 但指针本身没有被置为nullptr
- 访问已释放内存导致未定义行为

GDB调试方法:
(gdb) run
点击"悬挂指针"按钮后:
(gdb) backtrace
#0  0x00007ffff7b2d8a0 in QWidget::setObjectName(QString const&) ...

(gdb) p deletedWidget
$1 = (QWidget *) 0x55555556e0d0  <-- 指针非空,但指向无效内存

(gdb) p *deletedWidget
$2 = {...}  <-- 可能显示垃圾数据或触发SIGSEGV

解决方案:
*/
// 正确代码:
class SafeMainWindow : public QMainWindow
{
    Q_OBJECT
    QPointer<QWidget> safePtr;  // QPointer会在对象删除时自动置空

public:
    SafeMainWindow() {
        safePtr = new QWidget();
        delete safePtr;  // 删除后safePtr自动变为nullptr
    }

    void useWidget() {
        if (safePtr) {  // 安全判断
            safePtr->show();
        } else {
            qDebug() << "Widget已被删除";
        }
    }
};

// ---

   // ========== 示例4:双重释放 ==========
   void MainWindow::on_doubleDeleteButton_clicked()
{
    qDebug() << "触发双重释放...";

    int* p = new int(42);
    delete p;  // 第一次释放

    // 崩溃点:重复释放同一块内存
    delete p;  // SIGSEGV here (或内存损坏)
}

/*
崩溃原因分析:
- 同一块内存被释放两次
- 第二次释放时,内存可能已分配给其他对象
- 破坏内存管理结构,导致堆损坏

GDB调试方法:
(gdb) run
点击"双重释放"按钮后:
*** Error in `./SegmentFaultDemo': double free or corruption (fasttop): 0x00005555556e0d30 ***

(gdb) backtrace
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:58

解决方案:
*/
// 正确代码:
void fixed_doubleDelete()
{
    int* p = new int(42);
    delete p;
    p = nullptr;  // 置空指针,防止悬挂

    if (p) {      // 安全判断
        delete p; // 不会执行
    }

    // 或者使用智能指针
    std::unique_ptr<int> smartPtr = std::make_unique<int>(42);
    // 自动管理,无需手动delete
}

// ---

   // ========== 示例5:栈溢出 ==========
   void recursiveFunction(int depth)
{
    // char buffer[1024];  // 每次调用分配1KB栈空间
    char buffer[10240000];  // 每次调用分配1KB栈空间
    qDebug() << "depth:" << depth;

    // 崩溃点:无限递归导致栈空间耗尽
    recursiveFunction(depth + 1);  // SIGSEGV here

    // 这行永远不会执行
    buffer[0] = '\0';
}

void MainWindow::on_stackOverflowButton_clicked()
{
    qDebug() << "触发栈溢出...";
    recursiveFunction(0);
    // fixed_recursiveFunction(0);
}

/*
崩溃原因分析:
- 每次函数调用消耗栈空间(局部变量+调用帧)
- 无限递归迅速耗尽有限的栈空间(通常8MB)
- 访问栈保护页触发SIGSEGV

GDB调试方法:
(gdb) run
点击"栈溢出"按钮后:
Program received signal SIGSEGV, Segmentation fault.
0x000055555555dc28 in recursiveFunction (depth=12345) at mainwindow.cpp:95

(gdb) backtrace
#0  0x000055555555dc28 in recursiveFunction (depth=12345)
#1  0x000055555555dc3d in recursiveFunction (depth=12344)
#2  0x000055555555dc3d in recursiveFunction (depth=12343)
... 重复数百行

(gdb) frame 0
(gdb) info locals
depth = 12345
buffer = "...."

解决方案:
*/
// 正确代码:
void fixed_recursiveFunction(int depth)
{
    if (depth > 100) {  // 递归深度限制
        qDebug() << "达到最大递归深度";
        return;
    }

    char buffer[1024];
    qDebug() << "递归深度:" << depth;

    // 业务逻辑...

    fixed_recursiveFunction(depth + 1);  // 有限递归
}

二、查看dmp文件

直接用vs打开这个dmp文件。
值得一提的是,我是用qt6.10+vs2022来测试的,但我电脑只装了vs2017的软件,vs2022只是配套qt6的构建包而已。结果vs2017也可以打开这个dmp文件,这是我没想到的。
打开后,界面如下:
在这里插入图片描述
它会告诉你崩溃的大致原因,但这也并不能帮助我们排查实际的问题。
这个时候,我们需要在这里调试程序。
首先,windeployqt补充一下qt的dll,这个传统艺能了,就不多说。
然后,设置一下路径:
在这里插入图片描述
这里的路径是pdb文件的路径,一般就是和exe和dmp相同的目录下:
然后,我们进行调试:
可以看到,它直接给我们定位到具体的代码行了:
在这里插入图片描述
原来是因为析构指针后,没有及时置空,导致我们使用了空指针,进而程序崩溃。
在这里插入图片描述
我们还可以看到调用对象和堆栈信息等。

三、总结

dump只是Windows平台下,vs构建的一种调试方式而已,而且也不是万能的。我个人其实比较偷懒,有时候会更愿意自己查代码,添加调试打印信息,顶多添加断点。这个习惯的结果就是,找工作的时候被问到你会怎么查找问题,就说不太上来,让人感觉你很不专业。
现在,起码亲自跑通过一遍,能说一些东西了吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值