把「用 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 三条纪律

  1. 先证伪再结论:任何「因为 X 所以崩」的结论,都必须能用命令指出 dump 里的证据(地址、栈帧、计数)。
  2. 符号不对,全部白搭:lmvm 的时间戳 / 镜像大小 / Unable to verify checksum 与 CI 归档记录对不上时,栈和变量都不可信——先解决符号,再谈结论(校验三件套见 §2.2)。
  3. 大 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 分析环境的三个"独立"

  1. 分析机独立于生产机:CPU/内存/磁盘(NVMe)富裕,GB 级 dump 载入与 !gcroot 都是重 IO。
  2. 符号缓存独立目录:不要把 C:\build\bin(手工放的 PDB)同时当符号服务器缓存目录,两者混用会导致缓存里塞满手工文件、命中率下降。
  3. 每次案件一个目录: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;子键名=进程文件名,覆盖全局

注意三条官方口径:

  1. 配了 AeDebug 自动调试的进程,WER 不再收 dump(两者互斥)。
  2. 服务崩溃的 dump 落在该服务账户的 Profile 目录(如 System 服务 %WINDIR%\System32\Config\SystemProfile),自定义 DumpFolder 要确认账户有写权限。
  3. .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 级)的操作纪律

  1. 先 lm / vertarget / .time 这类 O(模块数) 的命令建立量级感。
  2. 统计类命令一律带 -stat、L<n>、-type <过滤>;永远不要先 !dumpheap -type X 列地址(可能上百万行)。
  3. ~*e k 前先 ~ 看线程数;线程多时先 ~*e k L4 概览,再 ~~[TID]s 定向。
  4. 慢命令(!gcroot、!dumpheap -stat)在符号未就绪时会一边下符号一边跑,先 .reload 再跑,并确认不是"在下符号"而不是"卡死"(!sym noisy 可判断)。
  5. 机器侧:把符号缓存放到 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 上位机的白屏就是这一类,方法通用:

  1. 算运行时长(dump 时刻 − 进程创建时刻)。跑几小时/几天才出现的无响应,几乎都是某资源单调增长。
  2. 先分"满负荷"还是"真在等":!runaway 看 UI 线程 CPU 时间——很大 = 满负荷干活(排水/循环);≈0 = 真在等(转 §6.4)。
  3. 找增长的东西:托管 !dumpheap -stat 看 DispatcherOperation、<>c__DisplayClass(闭包)、Timer、日志项、事件订阅者;原生 !heap -stat、!handle 0 1 <Type> 分类计数、线程数。
  4. 归因生产者:托管用闭包类型名直接锁定方法(!dumpheap -type <>c__DisplayClass -stat)+ !gcroot 证明被队列持有;原生用页堆 !heap -p -a 或 UMDH 差分(§9.2)。
  5. 算两个率:净增长率 = 积压量 ÷ 运行秒数;生产率 = 每周期投递量 × 频率 × 生产者数。净增长率 ≪ 生产率 ⇒ 消费端已满负荷,任何额外工作都会饿死关键路径。

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 类型对象本身就是被等待目标。

