前言
qt开发遇到问题的时候,我们肯定需要一些调试手段。除了qt自带的gdb调试,还有一种dump的调试方式,这个之前我有看别人用过,但自己一直没有亲自尝试过,所以现在刚好学会了,想要记录一下。
先记录一下转载的文章:
Qt调试技巧之QtCreator调试
一、生成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构建的一种调试方式而已,而且也不是万能的。我个人其实比较偷懒,有时候会更愿意自己查代码,添加调试打印信息,顶多添加断点。这个习惯的结果就是,找工作的时候被问到你会怎么查找问题,就说不太上来,让人感觉你很不专业。
现在,起码亲自跑通过一遍,能说一些东西了吧。


1万+

被折叠的 条评论
为什么被折叠?



