ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Qt崩溃自动抓取Dump:崩溃现场可回放

Qt崩溃自动抓取Dump:崩溃现场可回放 简介面向 Qt 开发者的崩溃捕获工具解决程序运行中异常退出后难以定位原因的问题可在段错误、异常终止等信号发生时自动收集堆栈与内存快照生成核心转储文件和运行日志便于用 GDB 加载分析崩溃现场。适用于桌面、嵌入式等图形界面应用也适合希望理解崩溃转储机制的初中级开发人员。压缩包共包含 56 个文件整体约 561KB以 C 源码、界面定义、工程配置和构建日志为主其中 cpp 与 h 实现功能ui 与 qrc 管理界面资源vcxproj 与 pro 适配两套构建体系log 与 tlog 记录编译及运行过程。已有 1501 人学习下载。资源附有完整可编译的工程可参照其捕获流程、日志输出与堆栈记录方式结合调试工具掌握崩溃分析思路并快速集成至自身应用以提升稳定性。1. Qt 程序崩溃自动抓 Dump崩溃现场不再靠用户猜很多 Qt 开发者手上的软件一到用户机器上就变成黑匣子主界面突然消失任务管理器里进程没了用户只能描述“点了一下按钮就崩了”。与其反复装远程工具去重放操作不如让进程在崩溃瞬间自己把现场留下来。这份「qt dump 工具软件崩溃自动生成日志」工程模板做的就是这件事Windows 下用 SetUnhandledExceptionFilter 挂载体崩溃钩子配合 MiniDumpWriteDump 输出 dmp 文件Linux 下用信号处理器抓 SIGSEGV/SIGABRT把调用栈写进日志。适合两类人一是接手别人维护的老 Qt 项目、正被“线上崩不知道崩在哪行”折磨的开发者二是想给软件在发布前补上崩溃回收能力的独立开发者。这套东西的核心取舍是把崩溃从不可复现变成可回放实际投入也就是一个模块的编译量。2. 崩溃捕获的选型MiniDump 与文本日志差在哪两套方案怎么选2.1 文本日志描述的是“过程”Dump 保存的是“瞬间”普通日志是被动记录代码库里哪一行加了 log哪一行才有记录。真正崩溃的那一刻你往往没有在野指针被解引用或者容器越界的路径上打点就算打了变量当前值、堆上对象的状态、其他线程分别卡在哪日志里也写不全。而 MiniDump 是进程崩溃瞬间的内存快照拿到 WinDbg 或者 Visual Studio 里直接打开能看到崩溃线程的完整调用栈、寄存器值甚至能翻看崩溃时刻某个指针指向的堆内存内容。同样一次崩溃文本日志能看到的是“大概走到了用户管理模块”dump 能看到的是“UserManager::login 里 this 指向 0xDDDDDDDD已经是被释放过的堆块”。这个差距不是多打几行日志能补上的。下表列一下我和团队在实际定位里对比出的差异信息维度纯文本日志MiniDump 文件调用栈完整性依赖打点位置经常缺关键帧完整保存含模块基址崩溃现场变量值手工打印可能没打可直接读内存 / 寄存器线程间状态只能凭日志时序猜全部线程快照都在文件体积/性能小几乎无性能开销内存映射快照10MB 起步MiniDump 也有粒度分级不是非得把整个进程内存都存下来。对多数桌面软件我会用MiniDumpWithDataSegs \| MiniDumpWithHandleData只抓已提交的内存数据段和句柄信息表体积普通在 10~30MB定位崩溃栈和变量状态足够。没有特殊要求不要上MiniDumpWithFullMemory一个内存占用 2GB 的软件一次完整 dump 能写出 2GB 文件用户磁盘和你的服务器都受不了。2.2 Windows 侧挂钩子SetUnhandledExceptionFilter 与 MiniDumpWriteDumpWindows 上崩溃捕获有两条路一条是 Qt 里常见的 qBreakpad 封装底层还是 Google Breakpad另一条是直接用系统 API 自己写异常过滤函数。工程模板里用的是后者因为依赖最少功能完全够用也不需要在目标机器上多带一个 DLL。核心代码是自定义的异常处理函数#include windows.h #include dbghelp.h #include cstdio #pragma comment(lib, dbghelp.lib) static LONG WINAPI CrashExceptionHandler(EXCEPTION_POINTERS* exInfo) { // 生成带时间戳的 dump 文件名避免同名覆盖 SYSTEMTIME st; GetLocalTime(st); char dumpPath[MAX_PATH] { 0 }; snprintf(dumpPath, MAX_PATH, crash_%04d%02d%02d_%02d%02d%02d.dmp, st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); HANDLE hFile CreateFileA(dumpPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile ! INVALID_HANDLE_VALUE) { MINIDUMP_EXCEPTION_INFORMATION dumpInfo; dumpInfo.ThreadId GetCurrentThreadId(); dumpInfo.ExceptionPointers exInfo; dumpInfo.ClientPointers FALSE; MiniDumpWriteDump( GetCurrentProcess(), GetCurrentProcessId(), hFile, MiniDumpWithDataSegs | MiniDumpWithHandleData, dumpInfo, NULL, NULL); CloseHandle(hFile); } return EXCEPTION_EXECUTE_HANDLER; }逻辑说明这个函数不是普通日志回调它运行在崩溃现场容量很小因此内部不能做下面三件事——分配大块内存、调用 Qt 的 QString 拼接、尝试加锁。文件名用系统时间戳避免多跑几次互相盖掉dumpInfo.ThreadId取的是当前线程这个线程就是崩溃线程后续用调试器分析时会直接定位到它异常信息指针exInfo原样传给 MiniDumpWriteDump里面记录了异常地址、异常码和寄存器上下文。参数选型MiniDumpWithDataSegs负责把进程的数据段、堆内存已提交部分写到文件MiniDumpWithHandleData把句柄表内容也带出来排查“句柄泄漏导致崩溃”的问题时有用。想要更轻量就去掉 HandleData更重的场景再加MiniDumpWithThreadInfo会让每个线程的状态信息更完整。异常过滤函数安装的位置有讲究int main(int argc, char* argv[]) { // 必须在 QApplication 构造之前安装 SetUnhandledExceptionFilter(CrashExceptionHandler); SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); QApplication app(argc, argv); QQmlApplicationEngine engine; // 业务初始化... return app.exec(); }逻辑说明放在 QApplication 构造之前是为了覆盖 Qt 平台插件加载阶段的崩溃。实际踩过的场景是发布包把 qwindows.dll 放错目录程序在加载平台插件时直接段错误此时如果 handler 没提前装好崩溃就是一片空白用户只会反馈“双击图标没反应”。SetErrorMode里两个参数用于关掉 Windows 的系统级错误弹窗否则崩溃时系统会先弹“xxx 已停止工作”你的人工处理函数可能根本没机会跑完。2.3 Linux 侧抓信号SIGSEGV 与 SIGABRT 的异步安全处理Linux 下 Qt 程序崩溃多数是 SIGSEGV段错误少数是 SIGABRT断言失败、异常未被捕获。信号处理器里能调用的函数非常有限标准答案是只做异步信号安全async-signal-safe的事。printf、std::cerr、Qt 的 qDebug 都不在这个名单里直接调用有大概率二次崩溃而且会死循环一样地崩。工程模板里的处理方式是把格式化好的字符串通过write()写到文件描述符再用backtrace_symbols_fd()把调用栈直接输出到同一个 fd#include execinfo.h #include signal.h #include unistd.h #include fcntl.h #include cstring static void DumpStackTraceToFile(int sig, siginfo_t* info, void* context) { // O_APPEND 保证多线程下写入不会相互覆盖 int fd open(crash.log, O_WRONLY | O_CREAT | O_APPEND, 0644); if (fd 0) _exit(128 sig); // 用 write 而非 printf保证异步信号安全 char buf[256]; int len snprintf(buf, sizeof(buf), \n crash signal %d, si_addr%p \n, sig, info ? info-si_addr : nullptr); write(fd, buf, len); void* frames[64]; int n backtrace(frames, 64); backtrace_symbols_fd(frames, n, fd); close(fd); _exit(128 sig); }逻辑说明si_addr是发生故障的内存地址段错误时它通常是 0 或者某个非法地址这个值能帮你快速区分“空指针解引用”和“访问已释放内存”调用栈最多抓 64 帧正常情况下 Qt 程序的栈深度到不了这个上限backtrace_symbols_fd是 glibc 提供的异步安全接口不会像backtrace_symbols那样 malloc 内存可以直接在信号处理里用。最后_exit强制退出不返回到被中断的代码。信号处理函数的安装方式struct sigaction sa; memset(sa, 0, sizeof(sa)); sa.sa_sigaction DumpStackTraceToFile; sa.sa_flags SA_SIGINFO | SA_RESETHAND; sigemptyset(sa.sa_mask); sigaction(SIGSEGV, sa, nullptr); sigaction(SIGABRT, sa, nullptr); sigaction(SIGBUS, sa, nullptr);参数说明SA_SIGINFO让回调收到 siginfo_t 指针这样si_addr才有值SA_RESETHAND的意思是处理过一次后恢复默认行为防止信号处理器本身出错时无限循环。这行参数在排查“崩溃后程序卡死”的问题时很关键不加的话如果 handler 里又触发了同类信号可能直接递归把调用栈撑爆。3. 崩溃日志双轨归档Dump 留定位文本日志留快速分诊3.1 单靠 dmp 文件还不够最少要补六项辅助信息dump 文件本身信息量很大但调试器之外的人打开它不方便。实际运维里客服收到用户反馈时需要有一个马上能打开、不依赖 Visual Studio 的文本日志用来快速判断“是不是同一个已知问题”。所以工程模板里每次崩溃都同时产出两类文件一个 dmp 给开发做深度分析一个 crash_当前时间.txt 给非开发角色做分诊。文本日志里我会固定写六项信息[崩溃时间] 2026-01-12 10:23:45 [崩溃线程] 0x3A4C 线程名: RenderThread [异常码] 0xC0000005 EXCEPTION_ACCESS_VIOLATION [模块] my_app_1.2.3.4.exe 基址 0x00400000 [堆栈摘要] 前 5 个符号: MyApp::Render::DrawRect ... [系统版本] Windows 10 22H2 build 19045问题分类时异常码最有用。0xC0000005 是访问违规0xC00000FD 是栈溢出0x80000003 是断言中断。看到 0xC00000FD 就不用让用户重新复现了直接查哪里申请了超大栈变量或者递归失控看到 0x80000003 就是 Q_ASSERT/Q_ASSERT_X 触发代码改掉就行。这一层分诊能省大量无效沟通。3.2 崩溃文件该放哪不能随手丢到 currentPath很多初级做法是把 dump 写到 exe 同目录或者当前工作目录这在开发机上没问题发布到用户机器上就麻烦了程序装在 Program Files 时没有写权限写到当前目录可能在用户解压的临时目录里清理即蒸发。工程模板里提供了一套基于 QStandardPaths 的路径管理代码大致是这样#include QStandardPaths #include QDir #include QDateTime QString GetCrashDirectory() { // Windows 下通常是 C:/Users/xxx/AppData/Roaming/你的产品名 // Linux 下通常是 ~/.local/share/你的产品名 QString dir QStandardPaths::writableLocation( QStandardPaths::AppDataLocation); QDir().mkpath(dir); // 多级目录一次性创建 return dir; } QString BuildCrashFileName(const QString suffix) { QString ts QDateTime::currentDateTime() .toString(yyyyMMdd_HHmmss_zzz); return GetCrashDirectory() QStringLiteral(/crash_%1.%2).arg(ts, suffix); }逻辑说明AppDataLocation 会以“公司名/产品名”的组合形式给你返回一个用户可写目录Windows 和 Linux 下都遵循各自平台的用户目录约定。参数上yyyyMMdd_HHmmss_zzz里带毫秒是为了防止一秒内连续崩多次覆盖文件suffix由调用方决定是 dmp 还是 txt因为这两类文件是同一时刻生成的。在 Windows 端的崩溃函数里只需把 CreateFileA 的路径从固定文件名替换成BuildCrashFileName(dmp).toLocal8Bit().constData()Linux 端信号处理器里不能调 Qt我再套一层单独在安装信号前准备一份crash.log路径字符串这样信号回调里只做open(固定路径)确保路径解析不在无信号安全保护的场景发生。3.3 启动时统一巡检旧日志清理与崩溃上报开关崩溃文件不能只生成不清理。一次 MiniDump 十几 MB如果用户每天崩一次一个月就是 300~400MB对普通用户磁盘是不小的负担。工程模板在 main() 里启动完成后异步扫描崩溃目录保留最近 10 个 dmp 和最近 20 个 txt 日志其余直接删除。核心逻辑#include QDirIterator #include QFileInfoList void CleanupOldCrashDumps() { QString dir GetCrashDirectory(); QFileInfoList dumps; QDirIterator it(dir, {*.dmp}, QDir::Files); while (it.hasNext()) { it.next(); dumps.append(it.fileInfo()); } // 按修改时间降序保留前 10 个其余删除 std::sort(dumps.begin(), dumps.end(), [](const QFileInfo a, const QFileInfo b) { return a.lastModified() b.lastModified(); }); for (int i 10; i dumps.size(); i) { QFile::remove(dumps[i].absoluteFilePath()); } }逻辑说明清理动作必须在崩溃发生后的下一次启动时做不能在崩溃次生少做原因是你不知道崩溃时磁盘还有多少剩余空间。删除策略选了“按数量”而不是“按天数”因为 dmp 文件大小不固定按天数可能在还剩大量空间时就已经删掉了用户在意的现场。QDirIterator只匹配顶层文件不会递归进子目录避免误删其他模块自己生成的备份。这份模板里还带了一个调试用的崩溃开关程序启动时检查qgetenv(MYAPP_INJECT_CRASH)如果值等于 1主窗口构建完成后主动抛一个空指针访问。这个开关平时关掉发版前打开跑一遍用来验证整套捕获链路在每个目标系统上都生效。4. 崩溃捕获常见错误排查五个我踩过的坑与修正4.1 崩了但没生成 dump检查 ErrorMode 和目录权限现象本地测试怎么崩都能出 dmp换到用户机器上报“还是没日志”查看 AppData 目录什么都没有。原因两种情况。一是发布安装包是 MSI 装的安装后的目录是 Program Files程序尝试把 dump 写进安装目录没有写权限CreateFileA 直接返回 INVALID_HANDLE_VALUE代码里只做 CloseHandle 没做错误处理静默失败了。二是没有调用 SetErrorModeWindows 在崩溃时优先弹了“已停止工作”对话框进程等在那里不执行异常过滤器。解决安装阶段把 crash 目录固定在 QStandardPaths::AppDataLocation并在 CreateFileA 失败时额外写一条系统事件日志main() 里 SetErrorMode 和 SetUnhandledExceptionFilter 一字不差都放最前面。从那以后我再没见过静默丢 dump 的情况。4.2 dump 生成了但 WinDbg 里看不到调用栈Release 编译 PDB 没留现象打开 dmp调试器提示“unloaded symbols”栈窗口只有几个地址看不到任何函数名。原因用户装的是 Release 构建但发布流程里没把 .pdb 和 exe 一起归档或者编译时为了减体积加了-fomit-frame-pointerMSVC 对应 /Oy。前者让符号无法匹配后者让栈帧链断裂。解决构建配置里保持 Release 同样生成 PDB发布时把 PDB 放进符号目录只对用户分发 exe编译选项不要全开优化。MSVC 下用/Zi /O2而不是/Ox /Os既能优化性能又能保留调试符号。我现在每次发布都在构建产物目录强制生成symbols.txt列出 exe、pdb、dll 的版本号归档到给客服的内部列表线上拿回来的 dmp 对上版本就能定位。4.3 信号处理器里调 printf 触发二次崩溃现象Linux 下运行崩溃后 crash.log 是空的或者文件里只有几行乱码字符程序退出码从 139 变成 134。原因signal handler 不满足异步信号安全要求。printf 内部维护 stdout 的缓冲区锁和 FILE 结构崩溃线程可能正是在持锁状态下被打断此时再调 printf 直接死锁或触发 SIGABRT。解决把所有输出都换成 write(fd, buf, len) 和 backtrace_symbols_fd这两者保证不需要锁和 malloc。格式化时用 snprintf 而不是 std::ostringstream后者在异常场景下可能分配内存。这是我压测时用无限递归触发栈溢出后连着崩了三次才改干净的。4.4 Qt 信号槽里抛出的 std::exception 没有被捕获现象槽函数里某个第三方库抛了未捕获异常Qt 事件循环把它吞了直接 terminate进程退出但 MiniDump 文件里没有该有的崩溃线程上下文。原因Qt 的 QApplication::exec 事件分发机制默认拦截了 C 异常并在某些编译配置下直接调用 std::terminate。SetUnhandledExceptionFilter 是 Win32 异常过滤钩子但它管不到 terminate 路径上已经飞远的 C 异常。解决在 main() 里加 set_terminate 兜底把异常信息写入日志后再主动触发崩溃捕获#include exception void OnTerminate() { // 捕获 std::terminate 路径先落一条文本记录 FILE* f fopen(terminate.log, a); if (f) { fputs(unexpected terminate\n, f); fclose(f); } // 主动触发访问违规让异常过滤器接管并写 dump volatile int* p nullptr; *p 0; } // main() 里 std::set_terminate(OnTerminate);逻辑说明set_terminate 的函数指针如果直接返回C 运行时会立刻 abort所以最后一个主动写空指针的操作是为了把流程引到已经注册好的 SetUnhandledExceptionFilter 上。注意这段代码的代价是丢掉了原始异常上下文但比用户端“闪退无痕”好太多。工程模板里把这个兜底默认开启。4.5 栈溢出时 MinidumpWriteDump 本身就会失败现象递归调用过深导致的 0xC00000FD 栈溢出函数入口进入了 CrashExceptionHandler但文件大小只有几 KB调试器提示 dump 数据不完整。原因栈已经耗尽时MiniDumpWriteDump 需要栈空间来构造 dump 结构它在同样枯竭的栈上调用必然失败或只写出半截。这与 4.3 的信号处理二次崩溃本质一样。解决给异常过滤器安排独立栈。Windows 上可以做一个专门用来执行 dump 的线程通过共享内存传参更常见的做法是预先在内存中 map 一块 1MB 备用栈空间崩溃时手动切换栈指针。工程模板提供的是精简方案用全局变量static const int reserveByte[1 * 1024 * 1024];配合_alloca在 handler 入口预占空间然后拆成两步走。这套方案能覆盖大多数非极端递归真正的无限递归场景还是需要看门狗进程做兜底。5. 进阶验证崩溃注入、WinDbg 还原现场与发布前的回归清单工程模板里加了一个独立的崩溃注入头文件专门用于自动化验证。发布前跑一遍这个注入用例能确认整套链路在目标机器上是通的而不是等到用户崩了才发现 dump 模块根本没生效。// crash_inject.h #pragma once #include cstdlib #include csignal inline void InjectCrashByType(int type) { switch (type) { case 0: // 空指针解引用 volatile int* p nullptr; *p 1; break; case 1: // 抛未捕获 C 异常 throw std::runtime_error(inject crash); break; case 2: // 断言失败 Q_ASSERT_X(false, Inject, intentional assert Q_ASSERT); break; default: abort(); } }在代码里调用InjectCrashByType(0)程序崩溃后去 GetCrashDirectory() 里检查是否同时生成了 dmp 和 txt 两个文件txt 里异常码是否为 0xC0000005。测试完成后把注入调用注释掉或放在环境变量开关之后。拿到 dmp 后我习惯先用 WinDbg 而不是 Visual Studio因为它不吃内存。打开方式是把 dump 文件拖进 WinDbg然后按顺序执行三条命令.sympath srv* !analyze -v k第一条设置符号路径srv*会走微软公共符号服务器然后!analyze -v自动给出异常分析结果通常几秒内就定位到崩溃模块和异常类型最后k打印线程调用栈。分析完重点看一眼STACK_TEXT里前 3 帧如果身影全是我们自己代码的函数名说明符号匹配成功如果都是 QCoreApplication 内部函数说明业务入口的函数符号没加载回到第 4 章的 PDB 归档问题去查。Linux 端验证简单直接终端里跑./my_app用 gdb 挂载进程执行set env MYAPP_INJECT_CRASH1再运行bt命令调出栈与 crash.log 里的 backtrace 对比符号顺序一致就说明捕获链路正常。最后一次重构这套 dump 模块时我在本地连续测了六个场景普通崩溃、子线程崩溃、无限递归、吊销的 QObject 槽触发、平台插件加载失败、无写权限的安装目录。每一个都强制确认“有 dump、有文本日志、文件名时间戳可区分”。从那以后每次发版我都在干净虚拟机上跑一遍崩溃注入用例确认产物齐全才开放更新入口。这个习惯帮我截住了至少三次 dump 模块自身翻车的问题希望帮到你。本文还有配套的精品资源点击获取
返回列表