手工追链三步(死锁分析的全部诀窍):

  1. 在等待方栈上取资源标识:RtlEnterCriticalSection 的首参数(CRITSEC 地址)、句柄值、或托管 Monitor 对象地址。
  2. 找持有者:原生 !locks / !cs <addr>;托管 !syncblk;内核 !locks -v <addr>。把 OwningThread 换算成线程索引。
  3. 看持有者在干什么:也在等另一资源 → 递归第 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 驱动责任判定与上报

  1. 栈上地址定位:ln <addr>、!lmi <addr> 直接给出模块+符号偏移;lm 找无符号(no pdb)第三方帧。
  2. 无符号/未签名驱动:lmvm <mod> 比对 Image/PDB 时间戳与磁盘文件(dir /u),向厂商索取“GUID+Age 匹配”的 PDB;sigverif/pnputil 辅助。
  3. !analyze 的 MODULE_NAME 常指微软二进制——用「栈上第一个非系统帧 + 池 tag + 内存损坏特征」三角定位真凶;“Windows 驱动兼容数据库”官方入口为 Partner Center/DevPortal 的 HCK/HLK 报告(!analyze -verify 不存在,需核实)。
  4. 最小证据包给厂商:dump 文件、!analyze -v 全文、FAILURE_BUCKET_ID、复现步骤与频率、驱动版本与符号(或请其用同 build 符号重跑 !for_each_module/栈回放)、verifier 日志。
  5. 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. 有数字:积压 1,051,371 比 大量积压 有力一万倍。
  2. 有对照:给出正常时的量级(或同类进程),否则"多"没有意义。
  3. 有排除:把查过但否掉的假设写出来,避免别人重复劳动,也提高结论可信度。
  4. 可复核:所有关键结论后附命令,别人拿同一个 dump 能跑出同样数字。

常见误判自查清单

误判 为什么错 正确做法
把 MODULE_NAME(崩溃指令所在模块)当责任人 崩溃点常是"受害者"(尤其是踩到别人内存) 看栈上第一个业务帧 + 损坏特征
把"卡在锁上的线程"当根因 它是等待方,根因是持有者 追到持有者,再看持有者在干什么
只看 UI/主线程 后台线程洪泛才是白屏主因 全线程概览 + 资源计数
符号没匹配却继续推断 栈与变量是错的 先修符号(lmvm 核验)
用 mini dump 下内存结论 堆内存根本没进 dump 看 dump 类型;重抓 Full
一次 dump 就断言"泄漏" 可能只是 GC 没跑/缓存正常增长 两次 dump 差分,或看是否可达(!gcroot)

12. 常见坑与规避(含实战验证)

环境与工具

  1. Store 版 WinDbg 的 WindowsApps 目录 ACL 导致 cdb.exe 无法直接执行/复制 → 复制整个 amd64 目录到普通目录,或改用 winget/WDK 版。
  2. Store 版常缺 ntsdexts.dll/uext.dll → !runaway/!handle/!process/!locks 报 Unable to add extension DLL → .load ntsdexts 或补齐 DLL(注意位数)。
  3. 杀软/EDR 扫描 dump 目录会让载入与 !gcroot 慢一个数量级 → 排除目录白名单。
  4. 符号在首次分析时才下载,慢得像卡死 → 先 .symfix + .reload,或 symchk /id <dump> 预取;!sym noisy 判断"在下符号"还是"真卡住"。
  5. 大 dump 放机械盘/内存不足 → 载入阶段就交换文件爆掉;放 NVMe,物理内存 ≥ dump 大小 ×1.5。

命令使用

  1. ~23a4s 语法错:~ 后面是线程索引;按 OS TID 必须 ~~[0x23a4]s(十六进制要带 0x)。
  2. ~*e k 在线程多 + 符号未热时可能几分钟不出结果 → 先 ~*e k L4,再定向。
  3. .foreach 解析命令输出时,SOS 的版本告警会混进 stdout 导致解析失败 → 先消除版本不匹配,或改手工抽查。
  4. SOS 与 CLR 小版本不一致通常只是警告、命令可用(实测 CLR 4.8.4400 + SOS 4.8.9290 可用);但输出乱码/统计明显错时要用 .cordll -lp 或 !setclrpath 校准 DAC。
  5. !pe 撞名:原生 exts 也注册了 pe(输出 MSPE:)。加载 SOS 后用 !printexception 全名最稳。
  6. !dumpheap -type X(不带 -stat)在大堆上会打出百万行 → 统计一律 -stat。
  7. 优化版 PDB 下 dv / !clrstack -a 显示 <no data> 是正常的(寄存器分配/内联),不要误判为"值为 null"。
  8. !gcroot 在大堆上要跑几分钟且只给一条根路径(要全部用 -all);栈上根可能因优化不可靠,需 !clrstack 复核。
  9. k 出来的栈明显"太浅"或"跳帧":FPO/内联导致 → kP(帧指针)或 dps @rsp L40 手工找返回地址。

