目的
生产环境遇上崩溃的问题怎么办?这真是一个令人头疼的问题,记得我以前遇上这个问题,全靠日志进行推测,那样效率真是太慢了。
现在说的是在windows环境上如何调试,下面一步步,说说这个过程:
过程
代码方面需要生成崩溃文件
崩溃文件记录崩溃的详情,这是最为关键的一步,代码如下:
// main.cpp
#include <QCoreApplication>
#include <QVector>
#include <QDebug>
#include <QDir>
#include <QDateTime>
#include <windows.h>
#include <dbghelp.h>
#include <tchar.h>
#pragma comment(lib, "dbghelp.lib")
// ============================================================
// 生成 Dump 文件的函数
// ============================================================
void WriteDumpFile(EXCEPTION_POINTERS* pExceptionInfo)
{
// Dump 文件保存到程序所在目录,文件名带时间戳
QString dumpDir = QCoreApplication::applicationDirPath();
QString dumpName = QString("crash_%1.dmp")
.arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"));
QString dumpPath = QDir(dumpDir).filePath(dumpName);
// 转换为宽字符路径(MiniDumpWriteDump 需要 wchar_t*)
std::wstring wDumpPath = dumpPath.toStdWString();
// 创建 Dump 文件
HANDLE hFile = CreateFileW(
wDumpPath.c_str(),
GENERIC_WRITE,
0,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
qDebug() << "CreateFile failed, error:" << GetLastError();
return;
}
// Dump 类型:包含完整内存、句柄、线程、未加载模块等
// 越详细越好,方便离线分析
MINIDUMP_TYPE dumpType = (MINIDUMP_TYPE)(
MiniDumpWithFullMemory | // 完整内存
MiniDumpWithFullMemoryInfo | // 内存区域信息
MiniDumpWithHandleData | // 句柄信息
MiniDumpWithThreadInfo | // 线程信息
MiniDumpWithUnloadedModules | // 已卸载模块
MiniDumpWithProcessThreadData | // 进程/线程数据
MiniDumpWithFullAuxiliaryState | // 辅助状态
MiniDumpIgnoreInaccessibleMemory // 忽略不可访问内存,避免二次崩溃
);
MINIDUMP_EXCEPTION_INFORMATION dumpExceptionInfo;
dumpExceptionInfo.ThreadId = GetCurrentThreadId();
dumpExceptionInfo.ExceptionPointers = pExceptionInfo;
dumpExceptionInfo.ClientPointers = FALSE;
BOOL bOK = MiniDumpWriteDump(
GetCurrentProcess(), // 目标进程
GetCurrentProcessId(), // 进程ID
hFile, // 输出文件句柄
dumpType, // Dump 类型
pExceptionInfo ? &dumpExceptionInfo : NULL, // 异常信息
NULL, // 用户自定义流
NULL); // 回调
CloseHandle(hFile);
if (bOK)
{
qDebug() << "Dump file saved to:" << dumpPath;
// 在控制台也输出一份,方便定位
OutputDebugStringW((L"Dump saved: " + wDumpPath + L"\n").c_str());
}
else
{
qDebug() << "MiniDumpWriteDump failed, error:" << GetLastError();
}
}
// ============================================================
// 未处理异常过滤器:崩溃时先写 Dump,再让系统处理
// ============================================================
LONG WINAPI MyUnhandledExceptionFilter(EXCEPTION_POINTERS* pExceptionInfo)
{
qDebug() << "========== Unhandled exception caught ==========";
qDebug() << "Exception code: 0x" << QString::number(
pExceptionInfo->ExceptionRecord->ExceptionCode, 16);
qDebug() << "Exception address: 0x" << QString::number(
(quintptr)pExceptionInfo->ExceptionRecord->ExceptionAddress, 16);
// 写 Dump
WriteDumpFile(pExceptionInfo);
// 返回 EXCEPTION_EXECUTE_HANDLER 表示已处理
// 这里返回 EXCEPTION_CONTINUE_SEARCH 让程序继续崩溃(便于调试器捕获)
// 生产环境中可以返回 EXCEPTION_EXECUTE_HANDLER 优雅退出
return EXCEPTION_EXECUTE_HANDLER;
}
// ============================================================
// 崩溃复现函数:QVector 越界写入,破坏堆
// ============================================================
void TriggerHeapCorruption()
{
qDebug() << "Creating QVector with reserve(10)...";
QVector<int> vec;
vec.reserve(10);
qDebug() << "size:" << vec.size()
<< "capacity:" << vec.capacity();
// 关键:越界写入,超出 capacity 边界,破坏相邻堆内存
// Release 模式下 operator[] 不做检查
qDebug() << "Writing out of bounds at index 100000 and beyond...";
// 故意越界写入:索引 10 超出 [0,9],且超出 capacity 边界
vec.data()[100000] = 1;
qDebug() << "Out-of-bounds write done.";
// 函数结束时 vec 析构 -> free -> 堆检查失败 -> 触发 0xC0000374
}
// ============================================================
// main
// ============================================================
int main(int argc, char* argv[])
{
QCoreApplication a(argc, argv);
// 安装未处理异常过滤器
SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
qDebug() << "Program started. PID:" << GetCurrentProcessId();
qDebug() << "Will trigger heap corruption...";
TriggerHeapCorruption();
qDebug() << "If you see this, no crash happened this run.";
return a.exec();
}
看看上面代码可知,就是把代码的异常信息,当时堆栈信息打印到一个文件里。
项目进行必须的设置
运行程序---程序崩溃
文件列表如下:
开始执行:
这个程序崩溃了,生成了dmp文件,上面可以看出: 1、c0000005,说明是内存溢出的错误,这是给我们宝贵的信息 2、0x000b2062是指崩溃的代码地址,这个地址很关键,之后找到具体那行代码出了错,全靠这个地址找啊,非常重要的地址。 虽然没有深入研究过pdb文件,但可以推知,pdb文件里就记录着代码地址与代码行的对应关系。
调试程序
在分析之前,需要确保手头有以下文件,并将它们放在同一个目录下,方便后续加载
文件类型 作用 缺失后的影响
Dump文件 (.dmp) 崩溃瞬间的内存快照 无法分析
PDB符号文件 (.pdb) 函数名、变量名、代码行号 只能看到地址,无法看到函数名和源码行
可执行文件 (.exe) 主程序二进制代码 无法反汇编主程序,堆栈不完整
依赖的DLL文件 Qt5Core.dll等依赖库 依赖库调用栈显示为地址
关键要求:exe/dll 和对应的 pdb 文件版本必须完全匹配(时间戳一致)。建议将上述所有文件放在同一个目录中,例如 D:\Crash_Analysis\。
用WinDbg打开崩溃文件:
从上面可知,加载符号后,用!analyze -v命令就很快找到问题原因,并定位到了代码行:
总结
感觉非常得简单,一下子就找到问题了。
很关键的是几个文件,内存快照文件,pdb文件,如图所示:
从上面可知,生成程序时,必须得生成pdb文件,并且放程序放到一块,否则,如何调试崩溃的程序是一个难题。
一句话总结:异常地址是 CPU 执行到的机器指令位置,通过编译器预先埋下的调试映射表,工具将这个物理地址“反查”回它所属的源代码文件及行号: