多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Qt跨平台崩溃捕捉机制实战:从SEH到信号处理,精准定位程序崩溃

Qt跨平台崩溃捕捉机制实战:从SEH到信号处理,精准定位程序崩溃 做客户端开发的朋友应该都有过这种经历程序在自己机器上跑得好好的一部署到用户那隔三差五就崩溃。用户描述得不清不楚——“点了就闪退”“开着开着就没了”。你让他发日志他还不知道怎么找文件。这时候Qt崩溃捕捉机制就成了救命稻草。说白了崩溃捕捉就是在程序真正挂掉之前自己先接管异常把当时的堆栈、寄存器、调用链、运行环境全部记录下来生成一个可以分析的转储文件或者日志。这样即便用户不会表达你也能拿着现场记录精准定位到具体代码行。这篇文章我用自己的实战经验把Qt程序崩溃捕捉的完整思路、跨平台实现、常见坑和排查方法都捋一遍。不管你是刚接触Qt开发的小白还是已经在维护线上产品的老手只要你的程序有发布到别人机器上的需求这套方案都值得花几分钟看完。1. 为什么Qt程序总是“死得不明不白”1.1 崩溃现场是第一生产力很多Qt程序崩溃难排查核心原因只有一个拿不到崩溃现场。本地调试时崩溃调试器会直接停在出问题的那一行变量、调用栈全部摆在眼前问题一目了然。但发布出去的Release版程序崩溃时用户看到的是“xxx已停止工作”或者干脆退出了你手里只有一个崩溃前的日志日志里可能只打到某一帧的普通输出根本不知道下一步发生了什么。程序员之间流传一句话先有现场后有真相。崩溃捕捉模块的存在就是为了在程序临终前“拍一张X光片”把当时内存里的关键状态、函数调用链、寄存器值全部固定下来。这样即使程序已经死了你依然可以像法医一样解剖它的尸体。我见过不少项目上线几个月崩溃反馈全靠用户录屏、截图、口头描述。效率低到令人发指。后来咬牙上了崩溃捕捉第一周就抓到了两个隐藏半年的空指针问题。从那以后我所有C/Qt项目都默认带一套崩溃捕捉哪怕是内部工具也一样——它不会占用多少开发成本但关键时刻能省下好几个通宵。1.2 先搞清楚哪些崩溃能抓、哪些不能抓不是所有崩溃都能被优雅捕获。我把它分成三类第一类是可捕获的硬件异常比如访问非法内存、除零、空指针解引用等。这类异常在CPU层就会触发异常中断操作系统会回调注册好的异常处理器我们可以在这里介入记录现场。第二类是可捕获的软件信号比如SIGSEGV段错误、SIGABRT程序主动中止、SIGFPE浮点异常等。这类在Linux/Unix平台表现明显Qt程序在特定场景下也会因为Q_ASSERT失败、qFatal调用而触发。第三类是基本无法捕获的比如系统OOM内存耗尽导致的进程被杀程序直接被kill -9强杀或者电源断电。这些场景下系统根本不给你反应时间崩溃捕捉模块也无能为力。我记得刚开始做崩溃捕捉时天真地以为只要注册一个“万能处理器”就能抓住所有崩溃。结果测试发现用户强杀进程、系统重启、Windows升级强制结束进程时处理器根本不会被调用。这点一定要在心里有数崩溃捕捉不是万能的它最大的价值是覆盖“程序自身逻辑错误导致的异常退出”把这一类问题抓到、榨干价值就已经很出色了。2. 方案选型从qInstallMessageHandler到系统级异常处理2.1 各显灵通的“伪崩溃捕捉”很多人一开始接触Qt最先听说的是qInstallMessageHandler这个函数。它可以接管qDebug、qWarning、qCritical、qFatal输出的所有消息。有些教程会告诉你在消息处理器里把日志写到文件程序就“有日志”了。但这里有个严重误区qInstallMessageHandler只能处理Qt框架层面输出的消息并不能捕捉真正的系统级崩溃异常。举个例子一个指针越界写入程序直接内存访问违规这时候Qt消息系统根本来不及反应整个进程就被系统干掉了。你顶多能在崩溃前的日志里看到最后一条日志运气好能大致猜到位置但绝大多数时候这点信息远远不够。还有人说可以用Qt的QProcess把程序拆成父子进程子进程崩了父进程可以感知并重启。这确实是一种方案但它只能解决“程序崩了自动拉起”依然拿不到崩溃现场不知道为什么会崩。而且重启逻辑如果没做对还容易引发无限重试在正式产品里反而是个事故隐患。这些办法不是没用但都不是真正的崩溃捕捉。真正的崩溃捕捉得在操作系统层面做文章。2.2 系统级异常处理才是正路Windows和Linux两大平台分别提供了各自的异常捕获机制Windows上叫SEHStructured Exception Handling结构化异常处理核心API是SetUnhandledExceptionFilter。只要程序注册了异常过滤器几乎所有未处理的Win32异常都会在进程终止前回调这个过滤器你可以在回调里调用MiniDumpWriteDump生成完整的转储文件。Linux上则依赖POSIX信号机制通过sigaction注册SIGSEGV、SIGABRT、SIGBUS、SIGFPE等信号的处理函数在信号处理函数里调用backtrace获取调用栈。这两套机制是所有高级崩溃捕捉工具的底层基石。包括Google开发的开源工具Crashpad/Breakpad本质上也还是封装了这些系统调用只是把数据收集、上传、符号化等外围功能做完善了。对于大多数Qt项目尤其是内部工具、中小型商业软件自己写一套轻量级的崩溃捕捉模块完全够用。不用引入一堆第三方依赖代码量也就两三百行维护成本很低。2.3 轻量自研 vs Breakpad/Crashpad有人会问直接上Breakpad不就行了吗为什么还要自己造轮子Breakpad/Crashpad确实很成熟功能全面支持崩溃自动上传、多平台。但它的问题是依赖体系比较复杂跨平台编译配置需要一定精力。接入Crashpad需要跟着Google的构建流程走项目里多了好多编译产物和配置。为了功能完整代码量大出了问题不好排查。很多功能比如上传到服务器对内部工具和中小项目是过度的。我的判断标准很简单如果项目需要覆盖Windows/Linux/macOS三端而且要处理海量用户的崩溃上报那直接上Crashpad是值得的。但如果你的项目主要部署在Windows和Linux崩溃反馈规模是几十到几百个用户那自己写一个模块两三小时就能落地后续想加字段、改格式都随心所欲。我自己在项目里的选择是基于Qt本身写一套跨平台崩溃捕捉模块Windows用SEH DbgHelpLinux用sigaction backtrace日志统一输出为自定义格式的崩溃报告文件Windows下额外生成dmp转储文件。这套方案我用了两年多稳定可靠。3. 核心原理拆解Windows和Linux是怎么通知我们“程序要死了”3.1 Windows SEH与SetUnhandledExceptionFilterWindows的所有用户态异常其实都是通过异常分发机制处理的。当程序触发访问违规、除零、非法指令时CPU会产生一个异常Windows内核捕获后交给用户态的异常处理器链。SetUnhandledExceptionFilter这个API的作用是往这个异常处理链的“最后环节”挂一个回调。如果程序自身没有处理这个异常比如没有try/catch或者catch之后没处理就扔掉了那么系统最终会调用你设置的这个过滤器。这里有个关键点这个过滤器只捕获“未处理”的异常。如果你的代码里有try...catch(...)把所有异常都吞了但实际逻辑又没处理干净那系统认为异常已经被“处理”了你的过滤器不会被调用。所以团队规范里一般都会强调不要在顶层一股脑吞掉所有异常至少要把异常信息记录完再决定怎么处理。在过滤器回调里系统会传一个EXCEPTION_POINTERS*结构体里面包含ExceptionRecord异常类型、异常地址、异常参数。ContextRecord发生异常时各寄存器的值。有了这些信息你再调用MiniDumpWriteDump就能生成一个包含完整线程上下文、调用栈、内存片段的dmp文件。这个文件可以直接拖进Visual Studio或WinDbg配合pdb符号文件精准定位到源码行。3.2 Linux Signal与sigactionLinux程序崩溃时内核会向进程发送信号。常见的崩溃相关信号有信号触发场景SIGSEGV段错误访问非法内存地址SIGABRT程序主动调用abort()或断言失败SIGBUS总线错误如非对齐访问SIGFPE浮点异常包括除零SIGILL非法指令signal()函数可以注册信号处理函数但它有一个痛点是历史遗留原因在不同系统上行为不一致有的系统在信号处理函数调用后会自动重置为默认行为导致同类信号第二次触发时直接退出。正确做法是用sigaction()它支持精细控制还不会出现兼容性问题。sigaction注册时可以指定一个siginfo_t结构来获取更多上下文信息比如触发信号的内存地址、系统调用细节。如果想要拿到崩溃时的调用栈可以在信号处理函数里调用backtrace()和backtrace_symbols()它们能展开当前函数调用链。需要特别注意的是backtrace_symbols()内部会调用malloc申请内存这在信号处理函数里是非常危险的——如果崩溃本身就是内存损坏导致的malloc内部极有可能再次崩溃。安全做法是在信号处理函数里只做最低限度的收集比如把程序计数器地址、栈地址写入一个预先分配好的缓冲区然后交给另一个进程/另一个地方去做符号化。3.3 在信号处理函数里“活下去”的生存法则很多人第一次写Linux信号处理函数时会把各种复杂逻辑都塞进去写日志、抛异常、做堆栈展开、调用Qt接口。这些都是大忌。信号处理函数必须遵守几个铁律只调用异步信号安全函数。这类函数在man page里明确标注为async-signal-safe比如write()、open()、futex()等。像printf、malloc、new、delete、std::vector的成员函数基本都不安全。不要尝试恢复重试。默认情况下发生段错误的信号处理函数返回后程序会重新执行导致崩溃的指令大概率再次进入崩溃循环卡死在信号处理里。必须在收集完信息后主动调用_exit()退出。不要在信号处理函数中使用Qt类。Qt的很多类内部有自己的锁和内存管理可能在崩溃时已经处于不一致状态调用它们等于自寻死路。我一般把信号处理函数设计成两个阶段第一阶段在信号处理器内部用write这类安全函数把原始指针地址、信号编号、关键寄存器值写入预分配缓冲区第二阶段在收到信号的线程退出后由另一个监控线程读取缓冲区再调用完整版的分析和日志写入逻辑。如果嫌这个设计复杂简单点做法就是信号处理函数内直接用openwrite把原始堆栈数据写进文件不经过任何C库。4. 完整实现一个可落地的Qt崩溃捕捉模块4.1 模块结构与初始化入口先看整体结构。我一般把崩溃捕捉封装成一个单例类CrashHandler暴露两个接口install()和uninstall()。所有平台细节在内部隐藏业务层只需要在main()函数最开头调用一次CrashHandler::instance()-install()。下面是头文件的基本骨架// crashhandler.h #pragma once #include QString class CrashHandler { public: static CrashHandler* instance(); // 安装崩溃捕捉logDir为日志输出目录 void install(const QString logDir); void uninstall(); // 写入一条普通运行日志非崩溃日志 void writeLog(const QString message); private: CrashHandler(); ~CrashHandler(); private: QString m_logDir; bool m_installed false; };安装时机很关键。我建议放在QApplication创建之前还是之后答案是尽量早最好在QApplication构造之前。因为Qt初始化阶段本身也可能发生崩溃比如插件加载失败、显卡驱动问题导致QOpenGL上下文创建失败。放早了这些崩溃也能被捕捉到放晚了可能程序刚启动就崩了还没来得及安装处理器。4.2 Windows实现MiniDumpWriteDump生成转储文件Windows平台的关键是SetUnhandledExceptionFilter和MiniDumpWriteDump。#include windows.h #include dbghelp.h #pragma comment(lib, dbghelp.lib) static CrashHandler* g_handler nullptr; static LONG WINAPI UnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo) { if (g_handler) { g_handler-writeDumpFile(pExceptionInfo); } return EXCEPTION_EXECUTE_HANDLER; } void CrashHandler::install(const QString logDir) { m_logDir logDir; g_handler this; SetUnhandledExceptionFilter(UnhandledExceptionFilter); m_installed true; } void CrashHandler::writeDumpFile(PEXCEPTION_POINTERS pExceptionInfo) { // 确定文件路径 QString dumpPath m_logDir QStringLiteral(/crash_) QDateTime::currentDateTime().toString(yyyyMMdd_hhmmss_zzz) QStringLiteral(.dmp); HANDLE hFile CreateFileW(dumpPath.toStdWString().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile INVALID_HANDLE_VALUE) { return; } MINIDUMP_EXCEPTION_INFORMATION mei {}; mei.ThreadId GetCurrentThreadId(); mei.ExceptionPointers pExceptionInfo; mei.ClientPointers TRUE; // 表示异常指针来自当前进程 // 生成转储文件 BOOL bRet MiniDumpWriteDump( GetCurrentProcess(), GetCurrentProcessId(), hFile, MiniDumpWithFullMemory | MiniDumpWithThreadInfo | MiniDumpWithProcessThreadData, mei, nullptr, nullptr ); CloseHandle(hFile); }这段代码有几个细节值得注意MiniDumpWriteDump生成的转储类型我用的是MiniDumpWithFullMemory。这个选项会把进程的完整虚拟内存全部写进文件对排查复杂内存问题很有帮助但代价是dmp文件大一个程序可能几十上百MB。如果觉得太大可以改成MiniDumpWithIndirectlyReferencedMemory或MiniDumpWithDataSegs文件大小会小很多但能分析的内容也会相应减少。我的做法是内部工具直接用FullMemory线上产品用MiniDumpWithIndirectlyReferencedMemory保证文件不至于太大。为什么设置ClientPointers TRUE这个字段告诉系统pExceptionInfo指针指向的是当前进程地址空间内的有效数据。如果设成FALSEDbgHelp会尝试用自己的方式重新读取异常信息在崩溃现场反而不安全。文件名加时间戳会有用户连续崩溃多次时间戳能保证每次崩溃都能生成独立文件不会被覆盖。在异常处理函数里千万不要做太复杂的事情比如写Qt的QFile、输出QMessageBox。Windows的SEH回调环境虽然不是POSIX信号那么苛刻但崩溃现场的内存状态不可预测调用堆栈可能已经损坏使用Qt类仍然有风险。我这里的实现就简单粗暴直接调Win32 API写文件不经过Qt安全系数最高。4.3 Linux实现backtrace堆栈采集Linux平台用sigaction注册信号处理配合backtrace采集堆栈再把堆栈信息写成文本日志。#include execinfo.h #include signal.h #include unistd.h #include fcntl.h static CrashHandler* g_handler nullptr; // 尽量使用async-signal-safe系统调用写数据 static void WriteAll(int fd, const char* buffer, size_t len) { size_t written 0; while (written len) { ssize_t n ::write(fd, buffer written, len - written); if (n 0) break; written static_castsize_t(n); } } static void SignalHandler(int sig) { // 打开崩溃日志文件 char path[256]; // snprintf在多线程下不安全但这里已经崩溃了尽量简单 // 注意snprintf本身不是async-signal-safe但实际情况中常用的方法是预先构造好文件名 // 或者使用open的O_APPEND配合全局路径缓冲区。 // 这里为了示例先直接用固定的崩溃文件路径 const char* filePath /tmp/qt_crash.log; int fd ::open(filePath, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd 0) { _exit(1); } char header[128]; // 手动拼接字符串避免非安全函数 // 一个简化做法是直接用write固定字符串 const char* msg Crash signal received\n; WriteAll(fd, msg, strlen(msg)); // 采集调用栈 void* buffer[128]; int nptrs ::backtrace(buffer, 128); // 将原始地址写入文件 // backtrace_symbols_fd 会将符号信息直接写入fd并且不需要malloc // 这个函数是glibc提供的专用函数适合信号处理场景 ::backtrace_symbols_fd(buffer, nptrs, fd); ::close(fd); _exit(1); } void CrashHandler::install(const QString logDir) { // 在真实项目中这里需要把logDir传下去但信号处理函数无法安全访问QString // 需要用静态缓冲区保存路径。此示例简化处理。 struct sigaction sa {}; sa.sa_sigaction [](int sig, siginfo_t* info, void* context) { SignalHandler(sig); }; sa.sa_flags SA_SIGINFO | SA_NODEFER; // SA_NODEFER允许同信号再次触发时继续执行我们的handler但需要自行保证安全 sigemptyset(sa.sa_mask); sigaction(SIGSEGV, sa, nullptr); sigaction(SIGABRT, sa, nullptr); sigaction(SIGBUS, sa, nullptr); sigaction(SIGFPE, sa, nullptr); sigaction(SIGILL, sa, nullptr); g_handler this; m_installed true; }Linux这边有几个坑我要重点说。不要用backtrace_symbols()要用backtrace_symbols_fd()。前者内部会调用malloc分配内存在信号处理器里调用遇到堆损坏基本就是二次崩溃。后者会把符号信息直接写入给定的文件描述符虽然格式丑一点但胜在安全。snprintf和QString都不能在信号处理函数里安全使用。上面的示例代码我用了固定的日志路径如果确实想动态生成路径必须提前用静态全局缓冲区构造好比如用线程本地存储或者安装时预先拼接。注册多个信号时信号处理函数是共享的。上面我用同一个处理函数处理SIGSEGV、SIGABRT等这样比较省事。但如果不同信号想区分处理逻辑也可以注册不同的处理函数。Linux信号处理函数执行完程序就真的死了。所以里面一定要以_exit()结尾不能再return回去否则整个系统会进入无限循环。这点无论如何都要记住。4.4 崩溃日志的字段设计与输出不管是dmp文件还是文本日志统一设计一个“崩溃摘要”会让分析和归档高效很多。我在项目里每个崩溃都会额外生成一个JSON格式的摘要文件包含{ app_name: MyQtApp, app_version: 2.1.0, build_number: 20241018, os_version: Windows 10 Pro 22H2, qt_version: 5.15.2, crash_time: 2024-10-18 14:32:08, exception_code: 0xC0000005, exception_name: 访问冲突, exception_address: 0x7FF6C3214A20, fault_module: MyQtApp.exe, thread_id: 8923, tls_area: ... }日志文件放在哪个目录也是有讲究的。Windows上我默认用QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)这个目录放用户应用程序数据最正规。Linux上则用~/.local/share/应用名或者/tmp的备选方案。部署时最好在程序启动时检查目录是否存在不存在就创建然后记录下来。5. 拿到崩溃文件之后怎么一步步定位Bug5.1 Windows用VS和WinDbg分析dmp文件Windows下生成的dmp文件要分析出源码级别信息必须保留和发布版本完全对应的pdb文件。这条铁律我在团队里反复强调pdb不是调试专用发布Release版也要生成并且要按版本归档保存。Visual Studio打开dmp文件直接点“使用仅限本机进行调试”如果pdb路径匹配会自动加载符号调用栈中每个帧都能对应到具体的cpp文件和行号。WinDbg命令行分析则更灵活lm m MyQtApp !analyze -v kb!analyze -v是WinDbg的自动分析命令会给出异常类型、发生位置、调用栈摘要对快速了解崩溃原因帮助极大。然后再用kb查看详细栈回溯。有一点要提醒如果用户机器上的dmp文件和本地pdb不是同一版本编译出来的符号会加载不上看到的全是偏移量。所以pdb的版本归档千万别马虎。我见过太多团队用了半年崩溃捕捉最后因为pdb管理混乱导致一堆dmp全废了。5.2 Linux用addr2line和gdb还原堆栈Linux这边我的崩溃日志里直接用backtrace_symbols_fd输出了符号信息里面包含模块名、函数名和偏移地址。但有时候符号会被strip掉只剩地址。这时候就用addr2line还原addr2line -e /path/to/QtApp -f -C 0x4012ab输出会显示函数名和对应的源码行号。注意ensure编译时加了-g调试选项否则地址到行的映射是不存在的。尽量在Release版里也保留-g选项体积增加不多但排查崩溃时是救命稻草。如果后端有core dump文件也可以直接用gdb分析gdb /path/to/QtApp core.1234 (gdb) bt fullbt full不仅能看调用栈还能打印每个函数的局部变量值分析效果比单纯的backtrace文本日志强得多。所以我在Linux端的崩溃捕捉还有一个增强方案在信号处理函数里调用abort()之前先执行系统默认的core dump生成然后自动生成core文件的符号索引信息。下面是简化版想法安装崩溃捕捉后设置setrlimit(RLIMIT_CORE, RLIM_INFINITY)确保core文件能完整生成这样即使自己写的堆栈日志不够详细还有core兜底。5.3 一个实战排查案例去年我们一个Qt程序上线后收到用户反馈“批量导入Excel时偶发崩溃”频率很低百次操作可能一次。原日志里只有最后一条“Import started”信息量约等于零。加上崩溃捕捉模块后一次性抓到了现场Windows dmp显示异常代码0xC0000005访问冲突。调用栈顶层是第三方开源库的xls_parseCell函数。栈回溯到我们程序里的SheetImporter::parseRow。对照pdb查行号发现代码里先把Excel行数据存到QVectorQString随后在循环里用at(index)读取。正常情况下index不会越界但某行Excel数据里包含了一个超长字符串第三方库解析时把内部行索引算错了一位导致at()访问了越界位置。改了导入前对行长的校验后问题彻底消失。整个过程如果用传统方式排查可能要看日志猜半天。崩溃捕捉让定位时间从几小时压缩到十几分钟。这就是这套模块最实在的价值。6. 填坑记录崩溃捕捉模块自身最容易踩的坑6.1 不要在异常处理代码里干“重活”这是所有崩溃捕捉模块的第一大坑。Windows的SEH回调也好Linux的信号处理函数也好执行时程序已经处于“半死不活”状态。此刻调用malloc、new、Qt容器、网络库都有很大概率继续触发异常导致捕捉失败甚至把原本的信息覆盖掉。我见过有人在崩溃处理里写复杂的QJsonObject拼装准备把崩溃信息发送到服务器。结果测试发现十个崩溃只有两三个能正常上报其余全在报告收集阶段就二次崩溃了。正确的做法是在崩溃处理函数里只做非常基础的收集工作把必要的原始数据写入文件后立即退出。复杂的格式化、网络上传、日志分析全部放到程序下次启动时由“崩溃报告模块”读取上次留下的原始文件再做加工和上报。6.2 多线程崩溃其实比想象中复杂Qt程序几乎全是多线程的。Qt早就普及了基于线程的事件循环模型工作线程、网络线程、渲染线程众多崩溃可能发生在任意线程。好消息是Windows的SetUnhandledExceptionFilter不管崩溃发生在哪个线程都能正常触发MiniDumpWriteDump会把所有线程的上下文都写进去分析时切到崩溃线程即可。Linux信号处理函数同样能在任意线程中被调用只要该线程没有被信号掩码屏蔽。但这里有一个隐藏问题如果崩溃线程不是主线程它调用了信号处理函数而主线程此时还在正常运行二者可能同时操作同一个崩溃日志文件产生写竞争。我常用的规避方法有两个用open()打开文件时加O_APPEND保证写入不会相互覆盖破坏。崩溃发生时在信号处理函数里向主线程发一个信号通知主线程暂停业务逻辑避免继续改变内存状态干扰崩溃现场信息。这个实现有一定复杂度但对分析价值影响很大。6.3 发布版符号信息丢失等于白抓为了追求发布包更小一些团队会在发布Qt程序时执行strip操作把ELF文件中所有符号信息删掉released出的二进制完全看不出函数名。这种操作对崩溃排查是毁灭性的。比如你拿到一个Linux崩溃日志上面是./QtApp() [0x4012ab] ./QtApp() [0x402138] /lib/x86_64-linux-gnu/libc.so.6(0x3c120) [0x7f9c...]只有地址没有名字除非你有当时的addr2line分析机会否则跟瞎子摸象差不多。我的建议编译时保留-g调试信息。即使要strip也只strip掉局部符号保留动态符号表strip --strip-unneeded或者更冷静一点内部版本不strip用户版本strip但保留符号文件单独归档。Windows方面则是pdb文件一定按版本存好别覆盖别丢。pdb一旦和exe不匹配dmp文件的可用性会大幅下降。6.4 如何验证崩溃捕捉真的可靠写完崩溃捕捉模块不能直接上线。必须做一轮专门的“崩溃演练”模拟各种典型崩溃场景验证模块真的能抓住它们。我常用的测试用例包括空指针解引用int* p nullptr; *p 1;栈溢出写一个无限递归函数。堆越界写new一块内存后疯狂往后写。除零int a 1 / 0;显式调用abort()。动态库中的崩溃模拟第三方库出问题。测试时观察两点崩溃后是否生成了对应的日志/dmp文件。生成的日志内容是否完整可用调用栈是否对得上。Windows端还可以专门测试在Release模式下、没有调试器附加的情况下崩溃因为SetUnhandledExceptionFilter在debugger环境下走的是另一条路径行为可能不同。本地用Visual Studio直接F5跑的时候异常被调试器接管不会触发你的过滤器这很容易让人误以为模块没生效。正确测试方式是不启动调试器直接运行exe让它自己崩。这点经常有人踩坑。Linux端则要测试信号处理函数内的安全路径可以故意在崩溃前破坏堆区看看会不会导致二次崩溃。如果能安稳扛过去说明模块设计得足够稳健。最后再分享一个小技巧崩溃文件生成后记得在程序下次启动时做个友好的提示。我一般会在启动时检查崩溃日志目录发现有新的dmp或core文件就弹出一个提示框询问是否要把崩溃报告发送给开发者。技术上很简单但这一个细节让用户配合度直线上升崩溃数据回收率能提高不少。这套崩溃捕捉方案不是银弹但它确实让我的Qt项目从“用户说崩了但无能为力”升级到“只要有崩溃就要能抓到可用的现场”。整个过程踩了不少坑上面提到的这些问题都是实打实砸出来的经验。如果你的项目还没有崩溃捕捉希望这篇文章能帮你少走弯路。
返回列表