判读

  1. !analyze -v 的 MODULE_NAME/IMAGE_NAME 只是"出事地点",FAILURE_BUCKET_ID 才是聚类依据;桶指向微软模块时,多半是别人踩了它的内存。
  2. 内核转储里 Small/Active dump 缺内容时 !analyze 会给出"信息不足"的误导性结论 → 看 ANALYSIS_SOURCE 与是否提示信息缺失。
  3. dump 的"运行时长"要自己算(文件时间 − 进程创建时间),它决定你是不是该走"单调增长资源"这条排查线。

  4. !locks 不接受地址参数(只有 -v/-o);按地址查用 !critsec <addr> 或 !cs <addr>。

  5. LockCount 不是"持有次数",Vista+ 是位压缩值(最低位=是否已锁、其余高位=等待者数的反码);网图文里的 "N waiters" 是加工过的。LockCount 极负值=LeaveCriticalSection 多调导致结构损坏,按内存破坏处理。
  6. !thread、!process 只在内核模式可用;用户态看线程用 ~、!teb、!uniqstack、!findstack。
  7. 原生线程池的文档化命令是 !tp,不是 !threadpool(后者用前需 .chain/!extmatch tpool 探测)。!wchain/!waitchain/!apartment/!commgr/!lock/!tinfo 均不存在(等待链请用 Win32 WCT 或资源监视器)。
  8. ProcDump -h 只对有窗口且窗口挂起的进程生效(≥5s 不响应消息,不可调);无窗口服务要改用 -c/-m/-p 触发器。-u 单独用是卸载 AeDebug,跟在 -c 后才是按单核归一。
  9. .dump /f 在用户态已不支持(只出 legacy 格式);一律 .dump /m*。忘写 /o 时若同名文件已存在会静默不写。

13. 团队落地建议

  1. 默认策略先铺:全线 WER LocalDumps 小型 dump(DumpType=1, DumpCount=3)+ 关键进程 Full(DumpType=2);内核 Automatic(CrashDumpEnabled=7)+ NMICrashDump=1 + LogEvent=1。
  2. 符号是发布产物的一部分:CI 每次发布 symstore add 归档 exe/dll/pdb(同 GUID+Age),并记录 版本号 → 符号库路径 → SHA256 映射表;没有这一步,任何线上 dump 最终都无法深入分析。
  3. 崩溃桶进工单系统:把 FAILURE_BUCKET_ID / 托管异常类型 + 栈顶帧做成聚类键,命中已知桶自动派单,只有新桶才需要人分析。
  4. 应用内自救:自研 MiniDumpWriteDump 上报(MiniDumpWithFullMemory | MiniDumpIgnoreInaccessibleMemory | MiniDumpWithTokenInformation),带版本号、用户操作序列摘要、可用内存等上下文;比事后抓 dump 命中率高。
  5. 能复现就升级武器:反复出现的内存破坏上页堆/Verifier;逻辑类难复现 bug 上 TTD 录制;只有趋势问题的先上 dotnet-trace/ETW。
  6. 每次案件留三样: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/-gle
  • dps/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/TEB w私有读写页 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/-et DLL 加载/卸载、线程创建/退出触发;-s 连续满足秒数;-n 个数;-64 强制 64 位 dump(WOW64 调试用);-accepteula 静默接受协议;-r 克隆进程 dump 降低停顿;-wer 把 dump 投递 WER 队列。
  • 注意:单独的 -u 是卸载 AeDebug;与 -c 连用才是“按单核计 CPU”。所谓 “Triaged64” 并非 ProcDump 参数(需核实出处;对应概念是 -mt triage 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/