把「用 WinDbg 分析 dump」从零散命令沉淀成可复制的方法论:dump 怎么采、环境怎么配、打开后按什么顺序查、每类症状走哪条命令链、结论怎么写。 覆盖 Windows 原生(C/C++/Win32/COM)、.NET Framework 与 .NET 6+、以及内核蓝屏(BSOD)三类现场。 文中命令均对照 2025–2026 年 learn.microsoft.com 官方文档逐条核实;社区流传但官方查无实据的写法集中在附录 B,落地前请先自查。
关键词:WinDbg · cdb · dump 采集 · WER LocalDumps · ProcDump · 符号与 PDB · !analyze · 死锁与等待链 · 线程池饥饿 · SOS · !gcroot · UMDH · TTD · BSOD · Driver Verifier
目录
- §0 速查页:开局命令六连、症状 → 命令链对照表、三条纪律
- §1 基础认知:dump 的本质与三条硬约束、dump/日志/ETW/TTD 取舍、类型与"能分析到什么"
- §2 工具链与环境:装哪个调试器(含 Store 版坑)、符号原理/路径/验证、
.reload选项、会话卫生、源码关联 - §3 Dump 采集:手段全景、
.dump/ProcDump/WER LocalDumps、内核转储、体积与隐私 - §4 通用流程:七步法 + 决策树 + 大 dump 操作纪律
- §5 原生崩溃:
!analyze逐段、异常码表、fastfail 子码、栈可信性与k家族、现场读取、bucket 语义、案例 - §6 挂起/死锁/高 CPU/内存增长:采集前置、等待点分类、临界区位域判读与追链、线程池饥饿、内核视角、完整案例
- §7 托管 .NET:SOS 加载与 DAC 校准、托管异常、GC 与
!gcroot、async/线程池、dotnet 工具族、两个案例 - §8 内核 BSOD:
!analyze -v逐段、常见 bugcheck 判读、命令清单(含 ✅/⚠️)、驱动定责、TTD - §9 进阶武器:页堆/Verifier、UMDH 差分、TTD、.NET 诊断工具族、cdb 批量与脚本/数据模型
- §10 命令速查总表 · §11 报告模板与判据 · §12 常见坑 23 条 · §13 团队落地建议
- 附录 A:采集侧完整参考 · §14 参考来源 · 附录 B:讹传命令清单
0. 速查页(先看这一页就够开干)
0.1 打开 dump 的标准开局(原生 / 通用)
:: 命令行打开(推荐:符号和二进制镜像路径一次给全) windbg -y "srv*C:\SymCache*https://msdl.microsoft.com/download/symbols" -i C:\build\bin -z E:\dumps\App.dmp cdb -z E:\dumps\App.dmp ; 无 GUI / 批量分析用 cdb :: 会话起手六连 .logopen C:\case\session.log ; 先开日志,命令与输出全留存 .symfix C:\SymCache ; 设微软公共符号 + 本地缓存 .reload ; 强制重载符号 vertarget ; OS 版本 / 构建号 / 位数 / 时间 lmvm <模块名> ; 核对 PDB 是否与 dump 匹配(时间戳/大小/GUID) !analyze -v ; 让引擎先跑一轮自动分析(崩溃型 dump 有效)
0.2 按症状选路径(最重要的一张表)
| 症状 | dump 里的本质 | 首选命令链 | 详见 |
|---|---|---|---|
| 崩溃 / 闪退 / 蓝屏 | 有异常记录或 bugcheck | !analyze -v → .exr -1 → .ecxr → kb → lmvm |
§5 / §8 |
| 白屏 / 界面无响应(跑久了恶化) | UI 线程被占用或有资源单调增长 | ~*e k L8 → UI 线程栈 → !runaway → 堆/队列统计 |
§6 |
| 卡死 / 完全冻结 | UI 线程被阻塞(锁、IO、消息泵) | !uniqstack → !locks -v / !critsec <addr> / !syncblk → 追到持有者 |
§6 |
| 内存持续上涨 | 某类对象/资源单调增长 | !heap -stat / !address -summary / !dumpheap -stat → !gcroot → UMDH 差分 |
§6.7 / §7.3 / §9.2 |
| CPU 占满 | 少数线程持续在跑 | !runaway 7 → 定位线程 → ~~[TID]s; kn; uf @rip |
§6.6 |
| 托管异常(.NET) | 0xE0434352 + 托管栈 | .loadby sos clr → !pe → !clrstack -a |
§7 |
0.3 三条纪律
- 先证伪再结论:任何「因为 X 所以崩」的结论,都必须能用命令指出 dump 里的证据(地址、栈帧、计数)。
- 符号不对,全部白搭:
lmvm的时间戳 / 镜像大小 /Unable to verify checksum与 CI 归档记录对不上时,栈和变量都不可信——先解决符号,再谈结论(校验三件套见 §2.2)。 - 大 dump 要收敛:GB 级 dump 上禁止无脑
~*e k/!dumpheap -type X(可能几分钟无输出);一律-stat/L8/ 定向线程。
1. 基础认知:dump 是什么,能回答什么
1.1 dump 的本质是一张「带元数据的进程/系统内存快照」
一个 minidump(.dmp / .mdmp)本质上是一组流(stream):模块列表、线程与栈内存、异常记录、句柄表、内存区内容、(可选)完整地址空间。内核 dump 则是某个时刻的物理内存内容。调试器做的所有事,都是「读流 + 用 PDB 解释字节」。
由此推出三条硬约束,直接决定分析上限:
- 只有被记录进 dump 的内存才能分析。minidump 缺内存 → 堆内容不可见 →
!heap -stat、!dumpheap、!gcroot结果残缺或报错。 - PDB 必须与产生 dump 的二进制完全一致(CodeView GUID + Age)。不匹配时栈回溯会给出「看似合理但错误」的符号,比崩溃更危险。
- dump 只有"现在",没有"之前"。它回答"此刻谁在做什么、持有什么、指向什么",不回答"这行内存是何时被写坏的"(那是页堆 / TTD / ETW 的活)。
1.2 dump / 日志 / ETW / TTD 的取舍
| 手段 | 优势 | 劣势 | 适合 |
|---|---|---|---|
| Dump | 事前零改造、零开销、按需触发 | 只有瞬时状态;回溯"为什么"要靠推断 | 生产环境崩溃、偶发卡死、内存增长 |
| 日志 / Telemetry | 轻量、贴合业务语义、可批量分析 | 覆盖不到意外路径;深度有限 | 业务链路、状态机、可预期的错误分支 |
| ETW / 采样分析(xperf、WPR、dotnet-trace) | 连续、低开销、能看趋势与热点 | 需要预先开启,事后无数据 | CPU 热点、IO 抖动、GC 行为、分配速率 |
| TTD(时间旅行调试) | 完整回放,可倒着执行,"因果直接可见" | 录制开销 10–20×,trace 动辄数 GB,仅用户态 | 本地/测试环境复杂逻辑 bug、难复现崩溃 |
经验法则:现场只有一次(生产、客户机器)→ dump;能复现 → 优先 TTD 或页堆;只有趋势可疑 → ETW/计数器先定位模块,再用 dump 定点解剖。
1.3 dump 类型与"能分析到什么"的对应关系
| 类型 | 内容 | 能做什么 | 不能做什么 |
|---|---|---|---|
用户态 Mini(默认 / DumpType=1) |
栈、模块表、异常记录、少量被引用内存 | 崩溃栈定位、异常码、模块版本、句柄表(若含) | 堆内容、对象图、GC 根、变量实际值 |
| 用户态 Heap / 私有读写内存 | 上述 + 私有可写页 | 多数内存泄漏与损坏分析、托管堆统计 | 代码段/只读数据细节 |
用户态 Full(-ma / DumpType=2) |
整个地址空间 | 一切(托管堆、原生堆、对象图、内存模式匹配) | 采集慢、体积≈私有提交内存、含敏感数据 |
| 内核 Complete | 全部物理内存 | 任意内核/用户结构、!chkimg |
页文件需 ≥ RAM |
| 内核 Kernel / Automatic | 仅内核空间 | 绝大多数 BSOD 归责 | 用户进程内容 |
| 内核 Small(triage) | bugcheck 参数 + 模块表 + 部分线程 | 定桶(FAILURE_BUCKET_ID)、初筛 | 深挖因果 |
一句话选型:要分析"对象、堆、根、变量值",就要 Full/Heap dump;只要"栈和异常",mini 足够。
2. 工具链与环境准备(分析质量的 80% 在这里)
2.1 装哪个调试器
| 工具 | 来源 | 用途 |
|---|---|---|
| WinDbg(新版,原 WinDbg Preview) | winget install Microsoft.WinDbg 或 https://aka.ms/windbg |
主力 GUI:现代界面、表格视图、TTD、JS/NatVis 脚本、数据模型 dx。与经典版同一引擎,命令完全兼容 |
| WinDbg (classic) | Debugging Tools for Windows(随 WDK/SDK) | 老系统、必须用完整扩展库(exts.dll/uext.dll/ntsdexts.dll)时 |
| cdb.exe / ntsd.exe | 同上 | 命令行批量分析、CI 里跑 !analyze -v、脚本化 |
| kd.exe | 同上 | 内核 dump / 内核实时调试 |
要点与坑:
- 新版 WinDbg 通过 扩展中心(Extension Library) 按需下载命令扩展(含 .NET 扩展)。Microsoft Store 版把可执行文件放在
WindowsApps下,ACL 限制直接执行/复制,脚本化调用cdb.exe时会遇到「找不到文件」的假象。 - 规避:把安装目录下的
amd64整目录复制到普通目录再调用;或直接装winget版本 / WDK 的 Debugging Tools(推荐做自动化分析时用后者)。 - 精简安装(Store 版)可能没有
ntsdexts.dll/uext.dll,于是!runaway、!handle、!process、!locks报Unable to add extension DLL。补法:text .load ntsdexts ; 或 .loadext ntsdexts.dll(新版用 .loadext 加载原生扩展 DLL) !extpath ; 查看扩展搜索路径 .chain ; 查看已加载扩展及其版本仍缺失就从完整 Debugging Tools 安装里拷贝ntsdexts.dll、exts.dll、uext.dll(注意位数要和 dump 位数一致)。 - 位数匹配:分析 64 位 dump 必须用 64 位调试器;分析 32 位 WOW64 进程用 32 位调试器(
-64参数可让 ProcDump 产出 64 位 dump)。
2.2 符号:原理、路径、验证
原理:加载时 PE 头里的 CodeView 调试目录记录了 PDB 文件名 + GUID + Age,调试器按这个三元组去找 PDB;符号服务器把文件组织成 <pdb名>\<GUID无横线><Age>\<pdb名>。因此符号能否命中与文件名/路径无关,与"是不是当初那份 PDB"强相关。
:: 1) 最省事:自动挂微软公共符号 + 本地缓存 .symfix C:\SymCache .sympath ; 打印当前路径(会显示展开后的完整路径) :: 展开后类似:cache*c:\MyCache;SRV*https://msdl.microsoft.com/download/symbols :: 2) 手工完整路径(内网 + 公网 + 本地构建目录,左→右顺序搜索) .sympath cache*;C:\build\bin;srv*C:\SymCache*https://msdl.microsoft.com/download/symbols;srv*\\symsrv\symbols :: 3) 环境变量(对 cdb/kd 更有效) set _NT_SYMBOL_PATH=srv*C:\SymCache*https://msdl.microsoft.com/download/symbols
- 符号路径语法要点:
srv*<本地缓存>*<符号存储>、cache*<目录>;每个目录条目实际会找<dir>\symbols\dll、<dir>\dll、<dir>。微软公共符号只支持 TLS 1.2+ 的 https。 - 只读缓存共享:
cache*\\fileserver\symcache(多机共用一份缓存)。 - 缓存清理用 AgeStore(按时间/容量删除旧符号)。
_NT_ALT_SYMBOL_PATH用于临时覆盖(如私有 PDB 覆盖公共 PDB)。- 二进制镜像路径
-i C:\build\bin:dump 里的 exe/dll 常与调试机器版本不同,-i让调试器去镜像目录取"当初那份",这对dv、??、dt解析与!chkimg至关重要。
符号是否可信,必须验证(不要跳过):
!sym noisy ; 打开符号加载详细输出 .reload /f ntdll.dll ; 强制重载单个模块,观察它逐个目录搜索失败的原因 lmvm myapp ; 看 PDB 路径 / Guid+Age / Timestamp / SizeOfImage / ImageSize lm ; 有 "no pdb" / "private pdb" / "export symbols" 标记的模块要单独处理 .reload /f ; 全量重载 !chkimg -d <模块> ; 内存里的代码 vs 磁盘/符号里的代码是否一致(查内存损坏/自改代码)
不匹配的典型症状与处置:
| 现象 | 原因 | 处置 |
|---|---|---|
Symbols Loaded (Export headers only) / no pdb |
找不到 PDB | symchk /id <dump> /s <path> 批量拉;或从 CI 符号库补齐 |
*** WARNING: Unable to verify checksum for X |
磁盘上的二进制与 dump 里那份不是同一个(反汇编/源码不可信) | 用 -i <镜像目录> 指到"当初那份" exe/dll |
| PDB 版本不匹配(调试器拒绝加载) | PDB 与二进制 GUID/Age 或版本不符 | 找回正确 PDB;临时排障可 .reload /i(官方原文:Ignores a mismatch in the .pdb file versions,且隐含 /f,仅用于应急判断,结论不可信) |
栈全是 Module!Unknown / 明显错位 |
符号不匹配 或 栈被破坏 | 先修符号;仍乱则按 §5.4 手工重建栈 |
变量显示 <optimized out> / @rbp-0x8 这种偏移 |
Release 优化 | 配 -i + 行信息;改用「寄存器 + dt 类型解释」路径 |
逐项核验符号的三件套(做正式结论前必须过一遍,并写进报告):
lmvm MyApp ; Timestamp、image_size、"Pdb file ... build ID"(GUID+Age) !lmi MyApp ; Time Stamp / Size / CheckSum :: 三者与 CI 归档记录逐字段比对;任一不符 → 本报告结论标注"符号未完全校验"
.reload 常用选项(官方):/f 强制立即加载(穿透 lazy loading)、/i 忽略 PDB 版本不匹配、/n 仅重载内核符号、/s 用系统模块表、/u 卸载、/v 详细输出、/o 覆盖下游缓存。镜像头信息损坏时可手工指定:.reload MyApp=<Base>,<Size>,<Timestamp>(三个值之间不能有空格)。
离线环境批量取符号:
symchk /id E:\dumps\App.dmp /s SRV*C:\SymCache*https://msdl.microsoft.com/download/symbols /oc C:\SymPkg symchk /r C:\build\bin /s SRV*C:\SymCache*https://msdl.microsoft.com/download/symbols /vd
2.3 会话卫生:让过程可复盘
.logopen /t C:\case\case-20261105.log ; /t 追加;全程留痕,报告可直接引用 .echo === 关键结论:xxx === ; 在日志里做标记,便于事后 grep .lines ; 查看源码行信息开关(需 PDB 含行信息;也可在菜单/设置里开) .logclose
- 新版 WinDbg:命令窗口支持 DML 链接、正则搜索、结果转表格视图(可排序/筛选/导出),大输出优先转表格再筛,比
grep文本更快。 - 保存/复用会话:新版可保存最近目标与启动配置(File 菜单),团队里让"打开某个 dump 的符号路径/镜像路径"变成一键动作。
- 输出重定向到文件做二次处理:
!dumpheap -stat > heapstat.txt(cdb 下重定向更可靠)。
2.5 源码关联:能看到行号与变量吗
真实可用的命令(官方文档核实):
.lines ; 打开/查看源码行信息开关(PDB 需含行信息) .srcpath C:\src\MyApp ; 源码搜索路径;.lsrcpath 查看 .srcfix / .srcfix+ <dir> ; 等价 .srcpath+ srv*,启用 source server 拉源码 .lsrcfix ; 查看源码搜索路径 ls ; 列当前函数源码行(配合 .lines) dv /V /p ; 变量表带 "源文件路径 @ 行号" 列(需 PDB 内源索引) !lines <地址> ; 该地址对应 "源文件路径 @ 行号"
- PDB 里带源路径(
/srcsvc前的传统/Fd+Source Index)时,即使本机没源码,STACK_TEXT/dv也会给出path @ line,可直接告诉开发者"去看哪个文件哪一行"。 - 用 Source Index(srcsrv) 打进 PDB 后,
.srcfix+srv*可让调试器自动从版本库拉源码(需srcsrv.dll与源索引命令可用;!srcs的参数需核实)。 - ⚠️ 网传
.srclog、srctrk、/srcsvl在当前官方文档中查无此命令,不要写进流程。 - 透明加密/DLP 环境下"调试器读不到源码"很常见(读到的和 IDE 里不一样):把
dv/!lines输出的行号交给开发者,比在分析机上打开源码更可靠。
2.4 分析环境的三个"独立"
- 分析机独立于生产机:CPU/内存/磁盘(NVMe)富裕,GB 级 dump 载入与
!gcroot都是重 IO。 - 符号缓存独立目录:不要把
C:\build\bin(手工放的 PDB)同时当符号服务器缓存目录,两者混用会导致缓存里塞满手工文件、命中率下降。 - 每次案件一个目录:
dump/ + log/ + notes.md,dump 本身只读存放并记录 SHA256 + 对应 PDB GUID+Age(后续找别人复现全靠这条信息)。
3. Dump 的采集(工程化落地)
分析做不好,八成是 dump 采得不对:该 Full 的采成 mini、崩溃后进程已退出没抓到、抓到了但没留符号。采集策略要先于分析技术落地。
3.1 用户态采集手段全景
| 手段 | 触发时机 | 类型/可控性 | 适用 |
|---|---|---|---|
| 任务管理器 →「创建转储文件」 | 人工,进程还活着 | minidump(DbgHelp),不可精细控 | 现场临时抓;Win11 另有「创建实时转储文件」 |
WinDbg/cdb 附加 → .dump |
人工/脚本 | MiniOptions 可精确控 | 已附加调试器时;/ma 全内存,/mA 忽略不可读内存 |
| ProcDump | 崩溃、CPU、内存、句柄计数器、窗口挂起、模块加载、线程事件 | -ma/-mm/-mt/-mp/-mc<掩码> |
生产首选,支持阈值 + 滚动 + AeDebug 常驻 |
| WER LocalDumps | 进程发生未处理异常时 | DumpType 0/1/2、CustomDumpFlags |
零改造铺量,注册表下发,服务/子进程都覆盖 |
MiniDumpWriteDump 自研 |
自己的 SetUnhandledExceptionFilter / VEHP |
完全自定义 | 应用内置崩溃上报(带脱敏与上传) |
AeDebug / IFEO + GFlag EnableDumps |
未处理异常 | 事后调试器 / 内置落 dump | 开发测试机 |
comsvcs.dll,MiniDump |
命令行 | full | 应急(常被用于导出 LSASS,生产环境按攻击告警处理) |
Linux:createdump |
崩溃 | DOTNET_DbgEnableMiniDump 等 |
.NET Core 3.0+ 容器/Linux 服务 |
3.2 关键命令与配置速查
:: ProcDump 常用 procdump -accepteula -ma <pid|name> D:\dumps\ :: 手工全量 procdump -ma -e 1 -n 3 -w MyApp.exe D:\dumps\ :: 首次机会异常,最多 3 个 procdump -ma -h -n 2 MyApp.exe D:\dumps\ :: 窗口挂起(5s 不响应消息) procdump -ma -c 20 -u -s 5 -n 3 MyApp.exe D:\dumps\ :: CPU>20% 持续 5 秒(-u = 阈值按单核归一) procdump -ma -p "\Process(MyApp)\Handle Count" 50000 MyApp.exe D:\dumps\ :: 性能计数器触发 procdump -ma -i D:\dumps :: 注册为 AeDebug 事后调试器(-u 卸载) :: 附加调试器后 .dump /mA /o D:\dumps\MyApp_full.dmp .dump /mfhut D:\dumps\MyApp_mid.dmp ; f全内存 h句柄 u卸载模块 t线程时间 .dump /mrR /o D:\dumps\MyApp_sanitized.dmp ; 抹掉栈局部变量与模块全路径(脱敏)
WER LocalDumps(可直接管理员 cmd 执行):
set L=HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps reg add "%L%" /v DumpFolder /t REG_EXPAND_SZ /d %PUBLIC%\CrashDumps /f reg add "%L%" /v DumpType /t REG_DWORD /d 1 /f reg add "%L%" /v DumpCount /t REG_DWORD /d 3 /f reg add "%L%\MyApp.exe" /v DumpFolder /t REG_EXPAND_SZ /d %PUBLIC%\CrashDumps\MyApp /f reg add "%L%\MyApp.exe" /v DumpType /t REG_DWORD /d 2 /f reg add "%L%\MyApp.exe" /v DumpCount /t REG_DWORD /d 5 /f :: DumpType: 0=Custom(配 CustomDumpFlags) 1=Mini 2=Full;子键名=进程文件名,覆盖全局
注意三条官方口径:
- 配了 AeDebug 自动调试的进程,WER 不再收 dump(两者互斥)。
- 服务崩溃的 dump 落在该服务账户的 Profile 目录(如 System 服务
%WINDIR%\System32\Config\SystemProfile),自定义DumpFolder要确认账户有写权限。 .NET托管内存分析要DumpType=2(Full)或 Procdump-ma;mini 里!dumpheap会缺对象。
内核(BSOD)采集:
reg add "HKLM\SYSTEM\CurrentControlSet\Control\CrashControl" /v CrashDumpEnabled /t REG_DWORD /d 7 /f reg add "HKLM\SYSTEM\CurrentControlSet\Control\CrashControl" /v NMICrashDump /t REG_DWORD /d 1 /f reg add "HKLM\SYSTEM\CurrentControlSet\Control\CrashControl" /v LogEvent /t REG_DWORD /d 1 /f
CrashDumpEnabled:0 关闭、1 Complete、2 Kernel、3 Small(triage)、7 Automatic(Win8+ 默认);1 + FilterPages=1 = Active Memory Dump(含用户内存、剔除无关页)。查询:Get-CimInstance Win32_OSRecoveryConfiguration | Select DebugInfoType,DebugFilePath,MiniDumpDirectory。
复现用主动崩溃:注册表 i8042prt|kbdhid|hyperkbd\Parameters\CrashOnCtrlScroll=1 后 右 Ctrl + ScrollLock ×2(bugcheck 0xE2);或 Sysinternals NotMyFault /crash。
3.3 体积、隐私与传输
- Full dump 体积 ≈ 进程私有提交内存,采集瞬间目标进程明显停顿(大内存进程可达数十秒)。生产全量铺 Full 前先算磁盘与页文件。
- Full dump 里含令牌、密钥、连接串、用户数据。外发厂商需:加密通道 + 审批 + 尽量用
-mt/WithoutOptionalData/.dump /mrR做降级与脱敏。 - 归档三件套:dump 的 SHA256 + 对应 exe/dll 版本 + PDB 的 GUID+Age。缺一条,别人就无法复现你的分析。
4. 通用分析流程:七步法
无论崩溃、卡死还是泄漏,都走同一套骨架。区别只在第 3、4 步走哪条支线。
第 1 步 建立上下文(3 分钟,别跳过)
vertarget ; OS 版本/构建/架构/uptime | ; 当前异常摘要(无异常时给出"未停在异常上") || ; 更短的进程/目标摘要 lm ; 模块全表(重点看:第三方模块、可疑注入、no pdb) lmvm <模块> ; 关键模块的版本/时间戳/PDB .time ; dump 时刻(相对调试器启动的"系统时间") .echo 运行时长=<用文件时间与进程创建时间自算>
要写进笔记的六项事实:目标进程、位数、OS 版本、应用版本、dump 类型与大小、dump 时刻与进程运行时长。运行时长是判断"时间相关型问题(越跑越差)"的第一个线索。
第 2 步 症状归类,选支线
问自己一个问题:「dump 里有没有异常记录?」
!analyze -v 有 EXCEPTION_CODE / 或内核有 BUGCHECK_CODE → 崩溃支线(§5 / §8)
无异常,但进程不干活 → 挂起支线(§6)
无异常,功能正常只是"变大/变慢" → 性能/资源支线(§6.3 §6.4 §7.3)
!analyze 常用参数(官方):-v[0..99](用户态 -v6 会给出全局与每线程发现)、-f(没有异常也强制分析)、-hang(挂起分析;用户态看线程栈是否有阻塞关系,内核态查锁与 DPC 队列)。
第 3 步 抓"当前最异常的现场"
- 崩溃类:
.exr -1看异常记录 →.ecxr切到异常上下文 →kb看栈 → 反汇编现场uf @rip。 - 挂起类:
~列线程 →~*e k L8浅栈概览 → 挑出"卡在哪儿的分组" → 定向深挖。 - 资源类:
!heap -stat(原生)/!dumpheap -stat(托管)/!address -summary(地址空间全景)先看量级。
第 4 步 从"现象"追到"责任"(三种追法)
| 追法 | 手法 | 典型问题 |
|---|---|---|
| 栈追因 | 栈上找第一个"业务帧"(非系统帧);kn 看偏移、uf 反汇编、dv 看变量 |
空指针、越界、错误参数 |
| 对象追根 | 拿到对象地址 → !dumpobj/dt 看字段 → !gcroot(托管)/!heap -p -a(原生带分配栈) |
泄漏、悬挂指针、谁持有 |
| 等待追链 | 谁在等 → 等什么资源 → 谁持有该资源 → 持有者又在干什么(递归一步) | 死锁、假死、饥饿 |
"追链要追到底":持有资源的那个线程如果又卡在别处,要继续追,直到出现「持锁者在做 IO / 在等消息 / 在等一个永远不会来的事件」这种可解释的终点。
第 5 步 交叉验证(防误判的关键)
至少用两条互相独立的证据支持同一结论,例如:
- 结论「Dispatcher 队列洪泛」→ 证据①
DispatcherOperation实例 100 万个;证据②这些对象的根是 Dispatcher 队列;证据③UI 线程栈正在执行队列排水代码;证据④净增长率 ≪ 生产率。 - 结论「某驱动释放后仍被引用」→ 证据①异常地址落在已释放池块(
!pool显示 free);证据②栈上业务帧持有该指针;证据③同地址在更早 dump 里存在(若能补抓)。
同时主动写排除项:死锁(否,因 X)、句柄耗尽(否,!handle 计数正常)、内存耗尽(否,!address -summary Commit 未顶格)。排除项是报告的说服力来源。
第 6 步 归一化:一句话根因 + 证据链 + 修复建议
模板见 §11。核心要求:根因必须是一句可执行的话("谁在什么条件下做了什么导致什么"),不是形容词("内存泄漏了")。
第 7 步 沉淀:命令、输出、结论进知识库
- 会话日志(
.logopen)+ 报告一起归档。 - 把「症状 → 命令链 → 判读标准」补进团队手册;同一 bug 的桶(
FAILURE_BUCKET_ID/ 堆栈签名)做聚类,同类客户案例直接复用结论。
4.1 决策树(打印贴墙版)
打开 dump ├─ !analyze -v 有 EXCEPTION_CODE / BUGCHECK_CODE ? │ ├─ 是 → 崩溃支线 │ │ ├─ .exr -1(异常码/地址/读写属性) │ │ ├─ .ecxr ; kb(栈可信?) │ │ │ ├─ 栈可信 → 找第一个业务帧 → uf/dv/?? 读现场 │ │ │ └─ 栈不可信 → kP / dps @rsp 手工回溯;.dframes │ │ ├─ 异常码含义(0xC0000005? 0xC0000374? 0xC0000409? 0xE0434352?) │ │ │ └─ 内存违规 → 是否 heap corruption → !heap / 页堆 / !chkimg │ │ └─ 崩溃点是"凶手"还是"受害者"?(多数 heap corruption 是受害者) │ └─ 否 → 进程还在跑但不干活 │ ├─ UI/主线程栈在业务代码里 → 被占用 → 找它为什么忙(循环/洪泛/大数据量) │ ├─ UI/主线程栈在 Wait* 里 → 被阻塞 → 追等待链(锁/IO/消息/IPC) │ ├─ !runaway 显示某线程 CPU 时间畸高 → CPU 支线 │ └─ 资源单调增长(堆/句柄/线程/GDI/队列)→ 泄漏支线(!heap -stat / !dumpheap -stat / !handle) └─ 无论哪条:符号是否可信(lmvm)? → 不可信先修符号
4.2 大 dump(GB 级)的操作纪律
- 先
lm/vertarget/.time这类 O(模块数) 的命令建立量级感。 - 统计类命令一律带
-stat、L<n>、-type <过滤>;永远不要先!dumpheap -type X列地址(可能上百万行)。 ~*e k前先~看线程数;线程多时先~*e k L4概览,再~~[TID]s定向。- 慢命令(
!gcroot、!dumpheap -stat)在符号未就绪时会一边下符号一边跑,先.reload再跑,并确认不是"在下符号"而不是"卡死"(!sym noisy可判断)。 - 机器侧:把符号缓存放到 NVMe、关掉杀软对 dump 目录的实时扫描(大文件读会拖慢一个数量级)。
5. 原生崩溃分析:从异常现场到责任代码
5.1 三步定位现场
0:000> !analyze -v ; 让引擎先给一版(异常码、栈、桶) 0:000> .exr -1 ; 异常记录:异常码、异常地址、参数、CONTEXT 地址 0:000> .ecxr ; 切换到"出事瞬间"的上下文(此后 k / r / dv 都看到出事点) 0:000> kn 30 ; 带帧号的栈 0:000> lmvm <栈顶业务模块> ; 核对符号是否匹配
!analyze -v 输出的读法(关键几段)
| 字段 | 含义 | 判读 |
|---|---|---|
EXCEPTION_CODE / (*** Exact match ...) |
异常码 | 见 §5.2 表 |
FAULTING_IP / EXCEPTION_PARAMETER1..N |
出事指令与参数 | 0x05 类异常的 P1=访问地址、P3=指令 |
STACK_TEXT |
栈 | 从 #00 往上找第一个业务帧 |
FOLLOWUP_IP / SYMBOL_NAME / MODULE_NAME / IMAGE_NAME |
引擎认为的"责任点" | 微软模块 ≈ 受害者,不是凶手 |
FAILURE_BUCKET_ID / BUCKET_ID |
聚类桶(微软支持按此归类) | 相同桶 = 同一问题,用于批量派单 |
PRIMARY_PROBLEM_CLASS / DEFAULT_BUCKET_ID |
问题大类(AV_、IP_、HEAP_CORRUPTION…) |
决定排查支线 |
STACK_COMMAND / ANALYSIS_SOURCE |
引擎用了哪条栈、规则来自哪 | MT(微软遥测桶)/SDK(规则) |
!analyze 参数:-v[0..99](用户态 -v6 会给全局与每线程的发现)、-f(无异常也强行分析)、-hang(挂起分析)、-show <码> <参数>(内核查码义)、-xml/-xmf(结构化输出,进流水线)。
5.2 常见异常码 → 下一步动作
| 异常码 | 名称 | 典型成因 | 下一步 |
|---|---|---|---|
0xC0000005 |
ACCESS_VIOLATION | 空指针/悬挂指针/越界;DEP 命中是执行了数据 | !address <p1> 看地址属性;判 READ/WRITE/EXECUTE;再看是不是 heap 块被回收 |
0xC0000374 |
HEAP_CORRUPTION | 堆元数据被破坏(更早的越界写/free 错配) | 崩溃点是受害者:!heap -triage、!heap -p -a <地址>、页堆复现 |
0xE06D7363 |
MSVC C++ 异常(first-chance) | 抛出 C++ 对象;Parameter[3] 是类型描述符 |
!cppexr -1 解出异常类型与对象地址;未被捕获才会升级成崩溃 |
0x40010006 |
调试提供程序通知(first-chance) | 常见于 CRT 调试堆打印「检测到堆损坏」 | 本身非致命;dump 断在此 → 立即转 !heap -triage |
0x80070057 |
E_INVALIDARG |
是返回值不是崩溃码;作为异常出现多为 COM/interop 或 throw 一个 DWORD |
查参数来源与调用点,别在崩溃指令上纠缠 |
0xC0000409 |
STACK_BUFFER_OVERRUN(__fastfail) |
安全检查失败主动快速终止,子码 = Parameter[0](见 §5.3) |
按子码分流:2 查拷贝溢出;1 CFG;7 是主动退出(找调用方,不是内存 bug) |
0xC00000FD |
STACK_OVERFLOW | 无限递归/超大栈上数组 | k 里看重复帧;!address @rsp 看 GuardPage |
0xC000001D |
ILLEGAL_INSTRUCTION | 新指令跑在不支持的 CPU 上;代码被踩 | u @rip 看指令是否合理;核对构建目标与运行 CPU |
0xC0000096 |
PRIVILEGED_INSTRUCTION | ring3 执行了特权指令(MSR/IO)——几乎都是执行流被劫持后落进不该落的地方 | 高度怀疑栈/返回地址被改写:校验栈可信度(§5.4) |
0xC0000097 |
(更正)TOO_MANY_PAGING_FILES | 系统状态码,应用崩溃里极罕见;网传「私有指令」是错的(私有指令是 0xC0000096) |
— |
0x80000003 |
BREAKPOINT | int 3、__debugbreak、被调试残留 |
看是否是代码里的断言/主动断点 |
0xC0000006 |
IN_PAGE_ERROR | 映射文件所在卷消失/网络盘断开 | 查 !address;查卷/网络共享 |
0xC0000602 |
fail-fast(含 CRT _crtFailure) |
CRT/UCRT 检测到致命错误 | 看 subcode |
0xE0434352 |
CLR 托管异常(.NET) | 未捕获的托管异常 | 转 §7:!pe + !clrstack -a |
0xC000041D |
终止/清理阶段的异常 | 进程退出路径(DllMain、静态对象析构、loader lock 内)抛异常 | 别停在最后一帧:!analyze -f + .exr -1 逐条回溯到最初的异常 |
| first-chance vs second-chance | 同一异常码出现两次:第一次被 SEH 吞掉、第二次无人处理才崩 | 用 !cppexr / .exr <地址> 逐个查看历史异常记录,根因通常在第一次 |
判读技巧:0xC0000005 的三参数含义是 P1=访问类型(0读/1写/8执行)、P2=访问地址、P3=指令地址(部分版本 P1 是地址,以 .exr -1 打印的 READ_WRITE_COMMAND 为准)。访问地址若为 0x0、0x8、0x10 这类小值 → 空指针 + 字段偏移;若是 0xDDDDDDDD/0xFEEEFEEE/0xCDCDCDCD/0xABABABAB → 释放后使用(UCRT 调试堆填充模式,见下)。
5.3 0xC0000409 的 fastfail 子码(Parameter[0],winnt.h FAST_FAIL_*)
| 子码 | 名称 | 含义与下一步 |
|---|---|---|
0 |
LEGACY_GS_VIOLATION | 旧式 /GS 栈保护失败,按栈溢出处理 |
1 |
VTGUARD_CHECK_FAILURE | CFG 间接调用检查失败:非法函数指针被调用 → 怀疑对象虚表/回调被覆写(安全相关) |
2 |
STACK_COOKIE_CHECK_FAILURE | /GS 安全 cookie 被覆写(经典栈溢出)→ 从崩溃帧向上找字符/内存拷贝 |
3 |
CORRUPT_LIST_ENTRY | 双向链表自校验失败(Blink->Flink != this)→ 并发无锁访问或 double-free |
4 |
INCORRECT_STACK | 栈指针异常(手工切栈/协程/栈破坏) |
5 |
INVALID_ARG | 参数校验失败(CRT _invalid_parameter)→ 查调用点入参 |
6 |
GS_COOKIE_INIT | Cookie 初始化失败(进程早期 / 被注入干扰) |
7 |
FATAL_APP_EXIT | 主动终止:abort()、_Exit、__fastfail(7) → 看 FOLLOWUP_IP 找调用方;不是内存 bug |
8 |
RANGE_CHECK_FAILURE | 范围检查失败(STL 迭代器越界等) |
9 |
UNSAFE_REGISTRY_ACCESS | 注册表回调中的不安全访问 |
0xFFFFFFFF |
INVALID_FAST_FAIL_CODE | 子码本身非法 → 是被踩内存伪造出来的 fastfail |
(CET / 新 SDK 另有 0x100+ 段子码,需核实后再引用。)
5.4 栈可信吗?不可信怎么办
栈是分析的地基,先花 30 秒判断它可不可信:
- 可疑信号:栈里出现明显不合法的帧、帧号跳变、同一函数无限重复、
Child-SP非单调、栈底不是已知的入口函数。 - 处置手段(由轻到重):
text kv ; x86:显示 FPO / 调用约定信息,先判断这些帧是不是"猜"出来的 .frame /a <地址> ; 回到物理帧(内联与优化隐藏的真实上下文) dps @rsp L60 ; 按指针宽度取词并符号化 → 手工重建返回地址链 dqs @rbp L40 ; 有调试信息时顺 frame pointer 链走 .dframes ; 展开被隐藏的内联帧 k家族精确差异(注意:p/P/v是小写/大写选项字母,不存在k /p这种写法):
| 形式 | 输出内容 | 什么时候用 |
|---|---|---|
k |
只有 ChildSP + RetAddr | 快速看栈深度 |
kb |
额外显示前 3 个参数(x64 是 home 区 4 参数,且不完整) | 常规首选 |
kn |
加帧号 | 配合 .frame /a N 定位 |
kp |
完整参数(类型 + 名字 + 值,需完整 PDB) | 要看真实入参 |
kP |
同 kp 但参数换行显示 |
参数多、一行放不下 |
kv |
显示 FPO / 调用约定信息 | x86 优化代码判断栈是否可信 |
kc / kd |
控制流分析 / 原始词输出 | 分析间接跳转、栈上残留 |
- x64 有
.pdata/unwind info,栈通常可靠,但内联帧不显示(用.frame /a、.dframes);x86 释放版没有 EBP 链(FPO)最容易给出错栈 → 先kv,仍乱就dps @esp L40扫栈。
5.5 读现场:变量、对象、内存
dv /v ; 参数与局部变量(需 PDB 行信息,优化版常缺失) dv /V MyFunc!*p* ; 按名字过滤 ?? MyAppNS::CDevice* pDev ; 用类型解释表达式(C++ 感知) dt MyApp!CDevice poi(0x12ff70) ; 按类型解剖内存;/r 3 控制递归深度 dt -r3 MyApp!CDevice @$rax @@masm(@rsp+0x28) ; 汇编风格地址表达式 r= poi(@rbp+0x18) ; 打印表达式 ub @rip L10 ; u @rip L20 ; 出事点前后指令 uf /c 3 MyApp!DoWork ; 反汇编整个函数(带源码行需 .lines) s -u @rbp L1000 "Error" ; 在栈附近搜字符串,常能挖出上下文提示
dv 显示 <optimized out> / <no data> 时不要放弃:改用「寄存器 + dt 类型解释 + 反汇编」这条可靠路径:从 u @rip 找到目标指针所在寄存器(如 mov rcx,[rbx+20h]),再 dt MyApp!CDevice @$rbx。
调试堆填充模式速查(判断"读的是不是刚被释放的内存"):0xFEEEFEEE 已释放(HeapFree 后填充)、0xCDCDCDCD 已分配未初始化堆内存、0xDDDDDDDD 已释放堆内存(UCRT)、0xABABABAB HeapAlloc guard 区、0xCCCCCCCC 未初始化栈内存、0xBAADF00D 未初始化本地堆。
5.6 判断"崩溃点是凶手还是受害者"
| 特征 | 结论 | 该查什么 |
|---|---|---|
| 崩在自己的业务函数、地址是自己刚算出来的指针 | 大概率现场就是凶手(逻辑 bug) | 直接读变量与算法 |
崩在 ntdll!RtlpAllocateHeap / RtlpFreeHeap / wcslen / STL 内部 |
受害者:更早的越界写/UAF 破坏了元数据 | 页堆 + Verifier 复现;!heap -stat、!chkimg |
| 崩溃线程只是"第一个读到脏数据"的线程 | 凶手在别的线程 | 结合资源生命周期、多线程访问点 |
| 只有特定机器/特定用户出现,同版本他人正常 | 环境(内存硬件、注入的第三方 DLL) | lm 找可疑注入;!chkimg 查内存损坏;换机对照 |
原生内存破坏的完整闭环工具是 §9.1(页堆 / Application Verifier / !heap -p -a)。dump 只能证明"这里脏了",页堆能证明"谁写的"。
5.7 FAILURE_BUCKET_ID 后缀语义(判读口径)
前提:桶名由 ext.dll 的启发式(triage.ini)生成,官方没有完整枚举表,且名称随调试器版本变化——当"分类线索"用,别当结论。DEFAULT_BUCKET_ID 是粗类(APPLICATION_FAULT / HEAP_CORRUPTION / STATUS_STACK_OVERFLOW / MEMORY_CORRUPTION_*);FAILURE_BUCKET_ID = 前缀 + 模块!函数+偏移。
| 前缀 / 后缀 | 含义 | 判读 |
|---|---|---|
AV_READ_<hex> / AV_WRITE_<hex> |
访问违规 + 访问模式,尾号是地址低位 | 常规空指针/越界 |
…_8、…_24 等极小尾值 |
near-null + 偏移访问 | 典型 this 或成员指针未判空;偏移就是字段偏移,用 dt 反查是哪个字段 |
AV_INVALID_POINTER_READ/WRITE |
地址既不在堆也不在栈区域 | 野指针/悬挂指针强信号 → 读该值 + !heap -x 判断是否已释放 |
AV_ICF_INSTRUCTION_READ/WRITE |
出错地址就是取指地址本身(IP 非法) | 执行流被劫持 / 返回地址被改写 → 按安全问题对待 |
IP_OFF_WITH_ICONST32 |
IP 落进非法代码,反汇编被解成 iconst32 |
函数指针/虚表被数据覆写的典型指纹 |
STACK_OVERFLOW_<mod>!func |
栈溢出 | kb 里必有大量同族函数重复帧 |
MEMORY_CORRUPTION_* 且出现 CHKIMG_EXTENSION 行 |
引擎怀疑代码段/镜像被改 | 照抄它给的命令(如 !chkimg -lo 50 -d <模块>);先排查硬件/超频/注入 |
5.8 完整案例:一次空指针访问的闭环分析
说明:以下栈与输出为演示用的简化真实格式,用于说明命令顺序与判读方式。
0:000> !analyze -v EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - 读取位置 0x0000000000000018 时发生访问冲突 READ_ADDRESS: 0000000000000018 FAULTING_IP: DeviceMgr!CDeviceManager::FlushAll+0x47 STACK_TEXT: DeviceMgr!CDeviceManager::FlushAll+0x47 DeviceMgr!CWorker::DoWork+0x1a3 kernel32!BaseThreadInitThunk+0x17 FAULTING_MODULE: deviceMgr FAILURE_BUCKET_ID: AV_INVALID_POINTER_READ_dt!CDeviceManager::FlushAll 0:000> .ecxr ; 切到出事现场 0:000> kn 10 ; 确认栈可信(帧地址单调、能对上符号偏移) 0:000> uf @rip ; 反汇编出事指令 00007ff6`1234`5677 48 8b 51 18 mov rdx,qword ptr [rcx+18h] ; ← 崩在这 00007ff6`1234`567b 48 85 d2 test rdx,rdx 0:000> r rcx ; rcx = 00000000`00000000 → 基址为空 0:000> ?? CDeviceManager::`vftable' ; 确认 +0x18 是哪个字段 0:000> dt DeviceMgr!CDeviceManager poi(@rbx+10) ; 用相邻寄存器把"本应有效的对象"挖出来 0:000> dv /v ; (若有行信息)this=0x0,pSession=0x0 0:000> lmvm DeviceMgr ; 核对符号:Guid+Age 与 CI 归档一致 → 栈与偏移可信 0:000> .shell -ci "!handle 0 0" findstr /i "File Event" ; 顺带确认句柄量级正常
判读链:[rcx+18h] 崩 + rcx=0 ⇒ 典型「空 this 或空成员指针 + 固定字段偏移」;FAILURE_BUCKET_ID 里的 AV_INVALID_POINTER_READ 与手工判读一致 ⇒ 属于逻辑 bug(未判空/生命周期错),不是内存破坏(内存破坏的地址通常是 0xFEEEFEEE 之类填充值或随机值)。
若地址是脏值而不是 0x18:例如 rdx = 00007ff6feeefeee`,结论立刻变成 UAF——继续!address、!heap -p -a`、页堆复现,不要再在崩溃函数里找 bug。
6. 挂起、无响应、死锁与资源类问题
这类 dump 没有异常记录,
!analyze -v只会说"未停在异常上"。思路:先问「界面为什么不干活」,再问「谁在妨碍它」。 本章命令存在性逐项对照 debuggercmds 命令参考核实:✅=有独立文档页;❌=参考中无此名(多为社区讹传);⚠️=需核实。
6.0 采集侧前置条件(漏了这步,本章一半命令用不了)
| 你要做什么 | dump 必须包含 | 采集命令 |
|---|---|---|
!runaway / .ttime(CPU 归属) |
线程时间流 | .dump /mt 或 /ma(官方明确:!runaway 只能用于 live 或带 t 的 dump) |
!handle(句柄类型/名称/等待状态) |
句柄流 | .dump /mh 或 /ma |
!address -summary 完整可信 |
全基础内存信息 F |
.dump /mF… 或 /ma |
!uniqstack -p、!for_each_* |
二级内存 i |
.dump /mif… |
| 堆内容 / 托管堆 / 对象图 | 全部内存 | .dump /ma;生产更稳用 /mA(忽略不可读页继续写;/ma 遇不可读页直接终止) |
ProcDump 抓 hang 的三个官方口径:
-h= 进程有挂起窗口(≥5 秒不响应窗口消息,判定与任务管理器"未响应"同源);无窗口服务进程、后台线程死锁永不触发,5 秒不可调。-s(默认 10)是"连续满足阈值的秒数",作用于-c/-m/-p这类阈值触发器,跟上面的 5 秒无关。-u单独使用 = 卸载 AeDebug 注册;跟在-c后才是"CPU 按单核归一"。- 无窗口进程改用
-c(CPU)/-m(提交内存)/-p(任意性能计数器,如句柄数)+-n -s;或计划任务定时procdump -ma <pid>。 - 其他:
-ma -mk(Full + 内核栈)、-r 5(Win8.1+ 进程快照并发抓,降低停顿)、-mp(MiniPlus,CLR 进程降级为 Full)。
6.1 线程全景(先广后深)
0:000> ~ ; 线程数与索引(Id: PID.TID);线程数异常本身就是问题 0:000> ~*e k L4 ; 全线程浅栈(大 dump 必带 L4,否则可能十几分钟不出结果) 0:000> ~9 e k ; 只对线程 9 执行 k,不切换当前线程(比 s 来 s 去安全) 0:000> !uniqstack ; ✅ 栈签名聚类 0:000> !uniqstack -p ; 带完整形参(需完整符号 + dump 含二级内存 i) 0:000> !runaway 7 ; ✅ 1=用户时间 2=内核时间 4=已运行时长,7=全打 0:000> !findstack "<符号>" ; ✅ 按栈内符号筛线程(线程多时比 ~*e k 快) 0:000> !teb ; ✅ 用户态 TEB:LastError、栈基限、TLS、Win32Thread 0:000> !analyze -hang ; ✅ 挂起分析(用户态分析线程间阻塞关系) 0:000> ~~[1a24]s ; 按 OS TID 切换(十六进制必须带 0x)
!uniqstack 官方输出格式与判读:
Total threads: 14 Duplicate callstacks: 11 (windbg thread #s follow): 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
- 大量
Duplicate callstacks是正常的(线程池都睡在同一点)。 - 危险信号在剩余那几个"少数派":若其中有两个以上停在
RtlpWaitForCriticalSection/WaitForSingleObject,且等待的地址互指,就是死锁候选。 - 官方措辞是
Duplicate callstacks;网传 "Identical stacks" ⚠️需核实。 - ⚠️
!thread官方明确只能内核模式使用,用户态 dump 里不要用(用~、~N~、!teb)。!tinfo❌参考中无此名。
6.2 等待点分类表(栈顶落在哪个系统调用上)
| 栈顶 | 含义与下一步 |
|---|---|
NtWaitForSingleObject |
单对象等待。上一帧找句柄 → !handle <h> f 看类型;是 Event/Mutex 就找"谁来 signal" |
NtWaitForMultipleObjects |
多对象等待(线程池、MsgWaitForMultipleObjectsEx、CoWaitForMultipleHandles);关注要求的消息是否无人投递 |
CoWaitForMultipleHandles / CoWaitForMultipleCrossAPTCalls |
STA/COM 消息泵内等待:该线程是 STA 且在跑消息泵;若它同时持有本进程其它锁 → 典型 COM 重入死锁 |
MsgWaitForMultipleObjects(Ex) |
GUI 线程标准等待。UI 卡死但栈停在这里 = 它没在等消息处理,根因常在别处 |
NtAlpcSendWaitReceivePort |
ALPC 同步跨进程/内核调用(RPC、部分 GDI/USER、终端服务、部分服务调用)。hang 高频真凶,需 !handle + WCT / Process Explorer 找对端进程 |
ZwReadFile / NtDeviceIoControlFile |
同步 IO 未完成(网络盘/SMB 断连、过滤驱动、页文件);检查是否误用重叠 IO |
RtlpWaitForCriticalSection → RtlEnterCriticalSection |
等临界区:kb 的 Args to Child 第一个参数就是 CRITSEC 地址(这是硬证据) |
LdrpAcquireLoaderLock / RtlpAcquireMutex / RtlAcquireResourceExclusive |
Loader Lock:全进程 DLL 加载/卸载被串行化,常见诱因是 DllMain 里做阻塞操作 |
| 业务函数内部(集合、字符串、序列化、内存分配) | 被占用而非被阻塞 → 转 §6.3 |
托管 WaitHandle.WaitOne / Monitor.Enter / Task.Wait |
.NET 等待 → §7 !syncblk + !clrstack |
一句话判据:栈在 Wait*/Enter* 里 → 被阻塞(追资源与持有者);栈在业务代码里 → 被占用(找工作量)。两者修复方向相反,判错全白干。
6.3 「被占用」型:找单调增长的生产源
我们一台 WPF 上位机的白屏就是这一类,方法通用:
- 算运行时长(dump 时刻 − 进程创建时刻)。跑几小时/几天才出现的无响应,几乎都是某资源单调增长。
- 先分"满负荷"还是"真在等":
!runaway看 UI 线程 CPU 时间——很大 = 满负荷干活(排水/循环);≈0 = 真在等(转 §6.4)。 - 找增长的东西:托管
!dumpheap -stat看DispatcherOperation、<>c__DisplayClass(闭包)、Timer、日志项、事件订阅者;原生!heap -stat、!handle 0 1 <Type>分类计数、线程数。 - 归因生产者:托管用闭包类型名直接锁定方法(
!dumpheap -type <>c__DisplayClass -stat)+!gcroot证明被队列持有;原生用页堆!heap -p -a或 UMDH 差分(§9.2)。 - 算两个率:净增长率 = 积压量 ÷ 运行秒数;生产率 = 每周期投递量 × 频率 × 生产者数。净增长率 ≪ 生产率 ⇒ 消费端已满负荷,任何额外工作都会饿死关键路径。
WPF 量化模型(可迁移到任何"优先级队列 + 单消费者"结构):Send(10) > Normal(9) > DataBind(8) > Render(7) > Loaded(6) > Input(5) > Background(4)。白屏 = Render 永轮不到 = 高于 7 的队列持续非空;无响应 = Input 轮不到 = 高于 5 的队列持续非空。
"时间相关型"必查清单:Dispatcher/消息队列积压、线程数、句柄数(含 GDI/USER 对象)、托管堆 LOH、静态集合、未解绑事件订阅、定时器、日志缓冲、缓存、连接池、未完成 async/Task、未关闭的文件/网络流。
6.4 死锁与等待链(用户态临界区)
命令存在性(避免踩讹传):
| 命令 | 结论 | 说明 |
|---|---|---|
!locks(=!ntsdexts.locks) |
✅ | 只接受 -v(含未上锁)/ -o(孤儿),不接受地址参数 |
!critsec <addr> |
✅ | 按地址看单个 CRITSEC,字段与 !locks 相同 |
!cs <addr> / !cs -t |
✅ | 另一实现:支持地址区间搜索、给关联等待栈(需完整符号);AppVerifier 活跃时给 CRITSEC 树 |
dt ntdll!_RTL_CRITICAL_SECTION <addr> |
✅ | 手工读原始结构 |
!handle <h> f |
✅ | 句柄类型/名称/对象专有信息(要求 dump 带句柄流) |
!wchain / !waitchain |
❌ | WinDbg 无此命令。等待链遍历是 Win32 API WCT(OpenThreadWaitChainSession/GetThreadWaitChain),支持 ALPC、COM、CRITSEC、Mutex、SendMessage、进程/线程等待。要用:自己写小程序调 WCT,或用资源监视器"分析等待链"/Process Explorer |
!lock、!tinfo、!commgr、!apartment、!so(COM 单例) |
❌/⚠️ | 参考中无此名(so 实为 Set Kernel Debugging Options,同名易误判)。用前 !extmatch com / !extmatch apartment / .chain 探测 |
!thread |
✅内核专用 | 用户态不可用 |
判读单个临界区(字段名照官方输出):
0:000> !critsec 000001a0`3f4c8120 CritSec AdminUI!g_cacheLock+0 at 000001a0`3f4c8120 LockCount -20 RecursionCount 1 OwningThread 1c78 EntryCount 10 ContentionCount 10 *** Locked
LockCount是分代语义:XP/2000 = "进入次数 − 1"(−1 表示未锁);Server 2003 SP1 及以后:最低位0=已锁 / 1=未锁,次低位表示是否已唤醒等待线程,其余位是等待者个数的反码:text 0:009> ? 0x1 & (-0n20) ; 最低位:0 → 已上锁 0:009> ? ((-1) - (-0n20)) >> 2 ; 等待者个数(示例得 4)官方输出不打印 "N waiters" 字样,网图文里有说明是加工过的。OwningThread是线程 ID,要回~列表按PID.TID的 TID 段换算成调试器索引再~Ns。EntryCount(非属主进入次数)、ContentionCount(竞争次数)只增不减,可当历史竞争热度;ContentionCount≫RecursionCount说明这把锁是热点。- ⚠️ 官方警告:多次误调
LeaveCriticalSection会把LockCount压到 −1 以下,CRITSEC 结构被破坏,所有线程永久等待——看到极负值要按内存破坏处理,不是死锁。
!handle 判读(f 显示对象专有信息):
0:000> !handle 1f4 f
Handle 1f4
Type Event
GrantedAccess 0x100003: Synch, QueryState, ModifyState
Name <none>
Object Specific Information
Event Type Auto Reset
Event is Waiting ← 对象仍未触发:等待不会自行结束
Flags 位:0x1 类型、0x2 基本信息、0x4 名称、0x8 对象专有信息。分类统计 !handle 0 1 <TypeName>(区分大小写):Event, Section, File, Port, Directory, SymbolicLink, Mutant, WindowStation, Semaphore, Key, Token, Process, Thread, Desktop, IoCompletion, Timer, Job, WaitablePort。注意 !handle 不告诉你哪个线程持有:Mutant(Mutex)属主在内核侧(需内核 dump 或 WCT);Thread/Process 类型对象本身就是被等待目标。
手工追链三步(死锁分析的全部诀窍):
- 在等待方栈上取资源标识:
RtlEnterCriticalSection的首参数(CRITSEC 地址)、句柄值、或托管Monitor对象地址。 - 找持有者:原生
!locks/!cs <addr>;托管!syncblk;内核!locks -v <addr>。把OwningThread换算成线程索引。 - 看持有者在干什么:也在等另一资源 → 递归第 1 步,出现环即死锁;在做慢事(IO/外部调用/大循环)→ 结论写「持锁者被 X 阻塞且无超时」;线程已退出/异常路径未释放 → 「锁泄漏」。
硬证据要求:等待方
RtlEnterCriticalSection的参数必须与锁地址逐位相同,否则你没证明它在等这把锁。
别漏掉的隐形锁:Loader Lock、COM/STA 跨套间调用(对方在跑消息泵、你在等它)、RTL Resource、自研共享内存/命名管道锁(dump 里无类型信息,只能靠地址与代码约定推断)、以及"其实没死锁,只是队列里排了几百万任务"(→ §6.3)。
COM / STA / RPC 可用手段:托管 !COMState ✅(列出各线程 COM apartment 模型,仅 Windows)、!clrthreads 的 APT 列 ✅(-live 只看活跃线程,-special 看 GC/终结器/卸载/定时器线程)、!syncblk ✅、!dumpheap -thinlock ✅、!dumpasync -waiting ✅(专治"await 永不完成");原生 RPC !rpcexts.* ✅(getthreadinfo/getcallinfo/getclientcallinfo/getendpointinfo/rpctime/thread/eerecord/rpcreadstack)。
STA 判据链:!clrthreads 取 APT 非空线程 → ~~[tid]s; k 看是否停在 CoWaitForMultipleHandles(CrossAPTCalls) → !syncblk 看它是否同时持托管锁 → !rpcexts.getthreadinfo 排除 RPC。栈顶 NtUserMsgWaitForMultipleObjectsEx + STA + 另有线程正 SendMessage 进它的窗口 = 典型跨线程 SendMessage 挂起。
6.5 线程池饥饿
官方文档化的原生线程池命令是 !tp(Exts.dll),不是 !threadpool(后者在参考中无独立条目 ⚠️,用前 .chain + !extmatch tpool 探测):
0:000> !tp ; 当前线程所属池 0:000> !tp pool 0 ; 0 = 所有线程池 0:000> !tp worker 0 ; !tp waiter 0 0:000> !tp wqueue <poolAddr> -1 -1 ; 全部优先级 / 全部 NUMA 的排队工作项(Win7+) 0:000> !tp wfac <addr> ; worker factory(Win7+) 0:000> !tp -? ; 本机可用子命令与参数
Flags:0x1 单行、0x2 成员信息、0x4 池工作队列。托管侧用 SOS:!threadpool ✅(排队中的工作请求数、完成端口线程数、定时器数)、!threadpoolqueue ✅(列出排队中的工作项,原生无对应物)。⚠️ 网传的 EpicThread / IdleThread / MinThreads / NumRunningWorkItems 等字段名在 !tp 官方页无样例,属需核实。
饥饿判据(三条同时成立才能断言,否则容易误判成"IO 慢"):① 排队工作项非空且持续增长;② worker 数已达上限且不再增长;③ 全部 worker 栈同构(停在同一把锁/同一等待点)。经典自饿模式:工作项里 Task.Wait() / WhenAll(...).Wait() / .Result,worker 全在等"必须靠新 worker 才能完成的任务"。修复:全链路异步、消除阻塞等待(提高 MinThreads 只是治标)。
6.6 高 CPU
0:000> !runaway 7 ; 用户态时间排序找离群线程(需 dump 带 t) 0:000> ~~[<TID>]s ; kn 30 ; uf @rip ; 看它在执行哪个循环 0:000> !address -summary ; 顺带排除"分配/GC 抖动导致的忙"
!runaway是累计值:只有"持续满载"才能直接下结论;间歇性高 CPU 要么隔几秒抓两份 dump 比差值,要么用 ETW /dotnet-trace采样。- 别拿一张快照硬说热点函数:dump 回答"谁此刻在跑",采样分析才回答"时间花在哪"。
- 托管 CPU 高常伴随 GC 压力:
!eeheap -gc、!dumpheap -stat看是否已堆积、!gchandles看 pin 过多导致压缩失败。
6.7 内存增长(原生)
0:000> !address -summary ; 全景:MEM_COMMIT 总量、堆/栈占用、Free 空洞 0:000> !heap ; 各堆基址 0:000> !heap -stat -h <heap> ; 按大小排序的分配分布(先 -stat,别一上来就 -segment 全量) 0:000> !heap -flt s <size> ; 按大小过滤可疑块(该选项在新版本可能被移除,见下方说明) 0:000> !heap -p -a <addr> ; 页堆/UST 开启时给出该地址的分配调用栈(神器) 0:000> !handle 0 1 Event ; 按类型统计句柄,找泄漏类型
已核实可用的 !heap 组合(比裸 -stat 更精准):
0:000> !heap -s ; 各堆摘要(Virtual/Commit/Free/Segments、堆类型 Segment Heap/NT Heap) 0:000> !heap -stat -h <heap> -grp S 30 ; 单堆按"每种分配大小"排序取前 30(-grp A 按大小 B 按块数 S 按总量) 0:000> !heap -triage ; ⚠️ 检出 LFH 破坏的唯一手段 0:000> !heap -srch 77176934 ; 全堆搜模式,会解析出 vftable 名 ← 找重复大对象极有效 0:000> !heap <addr> -x -v ; 定位块 + 全地址空间反查"谁指向它"(找悬挂引用) 0:000> !heap -flt s 0x50 ; 按大小过滤(⚠️ 官方注明新版本可能已移除,用 !heap -? 确认本机) 0:000> !heap -l ; 找"无引用的 busy 块"(大进程要几分钟)
判读路径:!address -summary 的 MEM_COMMIT 与业务预期不符 → 按 Heap / Stack / Image / VAR / Free 逐项对账 → !heap -stat 找大户堆 → 按大小/tag 归因(给自己的分配打自定义 tag 在这一步价值极大)→ !heap -srch / -flt 抽查块内容(字符串、结构签名)反推类型 → !heap -p -a 或 UMDH 拿分配栈。
关键分叉:若增长集中在 VAR(自定义分配器 / VirtualAlloc 自建池)而 Heap 平稳,!heap -stat 是看不到的 → 改用 !address -f:VAR -o:csv 导出分箱再统计。这类"自建池增长"在自研缓存、内存池、图像/波形缓冲区里最常见。句柄/线程/GDI 泄漏在 dump 里只能看总量,"是否随时间增长"必须靠运行期计数器(任务管理器加列 / Get-Counter)或两次 dump 差分。
6.8 内核视角速查(需 kernel / complete dump)
kd> !stacks 2 ; ✅ 官方明确推荐用它代替 !process 做多线程/资源冲突/死锁的全系统速览
; 列:Proc.Thread / ETHREAD / ThreadState / Blocker
kd> !locks ; ✅ 内核 ERESOURCE 概览("2263 total locks, 3 locks currently held")
kd> !locks -v <addr> ; ✅ 单锁详情:持有线程 + WAIT: (WrQueue) KernelMode … + 其栈
kd> !locks -d ; ✅ 连无竞争锁一起列
kd> !qlocks ; ✅ queued spin locks
kd> !deadlock ; ✅ 需 Driver Verifier「Deadlock Detection」已触发 bugcheck 0xC4
kd> !process 0 0 ; ✅ 全进程 → ETHREAD 列表
kd> !thread <ETHREAD> 0x1f ; !thread -t <tid> 0x1f ; ✅(-t 表示参数是 TID)
kd> !findthreads <addr> ; ✅ 哪些线程栈上出现该地址
kd> !findfilelockowner ; !devhandles ; !blockeddrv ; !iovirp ; !irpfind ; !processirps ; ✅
kd> !devobj <dev> ; !irp <irp> ; !wdfkd.wdfspinlock / wdfobject / wdfdevicequeues / wdfopenhandles
kd> !wudfext.wdfumirp(s) ; ✅ UMDF 侧
内核模式的三个习惯差异:~ 列出的编号是处理器而不是线程;不能用 TID 控制线程,必须 !process/!thread 找到 ETHREAD 后 .thread <ETHREAD> 切换;!address 在内核下只搜内核内存(即使已 .process 跟随某进程)。
内核 !locks 输出中 <*> 表示该行线程是持有者,-01 是线程计数;⚠️ 网传的 "OWNER / WAITERS"、"RECURSE_LIST" 标签未见于官方示例。❌/⚠️ !dstate、!runnable、!dptr、!io、!wdflock、!wdf!irp 在参考中无条目(WDF 正确入口是 !wdfkd.* 与 !wudfext.*)。
6.9 完整案例:UI 冻结 → 锁持有者 → 持有者卡在无超时网络读
0:000> .symfix c:\sym ; .reload /f 0:000> ~ . 0 Id: 2a4c.2a50 Suspend: 1 Teb: 000000a6`1234f000 Unfrozen ← UI 线程 4 Id: 2a4c.1c78 Suspend: 1 Teb: 000000a6`12349000 Unfrozen 0:000> !uniqstack Total threads: 14 Duplicate callstacks: 9 (windbg thread #s follow): 2 3 5 6 7 8 10 11 13 0:000> !runaway 7 User Mode Time 0:0:2a50 0:00:04.109 4:1c78 0:00:00.000 Kernel Mode Time 0:0:2a50 0:00:00.203 4:1c78 0:00:03.921 ← 只有线程 4 内核时间畸高 0:000> !critsec 000001a0`3f4c8120 CritSec AdminUI!g_cacheLock+0 at 000001a0`3f4c8120 LockCount -20 RecursionCount 1 OwningThread 1c78 EntryCount 10 ContentionCount 10 *** Locked 0:000> ? ((-1) - (-0n20)) >> 2 ; 4 → 四个等待者 0:000> ~0 k ; 等待方:证明等的就是这把锁 01 ntdll!RtlpWaitForCriticalSection+0xaa 02 ntdll!RtlEnterCriticalSection+0x46 03 AdminUI!CacheManager::Lookup ( __int64 = 0x000001a0`3f4c8120) ← 与锁地址逐位相同 04 AdminUI!MainFrame::OnTimer+0x3a 0a USER32!DispatchMessageWorker 0:000> ~4 k ; 持有方:为什么不释放 00 ntdll!ZwReadFile 02 winhttp!WinHttpReadData+0x1b2 03 AdminUI!BackendClient::FetchSnapshot+0x94 05 AdminUI!CacheManager::Lookup+0x2f ← 持锁之后才做的同步网络读 07 AdminUI!CacheRefresherThread+0x27 0:000> !handle 1f4 f Handle 1f4 Type File … Object Specific Information Event is Waiting
根因:线程 4(TID 0x1c78)持有 AdminUI!g_cacheLock(0x1a0'3f4c8120,LockCount=-20 → 4 个等待者),并在持锁状态下同步调用 WinHttpReadData → ZwReadFile;后端无响应且未设接收超时,锁永不释放。UI 线程 0(0x2a50)在 OnTimer → CacheManager::Lookup 等待同一把锁(硬证据:RtlEnterCriticalSection 首参数与锁地址逐位相同),消息循环停摆 → 表现为界面卡死。
排除项:非死锁(等待链为单链、无环);非 CPU 型(UI 线程 CPU 时间≈0);非资源耗尽。
修复:锁内禁止 IO(取快照引用 → 解锁 → 锁外刷新 → 二次校验回填);WinHttpSetTimeouts 设 resolve/connect/send/receive 超时;刷新改线程池异步 + 缓存降级。
可复现证据:断开后端必现;procdump -ma -mk -h AdminUI.exe 采集;间隔 3 秒的 3 份 dump 栈同构(排除瞬时抖动)。
7. 托管(.NET)Dump 分析
口径:本章节命令与参数已对照 learn.microsoft.com(dotnet/core/diagnostics 与 debugger 目录)核实;标注「需核实」的是社区流传但官方文档查无实据的写法。
7.1 环境与加载
Framework 与 Core 的差别:.NET Framework 运行时是 clr.dll,DAC 为 mscordacwks.dll,自带 Framework64\v4.0.30319\SOS.dll;.NET Core/.NET 5+ 运行时是 coreclr.dll,DAC 为 mscordaccore.dll,SOS 由 dotnet/diagnostics 发布。SOS、DAC 三者必须与目标运行时版本严格匹配。
; .NET Framework(可先 sxe CLRN → g → sxd CLRN 断在 CLR 加载后再加载) .loadby sos clr .cordll -ve -u -lp C:\Windows\Microsoft.NET\Framework64\v4.0.30319\ ; 手动指定 DAC ; .NET Core 2.x 老式加载(3.x 后不推荐) .loadby sos coreclr ; 新版推荐:安装统一 SOS 插件(dotnet-sos),新 WinDbg(≥10.0.18317.1001) 自动加载 dotnet tool install -g dotnet-sos dotnet-sos install .load %USERPROFILE%\.dotnet\sos\sos.dll ; 旧 WinDbg 手动加载 .chain ; 验证扩展加载及其镜像版本 !sos.help ; 验证 SOS 可用
SOS 版本/DAC 不匹配处置:报 Failed to load extension、Failed to read data type names、DAC 版本告警或 !dumpheap 输出乱码时:新版 SOS 用 !setclrpath <含 mscordaccore.dll/coreclr.dll 的运行时目录>、多运行时用 !runtimes 切换、!eeversion 查看 runtime/SOS 版本;lldb 下 sethostruntime。Framework 场景用 .cordll -lp 指到正确 v4 目录。!setswitches:官方文档未见,需核实。
确认运行时/AppDomain:!eebase(旧版 SOS,看 CLR 构建版本)、!eeheap -gc(各代堆/段范围)、!dumpdomain(.NET Core 只有一个 AppDomain)。
跨平台边界:WinDbg 新版可在 Windows 打开 Linux core dump 分析(需 dotnet-symbol 取同版本 libcoreclr.so/libmscordaccore.so 并 !setclrpath);macOS dump WinDbg/VS 都打不开,只能 lldb。dotnet-dump analyze 要求同 OS、同架构、同发行版族,Alpine(无完整 lldb)首选 dotnet-dump。
7.2 托管异常分析
0xE0434352 即 "@cpc",CLR 托管异常在原生层的 SEH 码,见 !threads 的 Exception 列。
.exr -1 ; 原生异常记录(0xE0434352 处) !printexception -nested -lines ; 全名最稳;pe 短名与原生 exts 的 !pe 冲突,加载 SOS 后 !pe 即走 SOS !clrstack ; 纯托管栈 !clrstack -a ; 含参数与 locals(-p 参数、-l 局部、-f 混入原生帧、-all 所有线程) !dumpstack / !dumpstack -EE ; 混合栈 / 仅托管段(补 -f 看不到的帧) !dso ; 当前栈上的托管对象
从崩溃地址反查方法:!ip2md <IP> → !dumpmd <MD>;正向用 !name2ee MyAssembly.dll MyNs.MyClass+MyMethod → !dumpmt -MD → U -il <MD>(带 IL 对照的反汇编)。!disas 未见于官方 SOS 文档(需核实,SOS 用 U/dotnet-dump 的 clru)。
线程与锁:!threads(ID/OSID/GC Mode(Cooperative/Preemptive)/Domain/Apt/Exception/Lock Count;新版 !clrthreads -live -special 还有 Abort、抢占列)。!syncblk 看 MonitorHeld(等待/持有计数)、Owning Thread Info、SyncBlock Owner——死锁核心证据。!dumpgcdata 存在(新版 SOS,显示 GC 数据)。
7.3 内存 / GC 专项
!dumpheap -stat ; 按 TotalSize(=Count×Size) 找大户;Count 大看实例数 !dumpheap -type Customer -stat ; 子串匹配类型名 !dumpheap -mt 00007f6c20a67498 ; 某类型全部实例地址 !dumpheap -min 100000 -stat ; 只看大对象(LOH 排查) !dumpobj 00007f6a... ; 别名 !do;官方文档仅 -nofields,-l/-details 需核实 !dumpvc <MT> <addr> ; 值类型字段 !dumparray -start 0 -length 10 -details <addr> ; -rc 官方未见,需核实 !gcroot <addr> ; 官方参数:-nostacks、-all(显示全部根) !du / !dus / !db <addr> ; 原生命令读字符串(!dus 读 UTF-16) !gchandles -perdomain ; Strong/Pinned/Dependent 句柄导致不回收 !finalizequeue -detail -allReady !eeheap -gc ; SOH/LOH/Gen0-2 段大小;Free 块在 -stat 里以 "Free" 类型出现
判读:-stat 顶部若是 String/Byte[]/自定义实体且 !gcroot 指向静态集合 → 真泄漏;若大头是 Free 或 !objsize -aggregate -stat(仅 WinDbg)对比 !dumpheap -stat 差值大 → 多为 GC 未回收/碎片,可用 !gcheapstat、新版 HeapStat -inclUnrooted 佐证,另拉一个 dump 对比增长。!objsize 存在;!findappdomainroots 官方文档无此命令(需核实,现有 !findappdomain 与新版 !pathto)。gcroot 局限:大堆要跑数分钟;栈根未必有效(需 !clrstack/U 复核);Mini dump 缺 GC 堆内存,必须 Full/Heap dump(SOS 有 MinidumpMode 命令限制 minidump 下不安全命令);用户提到的 -EE 参数未见,需核实。
7.4 任务 / 异步 / 线程池
!dumpasync(较新版 SOS,约 .NET 6 时代引入,具体起始版本号需核实):已核实参数 [-mt <MT>] [-type <类型片段>] [-waiting] [-roots],遍历堆找 async void/Task/Task<T>/ValueTask 状态机,输出字段、continuation 与根——找"泄漏的 await"。!tasks 不存在;辅助用 !dumpheap -type System.Threading.Tasks、新版 !taskstate <addr>、!parallelstacks。线程池:SOS 官方有 !threadpool(队列工作项数/完成端口线程/定时器),新版另有 !threadpoolqueue、!timerinfo;原生 exts 的 !threadpool 面向 OS 线程池,分析 .NET Core 以 SOS 版为准;!tppool 未见于任何官方文档(不存在/需核实)。死锁标准链路:!syncblk 找锁持有链 → 切线程 !clrstack 交叉验证 A 持锁等 B、B 持锁等 A。
7.5 与 dotnet CLI 工具的分工
dotnet-dump collect -p <pid> --type Full|Heap|Mini|Triage -o d.dmp ; 默认 Full;选项是 -p|--process-id(非 --process) dotnet-dump analyze d.dmp ; 内置 clrstack/dumpheap/gcroot/syncblk…,也可加 sos 前缀 dotnet-dump analyze d.dmp -c dumpheap -c "gcroot 0x..." -c exit
--crashdump 类 analyze 选项 Learn 页未见,需核实。dotnet-dump 只解托管、无原生栈,跨 Linux/Alpine 分析首选;WinDbg 适合混合模式(原生崩溃+托管)、可视化表格与扩展生态。dotnet-stack 看活进程线程栈;dotnet-gcdump 生成堆快照(对象图,开销小,适合生产);dotnet-trace 采 EventPipe(分配速率、方法耗时);dotnet-counters 看实时指标。自研巡检/批量分析用 ClrMD(Microsoft.Diagnostics.Runtime)。
7.6 新版 WinDbg 的托管支持
WinDbg(Store 版/SDK 10.0.18317.1001+)自动从扩展中心加载 .NET 扩展(含 SOS 全部命令,新增 !dumplock、!dumpconcurrentdictionary 等,Native AOT 部分支持);命令输出可切表格视图排序筛选、导出 CSV(网格导出操作路径细节需核实)。WinForms/WPF 专用可视化扩展:官方文档未见,需核实。
7.7 Dump 类型要求
托管堆分析需要含 GC 堆的 dump:Mini 只有栈/模块/异常记录,!dumpheap -stat 会缺对象、!gcroot 直接失效。取 Full/Heap:Procdump -ma(完整);WER LocalDumps 注册表 DumpType=2(已核实 2=Full dump,0=Custom、1=Mini,键在 HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps,可加 DumpFolder/DumpCount)。Linux(.NET Core 3.0+,createdump):已核实环境变量 DOTNET_DbgEnableMiniDump=1、DOTNET_DbgMiniDumpType(1=Mini,2=Heap 默认,3=Triage,4=Full)、DOTNET_DbgMiniDumpName(支持 %p %e %h %t 模板,默认 /tmp/coredump.<pid>)、DOTNET_CreateDumpDiagnostics/-VerboseDiagnostics/-LogToFile、DOTNET_EnableCrashReport。网传的 DOTNET_DumpEnabled/Dump_Type/Dump_Manager 未见于当前官方文档(疑为 2.x 旧名,需核实)。单文件与 NativeAOT 仅支持 Full。
7.8 实操案例
(a) 内存泄漏(静态集合持有):
0:000> !dumpheap -stat MT Count TotalSize Class Name 00007f6c21625038 6 4063376 testwebapi.Controllers.Customer[] 00007f6c20a67498 200000 4800000 testwebapi.Controllers.Customer 00007f6c1dc00f90 206770 19494060 System.String Total 428516 objects 0:000> !dumpheap -mt 00007f6c20a67498 | head ; 取一个实例 00007f6ad09421f8 0:000> !dumpobj 00007f6ad09421f8 ; Customer,字段 Name/Id 0:000> !gcroot 00007f6ad09421f8 Thread 3f68: -> 00007F6BDFFFF038 System.Object[] -> 00007F69D0033588 testwebapi.Controllers.CustomerCache -> 00007F69D00335A0 System.Collections.Generic.List`1《...Customer》 -> 00007F6C000148A0 testwebapi.Controllers.Customer[] -> 00007F6AD0942258 testwebapi.Controllers.Customer
根是静态 CustomerCache 内的 List:只增不删 → 改 LRU/弱引用缓存。
(b) 托管死锁:
0:000> !syncblk Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner 43 00000246E51268B8 603 1 0000024B713F4E30 5634 28 00000249654b14c0 System.Object 44 00000246E5126908 3 1 0000024B713F47E0 51d4 29 00000249654b14d8 System.Object 0:000> ~~[5634]e .; !clrstack -a ; 线程28:持有 14c0,卡在 Monitor.Enter(14d8) 0:000> ~~[51d4]e .; !clrstack -a ; 线程29:持有 14d8,卡在 Monitor.Enter(14c0)
互相等待对方锁对象 → 环路,统一加锁顺序或改 lock 超时/Monitor.TryEnter。
7.9 常见坑
- locals 显示
<no data>:Release 优化或 PDB 不匹配;需与 dump 一致的 PDB 并配好符号路径,仍可能因寄存器分配拿不到名字(官方文档明确 locals 可能只给地址)。 !clrstack栈不全:尾调用/内联吞帧,用!dumpstack -EE看真实混合栈,!ip2md逐地址补;"混模式必须先.load sos之外还要执行某.net命令"的说法未见于官方文档(需核实)。!pe撞名(原生 exts 也注册 pe,输出 "MSPE:"),需要原生记录时用全限定!exts.pe。- 拿 mini dump 跑
!dumpheap/!gcroot:结果残缺或直接报错。先判断 dump 类型:mini 通常几十 KB~几 MB、Full 与进程私有提交内存同量级;!address -summary里几乎读不到私有可写页 = 没有堆内容;SOS 的MinidumpMode可限制 minidump 下不安全命令。结论是 mini 就回去重抓-ma/DumpType=2。 !mk/!mli:CLR 2.0 时代旧 SOS 的命令,已废弃,现由!ip2md/!dumpil/!name2ee取代。- 多运行时混载(Framework+Core 同进程/双版本 CLR):必须用
.cordll或!setclrpath指定正确 DAC,否则统计全错。
8. 内核 Dump(BSOD)分析
口径:✅=已对照官方文档核实;「需核实」=未能在官方文档中确认,勿当定论。内核分析另需:内核版本符号必须精确匹配(
vertarget先对构建号)。
8.1 标准流程
kd -z C:\dumps\MEMORY.DMP -y SRV*C:\SymCache*https://msdl.microsoft.com/download/symbols 0: kd> vertarget ; 确认构建号 0: kd> !analyze -v ; 自动分析(打开内核 dump 时也会自动跑一轮) 0: kd> lm; lmvm <mod> ; 模块与时间戳/校验和 0: kd> .bugcheck; kb; kn; .cxr; .ecxr; kb ; 深挖现场
!analyze 真实选项(✅):-v[0..99]、-f、-hang、-show <码> <参数>、-xml/-xmf/-xmi/-xcs、-c -load <已知问题XML>。-depend/-verify 未见于文档(需核实/疑不存在)。
逐段判读:BUGCHECK_CODE(查官方 0x 参考页);PRIMARY_PROBLEM_CLASS / DEFAULT_BUCKET_ID(AV_nt!IopXxx 之类=地址访问类+函数);PROCESS_NAME / IMAGE_NAME;MODULE_NAME / FAULTING_MODULE(崩溃 PC 所在模块——⚠️ 若属微软系统文件,多半是别人踩内存,勿直接归责);FAILURE_BUCKET_ID(微软支持按此聚类,_ADDR_SHORT 等后缀描述访问类型);ANALYSIS_SOURCE(MT=微软遥测桶/SDK=规则引擎);STACK_TEXT 从 #00 向上,找到“最后一个非 nt 帧”;FOLLOWUP_IP / SYMBOL+0x 精确到指令;CUSTOMER_CRASH_COUNT / BSOD_PACKAGE 出现次数;有 verifier 位会提示 DRIVER_VERIFIER_*。
8.2 BSOD 码核心判读(0xD4/0x51/0x34 名称已核实)
- 0x1A MEMORY_MANAGEMENT:内核池/页表自伤,看子码(0x41284=PTE 损坏、0x41296=工作表项损坏等,逐码查 0x1A 文档页);反复同址=确定性损坏者,
!pool/!chkimg。 - 0x3B SYSTEM_SERVICE_EXCEPTION:系统调用路径中异常未兜住,栈中找非系统帧;伴随
!analyze的异常记录(.exr -1)。 - 0x50 PAGE_FAULT_IN_NONPAGED_AREA:访问已被释放/无效的“看似内核地址”,Param1=地址、Param4=0读/1写;典型 use-after-free;地址若像用户态指针被内核误用亦常见。
- 0xD1 DRIVER_IRQL_NOT_LESS_OR_EQUAL:IRQL≥2 访问分页内存;栈帧 #2/#3 返回地址通常是肇事驱动;
!analyze的TRAP_FRAME给 PC。 - 0xC4 DRIVER_CORRUPTED_EXPOOL:Ex 池头损坏;
!verifier看是否曾开 verifier;!pool <addr>还原池块与 tag。 - 0xD4 SYSTEM_SCAN_AT_RAISED_IRQL_CAUGHT_IMPROPER_DRIVER_UNLOAD(✅):驱动在不平衡/非法卸载后仍被扫描引用——查最近卸载的第三方驱动。
- 0x7E / 0x7F:0x7E=内核未处理异常(同 0x3B 思路,异常码 Param1 关键);0x7F=内核模式 in-flight 异常,其中 0x7F 0x8=双异常,几乎都是内核栈溢出(
kb见重复帧;!stackusage需核实命令名)。 - 0x4E/0x78 POOL_CORRUPTION(旧系统):同 0xC4 家族;现代系统转成 0x139/0x190。
- 0xF4 CRITICAL_OBJECT_TERMINATION:csrss/wininit 等关键进程内对象被终止;
!process 0 0找死进程,转储里!thread看终止原因,多为安全软件/驱动杀进程。 - 0xEF CRITICAL_PROCESS_DIED:关键进程死亡;先问“哪个进程”(ProcessName),常为过滤驱动/杀软连带击杀。
- 0x7A KERNEL_DATA_INPAGE_ERROR:读页文件失败:磁盘坏道/存储驱动/内存位翻转/杀软拦截;查子状态码(0xC000009A、0xC0000185 IO 设备错误等)+ 系统日志 disk 事件。
- 0x133 DPC_WATCHDOG_VIOLATION(用户写的 “TIMEOUT” 正名 VIOLATION):Param1=0 单 DPC 超 2×阈值 / =1 看 DPC 队列超时;参数 2/3 为时钟节拍;用
!thread+kb找跑飞 DPC 的驱动;网卡/显卡驱动高频。 - 0x116 VIDEO_TDR_FAILURE(正名带 FAILURE;0x117 为 TIMEOUT_DETECTED):显示驱动 TDR 超时后挂掉,IMAGE_NAME 即显卡驱动;结合 dxgkrnl 帧与 DriverStore 版本。
- 0x9F DRIVER_POWER_STATE_FAILURE:睡眠/唤醒 IRP 超时卡在某个设备对象;Param 指向 pending IRP,
!irp <addr>看当前栈位置定位驱动;典型蓝牙/网卡/USB 驱动。 - 0x139 KERNEL_SECURITY_CHECK_FAILURE:内核补丁保护检查失败(池头/链表/异常框架),Param1 为检查类型;根因常为 double-free/UAF,
!poolval(需核实,现代多为!poolused+verifier special pool 定位)。 - 0x51 REGISTRY_ERROR(✅):注册表内部状态错误,多与注册表筛选/安全软件或 hive 损坏相关。
- 0x34 CACHE_MANAGER(✅):映射/缓存管理器损坏(典型:内存中数据与镜像文件校验不符,
!chkimg可验证内存损坏/超频)。 - 0x135:未在当前官方 Bug Check 参考中找到对应码(需核实,疑笔误,0x134=WDDM_INTERNAL_SHARING_VIOLATION、0x139 更常见)。
8.3 内核命令清单(逐项核对:✅=命令参考有独立条目;⚠️=未见条目/讹传,勿当定论)
kd> !analyze -show 0xD1 0,0,0,0 ; ✅ 查码义
kd> !stacks 2 [Filter] ; ✅ 全系统内核栈速览(官方推荐用于资源冲突/死锁,优于 !process)
kd> !process 0 0 / 0 3 ; ✅ ;!thread <ETHREAD> 0x1f 或 !thread -t <tid> 0x1f
kd> !locks ; !locks -v <addr> ; !locks -d ; ✅ ERESOURCE;-v 给持有线程 ETHREAD + WAIT 行 + 栈
kd> !qlocks ; ✅ queued spin locks
kd> !deadlock ; ✅ 需 Driver Verifier「Deadlock Detection」已触发 bugcheck 0xC4
kd> !findthreads <addr> ; ✅ 哪些线程栈上出现该地址
kd> !findfilelockowner / !devhandles / !blockeddrv / !iovirp / !irpfind / !processirps ; ✅
kd> !irp <addr> / !irb / !irplog deviceext ; ✅ IRP 与 IRP 日志
kd> !devobj <dev> / !drvobj \Driver\Xxx 3 / !devstack <obj> / !object <path|addr> ; ✅
kd> !pool <addr>[-t tag] ; !poolused 2 ; poolmon ; ✅ 池分析与 tag 计数
kd> !verifier ; ✅ Driver Verifier 状态
kd> !vm ; !address -summary ; !pte <addr> ; ✅ 内存视图
kd> !chkimg -d <mod> / -fast ; ✅ 内存镜像 vs 磁盘镜像(查硬件位翻转/超频/自改代码)
kd> !lmi <addr> ; lm ; lmvm <mod> ; ln <addr> ; ✅ 地址归属与模块版本
kd> !for_each_module "<cmd>" ; ✅ 全模块批处理
kd> .load wdfkd.dll → !wdfkd.wdfdevice / wdfobject / wdfspinlock / wdfdevicequeues /
wdfopenhandles / wdftimer ; ✅ KMDF;UMDF 用 !wudfext.wdfumirp(s)
kd> !devnode / !dpc / !exqueue / !workqueue / !bugdump / !poolval / !objhdr /
!runnable / !dstate / !dptr / !io / !wdflock / !wdf!irp
; ⚠️ 命令参考中无条目(讹传居多),先用 .chain/!extmatch 探测
8.4 驱动责任判定与上报
- 栈上地址定位:
ln <addr>、!lmi <addr>直接给出模块+符号偏移;lm找无符号(no pdb)第三方帧。 - 无符号/未签名驱动:
lmvm <mod>比对 Image/PDB 时间戳与磁盘文件(dir /u),向厂商索取“GUID+Age 匹配”的 PDB;sigverif/pnputil辅助。 !analyze的 MODULE_NAME 常指微软二进制——用「栈上第一个非系统帧 + 池 tag + 内存损坏特征」三角定位真凶;“Windows 驱动兼容数据库”官方入口为 Partner Center/DevPortal 的 HCK/HLK 报告(!analyze -verify 不存在,需核实)。- 最小证据包给厂商:dump 文件、
!analyze -v全文、FAILURE_BUCKET_ID、复现步骤与频率、驱动版本与符号(或请其用同 build 符号重跑!for_each_module/栈回放)、verifier日志。 - Driver Verifier 复现(生产慎用,配 DMS/专用机):
verifier /standard /driver xxx.sys ; 特殊池/IRQL检查/句柄等标准项 verifier /faults 4 /driver xxx.sys ; 故障注入(Iop/Irp 等故障位) verifier /specialpool /driver xxx.sys ; 特殊池(越界立崩,快速定界;参数写法需核实) verifier /query; verifier /reset
8.5 时间旅行调试(TTD)(✅ 已核实概览/录制文档)
- 录制:WinDbg(新版)
File > Start debugging > Launch executable (advanced) / Attach to process勾选 Record with Time Travel Debugging;可 “Record subset of execution” 限定模块。旧文档中的|; …\TTDLoader.exe <exe>命令行方式属早期方案(需核实当前可用性);命令行录制另有 TTD.exe 独立工具。 - 回放:直接打开
.run(自动生成.idx索引,索引可达 trace 两倍大;重建索引!tt.index/!index✅)。时间控制:.timemachine goto <timecode>(真实命令,.ttime是另一命令“显示线程时间”,勿混;当前文档以 UI 为主,.timemachine页面已不在新版目录,需核实);.ttime语义已核实区分。 - 限制(✅):仅用户态(内核进程不可录);录制开销约 10–20×;回放只读不可改内存;PPL 保护进程无法注入;AV/EMET/部分 Electron 应用冲突;trace 动辄数 GB,且可能含 PII;跨版本回放要求 WinDbg/记录器版本匹配(细节需核实)。
8.6 现代工具补充
- 新版 WinDbg(Store/“WinDbg Next”,文档已迁
debuggercmds目录)与 WinDbg Preview 并存,经典 WinDbg 仍在。!wdbg、!bugdump等均非真实命令(已核实不存在,勿写入指南)。 - BlueScreenView / WhoCrashed:第三方,解析 bugcheck 参数+模块表给出“可能肇事驱动”,适合外行快速初筛;局限:不理解栈与池损坏因果,误报率高(把加载列表末尾或显卡驱动当凶手)、对新压缩 dump/Active dump 支持滞后(需核实),一切以
!analyze -v+ 人工栈判读为准。 - “msctf”(微软输入法框架 dll)与本主题无关,仅偶尔作为用户态帧出现在崩溃栈中。
9. 进阶武器:当 dump 不够用时
9.1 页堆(Page Heap)+ Application Verifier:抓「写坏的瞬间」
内存破坏类问题(heap corruption、use-after-free、越界写)在 dump 里几乎都是"受害者现场"——崩在 RtlpAllocateHeap 里,凶手早已离场。唯一能把「破坏发生点」变成「崩溃点」的手段是提前埋防护:
:: 对目标进程开启全量页堆(调试机/测试环境专用,内存与性能开销大) gflags /p /enable MyApp.exe /full :: 或用 Application Verifier(图形界面,可分类别:Heaps / Handles / Locks / TLS / Filesys / Registry) appverif.exe :: 关闭 gflags /p /disable MyApp.exe
开启后每次分配都有 guard page,越界写会立即在写入指令处崩,dump 里的栈直接指向凶手。此时分析流程退化为普通崩溃分析:.ecxr; kb → 看是谁写了这块内存。
相关调试器命令:!heap -p -a <地址>(页堆开启时给出该地址的分配调用栈,是原生版的 !gcroot)、!verifier(查看 verifier 状态)、!htrace(句柄追踪,需 gflags /h 开启)。
9.2 UMDH:原生堆差分,量化泄漏
适用"原生堆随时间增长"。原理:两次快照按调用栈聚合分配字节数,取差。
:: 1) 打开用户模式栈回溯数据库(改完要重启目标进程) gflags /i MyApp.exe +ust :: 默认数据库 32MB(x86) / 64MB(x64),可在 IFEO 的 "Stack Backtrace(Megs)" 调大 :: 用完务必关掉: gflags /i MyApp.exe -ust set _NT_SYMBOL_PATH=C:\build\bin;srv*C:\SymCache*https://msdl.microsoft.com/download/symbols set OANOCACHE=1 :: 必须!禁用 BSTR 缓存,否则结果不可信;服务进程要设"系统环境变量"后重启服务 :: 2) 采两次快照(中间执行"一轮可复现的增长",例如操作 10 次) umdh -p:124 -f:log1.txt :: … 操作 … umdh -p:124 -f:log2.txt :: 3) 差分(-d 把字节/次数转成十进制,便于排序) umdh -d log1.txt log2.txt > cmp.txt
差分文件按字节增量降序排列,形如:
+ 5320 ( f110 - 9df0) 3a allocs BackTrace00B53 Total increase == 5320
拿 BackTrace00B53 回原始 log2.txt 里搜,得到"每块 0x428 字节 × 0x14 次"及完整分配栈——栈里最后一个属于你自己代码的帧就是嫌疑点:
00005320 bytes in 0x14 allocations (@ 0x00000428) by: BackTrace00B53 ntdll!RtlAllocateHeap+0x00000808 MyApp!operator new+0x0000000E MyApp!DisplayMyGraphics+0x0000001E ← 最后一帧"你的代码"
补充:gflags /i MyApp.exe +hpa(Full Page Heap)之后可用 !heap -p -a <addr> 直接看块归属、块内偏移、已分配/已释放,有栈就带分配栈;官方指定的手工还原方式是 dt DPH_BLOCK_INFORMATION <headerAddr> + dds <StackTrace>。⚠️ !heap -p -t[c|s] 仅 XP 支持,官方现在让改用 UMDH。VS 诊断工具 / dotMemory 只证明"哪些对象还活着",UMDH 差分才证明"未释放的分配来自哪条栈"。判读要点:只看净增长最大的栈;Operator new(...) 之上的第一个业务帧就是泄漏点;若增长伴随大量 Free 说明只是碎片而非泄漏。托管侧对应用 dotnet-gcdump + Visual Studio 快照差异,或 !dumpheap -stat 两次对比。
9.3 TTD(时间旅行调试):能录就录
- 录制:新版 WinDbg 启动/附加时勾选 Record with Time Travel Debugging;命令行用
TTD.exe(可无调试器的机器上录制)。 - 产物
.run+ 索引.idx(索引可达 trace 两倍大);!tt.index重建索引。 - 能力:倒着执行、任意时刻查内存/变量、Timelines(断点/内存读写/调用/异常的可视化事件轴)、
dx+ LINQ/JS 查询(如"列出所有异常""某模块何时加载")。 - 限制(官方):仅用户态;录制开销约 10–20×;回放只读(不能改内存);PPL 保护进程注入不了;杀软/EMET/部分 Electron 应用冲突;trace 含 PII,外发注意;GB 级体积。
TTD 最适合的场景:能复现的复杂逻辑 bug(状态机、并发时序、"值为什么变成这样")。它把"回溯因果"从推断变成查看。
9.4 .NET 诊断工具族(Windows/Linux 通吃)
| 工具 | 干什么 | 什么时候用 |
|---|---|---|
dotnet-dump collect / analyze |
采集 + 分析(内置 SOS 命令) | Linux/容器;不想装 WinDbg |
dotnet-stack |
看活进程线程栈 | 现场快速判断"卡在哪",零 dump |
dotnet-gcdump |
GC 堆快照(对象图,开销小) | 生产内存增长(比 Full dump 便宜得多) |
dotnet-trace |
EventPipe 跟踪(分配速率、方法耗时) | CPU/分配热点 |
dotnet-counters |
实时指标 | 判断"是否在涨" |
ClrMD (Microsoft.Diagnostics.Runtime) |
编程式读 dump | 批量巡检、自动化报表 |
9.5 自动化:cdb 批量出报告
:: 批量:对目录下所有 dump 跑一轮自动分析并落盘 for %f in (E:\dumps\*.dmp) do cdb -z "%f" -c ".symfix C:\SymCache; .reload; !analyze -v; lm; ~*e k L8; qd" > "%~nf.report.txt"
CI 里把 !analyze -v 的 FAILURE_BUCKET_ID(内核/崩溃)或堆栈签名提取出来入库聚类,客户报障先看是否命中已有桶——这是把"个案分析"变"批量运营"的关键一步。
9.6 WinDbg 脚本与数据模型
- 命令脚本:
.shell(调外部命令)、$$< script.txt(执行脚本文件)、.foreach/.if/r $t0=(临时变量)、$$ 注释。适合"对每个模块跑同一命令"这类批量动作,注意大 dump 上循环全线程极易卡死。 - JavaScript 脚本(新版):
j -?/ Scripting 窗口,可枚举进程/线程/模块、生成表格,官方推荐用于复杂遍历与报表。 - 数据模型
dx:dx -g Debugger.Session.Process.Threads.Select(t => new {Id=t.Id, StartAddress=t.StartAddress})这类 LINQ 式查询,配合 NatVis 能把"对象树"变成可展开表格;是替代!扩展做定制分析的正规途径。
10. 命令速查总表
10.1 会话与符号
| 命令 | 作用 | 备注 |
|---|---|---|
.logopen [/t] <file> / .logclose |
记录会话 | /t 追加;分析必开 |
.symfix [dir] / .sympath |
设/看符号路径 | .symfix+ 追加不覆盖 |
.reload [/f] [mod] |
重载符号 | /f 忽略缓存强取 |
!sym noisy / !sym quiet |
符号加载诊断 | 找 PDB 找不到时先开 |
lm / lmvm <m> / lmt / lm f |
模块表 / 详细版本 / 按时间排序 | lmvm 看 Timestamp、PDB、ImageSize |
!lmi <addr|mod> |
模块详细信息 | 定位地址属于哪个模块 |
.echo / $$ 注释 / ${"..."} |
打标记 | 日志里做锚点 |
.chain / .load <dll> / .loadext |
扩展查看与加载 | ntsdexts/exts/uext/sos |
.cordll -ve -u -lp <path> |
指定 CLR DAC 路径 | 托管分析版本不匹配时用 |
vertarget / vertarget -r |
目标 OS 版本 | 内核 dump 首查 |
.time / !analyze -show <码> |
dump 时刻 / 查 bugcheck 码含义 | — |
10.2 现场与栈
| 命令 | 作用 | 坑 |
|---|---|---|
!analyze -v [-hang \| -f \| -show 码] |
自动分析 | 只是起点,桶可能指错模块 |
.exr -1 / .exr <addr> |
异常记录(码/地址/参数/上下文指针) | -1 = 当前异常 |
.ecxr |
切到异常上下文(寄存器/栈停在出事点) | 崩在系统帧时必用 |
k / kb / kn / kp / kv / kP |
栈回溯(带参/带序号/帧指针/清符号/显式 FPO) | 无帧指针时 kb 可能错,改 kP |
~ / ~N s / ~~[TID]s |
线程列表 / 按索引切 / 按 OS TID 切 | ~23a4s 是错的(那是线程索引) |
~*e k L8 |
全线程浅栈概览 | 大 dump 慎用;先估线程数 |
!uniqstack |
按栈签名聚类(找"一堆线程卡在同一处") | 需 ntsdexts |
!runaway / .ttime |
各线程用户/内核 CPU 时间(.dump 时需带 t MiniOption 才有线程时间) |
高 CPU 首查 |
.frame /a /r / .dframes |
帧详情 / 显示被隐藏的帧 | 内联帧、tail call 排查 |
dps @rsp L40 |
用符号解析内存中的"疑似返回地址" | 手工重建被破坏的栈 |
u / uf / ub <addr> |
反汇编(函数/向前) | 配合 /n 关符号偏移 |
dv [/v] |
参数与局部变量 | 需行信息 + 未优化;常见 <optimized out> |
10.3 内存与数据
| 命令 | 作用 |
|---|---|
db/dw/dd/dq <addr>、dqs、dps |
按字节/字/双字/四字节显示;*s 用符号解析 |
poi(addr)、@@masm(...)、?? expr、r= expr |
解引用、表达式求值、C++ 类型求值 |
dt <模块!类型> <地址> [/r 3] [/y] |
按类型显示结构体(递归深度/偏移) |
?? MyNs::MyClass* p |
带类型解释指针 |
!tp pool 0 / !tp wqueue <pool> -1 -1 |
原生线程池(文档化命令是 !tp,不是 !threadpool) |
!heap -triage / !heap -x <addr> |
堆损坏一键体检 / 某地址属于哪个堆、是否空闲 |
!cppexr -1 / !gle |
解码 C++ 异常对象(0xE06D7363)/ 当前线程 LastError 与来源 |
!address [-summary] [<addr>] |
地址空间全景/单地址属性(MEM_FREE/STACK/IMAGE) |
!heap -stat / -a <addr> / -h <heap> -segment / -p -a <addr> |
原生堆统计/地址归属/堆详情/页堆分配栈 |
!handle / !handle <h> f / !handle 0 1 <Type> |
句柄表;f=对象专有信息(dump 需带句柄流 .dump /mh) |
!teb / !peb |
用户态 TEB(LastError、栈基限、TLS)/ PEB(命令行、子系统) |
!process / !thread / !stacks / !handle 之外的内核对象命令 |
!thread、!process 仅内核模式;多线程速览首选 !stacks 2 |
!chkimg [-d] [mod] |
内存镜像 vs 磁盘镜像,查代码被改/内存硬件错 |
s -u/-d/-b <range> <pat> |
搜内存(找字符串、找重复模式定位泄漏对象) |
10.4 托管(SOS)速查
.loadby sos clr ; .NET Framework .loadby sos coreclr ; 旧 .NET Core dotnet-sos install ; 新版统一 SOS(WinDbg 会自动加载) !eeversion ; !runtimes ; !setclrpath <dir> ; 版本与 DAC 校准 !threads ; !clrthreads -live ; 线程(含 GC Mode / 异常 / 锁数) !pe / !printexception -nested -lines ; 托管异常(0xE0434352) !clrstack [-a -f -p -l] ; !dumpstack [-EE] ; !dso !syncblk ; Monitor 锁持有与争用(死锁核心) !dumpheap -stat [-type X] [-mt MT] [-min N] !dumpobj <addr> ; !dumpvc ; !dumparray ; !do !gcroot <addr> [-all -nostacks] ; !gchandles ; !finalizequeue ; !objsize !eeheap -gc ; !dumpdomain !dumpasync [-type X -waiting -roots] ; async/Task 泄漏(较新版本) !threadpool ; !threadpoolqueue ; !timerinfo
10.5 内核(BSOD)速查
!analyze -v ; .bugcheck ; !analyze -show <码> <p1..p4> vertarget ; lm ; lmvm <mod> ; !lmi <addr> kb / kn / kp ; .ecxr ; .exr -1 ; dps @rsp !process 0 3 ; !thread ; !stacks ; !irp <addr> ; !irplog deviceext !pool <addr> ; !poolused 2 ; poolmon ; !verifier !vm ; !address -summary ; !pte <addr> ; !locks !drvobj \Driver\Xxx 3 ; !devstack <obj> ; !object / !ob !chkimg -d <mod> ; 怀疑内存硬件/超频时 !wdfkd.* (需 .load wdfkd.dll) ; KMDF/UMDF 对象
11. 结论怎么写:报告模板与判据
分析做得再深,报告写不清等于没做。推荐固定五段式:
# <进程名> Dump 分析报告 ## 0. 样本与环境 - dump:<路径/大小/类型(Full|Mini)/抓取方式/SHA256> - 时刻:dump 时间 <..>,进程已运行 <..>(判断是否"时间相关型") - 目标:<进程/版本/位数> / OS <版本构建> / 运行时 <CLR|coreclr|native> - 工具:WinDbg <版本> / 扩展 <SOS 版本> / 符号 <是否全部命中,未命中模块列出> ## 1. 根因(一句话) <谁> 在 <什么条件> 下 <做了什么>,导致 <机制>,表现为 <症状>。 (反例:不行——"内存泄漏导致卡死";行不行——"PLC 定时器每 300ms 对每个料盒 BeginInvoke 一次,Normal 优先级队列积压 105 万条,饿死 Render/Input 导致白屏") ## 2. 证据链(每条都要有命令与数据) - 现场:<UI 线程 TID 栈在 ..>(命令 ~~/e k) - 量化:<积压 1,051,371 个 DispatcherOperation,净增长率 1.6/s vs 生产率 ~2000/s> - 归属:<其中 1,037,970 个闭包类型 BoxHandingDefine+<>c__DisplayClass..> - 根:<!gcroot 显示根为 Dispatcher 队列> - 排除:死锁(!syncblk 无环)/ 线程池饥饿(!threadpool 正常)/ 句柄(!handle 计数正常)/ 内存耗尽(!address Commit 正常) ## 3. 影响与触发条件 - 触发条件、复现概率、影响面(版本范围、机型、场景) ## 4. 修复建议(分级) - P0 止血(可热改/配置开关):… - P1 根治(架构/并发模型):… - P2 优化:… ## 5. 验证方法(可量化) - 指标:<队列积压 < 100;内存增速 < 1MB/h;…> - 命令:<!dumpheap -type DispatcherOperation -stat / Get-Counter ...>
判据写作的四条硬要求
- 有数字:
积压 1,051,371比大量积压有力一万倍。 - 有对照:给出正常时的量级(或同类进程),否则"多"没有意义。
- 有排除:把查过但否掉的假设写出来,避免别人重复劳动,也提高结论可信度。
- 可复核:所有关键结论后附命令,别人拿同一个 dump 能跑出同样数字。
常见误判自查清单
| 误判 | 为什么错 | 正确做法 |
|---|---|---|
把 MODULE_NAME(崩溃指令所在模块)当责任人 |
崩溃点常是"受害者"(尤其是踩到别人内存) | 看栈上第一个业务帧 + 损坏特征 |
| 把"卡在锁上的线程"当根因 | 它是等待方,根因是持有者 | 追到持有者,再看持有者在干什么 |
| 只看 UI/主线程 | 后台线程洪泛才是白屏主因 | 全线程概览 + 资源计数 |
| 符号没匹配却继续推断 | 栈与变量是错的 | 先修符号(lmvm 核验) |
| 用 mini dump 下内存结论 | 堆内存根本没进 dump | 看 dump 类型;重抓 Full |
| 一次 dump 就断言"泄漏" | 可能只是 GC 没跑/缓存正常增长 | 两次 dump 差分,或看是否可达(!gcroot) |
12. 常见坑与规避(含实战验证)
环境与工具
- Store 版 WinDbg 的
WindowsApps目录 ACL 导致cdb.exe无法直接执行/复制 → 复制整个amd64目录到普通目录,或改用winget/WDK 版。 - Store 版常缺
ntsdexts.dll/uext.dll→!runaway/!handle/!process/!locks报Unable to add extension DLL→.load ntsdexts或补齐 DLL(注意位数)。 - 杀软/EDR 扫描 dump 目录会让载入与
!gcroot慢一个数量级 → 排除目录白名单。 - 符号在首次分析时才下载,慢得像卡死 → 先
.symfix+.reload,或symchk /id <dump>预取;!sym noisy判断"在下符号"还是"真卡住"。 - 大 dump 放机械盘/内存不足 → 载入阶段就交换文件爆掉;放 NVMe,物理内存 ≥ dump 大小 ×1.5。
命令使用
~23a4s语法错:~后面是线程索引;按 OS TID 必须~~[0x23a4]s(十六进制要带0x)。~*e k在线程多 + 符号未热时可能几分钟不出结果 → 先~*e k L4,再定向。.foreach解析命令输出时,SOS 的版本告警会混进 stdout 导致解析失败 → 先消除版本不匹配,或改手工抽查。- SOS 与 CLR 小版本不一致通常只是警告、命令可用(实测 CLR 4.8.4400 + SOS 4.8.9290 可用);但输出乱码/统计明显错时要用
.cordll -lp或!setclrpath校准 DAC。 !pe撞名:原生exts也注册了pe(输出MSPE:)。加载 SOS 后用!printexception全名最稳。!dumpheap -type X(不带-stat)在大堆上会打出百万行 → 统计一律-stat。- 优化版 PDB 下
dv/!clrstack -a显示<no data>是正常的(寄存器分配/内联),不要误判为"值为 null"。 !gcroot在大堆上要跑几分钟且只给一条根路径(要全部用-all);栈上根可能因优化不可靠,需!clrstack复核。k出来的栈明显"太浅"或"跳帧":FPO/内联导致 →kP(帧指针)或dps @rsp L40手工找返回地址。
判读
!analyze -v的MODULE_NAME/IMAGE_NAME只是"出事地点",FAILURE_BUCKET_ID才是聚类依据;桶指向微软模块时,多半是别人踩了它的内存。- 内核转储里
Small/Activedump 缺内容时!analyze会给出"信息不足"的误导性结论 → 看ANALYSIS_SOURCE与是否提示信息缺失。 -
dump 的"运行时长"要自己算(文件时间 − 进程创建时间),它决定你是不是该走"单调增长资源"这条排查线。
-
!locks不接受地址参数(只有-v/-o);按地址查用!critsec <addr>或!cs <addr>。 LockCount不是"持有次数",Vista+ 是位压缩值(最低位=是否已锁、其余高位=等待者数的反码);网图文里的 "N waiters" 是加工过的。LockCount极负值=LeaveCriticalSection多调导致结构损坏,按内存破坏处理。!thread、!process只在内核模式可用;用户态看线程用~、!teb、!uniqstack、!findstack。- 原生线程池的文档化命令是
!tp,不是!threadpool(后者用前需.chain/!extmatch tpool探测)。!wchain/!waitchain/!apartment/!commgr/!lock/!tinfo均不存在(等待链请用 Win32 WCT 或资源监视器)。 - ProcDump
-h只对有窗口且窗口挂起的进程生效(≥5s 不响应消息,不可调);无窗口服务要改用-c/-m/-p触发器。-u单独用是卸载 AeDebug,跟在-c后才是按单核归一。 .dump /f在用户态已不支持(只出 legacy 格式);一律.dump /m*。忘写/o时若同名文件已存在会静默不写。
13. 团队落地建议
- 默认策略先铺:全线 WER LocalDumps 小型 dump(
DumpType=1,DumpCount=3)+ 关键进程 Full(DumpType=2);内核 Automatic(CrashDumpEnabled=7)+NMICrashDump=1+LogEvent=1。 - 符号是发布产物的一部分:CI 每次发布
symstore add归档exe/dll/pdb(同 GUID+Age),并记录版本号 → 符号库路径 → SHA256映射表;没有这一步,任何线上 dump 最终都无法深入分析。 - 崩溃桶进工单系统:把
FAILURE_BUCKET_ID/ 托管异常类型 + 栈顶帧做成聚类键,命中已知桶自动派单,只有新桶才需要人分析。 - 应用内自救:自研
MiniDumpWriteDump上报(MiniDumpWithFullMemory | MiniDumpIgnoreInaccessibleMemory | MiniDumpWithTokenInformation),带版本号、用户操作序列摘要、可用内存等上下文;比事后抓 dump 命中率高。 - 能复现就升级武器:反复出现的内存破坏上页堆/Verifier;逻辑类难复现 bug 上 TTD 录制;只有趋势问题的先上
dotnet-trace/ETW。 - 每次案件留三样:dump(含 SHA256)、会话日志(
.logopen)、五段式报告。这是团队分析能力唯一会累积的资产。
14. 参考来源
本文档撰写时直接核对过的官方页面(节选)
- Analyzing a User-Mode Dump File — https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/analyzing-a-user-mode-dump-file
- !analyze (WinDbg) — https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-analyze
- Symbol path for Windows debuggers — https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/symbol-path
- Microsoft public symbol server — https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/microsoft-public-symbols
- Install the Windows debugger(新版 WinDbg / classic) — https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/
- What is WinDbg?(新版特性) — https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/windbg-overview
- Time Travel Debugging Overview — https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/time-travel-debugging-overview
- Collecting User-Mode Dumps(WER LocalDumps) — https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
采集侧与分析侧完整参考(专题调研核实清单)
- Collecting User-Mode Dumps(WER LocalDumps): https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
- MINIDUMP_TYPE 枚举: https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ne-minidumpapiset-minidump_type
- MiniDumpWriteDump: https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump
- .dump (Create Dump File): https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-dump--create-dump-file-
- Configure system failure and recovery options(CrashControl): https://learn.microsoft.com/en-us/troubleshoot/windows-client/performance/configure-system-failure-and-recovery-options
- Forcing a System Crash from the Keyboard: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/forcing-a-system-crash-from-the-keyboard
- Varieties of Kernel-Mode Dump Files: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/varieties-of-kernel-mode-dump-files
- Automatic Memory Dump: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/automatic-memory-dump / Active Memory Dump: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/active-memory-dump
- !analyze (WinDbg): https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-analyze
- Using the !analyze extension: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/using-the--analyze-extension
- Bug Check Code Reference: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/bug-check-code-reference2 (0xD4: bug-check-0xd4--system-scan-at-raised-irql-caught-improper-driver-unlo;0x51: bug-check-0x51--registry-error;0x34: bug-check-0x34--cache-manager)
- SymChk Command-Line Options: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/symchk-command-line-options
- ProcDump: https://learn.microsoft.com/en-us/sysinternals/downloads/procdump
- Time Travel Debugging Overview: https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/time-travel-debugging-overview / Record a trace: https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/time-travel-debugging-record
- Crash dump analysis using WinDbg: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/crash-dump-files / Task Manager live dump: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/task-manager-live-dump
托管 / SOS 参考
- https://learn.microsoft.com/en-us/dotnet/core/diagnostics/sos-debugging-extension
- https://learn.microsoft.com/en-us/dotnet/core/diagnostics/debugger-extensions
- https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-dump
- https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-sos
- https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dumps
- https://learn.microsoft.com/en-us/dotnet/core/diagnostics/collect-dumps-crash
- https://learn.microsoft.com/en-us/dotnet/core/diagnostics/debug-linux-dumps
- https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/debugging-managed-code
- https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
- https://github.com/dotnet/diagnostics/blob/main/documentation/dotnet-dump-instructions.md
挂起 / 等待链 / 线程池(命令存在性核实页面)
!locks(ntsdexts): https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-locks---ntsdexts-locks-- 临界区判读(Displaying a Critical Section): https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/displaying-a-critical-section
- Debugging a Deadlock: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/debugging-a-deadlock
!handle: https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-handle!runaway: https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-runaway!thread(内核专用): https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-thread!stacks: https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-stacks!tp(原生线程池): https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-tp!deadlock: https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-deadlock- Wait Chain Traversal(Win32 API,非 WinDbg 命令): https://learn.microsoft.com/en-us/windows/win32/debug/wait-chain-traversal
- 调试器扩展清单: https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/debugger-extensions
原生崩溃分析(异常码 / 栈 / 符号 / 堆)
!exr/.ecxr: https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-exr--display-exception-record- ・ https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-ecxr--display-exception-context-record-k(kb/kc/kd/kp/kP/kv): https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/k--kb--kc--kd--kp--kp--kv--display-stack-backtrace-!cppexr(C++ 异常解码): https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-cppexr!gle(LastError): https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-gledps/dqs/dds: https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/dds--dps--dqs--display-words-and-symbols-.reload/.symfix/!lmi/!chkimg: https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-reload--reload-module- ・ https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-symfix--set-symbol-store-path- ・ https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-lmi ・ https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-chkimg!heap/!htrace/gflags/ UMDH: https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-heap ・ https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/-htrace ・ https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/gflags ・ https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/preparing-to-use-umdh.srcfix(source server): https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-srcfix---lsrcfix--use-source-server-dt/dv: https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/dt--display-type- ・ https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/dv--display-local-variables-
说明:文中「需核实」项均为社区流传但在官方命令参考中未找到条目的写法(如
!wchain、!threadpool、!apartment、.srclog、!bugdump、0xC0000097=私有指令等)。落地前请在自己的调试器里用.chain/!extmatch <关键字>//?实测。
附录 A:采集侧完整参考(采集手段细节 / 内核转储类型 / 符号工程化 / 落地脚本)
与 §3 的关系:§3 是操作摘要,本附录是完整参考。✅=已对照官方文档核实;「需核实」=未能确认。
A.1 用户态 dump 采集方式全景
1)任务管理器
- 经典入口:详细信息页右键进程 →「创建转储文件」,生成于 %TEMP%\<进程名>.dmp(个别版本带 PID 后缀,需核实)。本质是 DbgHelp minidump(含线程栈、模块表及大部分内存),可离线分析,但只能人工触发、进程挂住/即将退出时来不及。
- Windows 11 新入口:「创建实时转储文件」(Task Manager live dump,✅ 官方文档有述),支持内核态与用户态进程。
2)WinDbg 附加后 .dump(✅ 已核实当前文档)
.dump [/ma|/mA|/m<MiniOptions>|/f] [/o] [/u] [/c <comment>] [/b[a]] <file>
/ma:全内存 minidump,等价/mfFhut(全内存+句柄+卸载模块+基本内存信息+线程时间);遇不可读内存即失败。/mA:同/ma但忽略不可读内存(对应MiniDumpIgnoreInaccessibleMemory)。/mf:加全内存;MiniOptions:f全内存F完整内存布局h句柄u卸载模块t线程时间i栈间接引用内存pPEB/TEBw私有读写页d数据段c代码段r抹掉栈局部变量(隐私)R抹模块全路径(隐私)yAVX 寄存器。/o覆盖已有文件;/u文件名追加唯一标识;/b打包 CAB 后删除原文件;/f用户态为旧式 full dump(不推荐);/2、/w独立选项当前文档未列出(需核实;w现作为 MiniOption 存在)。- 内核态专用:
/k仅内核内存、/ka活跃内核+用户内存、/f完整内存转储(不崩溃主动落盘)。
3)ProcDump(✅ 已核实 v12 文档)
procdump -ma 4572 ; 全量 dump(-mm mini、-mt triage、-mp miniPlus、-mc <MINIDUMP_TYPE掩码>) procdump -ma -e 1 -n 10 -f NotFound w3wp.exe ; 首次/二次机会异常,最多10个,内容过滤 procdump -n 3 -s 5 -c 20 consume ; CPU>20% 持续5秒触发,共3个(滚动命名 进程_YYMMDD_HHMMSS.dmp) procdump -h hang.exe ; 窗口挂起(5秒不响窗消息)触发 procdump -ma -p "\Process(Outlook)\Handle Count" 10000 app.exe ; 性能计数器触发 procdump -ma -i c:\dumps ; 注册为 AeDebug 事后调试器(-u 卸载)
-e 1含首次机会异常;-ld/-ud/-ct/-etDLL 加载/卸载、线程创建/退出触发;-s连续满足秒数;-n个数;-64强制 64 位 dump(WOW64 调试用);-accepteula静默接受协议;-r克隆进程 dump 降低停顿;-wer把 dump 投递 WER 队列。- 注意:单独的
-u是卸载 AeDebug;与-c连用才是“按单核计 CPU”。所谓 “Triaged64” 并非 ProcDump 参数(需核实出处;对应概念是-mttriage dump 与内核 triage dump)。
4)Win32 API MiniDumpWriteDump 与 MINIDUMP_TYPE(✅ 已核实枚举值)
关键标志:MiniDumpWithFullMemory 0x2、WithHandleData 0x4、WithUnloadedModules 0x20、WithIndirectlyReferencedMemory 0x40、WithProcessThreadData 0x100、WithPrivateReadWriteMemory 0x200、WithoutOptionalData 0x400、WithFullMemoryInfo 0x800(即常说的“MemoryInfo”,无独立 WithMemoryInfo 标志)、WithThreadInfo 0x1000、WithCodeSegs 0x2000、WithoutAuxiliaryState 0x4000、WithFullAuxiliaryState 0x8000(“WithAuxiliaryMemory” 不存在,正名如此)、WithTokenInformation 0x40000、FilterTriage 0x100000、WithAvxXStateContext 0x200000、WithIptTrace 0x400000(“WithIpti” 系笔误)。新版另有 MiniDumpWithMemoryCompressed(压缩 dump,Win11+)。工程惯例:MiniDumpWithFullMemory | MiniDumpIgnoreInaccessibleMemory | MiniDumpWithTokenInformation。
5)WER LocalDumps(✅ 已核实,Vista SP1/Server2008 起)
HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps:
- DumpFolder(REG_EXPAND_SZ,默认 %LOCALAPPDATA%\CrashDumps;服务崩溃写到其账户 Profile 目录,如 System 服务 = %WINDIR%\System32\Config\SystemProfile)
- DumpCount(默认 10,超出删最旧);DumpType:0=自定义、1=迷你、2=完整;CustomDumpFlags(仅 DumpType=0 生效,MINIDUMP_TYPE 位掩码,默认 0x121)。
- 支持 LocalDumps\<程序名>.exe 子键做进程级覆盖;与 WER 上报解耦(WER 被禁用也能收)。
- ⚠️ 官方明确:配置了 AeDebug 自动调试的进程,WER 不再收 dump(两者互斥)。
- MaximumRotateFiles、DontAddDisruptionExclusion、WerDump 均未见于官方文档(需核实,疑为讹传)。崩溃事件:系统日志 1001(WER);WER 报告队列在 %ProgramData%\Microsoft\Windows\WER\ReportQueue / ReportArchive。
6)AeDebug / IFEO / GFlag
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug (Auto=1, Debugger="cdb -p %ld -e %ld -g ...") HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\<image>\AeDebug(32位看 Wow6432Node)
- IFEO 的
GlobalFlag置 EnableDumps 位可让系统在未处理异常时用内置 MiniDumpWriteDump 落 dump(gflags GUI 勾选;gflags -r -d具体语义与位值需核实)。ProcDump-i即自动写 AeDebug。
7)comsvcs.dll MiniDump 技巧:rundll32 C:\Windows\System32\comsvcs.dll, MiniDump <PID> <out.dmp> full。是合法系统组件导出函数,但常被攻击者用于导出 LSASS 窃取凭据,生产环境应视同攻击行为告警;仅建议应急且用后立即删除产物。
8)难采场景:服务崩溃→LocalDumps + 正确 ACL 的共享 DumpFolder;子进程→ProcDump -w/-x 等待/拉起即挂;启动早期崩溃→AeDebug 事后调试器 + 开机计划任务回捞;GUI 挂起→-h 或资源监视器“分析等待链”。
9)大小与隐私取舍
- full dump(-ma / /ma / DumpType=2):体积≈私有提交内存,采集时目标挂起明显;含令牌/密钥等敏感内存,外发必须走加密通道并脱敏审批。
- minidump(DumpType=1 / -mm):仅栈+模块+少量引用内存,KB~MB 级,优先大规模铺。
- triage(-mt / MiniDumpFilterTriage):尝试去除敏感信息(不保证)。隐私过滤项:.dump /mrR、MiniDumpFilterMemory 0x8、WithModulePaths 过滤 0x80、WithoutOptionalData 0x400。
- 压缩传输:.dump /b(CAB)、Win11 MiniDumpWithMemoryCompressed;建议归档时校验 SHA256 + 记录 PDB GUID/Age。
A.2 内核 dump(BSOD)采集
1)注册表 HKLM\SYSTEM\CurrentControlSet\Control\CrashControl(✅ 已核实)
| 值 | 含义 |
|---|---|
CrashDumpEnabled |
0=无;1=Complete;2=Kernel;3=Small(mini);7=Automatic(Win8+ 默认)(用户问的 2/3 含义以此为准 ✅) |
DumpFile / MinidumpDir |
Complete/Kernel 落点(默认 %SystemRoot%\Memory.dmp)/ 小转储目录(默认 %SystemRoot%\Minidump) |
FilterPages |
与 CrashDumpEnabled=1 合用 = Active Memory Dump(✅) |
LogEvent / SendAlert / AutoReboot / Overwrite |
写系统日志事件 / 管理告警 / 自动重启 / 覆盖旧 dump |
NMICrashDump=1 |
允许 NMI 触发崩溃转储(常用于 BMC;对应 dump 判读需结合 WHEA,具体 bugcheck 表现需核实) |
ExcludeFromPageFile(REG_MULTI_SZ,防指定驱动被换出)——需核实;CrashOnAuditFail 实际位于 Control\Lsa 而非 CrashControl(更正)。
2)查询/设置当前配置
Get-CimInstance -ClassName Win32_OSRecoveryConfiguration | Select-Object DebugInfoType, DebugFilePath, MiniDumpDirectory, AutoReboot # Set-CimInstance -Property @{DebugInfoType=7} 修改
wmic recoveros set DebugInfoType = 7(✅,GUI = 系统属性→“启动和故障恢复”;msinfo32 组件也可查看)。
3)各类型保留内容
- Complete(1):全部物理内存。页文件 ≥ RAM+1MB;能查任何内核/用户结构、做 !chkimg。
- Kernel(2):仅内核空间(代码/池/非分页表等),无用户内存;64 位页文件 ≤ RAM+128MB 即可。
- Small(3):约几十~几百 KB“triage”级(历史称 256KB,需核实现值):bugcheck 参数、模块列表、部分线程上下文。够定桶(FAILURE_BUCKET_ID),不够深挖。页文件 ≥2MB(启动卷)。
- Automatic(7):等同 Kernel,但系统自动调整页文件大小(默认推荐)。
- Active Dump:= Complete+FilterPages,剔除空闲/零页/文件缓存,包含用户内存;Azure/大型服务器推荐。所谓 “Triaged64” 非官方 dump 类型(需核实)。
- 转储栈依赖启动卷上的存储驱动栈;第三方卷过滤/存储驱动可能使 dump 失败退化为小转储(0x7A/0xED 伴随排查)。
4)现代环境
- VBS/Secure Kernel:VSM 隔离内存主机侧不可读,不进 dump(需核实具体表述);HVCI 只影响未签名驱动加载。BitLocker 卷上的 Memory.dmp 静态被加密,内容本身是明文内存;hiberfil.sys 可离线提取内存取证(Volatility 等社区方案,需核实)。
- 手动崩溃:① 键盘:i8042prt(PS/2)、kbdhid(USB)、hyperkbd(Hyper-V)Parameters\CrashOnCtrlScroll=1,重启后右 Ctrl + ScrollLock×2 → 0xE2(✅);crashdump\Dump1Keys/Dump2Key 可改组合键(✅)。② Sysinternals NotMyFault /crash。③ NMI(服务器带外)。.crash 不是真实命令(真实做法:kd 里 .dump /f 或 CrashControl 组合,更正)。
- kd 打开内核 dump 注意:符号必须匹配内核版本(-y srv*C:\SymCache*https://msdl.microsoft.com/download/symbols);vertarget 看版本;!analyze -v 先跑;NMI/硬件崩溃栈不可信,转 !whea 与平台日志。
A.3 符号与二进制归档(工程化)
symstore add /r /f C:\build\bin\* /s \\symserver\symbols /t MyApp /v 1.2.3 /compress
- 库结构:
<share>\<pdb名>\<GUID+Age>\xxx.pdb|dll;HTTP 访问需symsrv.dll+dbghelp.dll部署到 IIS 虚拟目录。 .sympath顺序=从左到右,典型:
.sympath cache*;cache*C:\SymCache;SRV*C:\SymCache*https://msdl.microsoft.com/download/symbols;SRV*\\symserver\symbols
(cache*=可写缓存,cache*<只读共享>=本地共享缓存)。环境变量 _NT_SYMBOL_PATH、备用路径 _NT_ALTERNATE_SYMBOL_PATH(用户写的 _NT_ALT_SYMBOL_PATH 拼写有误,需核实旧文档)。
- Source Index:CI 里设 TGTDIR/SRCSRV_SOURCE_SERVER_COMMAND 等后 srcsrv -i <pdb>(srctool -r 验证),配合 source server 让 lst 直接拉源码。
- PDB 归档以 CodeView GUID+Age 为唯一键;exe 与 PDB 必须成对入镜像库(symstore add 同名冲突可用 /i 自动改名,需核实参数义);地址漂移调试用 rebase 工具。私有 PDB 外发给厂商前须授权审批(含源码路径泄露风险)。
A.4 校验工具
symchk /r C:\bin /s <sympath> /vd批量验;/om manifest + /im manifest离线清单法(✅ 选项已核实,含/ip <pid>、/id <dump>、/p强制私有、/s强制公有、/oc <dir>建符号树)。- WinDbg 内:
!sym noisy→.reload /f module.dll看逐目录搜索失败原因;!sym查看 SYMOPT;!chkimg校验镜像与符号一致。!chkitf未见于官方文档(疑误传);checkPDB非官方工具名(需核实)。
A.5 生产落地推荐配置
:: WER LocalDumps:全线默认小型策略 + 关键进程完整 dump(管理员 cmd) set L=HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps reg add "%L%" /v DumpFolder /t REG_EXPAND_SZ /d %%PUBLIC%%\CrashDumps /f reg add "%L%" /v DumpType /t REG_DWORD /d 1 /f reg add "%L%" /v DumpCount /t REG_DWORD /d 3 /f reg add "%L%\MyApp.exe" /v DumpFolder /t REG_EXPAND_SZ /d %%PUBLIC%%\CrashDumps\MyApp /f reg add "%L%\MyApp.exe" /v DumpType /t REG_DWORD /d 2 /f reg add "%L%\MyApp.exe" /v DumpCount /t REG_DWORD /d 5 /f :: 自定义标志位(DumpType=0 时生效): reg add "%L%\MyApp.exe" /v CustomDumpFlags /t REG_DWORD /d 0x3121 /f :: 内核侧:Automatic 转储 + NMI + 事件日志 reg add "HKLM\SYSTEM\CurrentControlSet\Control\CrashControl" /v CrashDumpEnabled /t REG_DWORD /d 7 /f reg add "HKLM\SYSTEM\CurrentControlSet\Control\CrashControl" /v NMICrashDump /t REG_DWORD /d 1 /f reg add "HKLM\SYSTEM\CurrentControlSet\Control\CrashControl" /v LogEvent /t REG_DWORD /d 1 /f
(转成 .reg 文件分发时注意:REG_EXPAND_SZ 必须写成 hex(2): 编码,%变量% 才会保留。)
要点:① 灰度机器 CrashDumpEnabled=7(Automatic)+ 必要时 Active;② CI 流水线每次发布 symstore 归档 exe+dll+pdb,命名 <产品线>\<版本>\,二进制镜像与 PDB 同 GUID+Age;③ 服务器 NMICrashDump=1、开启 LogEvent;④ 转储目录 ACL 最小化 + 定期压缩外发 + 敏感机器用 -mt/WithoutOptionalData;⑤ 手册化:崩溃 → FAILURE_BUCKET_ID 聚类 → 命中内部桶直接派单。
附录 B:「不要轻信网图」——已核实不存在或被讹传的写法清单
| 流传写法 | 核实结论 | 正确做法 |
|---|---|---|
!wchain / !waitchain |
WinDbg 无此命令 | 等待链是 Win32 API WCT(OpenThreadWaitChainSession/GetThreadWaitChain);或用资源监视器"分析等待链"、Process Explorer |
!locks <地址> |
用户态 !locks 只接受 -v / -o |
按地址用 !critsec <addr> 或 !cs <addr> |
输出里的 4 waiters 一行 |
官方输出不打印该字样 | 由 LockCount 位域算出:? ((-1) - <LockCount>) >> 2 |
!lock / !tinfo / !commgr / !apartment / !so(COM 单例) |
命令参考中无条目(so 实为 Set Kernel Debugging Options) |
.chain、!extmatch <关键字> 探测到再用;STA 用 SOS !COMState / !clrthreads 的 APT 列 |
!threadpool(原生) |
文档化命令是 !tp |
托管用 SOS !threadpool / !threadpoolqueue |
!thread 在用户态 dump 里用 |
仅内核模式可用(用户态也没有 WaitReason 字段) | 用户态用 ~、!teb、!uniqstack、!findstack |
!dstate / !runnable / !dptr / !io / !bugdump / !poolval / !objhdr / !dpc / !devnode / !exqueue / !workqueue / !wdflock / !wdf!irp |
命令参考中均无条目 | 先探测再用;WDF 用 !wdfkd.*、UMDF 用 !wudfext.wdfumirp(s) |
!analyze -depend / -verify |
官方选项只有 -v[0..99]、-f、-hang、-show、-xml/-xmf/-xmi/-xcs、-c -load |
— |
!mk / !mli |
旧 SOS 命令,已废弃 | !ip2md、!name2ee、!dumpil |
!pe 一定输出托管异常 |
原生 exts 也注册了 pe(输出 MSPE:) |
用全名 !printexception;原生记录用 .exr -1 |
0xC0000097 = 私有指令 |
0xC0000096 才是 PRIVILEGED_INSTRUCTION;0xC0000097 是 TOO_MANY_PAGING_FILES |
见 §5.2 |
0x80131623 等"CLR 崩溃码" |
未找到权威定义 | 不要写进结论 |
k /p |
k 用大小写选项字母(kb/kn/kp/kP/kv/kc/kd),没有斜杠选项 |
见 §5.4 |
.dump /f(用户态) |
现文档标注 Not supported(只出 legacy 格式) | 用 .dump /m*;全内存 /ma、生产 /mA;忘写 /o 时同名文件存在会静默不写 |
ProcDump -h 抓任何挂起 |
仅覆盖有窗口且窗口 ≥5s 不响应消息的进程;5 秒不可调 | 无窗口服务用 -c / -m / -p 计数器触发器 |
ProcDump -u = 卸载 |
双义:单独用=卸载 AeDebug 注册;跟在 -c 后=CPU 按单核归一 |
见 §3.2 / §6.0 |
ProcDump Triaged64 参数 |
无此参数 | triage dump 用 -mt |
WER MaximumRotateFiles / DontAddDisruptionExclusion |
官方 LocalDumps 文档无此值 | 用 DumpCount(超限删最旧) |
.srclog / srctrk / /srcsvl |
当前官方文档查无 | .srcfix / .srcpath / .lines / ls / !lines |
.ttime 是 TTD 时间机器 |
.ttime 是显示线程时间;TTD 的时间控制是 .timemachine(新版以 UI 为主) |
别混用 |
DOTNET_DumpEnabled / Dump_Type(Linux) |
现行为 DOTNET_DbgEnableMiniDump / DOTNET_DbgMiniDumpType(1 Mini/2 Heap/3 Triage/4 Full) / DOTNET_DbgMiniDumpName |
见 §7.7 |
| 「SOS 版本不匹配就完全不可用」 | 小版本差异通常只是警告、命令可用(实测 CLR 4.8.4400 + SOS 4.8.9290 可用);但 DAC 与运行时不匹配会导致统计/对象图错误 | 乱码或统计异常时用 .cordll -lp / !setclrpath / !runtimes 校准 |
用法建议:这张表就是"新人第一次分析 dump 前必读的排雷清单"。凡 commands 参考里查不到的写法,一律先
.chain+!extmatch <关键字>+<命令> -?三连探测,再决定是否写进结论。
后记
这份指南是把几次现场排障(原生崩溃、WPF 白屏、托管内存增长)里反复用到的判断路径整理成册,重点不在命令本身,而在先看什么、什么时候可以下结论、什么写法其实是网传的。文中「需核实」的标记不是谦虚:WinDbg 社区里流传的命令名有相当一部分在官方文档里根本不存在,抄之前值得先 .chain 看一眼自己环境到底加载了哪些扩展。
如果这份指南帮你省下几个晚上,欢迎转载,注明出处即可:https://www.mileskuang.cn/
评论 (0)
登录后可以发表评论
登录 注册暂无评论,快来抢沙发吧!