☰
Windows C++ 崩溃 dump 捕获、符号与栈分析避坑指南
2026/10/24 4:32:59 网站建设 项目流程

简介:面向在 Windows 平台从事 C++ 开发、需要定位线上或测试环境崩溃问题的开发者,这份压缩包提供一份可直接参考的迷你转储生成示例。包内共 1 个 cpp 文件,压缩后仅 731B,核心内容集中在 main.cpp,示范如何借助 Windows 异常处理机制与 MiniDumpWriteDump 接口,在未捕获异常发生时写出包含堆栈、线程与进程状态等信息的 dump 文件,并涉及 dbghelp 相关依赖与调试信息配置的配合思路。已有 1629 人学习,说明该主题在 C++ Windows 崩溃排查场景中具有稳定需求。读者可据此快速理解异常过滤器、转储级别选择以及发布版本调试符号准备之间的关系,为后续用 WinDbg 或 Visual Studio 分析崩溃现场打下基础,也便于将示例裁剪进自己的工程做最小化验证。

1. 程序崩了却没留下 dump:为什么 C++ 工程在客户机上总是“死得无声无息”

一个 Windows 上的 C++ 程序,开发机上跑得好好的,到了客户机就闪退,事件查看器里只有一句“应用程序发生异常”,别的什么都没有。你远程连过去,重启一次复现不了,用户也说不清自己点了什么。这种时候最想要的就是一份 dump 文件,可它偏偏没生成——程序确实崩溃了,只是崩溃那一刻没有人去写它。默认情况下系统只会静默结束进程,把现场丢给错误报告,本地一点痕迹都不留;更麻烦的是,你手动加了SetUnhandledExceptionFilter,在 IDE 里调试时却永远等不到 dump,于是误以为代码写错了。C++ 崩溃抓 dump 这件事,难点从来不在MiniDumpWriteDump这个 API 本身,而在“它什么时候被调用、调用时环境还健不健康、写出来的文件能不能被解析”。下面按故障类型一层层拆,每一步都给能直接抄的命令和配置。

2. 崩溃当场没留下 dump:从异常过滤器到 MiniDumpWriteDump 的最小可运行实现

2.1 过滤器没被调用:三种最常见也最难发现的失效

按踩坑的频次排序,第一种是“本地调试时看不到 dump”。只要进程被调试器附加,未处理异常会先交给调试器,UnhandledExceptionFilter根本不会触发,这时候你写什么代码都不会出文件,很多人就是在这一步怀疑人生,把正确的代码删了重写。验证方式是脱离调试器直接双击运行,或者在调试器里选择“不中断、交给程序处理”。第二种是过滤器被覆盖。CRT 初始化、某些第三方库、图形框架在启动时也会调用SetUnhandledExceptionFilter,后注册的会把前面的顶掉,所以你写在库初始化之前的过滤器可能早就失效了。稳妥做法是在main或WinMain的第一行注册,并在注册后立刻回读一次状态做日志,确认自己确实是最后注册的那个。第三种是异常根本没走 SEH,比如abort()、纯虚函数调用、CRT 参数检查失败、std::terminate,这些路径在 Windows 上要么直接终止进程,要么走另一套 handler,未处理异常过滤器压根收不到。

还有一种隐性失效值得单独说:栈溢出。栈已经耗尽的时候,过滤器函数连自己的栈帧都建不起来,MiniDumpWriteDump更是别想跑。常见做法是启动时用SetThreadStackGuarantee预留一段应急栈,或者在过滤器里判断异常码是EXCEPTION_STACK_OVERFLOW时先调_resetstkoflw恢复栈再写 dump。血泪经验是:线上真正难查的崩溃,十有八九是栈溢出和堆破坏,这两种恰恰是最容易写不出 dump 的场景,所以应急栈这一步别省。

2.2 一份能直接编进工程的最小实现

不用封装成多复杂的框架,下面这段就够跑起来。关键是文件名要带进程 ID 和时间戳,否则多进程或者短时间连续崩溃会互相覆盖,最后你拿到的是一份残缺文件。

