
简介这是一份面向Qt开发者的崩溃日志自动生成工具源码包针对GUI程序在运行中突发异常终止、难以定位堆栈的痛点提供在崩溃瞬间捕获并记录调试信息的完整实现。包内共56个文件以cpp与h源码、vcxproj与filters工程配置、log与tlog构建日志、obj与pdb编译产物为主另含pro、ui、qrc等Qt工程文件压缩包约561KB结构紧凑可直接参考集成到现有项目中。资源围绕核心转储与信号捕获展开涵盖SIGSEGV、SIGABRT等异常处理、堆栈跟踪记录、线程状态与日志输出等关键环节并附有QDebugOut等示例模块便于理解崩溃信息的采集与落盘流程。已有1501人学习下载适合具备一定C与Qt基础、希望提升软件稳定性与排错效率的开发者研读可据此搭建自己的崩溃日志收集方案缩短问题定位时间。1. Qt 崩溃现场为什么你的 dump 工具总在客户机器上失灵软件在测试机上跑了一周没事交付到客户现场第二天就闪退用户只丢来一句「点了下按钮就没了」。这种场景做 Qt 桌面端的同行基本都遇到过。Qt dump 工具配合软件崩溃自动生成日志要解决的就是这件事让程序在挂掉的那一刻自己把调用栈、线程状态、加载模块、崩溃地址落成一份可读文件而不是留一个冷冰冰的「已停止工作」对话框。它适合两类人一是用 Qt 做 Windows 桌面交付、需要远程定位线上崩溃的开发者二是想把 dump 采集、符号解析、日志归档串成一套可复现流程的团队。核心链路其实就三段崩溃时捕获异常、生成 minidump、事后用符号还原栈。难的不是写代码而是让这套机制在没装调试环境的客户机上照样跑通这也是后面几章要拆开讲的重点。2. 崩溃捕获的三种入口SetUnhandledExceptionFilter、vectored handler 与 Qt 消息处理Windows 上让 Qt 程序在崩溃时自动生成日志入口不止一个。选错入口会出现「有的崩溃能抓到、有的抓不到」的玄学现象。这一章把三种主流入口讲清楚再给出可直接抄的最小实现。2.1 三种捕获入口的适用边界第一种是SetUnhandledExceptionFilter注册一个顶层异常过滤器。当异常一路向上没人处理时系统回调你的函数此时进程还活着可以安全地写 minidump。它覆盖绝大多数访问违例、除零、栈溢出部分情况等结构化异常是桌面端最常用的入口。第二种是向量化异常处理AddVectoredExceptionHandler。它在异常分发的最前面被调用比__try/__except和顶层过滤器都早。好处是能抓到被 Qt 或第三方库内部catch掉的异常坏处是它会在正常异常流程里也被触发比如调试器断点、C 异常展开必须判断异常码再决定是否落盘否则会误报。第三种是 Qt 层面的消息处理比如重写QApplication::notify或安装消息处理器。它只能捕获 Qt 事件循环里抛出的 C 异常对段错误、访问违例这类系统级异常无能为力。所以它只能作为补充不能当主力。实际项目里我一般这么组合SetUnhandledExceptionFilter做主力AddVectoredExceptionHandler做补充专门盯那些被吞掉的异常。两者都指向同一个 dump 写入函数避免逻辑重复。2.2 最小可用的 dump 生成代码下面这段是 Windows 下 Qt 项目里最常抄的骨架放在main函数最开始注册即可。#include windows.h #include dbghelp.h #include QCoreApplication #include QDir #include QDateTime #pragma comment(lib, dbghelp.lib) // 写 minidump 的核心函数两个入口共用 static LONG WINAPI WriteDump(EXCEPTION_POINTERS* pException) { // dump 落到可执行文件同级的 crash_dumps 目录避免权限问题 QString dir QCoreApplication::applicationDirPath() /crash_dumps; QDir().mkpath(dir); QString file dir /crash_ QDateTime::currentDateTime().toString(yyyyMMdd_hhmmss_zzz) .dmp; HANDLE hFile CreateFileW( reinterpret_castconst wchar_t*(file.utf16()), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile INVALID_HANDLE_VALUE) return EXCEPTION_EXECUTE_HANDLER; MINIDUMP_EXCEPTION_INFORMATION info; info.ThreadId GetCurrentThreadId(); info.ExceptionPointers pException; info.ClientPointers FALSE; // MiniDumpWithDataSegs 保留全局变量WithHandleData 保留句柄信息 MINIDUMP_TYPE type static_castMINIDUMP_TYPE( MiniDumpWithDataSegs | MiniDumpWithHandleData | MiniDumpWithThreadInfo | MiniDumpWithIndirectlyReferencedMemory); MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, type, info, nullptr, nullptr); CloseHandle(hFile); return EXCEPTION_EXECUTE_HANDLER; } // 向量化入口只处理真正的崩溃码放过 C 异常 static LONG CALLBACK VectoredHandler(EXCEPTION_POINTERS* pException) { DWORD code pException-ExceptionRecord-ExceptionCode; if (code EXCEPTION_ACCESS_VIOLATION || code EXCEPTION_STACK_OVERFLOW || code EXCEPTION_ILLEGAL_INSTRUCTION || code EXCEPTION_INT_DIVIDE_BY_ZERO) { WriteDump(pException); } return EXCEPTION_CONTINUE_SEARCH; } int main(int argc, char* argv[]) { QApplication app(argc, argv); SetUnhandledExceptionFilter(WriteDump); AddVectoredExceptionHandler(1, VectoredHandler); // ... 正常启动逻辑 return app.exec(); }逻辑说明WriteDump是唯一落盘点两个入口都调它保证行为一致。文件名带毫秒时间戳避免同一秒多次崩溃互相覆盖。VectoredHandler里先判异常码只对访问违例、栈溢出、非法指令、整数除零这几类真正致命的码落盘其余返回EXCEPTION_CONTINUE_SEARCH交还给系统这样不会干扰正常的 C 异常展开和调试器。参数说明MINIDUMP_TYPE是取舍的关键。MiniDumpNormal最小但栈信息不全MiniDumpWithFullMemory最全但动辄几百 MB客户机器上写盘慢还可能失败。我一般用MiniDumpWithDataSegs | MiniDumpWithHandleData | MiniDumpWithThreadInfo | MiniDumpWithIndirectlyReferencedMemory这个组合既能还原调用栈和全局变量体积又控制在几 MB 到几十 MB适合远程回传。提示MiniDumpWithIndirectlyReferencedMemory会顺着指针抓一部分间接引用的内存对分析「对象已被释放还在用」这类崩溃很有用但会明显增大体积内存吃紧的嵌入式场景可以去掉。2.3 让 dump 目录可配置、可回传硬编码路径在客户机上经常翻车因为安装目录可能没有写权限。常见做法是把 dump 目录做成可配置项优先写用户目录失败再退回临时目录。static QString ResolveDumpDir() { // 优先用环境变量指定的目录方便运维统一收集 QByteArray env qgetenv(APP_DUMP_DIR); if (!env.isEmpty()) return QString::fromLocal8Bit(env); // 其次写用户 AppData权限最稳 QString userDir QStandardPaths::writableLocation( QStandardPaths::AppLocalDataLocation) /crash_dumps; if (QDir().mkpath(userDir)) return userDir; // 最后退回系统临时目录 return QDir::tempPath() /crash_dumps; }逻辑说明三级回退保证在任何权限环境下都能落盘。环境变量优先是为了让运维在批量部署时统一指定一个可回传的目录比如映射盘或同步目录。QStandardPaths::AppLocalDataLocation在 Windows 上对应%LOCALAPPDATA%/组织/应用普通用户一定有写权限比安装目录可靠得多。参数说明qgetenv返回QByteArray用fromLocal8Bit转换是为了兼容中文路径。如果你的部署环境路径全是英文用fromUtf8也行但中文 Windows 上fromLocal8Bit更稳。3. 符号解析没有 PDB 的 dump 就是一堆十六进制dump 文件本身只是内存快照没有符号文件PDB你打开它看到的全是地址等于看天书。这一章讲清楚 PDB 怎么生成、怎么和 dump 配对、怎么在没装 Visual Studio 的机器上还原调用栈。3.1 编译期就要为崩溃分析做准备很多人崩溃后才发现 release 包没生成 PDB或者 PDB 和 exe 对不上这是最常见的血泪教训。正确做法是在工程文件里就把调试信息打开并且让每个发布版本都归档对应的 PDB。# .pro 文件里Release 也要带调试信息 QMAKE_CXXFLAGS_RELEASE /Zi QMAKE_LFLAGS_RELEASE /DEBUG /OPT:REF /OPT:ICF逻辑说明/Zi让编译器生成 PDB/DEBUG让链接器生成对应的 PDB 并把调试信息关联进 exe。/OPT:REF和/OPT:ICF是 release 常规优化去掉未引用代码和合并相同函数不影响调试信息生成。这样 release 包既有优化又有符号崩溃分析才有的放矢。参数说明如果用的是 CMake对应写法是set(CMAKE_CXX_FLAGS_RELEASE ${CMAKE_CXX_FLAGS_RELEASE} /Zi)和set(CMAKE_EXE_LINKER_FLAGS_RELEASE ${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG)。MSVC 2019 及以上版本对/DEBUG配合/OPT已经处理得很好不用担心优化和调试冲突。注意每次发布都要把当次的 exe、pdb、dump 三者一起归档用版本号或构建号命名。PDB 和 exe 必须严格同一次构建产出混用会导致栈解析错位比没有符号还坑。3.2 用 cdb 或 WinDbg 还原调用栈客户回传 dump 后在分析机上用 cdb 命令行就能还原栈不必开图形界面。关键是设置好符号路径让调试器能找到你的 PDB 和系统 DLL 的符号。# 设置符号路径本地 PDB 目录 微软公共符号服务器 set _NT_SYMBOL_PATHD:\builds\v1.2.3\pdb;srv*C:\symbols*https://msdl.microsoft.com/download/symbols # 用 cdb 打开 dump自动分析并打印调用栈 cdb -z D:\dumps\crash_20240101_120000_123.dmp -c .ecxr; kb; !analyze -v; q逻辑说明_NT_SYMBOL_PATH里第一段是你自己构建产物的 PDB 目录第二段srv*...是微软公共符号服务器用来解析ntdll、kernel32这些系统模块。-z指定 dump 文件-c后面是自动执行的命令序列。.ecxr把上下文切到异常发生时的线程kb打印当前调用栈!analyze -v做一次自动分析通常会直接告诉你崩溃类型和可疑模块。参数说明kb显示栈帧和参数kp会额外显示参数值kn只显示帧号。分析 Qt 程序时栈里会混着Qt5Core.dll、Qt5Gui.dll的帧这些帧如果没有 Qt 的 PDB 就只能看到导出函数名属于正常现象重点看你自己模块的帧。!analyze -v输出里的FAULTING_IP和STACK_TEXT是最该先看的两段。3.3 符号服务器与批量归档的取舍团队协作时把 PDB 集中放到符号服务器比如基于 SymStore 的共享目录能省很多事。但小团队没必要上重型方案一个按版本号分目录的共享盘就够用。方案适用规模优点代价本地 PDB 目录单人 / 小团队零搭建成本换机器就找不到共享盘按版本归档十人以内简单直观需人工维护命名规范SymStore 符号服务器中大型团队自动索引、按 GUID 匹配需要专人维护我一般建议十人以内的团队用共享盘目录结构按产品/版本/构建号/分层每次 CI 构建自动把 exe 和 pdb 拷进去。等团队大了、版本多了再迁到 SymStore。别一上来就搭符号服务器维护成本会吃掉你本就不多的调试时间。4. 避坑与排查dump 工具最常见的五类翻车这套机制看着简单真到客户现场各种问题都会冒出来。下面五条是我踩过的坑按「现象 → 原因 → 解决」写照着排查能省不少时间。4.1 dump 文件生成了但大小为 0现象崩溃后目录里确实出现了.dmp文件但大小是 0 字节打不开。原因MiniDumpWriteDump在写盘过程中进程已经处于不稳定状态或者目标目录没有写权限CreateFileW返回了无效句柄但代码没检查直接往下走。解决在CreateFileW之后立刻判断INVALID_HANDLE_VALUE失败就换目录重试。另外MiniDumpWriteDump的返回值也要检查返回FALSE时用GetLastError看具体原因。写盘路径优先选用户目录别写安装目录。4.2 栈里全是地址看不到函数名现象用 cdb 打开 dumpkb输出的栈帧全是0x00007ff...这种地址没有函数名。原因符号路径没配对或者 PDB 和 exe 不是同一次构建。也可能是 dump 里记录的模块路径和本地 PDB 目录对不上。解决先用lm命令列出 dump 里加载的所有模块确认你的 exe 的 GUID 和时间戳再拿这个去匹配 PDB。!sym noisy打开符号加载日志能看到调试器到底在哪些路径找过 PDB。最稳的办法是每次构建把 exe 和 pdb 一起归档分析时用同一份。4.3 崩溃发生在 Qt 事件循环里栈指向 Qt 内部现象!analyze -v显示崩溃点在Qt5Core.dll或Qt5Gui.dll里看不出自己代码的问题。原因很多崩溃是「对象已析构但事件还在队列里」导致的栈顶落在 Qt 的事件分发里真正的元凶是之前某个delete或deleteLater时机不对。解决看!analyze -v里的STACK_TEXT往下翻找到第一个属于你自己模块的帧那通常是调用源头。配合MiniDumpWithIndirectlyReferencedMemory抓到的内存能看出被访问的对象是不是已经释放。这类问题多半要靠代码审查dump 只能帮你定位到大致范围。4.4 客户机器上根本不生成 dump现象本地测试崩溃能生成 dump客户机器上崩溃后什么都没有。原因客户机器上装了某些安全软件会拦截MiniDumpWriteDump这类行为或者程序以受限权限运行写盘被拒也可能是客户用的是精简版系统dbghelp.dll版本过旧。解决先确认 dump 目录是否可写让客户手动在该目录建个文件试试。如果被安全软件拦截把 dump 目录加入白名单或者改用先写内存再异步落盘的策略。dbghelp.dll建议随程序一起分发一份较新版本放在 exe 同级目录避免依赖系统版本。4.5 多线程崩溃时抓到的栈不对现象程序是多线程的崩溃后 dump 里的栈指向一个无关线程看不到真正出问题的线程。原因SetUnhandledExceptionFilter回调时GetCurrentThreadId拿到的是触发异常的线程但如果异常在别的线程被处理或者用了AddVectoredExceptionHandler没正确传ExceptionPointers就会错位。解决始终用pException-ExceptionRecord里的信息MINIDUMP_EXCEPTION_INFORMATION的ThreadId用GetCurrentThreadId()在异常回调里取是准确的。分析时用~*k打印所有线程的栈再结合!analyze -v指出的故障线程交叉验证。别只看一个线程的栈就下结论。5. 进阶把 dump 采集接进日志体系与自动化分析dump 只是原料真正省时间的是把它接进日志体系让崩溃信息自动流转、自动初筛。这一章讲两个我常用的进阶技巧。5.1 dump 与文本日志的关联光有 dump 不够崩溃前用户点了什么、程序走到哪一步这些上下文在文本日志里。我一般会在程序启动时生成一个会话 ID写进日志的每一行同时把会话 ID 写进 dump 文件名。这样拿到 dump 后用会话 ID 就能捞出对应的日志片段。// 启动时生成会话 ID全局保存 static QString g_sessionId QUuid::createUuid().toString(QUuid::WithoutBraces).left(8); // 日志格式里带上会话 ID qInstallMessageHandler([](QtMsgType type, const QMessageLogContext ctx, const QString msg) { QString line QString([%1][%2] %3) .arg(g_sessionId) .arg(QDateTime::currentDateTime().toString(hh:mm:ss.zzz)) .arg(msg); // 写入文件... }); // dump 文件名里也带上会话 ID QString file dir /crash_ g_sessionId _ QDateTime::currentDateTime().toString(yyyyMMdd_hhmmss_zzz) .dmp;逻辑说明会话 ID 是贯穿日志和 dump 的纽带。用户回传时只要给一个会话 ID你就能同时定位到日志和 dump不用在一堆文件里猜哪个对应哪次崩溃。qInstallMessageHandler是 Qt 提供的全局日志钩子所有qDebug、qWarning都会走这里统一格式后落盘。参数说明QUuid::WithoutBraces去掉花括号left(8)取前 8 位够用又不至于文件名太长。日志时间戳精确到毫秒方便和 dump 里的时间对齐。如果日志量大记得做滚动切割别让单个日志文件无限增长。5.2 用脚本做 dump 初筛客户回传的 dump 可能一次几十个人工一个个开太慢。我一般写个批处理脚本用 cdb 自动跑一遍把每个 dump 的崩溃类型和故障模块提取出来先按模块归类再决定哪些需要细看。echo off set _NT_SYMBOL_PATHD:\builds\latest\pdb;srv*C:\symbols*https://msdl.microsoft.com/download/symbols for %%f in (D:\dumps\*.dmp) do ( echo %%~nxf cdb -z %%f -c .ecxr; !analyze -v; q | findstr /C:FAULTING_MODULE /C:EXCEPTION_CODE /C:FAULTING_IP )逻辑说明脚本遍历 dump 目录对每个文件跑一次!analyze -v用findstr只保留故障模块、异常码、故障指令地址三行关键信息。这样一眼就能看出是不是同一个模块反复崩溃优先修高频的那个。参数说明findstr的关键字要和!analyze -v的实际输出匹配不同 WinDbg 版本字段名略有差异跑一次看输出再调整。符号路径指向最新构建的 PDB 目录如果 dump 来自旧版本要换成对应版本的 PDB 目录否则解析会错位。5.3 一个我坚持了多年的习惯每次发版前我会故意在测试环境触发一次崩溃确认 dump 能正常生成、能正常解析、日志能对上。这个动作花不了五分钟但能避免「客户崩溃了才发现 dump 机制根本没生效」这种最尴尬的局面。dump 工具的价值不在于写得多漂亮而在于它在你最需要的时候真的能用。把符号归档、目录权限、回传路径这三件事当成发版检查项固定下来比事后救火划算得多。希望帮到你。本文还有配套的精品资源点击获取