【WinDbg 入门:用 C++ 自动生成 Dump,并学会分析崩溃现场】

【WinDbg 入门:用 C++ 自动生成 Dump,并学会分析崩溃现场】 WinDbg 入门用 C 自动生成 Dump并学会分析崩溃现场最近一直在琢磨 WinDbg 调试器作为 Windows 平台 C/C 开发的“排错神器”它总能在程序崩溃、卡死、内存泄漏时帮上大忙。对于刚接触它的新手来说不用一上来就死记硬背一堆命令先理清这条核心调试主线学习起来会轻松很多程序崩溃 ↓ 生成 Dump 文件 ↓ 准备对应 PDB ↓ 用 WinDbg 打开 Dump ↓ 查看异常类型 ↓ 查看调用栈 ↓ 定位到具体函数和代码位置今天这篇博客就用一个实操性极强的 C 练习程序带大家完整走一遍流程——用SetUnhandledExceptionFilter MiniDumpWriteDump自动生成 Dump 文件再用 WinDbg 一步步分析崩溃现场。这个练习程序特意做了三个常见崩溃场景空指针写入、整数除零、多层调用后崩溃而且会把 Dump 自动生成到 exe 旁边的 dumps 目录新手也能轻松找到。一、先搞懂WinDbg 到底是什么WinDbg 是微软官方推出的 Windows 平台调试工具专门用来解决 C/C 程序的各种“疑难杂症”比如程序崩溃 程序异常 程序卡死 线程死锁 CPU 占用高 内存泄漏 访问冲突 堆破坏如果做 Windows 客户端开发WinDbg 基本是必备工具。大家可以这样类比理解降低记忆成本Windows 平台WinDbg Linux 平台GDB这里要特别说一句实际项目中WinDbg 最常用的不是“边写代码边单步调试”而是“事后排错”流程大概是这样客户程序崩溃 ↓ 生成 .dmp 文件 ↓ 开发拿到 Dump ↓ 用 WinDbg 打开 ↓ 分析异常类型、调用栈、模块、线程这也是我们今天重点学习的场景——通过 Dump 文件还原崩溃现场。二、WinDbg 的两种核心分析方式WinDbg 分析问题主要分两种新手建议先从第一种入手更容易建立信心1. 静态分析 Dump 文件重点 2. 动态调试目标进程1. 静态分析 Dump 文件实际项目最常用静态分析的核心是“事后复盘”流程很简单程序运行 ↓ 程序发生异常或崩溃 ↓ 生成 Dump 文件 ↓ 开发人员拿到 Dump 文件 ↓ 用 WinDbg 打开 Dump 分析可以把 Dump 文件理解成「程序崩溃那一刻的现场快照」——程序虽然已经退出但 Dump 里保存了当时的异常信息、线程信息、调用栈、模块信息、寄存器状态甚至部分内存数据足够我们还原“事故真相”。这种方式特别适合这些场景客户现场崩溃无法远程调试 线上环境异常 偶发问题很难复现 程序已经退出但保留了 Dump 的情况2. 动态调试目标进程动态调试就是“实时监控”把 WinDbg 附加到正在运行的程序上流程如下程序正在运行 ↓ WinDbg Attach 到目标进程 ↓ 程序继续运行 ↓ 一旦异常WinDbg 中断 ↓ 开发人员查看调用栈、线程、变量、内存用一句通俗的话区分两种方式静态分析事后看事故现场照片 动态调试现场盯着程序出问题新手建议先吃透静态分析再学动态调试循序渐进更高效。三、Dump 文件崩溃现场的“快照”Dump 文件也叫转储文件常见扩展名是\.dmp它保存的是程序运行某一刻的完整状态里面主要包含这些关键信息异常信息为什么崩溃 线程信息哪些线程在运行 调用栈程序崩溃前走了哪些步骤 模块列表加载了哪些 exe/dll 寄存器状态CPU 当时的状态 部分内存数据关键变量的值 进程运行状态对 WinDbg 静态分析来说Dump 文件就是最核心的“输入材料”——没有 Dump就没法复盘崩溃现场。MiniDump 和 Full Dump 的区别新手必看Dump 文件主要分两类新手不用纠结先掌握 MiniDump 就够了1. MiniDump小型转储体积很小通常只有几 MB主要保存最核心的崩溃信息异常类型、异常地址 线程调用栈 模块信息、寄存器信息 少量必要内存优点很明显文件小、方便上传、适合自动收集大多数崩溃问题用它就够了缺点是信息不完整复杂的内存问题可能分析不了。2. Full Dump完整转储包含的信息最完整但文件也很大——可能比程序本身大好几倍。很多新手会疑惑“我的 exe 才几十 KBDump 怎么有几十 MB”其实原因很简单exe 只是程序文件本体 程序运行后会加载 DLL 程序运行后会分配堆内存 程序运行后会创建线程 Dump 保存的是运行时现场不是程序本身所以 Dump 文件大是正常的练习时我们用 MiniDump但会配置更多信息方便观察。四、PDB 文件分析 Dump 的“钥匙”很多新手打开 Dump 后看到的都是一堆乱码一样的地址找不到函数名问题就出在 PDB 文件上。PDB 全称是Program Database File中文叫程序数据库文件也叫调试符号文件它的作用就像“地址说明书”保存了这些关键信息函数名比如 CauseNullPointerWrite 变量信息比如 value 是 null 源码文件路径、源码行号 地址和函数的对应关系没有 PDB 时WinDbg 只能显示一堆地址00007ff612345678 00007ff61234abcd有了正确的 PDBWinDbg 就能显示清晰的函数名甚至定位到源码行DumpPractice!CauseNullPointerWrite DumpPractice!CauseStackCrashLevel3 DumpPractice!RunScenario DumpPractice!main这里有个关键注意点PDB 必须和 EXE/DLL 是同一次编译的产物不能拿其他版本的 PDB 凑数也不要随便改 PDB 文件名否则会加载失败。总结一下这几个文件的关系Dump崩溃现场 EXE/DLL程序本体 PDB地址说明书 WinDbg把它们组合起来分析问题五、实操环节用 C 自动生成 Dump理论讲完进入最核心的实操——写一个 C 程序自动生成 Dump 文件。这个程序叫DumpPractice没有复杂的业务逻辑纯粹为了帮大家练习如何让程序崩溃、如何生成 Dump、如何用 WinDbg 分析。程序核心用了两个 APISetUnhandledExceptionFilter捕获未处理异常和MiniDumpWriteDump生成 Dump 文件这也是实际项目中最常用的 Dump 生成方式。完整代码可直接复制编译#includewindows.h#includedbghelp.h#includecstdio#includecstring#includecwchar#includestring#pragmacomment(lib,Dbghelp.lib)namespace{std::wstring g_lastDumpPath;// 获取 DumpPractice.exe 所在目录避免 Dump 生成到找不到的地方std::wstringGetExeDirectory(){wchar_texePath[MAX_PATH];ZeroMemory(exePath,sizeof(exePath));DWORD lengthGetModuleFileNameW(NULL,exePath,MAX_PATH);if(length0||lengthMAX_PATH){returnL.;}wchar_t*lastSlashwcsrchr(exePath,L\\);if(lastSlashNULL){returnL.;}*lastSlashL\0;returnexePath;}// 生成 Dump 路径包含时间和 PID避免多次运行覆盖std::wstringBuildDumpPath(){conststd::wstring exeDirectoryGetExeDirectory();conststd::wstring dumpDirectoryexeDirectoryL\\dumps;CreateDirectoryW(dumpDirectory.c_str(),NULL);SYSTEMTIME now;ZeroMemory(now,sizeof(now));GetLocalTime(now);wchar_tfileName[MAX_PATH];ZeroMemory(fileName,sizeof(fileName));swprintf_s(fileName,L%ls\\DumpPractice_%04u%02u%02u_%02u%02u%02u_pid%lu.dmp,dumpDirectory.c_str(),now.wYear,now.wMonth,now.wDay,now.wHour,now.wMinute,now.wSecond,GetCurrentProcessId());returnfileName;}// 核心函数捕获异常后生成 DumpLONGWriteDumpForException(EXCEPTION_POINTERS*exceptionInfo){g_lastDumpPathBuildDumpPath();// 创建 .dmp 文件HANDLE dumpFileCreateFileW(g_lastDumpPath.c_str(),GENERIC_WRITE,0,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);if(dumpFileINVALID_HANDLE_VALUE){std::printf(CreateFileW failed. GetLastError%lu\n,GetLastError());returnEXCEPTION_EXECUTE_HANDLER;}// 配置异常信息告诉系统哪个线程崩溃、异常现场在哪里MINIDUMP_EXCEPTION_INFORMATION exceptionParam;ZeroMemory(exceptionParam,sizeof(exceptionParam));exceptionParam.ThreadIdGetCurrentThreadId();exceptionParam.ExceptionPointersexceptionInfo;exceptionParam.ClientPointersFALSE;// 配置 Dump 类型包含更多信息方便新手观察constMINIDUMP_TYPE dumpTypestatic_castMINIDUMP_TYPE(MiniDumpWithFullMemory|MiniDumpWithHandleData|MiniDumpWithThreadInfo|MiniDumpWithUnloadedModules);constBOOL okMiniDumpWriteDump(GetCurrentProcess(),GetCurrentProcessId(),dumpFile,dumpType,exceptionParam,NULL,NULL);CloseHandle(dumpFile);if(ok){std::wprintf(L\nDump written: %ls\n,g_lastDumpPath.c_str());std::printf(Open it with WinDbg, then run: !analyze -v; .ecxr; k; dv\n);}else{std::printf(MiniDumpWriteDump failed. GetLastError%lu\n,GetLastError());}returnEXCEPTION_EXECUTE_HANDLER;}// 注册顶层未处理异常过滤函数LONG WINAPIWriteDumpOnUnhandledException(EXCEPTION_POINTERS*exceptionInfo){returnWriteDumpForException(exceptionInfo);}// 场景1空指针写入访问冲突__declspec(noinline)voidCauseNullPointerWrite(){std::printf(About to write through a null pointer.\n);volatileint*valueNULL;*value0x1234;}// 场景2整数除零另一种异常类型__declspec(noinline)intCauseDivideByZero(){std::printf(About to divide by zero.\n);volatileintnumerator100;volatileintdenominator0;returnnumerator/denominator;}// 场景3多层调用后崩溃练习看调用栈__declspec(noinline)voidCauseStackCrashLevel3(intmarker){std::printf(Stack marker %d. The next frame crashes.\n,marker);CauseNullPointerWrite();}__declspec(noinline)voidCauseStackCrashLevel2(intmarker){CauseStackCrashLevel3(marker1);}__declspec(noinline)voidCauseStackCrashLevel1(){CauseStackCrashLevel2(41);}__declspec(noinline)voidCauseStackCrash(){std::printf(About to crash through a small call chain.\n);CauseStackCrashLevel1();}// 打印使用说明voidPrintUsage(constchar*exeName){std::printf(Usage:\n);std::printf( %s null - write through a null pointer\n,exeName);std::printf( %s divzero - trigger integer divide by zero\n,exeName);std::printf( %s stack - crash after several stack frames\n,exeName);std::printf(\nDouble-click or run without arguments defaults to: null\n);}// 根据命令行参数选择崩溃场景voidRunScenario(constchar*scenario){if(std::strcmp(scenario,null)0){CauseNullPointerWrite();}elseif(std::strcmp(scenario,divzero)0){std::printf(Result %d\n,CauseDivideByZero());}else{CauseStackCrash();}}// 单独封装 __try/__except避免编译报错intRunScenarioAndWriteDump(constchar*scenario){__try{RunScenario(scenario);}__except(WriteDumpForException(GetExceptionInformation())){return1;}return0;}}// namespaceintmain(intargc,char*argv[]){// 注册异常过滤函数捕获未处理异常SetUnhandledExceptionFilter(WriteDumpOnUnhandledException);std::printf(DumpPractice PID: %lu\n,GetCurrentProcessId());std::wprintf(LDump directory: %ls\\dumps\n\n,GetExeDirectory().c_str());// 默认场景null双击 exe 直接运行constchar*scenarioargc1?argv[1]:null;if(argc1){std::printf(No scenario specified; defaulting to: null\n\n);PrintUsage(argv[0]);}// 校验命令行参数if(std::strcmp(scenario,null)!0std::strcmp(scenario,divzero)!0std::strcmp(scenario,stack)!0){std::printf(Unknown scenario: %s\n\n,scenario);PrintUsage(argv[0]);return2;}returnRunScenarioAndWriteDump(scenario);}代码核心逻辑拆解新手必看这个程序启动后主要做了 6 件事链路很清晰main ↓ SetUnhandledExceptionFilter注册异常回调 ↓ RunScenarioAndWriteDump执行场景并捕获异常 ↓ RunScenario选择崩溃场景 ↓ 触发崩溃3种场景之一 ↓ WriteDumpForException生成 Dump ↓ MiniDumpWriteDump核心 API写入 Dump 文件这里有几个新手容易忽略的细节特意标注出来1. 为什么要获取 exe 所在目录很多新手双击 exe 运行时当前工作目录可能不是 exe 所在目录如果直接生成相对路径的 Dump很容易找不到文件。代码里通过GetExeDirectory\(\)函数把 Dump 固定生成到exe 所在目录\\dumps新手能快速找到。2. 为什么 Dump 文件名要加时间和 PID生成的 Dump 文件名类似DumpPractice\_20260515\_153012\_pid12345\.dmp这样做有两个好处一是多次运行不会覆盖之前的 Dump二是能通过时间和 PID 区分不同的崩溃记录练习和实际项目中都很实用。3. 为什么用 MiniDumpWithFullMemory虽然函数叫MiniDumpWriteDump但代码里配置了MiniDumpWithFullMemory这会让 Dump 文件大一点但包含的信息更多比如局部变量、堆内存、线程信息对新手练习来说信息越全越容易观察和理解。4. 为什么同时用 SetUnhandledExceptionFilter 和 __try/__except这是为了保证练习时能稳定生成 DumpSetUnhandledExceptionFilter是实际项目中常用的“顶层异常回调”方式而\_\_try/\_\_except是 Windows SEH 结构化异常处理能避免某些环境下异常捕获失败确保新手练习时每次都能生成 Dump。六、三个崩溃场景新手重点练习程序支持 3 种崩溃场景每种场景对应不同的异常类型建议都练一遍加深对 WinDbg 分析的理解。1. null空指针写入默认场景运行方式双击 exe或命令行输入DumpPractice\.exe null对应函数CauseNullPointerWrite\(\)。崩溃类型Access Violation访问冲突WinDbg 中异常代码为c0000005原因是程序试图向空指针NULL写入数据。2. divzero整数除零运行方式命令行输入DumpPractice\.exe divzero对应函数CauseDivideByZero\(\)。崩溃类型Integer Divide By Zero整数除零WinDbg 中异常代码为c0000094原因是分母为 0。3. stack多层调用后崩溃运行方式命令行输入DumpPractice\.exe stack对应调用链CauseStackCrash → CauseStackCrashLevel1 → CauseStackCrashLevel2 → CauseStackCrashLevel3 → CauseNullPointerWrite这个场景最适合练习看调用栈——不是直接在 main 里崩溃而是经过多层调用能帮你熟练掌握“通过调用栈还原崩溃路径”的核心能力。七、编译 运行 生成 Dump一步一步来新手不用怕跟着步骤来就能顺利生成 Dump1. 编译程序用 Visual Studio 创建 C 控制台项目项目名建议叫DumpPractice把上面的代码复制到main\.cpp然后配置生成 PDB关键步骤项目属性 → 配置属性 → 链接器 → 调试 → 生成调试信息是 新版本 VS 可检查C/C → 常规 → 调试信息格式 → 程序数据库 /Zi编译后会生成两个关键文件DumpPractice\.exe程序本体和DumpPractice\.pdb符号文件缺一不可。2. 运行生成 Dump有 4 种运行方式新手推荐前两种方式1双击 DumpPractice.exe默认 null 场景 方式2命令行输入 DumpPractice.exe null空指针写入 方式3命令行输入 DumpPractice.exe divzero整数除零 方式4命令行输入 DumpPractice.exe stack多层调用崩溃运行后会在 exe 旁边生成dumps文件夹里面就是生成的\.dmp文件。八、WinDbg 分析 Dump核心实操终于到了 WinDbg 的核心环节打开 WinDbg Preview 或 WinDbg跟着步骤来新手也能轻松分析。步骤1打开 Dump 文件打开 WinDbg选择File → Open dump file找到 dumps 文件夹里的\.dmp文件打开即可。步骤2设置 PDB 路径关键假设你的程序编译后在D:\\Study\\DumpPractice\\x64\\Debug64位 Debug 版本里面有 exe 和 pdb 文件在 WinDbg 命令行输入.sympath D:\Study\DumpPractice\x64\Debug .reload /f如果是 32 位 Debug 版本路径改成D:\\Study\\DumpPractice\\Debug即可。这一步是为了让 WinDbg 找到 PDB 文件否则看不到函数名。步骤3执行核心命令新手必记打开 Dump 后按顺序执行以下命令就能还原崩溃现场新手建议记熟这 6 条命令1. !analyze -v自动分析异常第一条必执行的命令WinDbg 会自动分析异常类型、崩溃地址、调用栈等信息。重点看EXCEPTION\_CODE异常代码、FAULTING\_IP崩溃地址、STACK\_TEXT调用栈。比如 null 场景会看到EXCEPTION\_CODE: c0000005访问冲突divzero 场景会看到EXCEPTION\_CODE: c0000094整数除零。2. .ecxr切换到异常上下文新手最容易犯的错误一打开 Dump 就看调用栈结果看不到正确的崩溃现场。执行\.ecxr后WinDbg 会切换到真正发生异常的线程和寄存器上下文后续查看调用栈才准确。3. kv查看详细调用栈执行\.ecxr后再执行kv就能看到完整的调用栈。比如 stack 场景会看到从main到CauseNullPointerWrite的完整调用链路能清晰看到程序是怎么一步步崩溃的。4. dv查看局部变量用来查看当前栈帧中的局部变量。比如 null 场景在CauseNullPointerWrite栈帧中执行dv能看到value 0x00000000明确知道是空指针导致的崩溃。5. .exr -1查看异常详情查看最近一次异常的详细记录比如访问冲突的参数是读内存失败还是写内存失败、访问的地址等能进一步定位崩溃原因。6. lmvm DumpPractice查看模块信息查看当前程序模块的详细信息重点看符号状态和PDB 路径确认 PDB 已经成功加载会显示Symbols loaded。新手练习流程建议收藏按这个顺序练习能快速掌握核心操作1. 编译程序确认生成 exe 和 pdb 2. 运行 DumpPractice.exe null生成 Dump 3. 用 WinDbg 打开 .dmp 文件 4. 输入 .sympath 配置 PDB 路径 5. 输入 .reload /f 重新加载符号 6. 输入 !analyze -v 自动分析异常 7. 输入 .ecxr 切换异常上下文 8. 输入 kv 查看详细调用栈 9. 输入 dv 查看局部变量 10. 输入 .exr -1 查看异常详情 11. 输入 lmvm DumpPractice 确认 PDB 加载成功练完 null 场景再依次练 divzero 和 stack 场景重点观察不同异常的代码和调用栈差异。九、常用命令速查新手必备整理了新手最常用的命令放在这里方便随时查看命令作用!analyze -v自动分析异常获取核心崩溃信息.ecxr切换到异常发生时的上下文.exr -1查看最近一次异常记录k查看基本调用栈kv查看详细调用栈dv查看当前栈帧局部变量lm查看所有加载的模块lmvm DumpPractice查看 DumpPractice 模块详细信息含 PDB 状态.sympath查看当前符号路径.sympath 路径添加符号路径加载 PDB.reload /f强制重新加载符号十、新手最容易踩的坑避坑指南1. 找不到 Dump 文件解决方案Dump 固定生成在exe 所在目录\\dumps去 exe 旁边找 dumps 文件夹即可不要去其他目录找。2. WinDbg 看不到函数名只有地址解决方案PDB 加载失败了。检查 3 点PDB 路径是否正确、PDB 和 exe 是否是同一次编译、有没有混淆 Debug/Release 或 x86/x64 版本。3. 调用栈看起来不对找不到崩溃函数解决方案先执行\.ecxr切换到异常上下文再执行kv查看调用栈不要直接打开 Dump 就看调用栈。4. Dump 文件很大担心占用空间解决方案练习时用MiniDumpWithFullMemory是为了方便观察实际项目中可以去掉这个配置生成体积更小的 MiniDump满足大部分崩溃分析需求。十一、总结WinDbg 入门的核心的是“思路”不是“命令”看到这里你已经掌握了 WinDbg 入门的核心流程。其实 WinDbg 不难新手不用死记硬背所有命令重点是记住这条调试链路程序注册异常处理函数 ↓ 程序发生崩溃 ↓ 异常处理函数拿到异常现场 ↓ MiniDumpWriteDump 生成 Dump ↓ WinDbg 打开 Dump ↓ 加载 PDB关键钥匙 ↓ !analyze -v 看异常类型 ↓ .ecxr 切换异常现场 ↓ kv 查看调用栈 ↓ dv 查看局部变量 ↓ lmvm 确认符号加载成功对新手来说只要能做到这几件事就算真正入门了1. 能让程序自动生成 .dmp 文件 2. 能用 WinDbg 打开 .dmp 文件 3. 能加载正确的 PDB 4. 能用 !analyze -v 看异常类型 5. 能用 .ecxr kv 看调用栈 6. 能从调用栈定位到自己的崩溃函数当你能通过DumpPractice\.exe stack看到完整的多层调用栈并且能清晰说出“程序是怎么一步步崩溃的”时WinDbg 的第一道门槛就已经跨过去了。后续可以慢慢探索更复杂的场景比如内存泄漏、死锁但入门阶段把今天的实操练熟就足够应对大部分基础的程序崩溃问题了。如果练习过程中遇到问题欢迎在评论区留言一起交流学习