// crash_dump.cpp —— 崩溃时写 dump 的最小实现 #include <windows.h> #include <dbghelp.h> #include <strsafe.h> #pragma comment(lib, "dbghelp.lib") static wchar_t g_dumpDir[MAX_PATH] = L"C:\\ProgramData\\AppCrash\\"; // 文件名带 pid + 时间戳,避免多进程或连续崩溃互相覆盖 static void BuildDumpPath(wchar_t* out, size_t cch) { SYSTEMTIME st; GetLocalTime(&st); StringCchPrintfW(out, cch, L"%lsapp_%lu_%04d%02d%02d_%02d%02d%02d.dmp", g_dumpDir, GetCurrentProcessId(), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); } static LONG WINAPI CrashFilter(EXCEPTION_POINTERS* ep) { wchar_t path[MAX_PATH] = {0}; BuildDumpPath(path, MAX_PATH); // 独占句柄:写盘期间不希望别的进程读到半截文件 HANDLE h = CreateFileW(path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (h == INVALID_HANDLE_VALUE) return EXCEPTION_EXECUTE_HANDLER; MINIDUMP_EXCEPTION_INFORMATION mei = {}; mei.ThreadId = GetCurrentThreadId(); mei.ExceptionPointers = ep; mei.ClientPointers = FALSE; // 同进程内调用必须为 FALSE MINIDUMP_TYPE type = (MINIDUMP_TYPE)( MiniDumpWithDataSegs | MiniDumpWithHandleData | MiniDumpWithThreadInfo | MiniDumpWithUnloadedModules | MiniDumpWithProcessThreadData | MiniDumpWithIndirectlyReferencedMemory); MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), h, type, &mei, nullptr, nullptr); CloseHandle(h); // 写完直接结束进程,不再交给系统弹“程序已停止工作” return EXCEPTION_EXECUTE_HANDLER; } void InstallCrashDumpHandler(const wchar_t* dir) { if (dir) StringCchCopyW(g_dumpDir, MAX_PATH, dir); CreateDirectoryW(g_dumpDir, nullptr); // 目录不存在先建,别指望系统帮你建 SetUnhandledExceptionFilter(CrashFilter); // 无调试器附加时才生效 }

MINIDUMP_EXCEPTION_INFORMATION里三个字段都有讲究。ThreadId必须是触发异常的那个线程,写成主线程 ID 会让分析器去错误的线程上找上下文,打开 dump 后kb出来一片空白。ExceptionPointers直接透传过滤器入参,不要自己伪造EXCEPTION_RECORD。ClientPointers是最容易被复制粘贴搞错的一个参数:dump 由本进程写自己,必须填FALSE;只有当你在另一个进程里给别人写 dump 时才填TRUE,填反了的表现是 dump 能生成、也能打开,但异常上下文全是空的,等于白写。

2.3 dump 类型怎么选:从只需调用栈到全内存

MINIDUMP_TYPE是一组位标志,选多了文件几百兆上传不动,选少了看不到局部变量和堆内容。下面这张表是我在几个项目里反复调整后固定下来的取舍。

标志体积影响实际能多看到什么
MiniDumpNormal最小,几百 KB线程栈、模块列表,够看崩溃点调用栈
MiniDumpWithDataSegs小全局变量、静态变量,能看配置和单例状态
MiniDumpWithHandleData小句柄表,查句柄泄漏和句柄误用
MiniDumpWithThreadInfo很小每个线程的时间、优先级、起始地址
MiniDumpWithUnloadedModules很小已卸载的 DLL,排查“延迟加载后被卸载”导致崩溃
MiniDumpWithIndirectlyReferencedMemory中等,不可控栈上指针指向的内存,能看字符串和部分对象
MiniDumpWithFullMemory极大,可能几个 G整个进程内存,可做堆分析和对象还原

我的默认组合就是 2.2 里那一串,体积通常在几 MB 到几十 MB,覆盖九成以上的崩溃。MiniDumpWithIndirectlyReferencedMemory要留个心眼:它的大小取决于崩溃时栈上有多少指针,偶尔会膨胀到意想不到的程度,如果 dump 目录所在的磁盘空间紧张,可以把它去掉。真正需要MiniDumpWithFullMemory的场景只有两类——怀疑堆破坏(要!heap系列命令)、需要还原某个对象的具体内容。这类 dump 建议单独走“用户手动触发”通道,而不是每次崩溃都全量写,否则上传和存储都会失控。

2.4 不走 SEH 的崩溃:terminate、purecall 与参数检查

上面那套代码对访问违例、除零、非法指令有效,但对 C++ 层面的致命错误无效。比如抛出异常却没人接,最终会走std::terminate;调用纯虚函数会走_purecall;strcpy_s这类安全函数失败会走 CRT 的无效参数处理器。这三条路径的默认行为是直接终止进程,SetUnhandledExceptionFilter收不到任何东西。补法是显式接管,然后在处理器里主动抛一个 SEH 异常,把控制权交回过滤器。

#include <cstdlib> #include <exception> #include <csignal> // 统一用一个自定义异常码,避免和系统异常码混淆 static const DWORD kCrashDumpCode = 0xE0000DMP; void InstallNonSehHandlers() { // 1. 未捕获的 C++ 异常 std::set_terminate([] { RaiseException(kCrashDumpCode, EXCEPTION_NONCONTINUABLE, 0, nullptr); }); // 2. 纯虚函数调用 _set_purecall_handler([] { RaiseException(kCrashDumpCode, EXCEPTION_NONCONTINUABLE, 0, nullptr); }); // 3. CRT 参数检查失败(如缓冲区过小) _set_invalid_parameter_handler([](const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, uintptr_t) { RaiseException(kCrashDumpCode, EXCEPTION_NONCONTINUABLE, 0, nullptr); }); // 4. abort():先关掉弹框行为,再让 SIGABRT 转成异常 _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); signal(SIGABRT, [](int) { RaiseException(kCrashDumpCode, EXCEPTION_NONCONTINUABLE, 0, nullptr); }); }

四个 handler 都是全局的,一个进程只能各设一个,如果你的工程里有别的库也在设,最后注册的生效,这一点和SetUnhandledExceptionFilter一样,需要在实际发布前确认。另外RaiseException在 handler 里调用是安全的,它只是构造一个异常记录走分发流程,不依赖堆分配。有人担心这里会无限递归,实际上EXCEPTION_NONCONTINUABLE保证了异常不会被继续分发,第二次触发会直接终止进程,不会死循环。

3. dump 写不出来:0 字节文件、返回 FALSE 与 GetLastError 的三类真相

3.1 ERROR_ACCESS_DENIED:权限、目录与进程令牌

最典型的报错是MiniDumpWriteDump返回FALSE,GetLastError()拿到 5。根因几乎都是路径不可写:写到Program Files下的安装目录、写到只读的共享盘、写到需要管理员权限的根目录。普通用户权限的进程对安装目录没有写权限,这是 Windows 的默认行为,不是你代码的问题。更隐蔽的一种是 UAC 提权场景:进程以管理员启动,但它拉起子进程时令牌被过滤,子进程实际是普通权限,于是同一个目录在主进程能写、在子进程写不了。解决办法是固定写到%ProgramData%下按产品名建的子目录,并在安装时给这个目录授予 Users 组写权限;临时兜底可以退到%TEMP%,但要注意%TEMP%可能被清理策略定期删除,不适合当正式方案。

// 多级目录回退 + 每级都记录错误码 static BOOL WriteDumpWithFallback(EXCEPTION_POINTERS* ep) { const wchar_t* candidates[] = { L"C:\\ProgramData\\AppCrash", // 首选:安装时已授权 L"%TEMP%", // 次选:当前用户临时目录 L"%USERPROFILE%\\AppCrash" // 兜底:用户目录 }; wchar_t dir[MAX_PATH] = {0}; wchar_t path[MAX_PATH] = {0}; for (const wchar_t* c : candidates) { ExpandEnvironmentStringsW(c, dir, MAX_PATH); CreateDirectoryW(dir, nullptr); // 已存在时返回失败,忽略即可 SYSTEMTIME st; GetLocalTime(&st); StringCchPrintfW(path, MAX_PATH, L"%ls\\app_%lu_%04d%02d%02d_%02d%02d%02d.dmp", dir, GetCurrentProcessId(), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); HANDLE h = CreateFileW(path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (h == INVALID_HANDLE_VALUE) { DWORD e = GetLastError(); // 立刻取,后续调用会覆盖 continue; } MINIDUMP_EXCEPTION_INFORMATION mei = { GetCurrentThreadId(), ep, FALSE }; BOOL ok = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), h, MiniDumpWithDataSegs | MiniDumpWithThreadInfo, &mei, nullptr, nullptr); DWORD err = ok ? 0 : GetLastError(); // 同样要在 CloseHandle 之前取 CloseHandle(h); if (ok) return TRUE; } return FALSE; }

这段代码里有个必须强调的顺序问题:GetLastError()必须在CloseHandle之前调用。CloseHandle内部会调用系统服务,成功时也可能把 last error 改成 0,于是你日志里记录的永远是 0,排查时被误导一大圈。这个坑我在两个项目里各踩过一次,后来统一约定:任何记录错误码的地方,取值语句紧跟在失败调用之后,中间不允许插入其他 API。

3.2 返回 FALSE 但 GetLastError 是 0

这个现象第一次遇到时非常迷惑:MiniDumpWriteDump明确失败,GetLastError干干净净是 0。原因在于 DbgHelp 内部有一部分失败路径(尤其是内存映射和压缩相关的分支)不设置线程的 last error,而是直接返回失败。判别方法是换个思路看返回值之外的信息:文件是否被创建了、大小是多少。如果文件被创建但大小是 0,说明失败发生在写入阶段而不是创建阶段,常见原因是磁盘空间不足、写到了不支持随机写的路径(比如某些网络重定向目录)、或者被杀软拦在了写入那一刻。如果文件压根没创建,那还是回 3.1 的权限问题。

针对这种“错误码为 0”的情况,可以挂一个MINIDUMP_CALLBACK_INFORMATION,在回调里拿到 DbgHelp 内部的状态。回调的CallbackType为MiniDumpWriteDumpCallback时能读到当前的写入进度和错误状态,虽然信息有限,但至少能确认它卡在哪个阶段。

static BOOL CALLBACK DumpCallback(void* /*param*/, const PMINIDUMP_CALLBACK_INPUT input, PMINIDUMP_CALLBACK_OUTPUT output) { if (input->CallbackType == MiniDumpWriteDumpCallback) { // Status 反映内部阶段,配合日志能判断卡在映射还是写盘 OutputDebugStringW(L"dump callback hit\n"); } return TRUE; // 返回 FALSE 会让写 dump 提前中止 }

回调里绝对不能做重活,不能分配大块内存,不能加锁,更不能写日志到网络。它运行在崩溃线程上,任何阻塞都可能导致整个转储超时。

3.3 dbghelp.dll 版本错配与位数错配

系统目录里那份dbghelp.dll版本比较老,很多较新的MINIDUMP_TYPE标志它不认识,表现是调用失败并返回0x80004005(一个笼统的失败码),或者 dump 写出来但缺少线程信息、缺少未加载模块。做法是把较新 SDK 里的dbghelp.dll复制到 exe 同目录,让它优先于系统目录被加载。注意这是个需要随程序一起分发的文件,别只在开发机上放了就以为万事大吉——客户机上没有它,行为会退化回老版本。

位数错配是另一类。32 位进程只能给 32 位进程写 dump,64 位对 64 位,交叉调用会失败或产出无法解析的文件。这个问题在多进程架构里特别容易出现:主程序是 64 位,崩溃的是它拉起的 32 位子进程,你在主程序里对着子进程句柄调MiniDumpWriteDump,结果一定不对劲。正确做法是让每个进程自己写自己的 dump,主进程只负责收集。

3.4 在异常处理里再崩一次:堆锁、加载器锁与超时

最糟的情况是MiniDumpWriteDump自己卡死。它要遍历模块列表、读取线程上下文,这些操作需要拿加载器锁和堆锁。如果崩溃发生时另一个线程正好持有堆锁并且挂在那里,写 dump 的线程就会一直等,用户看到的是“程序无响应”,任务管理器强杀之后 dump 自然也没有。这种超时在黑匣子式的线上环境里特别常见,日志上看就是“崩溃了但没有任何产物”。

常见的缓解手段有三条。一是把写 dump 的动作放到一个独立的监视线程里,用事件通知,崩溃线程只负责触发和等待一个有限时间。二是在过滤器里先用CreateFileW算好路径、把文件句柄准备好,减少崩溃后的工作量。三是设置超时保护,等待超过设定时间就放弃写入直接退出,宁可丢一份 dump 也不要让进程僵在那里。

// 用等待超时保护,避免写 dump 卡死整个进程 static HANDLE g_hDumpDone = nullptr; // 启动时 CreateEventW 创建 static LONG WINAPI CrashFilterWithTimeout(EXCEPTION_POINTERS* ep) { // 触发监视线程(伪代码:把 ep 存到全局并 SetEvent) // ... // 最多等 20 秒,超时就放弃,保证进程能被正常回收 WaitForSingleObject(g_hDumpDone, 20000); return EXCEPTION_EXECUTE_HANDLER; }

超时时间不要设太长,20 秒已经是用户耐心的极限。另外监视线程要在进程启动早期就创建好,别等崩溃了再CreateThread,那时候堆可能已经坏了。

4. 栈全是问号:符号不匹配导致的 dump 打不开、看不出

4.1 PDB 的 GUID 和 Age 必须和 dump 里记录的模块对上

dump 能打开但调用栈是app+0x1a2b这种形式,或者干脆一片???,九成是符号没对上。每个模块在编译链接时会生成一个 PDB,里面带着 GUID 和 Age,这两个值同时被写进 exe 的调试目录和 dump 的模块信息里。分析时调试器按 GUID+Age 去符号路径里找匹配的 PDB,找不到就只给地址。这里最容易翻车的是“代码没变但重新编译了一次”——GUID 变了,旧的 PDB 立刻失效,而你手上的 dump 是上一版程序产生的,于是永远对不上。血泪经验是:每次发布必须把当次的 exe、pdb 和 dump 一起归档,别指望“代码差不多,用新 PDB 凑合看一下”,凑合的结果就是你在错误的行号上分析半天。

验证方法很简单,在分析器里加载 dump 后看模块列表,对比 PDB 路径和实际文件的时间戳。如果 PDB 改了名字,也可以在符号路径里做映射;但根本解法还是归档。

4.2 符号路径怎么配:本地缓存加内部符号库

符号路径不要只写一个网络目录,网络抖一下整个分析就卡住。推荐组合是“本地缓存 + 内部符号库”,格式上是srv*本地缓存路径*符号源路径,调试器会先查本地缓存,没有再去源上取并缓存下来。

.sympath srv*C:\symbols*\\fs01\symbols\public .reload /f

reload /f的f是强制重新加载,不做这个动作时调试器会用懒加载,第一次看栈可能不全。符号源里除了自己产品的 PDB,还要放系统模块的符号,否则栈走到系统 DLL 就断了——很多崩溃的根因恰恰在系统调用返回之后的回调里。

4.3 Release 下的内联与 FPO:为什么栈是断的

调试版 dump 一切正常,发布版一打开栈就断成几截,这是优化导致的。函数内联之后,栈上不再有独立的帧,调试器只能显示最外层的函数;帧指针省略(FPO)让调试器无法通过 EBP 链回溯,只能靠 PDB 里的展开信息猜,猜不准就断。应对办法有三条:发布版保留/Zi并生成 PDB(不要用/DEBUG:NONE);链接时避免用/OPT:REF把未引用的函数裁掉太多,否则部分栈帧的符号会缺失;关键模块可以考虑对核心崩溃路径关掉激进优化,代价是那部分性能略降。

另一个常被忽略的点是静态链接 CRT。静态 CRT 会把大量运行时函数编进你的 exe,符号都落在你自己的 PDB 里,栈上会出现一堆看起来像业务代码的运行时函数,读起来很折磨。这不是错误,只是噪音,分析时按模块和函数名前缀过滤掉就行。

4.4 一套可以照抄的分析命令序列

拿到 dump 之后不要乱点,按固定顺序走一遍,绝大多数崩溃五分钟内能定性。

.ecxr // 切到异常发生的线程和上下文,第一步永远是这句 !analyze -v // 让分析器给初步结论:异常码、出错模块、可能原因 kb // 当前线程调用栈,带参数 ~*kb // 所有线程的栈,看有没有死锁或等待链 lm vm 你的模块名 // 确认模块版本、时间戳、PDB 是否加载成功 .exr -1 // 查看完整异常记录,包括嵌套异常

.ecxr必须第一个执行,它把上下文切到异常线程,后面的kb才有意义。如果跳过这一步直接kb,看到的是分析器当前所在线程,往往是系统线程,栈上一堆ntdll,很容易误判成“崩溃在系统里”。!analyze -v给出的“可能原因”只是启发式结论,不要直接当答案,它的价值在于快速告诉你异常码和出错模块,剩下的还得自己看栈。

5. dump 方案避坑:五条踩过的坑,每条都能让你白等一晚

5.1 坑一:调试时永远没有 dump

现象:本地用 IDE 跑,程序照样崩,dump 目录空空如也,代码逻辑检查了十遍也没问题。

原因:调试器附加时,未处理异常优先交给调试器,UnhandledExceptionFilter不触发。

解决:验证时脱离调试器直接运行 exe;必须在调试器里测的话,把调试器配置成“异常不中断、继续执行”,让异常走到程序自己的过滤器。

5.2 坑二:dump 文件 0 字节,日志里错误码是 0

现象:崩溃后目录里多了一个 0 字节的 dmp,日志记录GetLastError() = 0,完全无法定位。

原因:错误码在CloseHandle之后才取,被覆盖成 0;真实原因多半是磁盘空间或者写入路径不支持随机写。

解决:把取错误码的语句紧贴失败调用,中间不插入任何 API;同时校验文件大小,为 0 就换下一个候选目录重试。

5.3 坑三:发布版 dump 打开后栈只有一层

现象:调试版 dump 栈完整,发布版打开只有顶层函数和一堆地址。

原因:内联加 FPO,加上发布版没带 PDB,或者 PDB 是后来重新编译生成的,GUID 对不上。

解决:发布版保留/Zi生成 PDB,把 exe 和 pdb 成对归档;对照模块列表确认 PDB 已加载,必要时把符号路径指向归档目录。

5.4 坑四:多进程程序只抓到主进程的 dump

现象:子进程崩溃,主进程无感,dump 目录里只有主进程的历史文件。

原因:子进程没有独立注册过滤器,主进程也收不到子进程的未处理异常。

解决:把InstallCrashDumpHandler做成公共初始化函数,每个进程启动时都调一次;主进程通过进程退出码和 dump 目录做收集。

5.5 坑五:加了 dump 之后崩溃率“变高”了

现象:上线 dump 机制后,崩溃上报数量翻了几倍,团队一度以为新版本引入了 bug。

原因:以前崩溃是静默的,用户重开就算了;现在每次未处理异常都会留下文件并被上报,把原本隐性的问题显性化了。

解决:把上线初期的数据当作基线,按异常码和模块聚类,优先处理高频项;同时给上报加采样和去重,避免同一问题刷爆存储。

6. 拿到 dump 之后:十分钟内定位崩溃点的排查顺序

dump 到手之后最忌讳的是东点一下西点一下。我现在的固定顺序是:先确认这份 dump 是否可用,再确定崩溃点,最后判断责任模块。第一步看模块列表和异常记录,确认 PDB 加载成功、异常码是什么;第二步.ecxr切上下文,kb看栈,判断是空指针、野指针、栈溢出还是堆破坏;第三步看所有线程,判断是单线程崩溃还是互相等待导致的二次问题。经验上,异常码0xC0000005是访问违例(绝大多数是空指针和已释放对象),0xC00000FD是栈溢出,0xC0000409是栈缓冲区检查失败,0xE0000DMP是我自己定义的 C++ 层崩溃,看到它就知道要去查terminate之前的那段逻辑。

异常码含义优先看什么
0xC0000005访问违例出错指令的寄存器、栈上的指针值、对象是否已释放
0xC00000FD栈溢出递归深度、栈上大数组、线程栈大小配置
0xC0000409栈缓冲区检查失败越界写、strcpy类调用、结构体布局变更
0xC0000374堆损坏需要全内存 dump,看堆块头和分配记录
0xE0000DMP自定义(C++ 层崩溃)terminate前的日志、纯虚调用、参数检查

验证一份 dump 是否“够用”,我有个简单办法:打开后直接看异常线程的栈顶三帧是否都有函数名和行号。如果三帧里有两帧是地址,说明符号或优化有问题,这时候继续分析就是在猜,不如先回去把 PDB 和编译选项修好再来看。还有一个习惯是每份 dump 都先把!analyze -v的输出整段保存下来,和工单一起归档,下次遇到同类异常码可以直接比对栈的形态,比重新分析快得多。

从那以后我每次上线前都强制走一遍“故意崩一次”的验证:在测试环境里人为触发一次空指针和一次terminate,确认 dump 目录真的多了文件、文件大小正常、用分析器能打开并看到完整栈,这三条全过才算这套机制可用。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询