windbg – 联机断点调试方法 日期 : 2026-04-03项目 : PCIe SD Host Controller Storport 驱动目的 : WinDbg KDNET 双机联机调试,bp 断点 + .crash 抓转储,定位 Win10 Storport DMA Remapping 0xC000009A 问题适用 : Windows 驱动开发者、内核调试工程师背景 : Win10 19045 + Kernel DMA Protection + DmaRemappingCompatible=1,Storport 框架在 PnP Start 路径返回 STATUS_INSUFFICIENT_RESOURCES (0xC000009A),驱动初始化已完成,错误来自 storport.sys 而非 bhtsddr.sys
目录
环境准备
KDNET 连接
阶段 1:符号设置
阶段 2:定位关键函数
阶段 3:设置断点
阶段 4:运行复现
阶段 5:抓 crash dump
阶段 6:离线分析 dump
快捷命令序列
代码索引
1. 环境准备 硬件与网络
项目
说明
目标机
Win10 19045(安装驱动),开启 Kernel DMA Protection
主机
WinDbg Preview 或经典版,任意 Windows 版本
连接方式
KDNET(以太网双机调试,推荐千兆或 USB 虚拟网卡)
测试环境
目标机应为可接受蓝屏重启的测试机
目标机配置(powercfg / bcdedit) 目标机管理员 PowerShell 执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 bcdedit /set testsigning on bcdedit /debug on bcdedit /dbgsettings net hostip:192.168 .1.100 port:50000 shutdown /r /t 0
如果目标机有多个网卡,指定调试网卡:bcdedit /set "{dbgsettings}" debugport 3(或 bcdedit /dbgsettings net ... busparams=3.0.0)
主机 WinDbg 连接 1 2 3 4 5 6 7 8 9 # WinDbg Preview(Microsoft Store) # File -> Kernel Debug -> Net -> 输入 port key 和目标机 IP # 经典 WinDbg 命令行(windbg.exe -k) windbg.exe -k net:portkey=xxxxxxxxxxxxxxxx,targethost=192.168.1.101 # 连接成功后,WinDbg 命令行出现: # Connected to target 192.168.1.101 on port 50000 on transport Net # Kernel Debugger connection established.
2. KDNET 连接 KDNET 连接建立后,WinDbg 底部状态栏会显示 Kernel Debug: 192.168.1.101:50000。此时目标机处于调试暂停状态(没有完全启动桌面)。
首次连接后的基础命令 1 2 3 4 5 6 7 8 9 10 11 12 // 解除目标机暂停,让它继续启动到桌面 g // 等待桌面出现后,在 WinDbg 命令行按 Ctrl+Break 中断目标机 // (或 WinDbg Preview -> Break, 或菜单 Debug -> Break) // 中断后验证连接 .version // 期望输出:Kernel Version 10.0.19041.xxx ..., 19045 是 Win10 20H2 // 查看当前进程(确认在 System 进程) !process -1 0
启用调试扩展 1 2 3 4 // 加载常用的内核调试扩展 .load winxpExts .load kdapi .chain
3. 阶段 1:符号设置 原则 :先确认符号加载成功,再下断点。符号不对,断点打在错误偏移,可能永远不命中或命中断错代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // ===== 1.1 启用 verbose 符号 ===== !sym noisy .on/unex "echo Target is now interactive" // ===== 1.2 设置符号路径 ===== // 优先:本地驱动 PDB(bhtsddr.pdb) // 其次:Microsoft 符号服务器(storport.pdb) .sympath+ c:\gitlab-bht\storport\SDSTORDriver\x64\Win8.1Debug .sympath+ srv*https://msdl.microsoft.com/download/symbols .sympath // ===== 1.3 重新加载符号 ===== // 先清除旧符号缓存 .cached n .reload // 等待下载 storport.pdb(约数秒到数十秒,取决于网络) // 如果提示下载超时,等 30 秒后重试 .reload // ===== 1.4 确认 storport.sys 符号已加载 ===== lm m storport // 期望输出示例: // start end module name // fffff8064a200000 fffff8064a38c000 storport (pdb symbols) // c:\symbols\storport.pdb\... <-- 本地缓存路径 // 如果看到 "(pdb symbols)" 表示成功;如果看到 "export" 只表示导出表,没有私有符号 // ===== 1.5 确认你的驱动已加载 ===== lm m bhtsddr // 期望输出示例: // fffff806... fffff806... bhtsddr (pdb symbols) // c:\gitlab-bht\storport\SDSTORDriver\x64\Win8.1Debug\bhtsddr.pdb
4. 阶段 2:定位关键函数
断点下在哪里 :参考 Microsoft 工程师 Ron Hutnik 的建议——断点应打在”已执行完你要看的那段 API/路径之后”的位置。这样命中时,调用栈和内存状态才是你要看的场景,而不是更早的中间状态。
4.1 你的驱动关键函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 // ===== BHTDispatchPnp ===== // 你的 PnP 分发总入口,0xC000009A 在这里被记录 // 代码位置:winscsientry.c line 2498, return at line 2631 x bhtsddr!BHTDispatchPnp // 期望:fffff806`4a3d0000 bhtsddr!BHTDispatchPnp // ===== SavedDispatchPnp ===== // 全局变量,存的是 storport.sys 的原始 IRP_MJ_PNP 处理函数 // 代码位置:winscsientry.c line 58, 初始化于 DriverEntry line 2932 x bhtsddr!SavedDispatchPnp // 期望:fffff806`4a3d0000 bhtsddr!SavedDispatchPnp // ===== BhtAllocateDmaBuffer ===== // 核心 DMA 分配函数,调用 StorPortAllocateDmaMemory(DMAR fix 核心) // 代码位置:winapi.c line 4101 x bhtsddr!BhtAllocateDmaBuffer // 期望:fffff806`4a3e0000 bhtsddr!BhtAllocateDmaBuffer // ===== os_alloc_dma_buffer ===== // 高层 DMA 缓冲分配 wrapper // 代码位置:winapi.c line 2004 x bhtsddr!os_alloc_dma_buffer // ===== scsi_HwUnitControl ===== // Storport 在 IRP_MN_START_DEVICE 期间调用的 UnitControl // 代码位置:winscsientry.c line 1454 x bhtsddr!scsi_HwUnitControl // ===== scsi_HwAdapterControl ===== // 适配器控制(ScsiRestartAdapter 等) // 代码位置:winscsientry.c line 1787 x bhtsddr!scsi_HwAdapterControl
4.2 storport.sys 内部关键函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 // ===== 4.2.1 搜索与 0xC000009A / STATUS_INSUFFICIENT_RESOURCES 相关的函数 ===== // 在 storport 内部搜索(注意 storport 的内部函数通常不是导出函数) x storport!*Insufficient* x storport!*Resource* // 如果上面没有结果(该字符串是动态拼接的),搜索 DMA 相关: x storport!*Dma* /s x storport!*Alloc* // ===== 4.2.2 搜索 PnP 处理函数 ===== // Storport 内部 PnP 分发(名字因 Win10 版本不同而变化) x storport!*Pnp* x storport!*Start* // ===== 4.2.3 搜索 AdapterControl(Storport 回调)===== x storport!*AdapterControl* // ===== 4.2.4 列出所有 storport 导出函数 ===== // 导出函数有限(通常几十个),但内部函数才是真正的处理逻辑 x storport!StorPort* // 重点关注: // StorPortAllocateDmaMemory -- 你的驱动调用的底层 DMA 分配 // StorPortFreeDmaMemory -- 对应的释放 // StorPortInitializePoFxPower // StorPortGetDeviceObjects // ===== 4.2.5 备选:将所有 storport 内部函数导出到文本文件 ===== // 当内部函数太多时,先导出再分析 x storport!* > c:\debug\storport_funcs.txt // 然后用编辑器打开该文件,搜索关键词: // - 含 "QueryInterface" / "Query_Interface" // - 含 "Start" / "Pnp" // - 含 "Dma" / "Remap" / "Iommu" // - 含 "Fail" / "Error" / "Status"
找不到 storport 内部符号? 这是正常的——Retail 版 storport.sys 通常只有导出函数有 PDB,私有符号(内部函数名、局部变量名)不对外公开。在这种情况下,调用栈里会显示 RVA+偏移(如 storport+0x12345),而不是友好函数名。可以继续按以下步骤抓 crash dump,发给 Microsoft 让他们的内部工具解析 RVA 对应源码。
5. 阶段 3:设置断点 策略 A:在「你的驱动」层下断——捕获 0xC000009A 已出现的时刻 原理 :BHTDispatchPnp 在 return status 前会将 status 记录到 DbgLog,命中此处时 storport 已经返回了 0xC000009A,可以直接抓调用栈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // ===== BP-A1:在 BHTDispatchPnp 退出点下断 ===== bp /p @$curprocess bhtsddr!BHTDispatchPnp+0xa0 // 断点解释: // BHTDispatchPnp 函数长度约 0x134 字节(line 2498-2632) // +0xa0 是大约 "return status" 的偏移(编译器优化后可能有几字节偏差) // /p @$curprocess 表示在 System 进程(PID 4)上下文中激活断点 // 如果上面的偏移命中了错误代码行,用以下方式精确定位: // 先查看 BHTDispatchPnp 的反汇编,找到 "ret" 指令的地址 uf bhtsddr!BHTDispatchPnp // 输出中找 "ret" 前几条指令,确认是传回 status 的那个 ret // 假设输出显示 "ret" 在 0xfffff806`4a3d00cc,则: bp /p @$curprocess 0xfffff806`4a3d00cc // ===== BP-A2:BhtAllocateDmaBuffer 失败时自动抓栈并 crash ===== // 当 BhtAllocateDmaBuffer 返回非 STOR_STATUS_SUCCESS 时触发 // 条件断点(只在外层调用栈不是 DriverEntry 时触发) bp /p @$curprocess bhtsddr!BhtAllocateDmaBuffer+0x50 // 条件:rbx != 0(status != STOR_STATUS_SUCCESS) // 注意:条件断点有性能开销,在正常路径下每次分配都会计算条件 // ===== BP-A3:在 os_alloc_dma_buffer 的 DMA 分配失败路径下断 ===== bp /p @$curprocess bhtsddr!os_alloc_dma_buffer+0x80
策略 B:用「事件断点」——驱动加载时自动设断 原理 :使用 WinDbg 的模块加载事件(sxe ld:modulename),当 bhtsddr.sys 被 System 进程加载时自动中断并设断点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // ===== BP-B1:bhtsddr.sys 加载时中断 ===== // 如果驱动尚未加载(lm m bhtsddr 无输出),使用事件断点 sxe ld:bhtsddr.sys g // 当 bhtsddr.sys 加载时,WinDbg 输出类似: // ModLoad: fffff806... bhtsddr.sys // Break instruction exception - code 80000003 (first chance) // 此时驱动已加载到内存,符号已解析: // ===== BP-B2:驱动加载后,立即设断点 ===== // 在 WinDbg 中断状态(目标机已暂停)下执行 bp /p @$curprocess bhtsddr!BHTDispatchPnp+0xa0 bp /p @$curprocess bhtsddr!BhtAllocateDmaBuffer+0x50 // 确认断点已设置 bl // 期望看到两个 bp,类型为 p(进程上下文) // ===== BP-B3:让目标机继续运行 ===== g
策略 C:系统任意模块加载时中断(可选) 1 2 3 4 5 6 7 8 9 // ===== BP-C1:任何模块加载时打印信息并继续 ===== sxe -c "kv; g" lmd // ===== BP-C2:在 storport.sys 加载时打印内部函数列表 ===== // storport.sys 通常系统启动时就加载了 // 如果你想在它刚加载时抓内部符号: // (先确认 .reload 拿到了完整的 storport 符号) .reload /f storport.sys x storport!* > c:\debug\storport_loaded_funcs.txt
确认断点列表 1 2 3 4 5 6 7 8 // 查看所有断点 bl // 期望输出示例: // 0 e fffff8064a3d00cc bhtsddr!BHTDispatchPnp+0xa0 .... // 1 e fffff8064a3e0050 bhtsddr!BhtAllocateDmaBuffer+0x50 ... // 如果没有看到任何 bp,说明驱动尚未加载,先用策略 B
6. 阶段 4:运行复现 让目标机继续运行
目标机上复现问题的步骤
安装驱动 (INF 中 DmaRemappingCompatible=1) 安装程序可能提示”需要重启”,确认安装完成
重启目标机 (目标机重启后,Storport 在 IRP_MN_START_DEVICE 路径返回 0xC000009A,触发你的断点)
观察 WinDbg :
目标机应再次停在 WinDbg(因为你的断点命中了)
WinDbg 命令行出现 Breakpoint 0 hit 或类似提示
不要按 g 继续 ,进入阶段 5
断点命中后的典型 WinDbg 输出 1 2 3 4 5 Breakpoint 0 hit bhtsddr!BHTDispatchPnp+0xa0: fffff806`4a3d00cc c3 ret // ★ 在此输入阶段 5 的命令 ★
7. 阶段 5:抓 crash dump
警告 :执行 .crash 后目标机会蓝屏(BSoD)并自动重启。请确保操作在可接受测试的环境中进行。
在 WinDbg 命令行按顺序执行(以下所有命令在一分钟内完成) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 // ===== DUMP-1:保存完整调用栈(最重要)===== kv // 期望看到类似: // # Child-SP RetAddr Call Site // 00 ffffd8d0c9f3b88 fffff8064a2b1234 storport+0x12345 <-- ★ 关键:0xC000009A 来源 // 01 ffffd8d0c9f3b90 fffff8064a2b5678 bhtsddr!BHTDispatchPnp+0xa0 // 02 ffffd8d0c9f3b98 fffff8064a2b8abc nt!IofCallDriver // 03 ... // ===== DUMP-2:保存所有寄存器状态 ===== // 重点关注:rax = 0xC000009A 表示 Storport 返回的错误状态 r // ===== DUMP-3:保存当前进程和线程信息 ===== // System 进程 (PID 4) 是驱动运行的地方 !process -1 0 !thread // !process -1 0 输出的 "Process Start Address" 应指向 bhtsddr.sys 的入口 // ===== DUMP-4:查看 bhtsddr 驱动关键全局变量 ===== // SavedDispatchPnp(被调用的 storport PnP 函数指针) dd bhtsddr!SavedDispatchPnp L1 // 期望:dd 输出的地址即 storport.sys 内部 PnP 处理函数 // 如果 !analyze -v 未输出友好名称,用 dt 反查该地址属于 storport 的哪个函数: ln poi(bhtsddr!SavedDispatchPnp) // 期望:(<PDB symbol>, fffff8064a2b1234) storport!XXXFunctionName // ===== DUMP-5:查看传入 BHTDispatchPnp 的 IRP ===== // 从 kv 输出找到 IRP 指针位置(通常在 [rcx] 或 [rsp+28]) // 以下命令需要根据 kv 实际输出的栈布局调整偏移 !irp poi(@rsp+28) 0 // ===== DUMP-6:生成 crash dump ===== // ★ 目标机将蓝屏重启 ★ .crash // WinDbg 提示: // "System took a bugcheck and should be restarted. // Creating dump file 'C:\Windows\MEMORY.DMP'..." // "Dump file successfully created" // Target system is no longer paused // ===== DUMP-7:dump 文件路径(在目标机下次启动后)===== // 默认路径(可在目标机系统属性 -> 高级 -> 启动和故障恢复 设置): // 小转储:C:\Windows\Minidump\Mini*.dmp (仅 256KB,引发的 BSoD 类型有限) // 内核转储:C:\Windows\MEMORY.DMP (仅内核内存,推荐) // 完整转储:C:\Windows\MEMORY.DMP (配置为完整模式时) // ===== DUMP-8:dump 文件复制(目标机重启后立刻做)===== // 在目标机重启后进入桌面,立即执行: copy C:\Windows\MEMORY.DMP D:\Dumps\bhtsddr_dmar_crash_$(Get-Date -Format "yyyyMMdd_HHmmss").dmp // 或通过主机 WinDbg 的 File -> Open Crash Dump 打开网络路径 // ===== DUMP-9:额外保存 kv 输出到文件(防止 dmp 丢失关键信息)===== .logopen c:\debug\bhtsddr_kv_output_$(date +%Y%m%d).txt kv .logclose
6. 离线分析 dump 在主机 WinDbg(全新会话)中打开 dump 文件进行分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 // ===== ANALYZE-1:打开 dump 文件 ===== .open C:\Dumps\bhtsddr_dmar_crash_20260403.dmp // ===== ANALYZE-2:设置符号路径 ===== .sympath+ c:\gitlab-bht\storport\SDSTORDriver\x64\Win8.1Debug .sympath+ srv*https://msdl.microsoft.com/download/symbols .reload /f // ===== ANALYZE-3:自动分析 ===== // 最关键的分析命令,会自动识别 BSoD 类型并给出初步结论 !analyze -v // 期望看到的 Bugcheck 类型: // BugCheck 9F: DRIVER_POWER_STATE_FAILURE // BugCheck 114: The specified resource manufacturer ID is invalid // (因为 .crash 不是真实崩溃,!analyze 可能无法给出完整根因,重点看调用栈) // ===== ANALYZE-4:手动查看调用栈 ===== // 确认 storport 返回 0xC000009A 的位置 kv // ===== ANALYZE-5:查看 storport 函数(如果有内部符号)===== // 如果 storport.pdb 包含私有符号: uf storport!XXXFunctionName // 如果只有 RVA(storport+0x12345),则无法进一步解析 // ===== ANALYZE-6:查看 DMA 池信息 ===== // 搜索 storport 分配的 DMA 池(名称通常含 "Stor") !poolused // 或搜索特定池标签 !poolfind Stor // ===== ANALYZE-7:将 dump 发给 Microsoft ===== 将以下文件打包: 1. C:\Windows\MEMORY.DMP(或 Mini*.dmp) 2. C:\debug\bhtsddr_kv_output_*.txt(kv 命令输出) 3. bhtsddr.pdb(确保版本完全匹配当前驱动) 4. storport.pdb(如有,从 %TEMP%\symbolcache 找到) 5. 驱动源码(winscsientry.c winapi.c 等,标注当前 git commit)
7. 快捷命令序列 将以下内容复制到一个 .bat 文件或 WinDbg 的 Command Workspace ,一键执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 ; ===== WinDbg 快捷序列:bhtsddr DMAR 断点调试 ===== ; 说明:连接 KDNET 目标机后,在目标机可接受调试状态时执行 ; 执行时机:驱动加载后,IRP_MN_START_DEVICE 之前 ; === 符号设置 === !sym noisy .cached n .sympath+ c:\gitlab-bht\storport\SDSTORDriver\x64\Win8.1Debug .sympath+ srv*https://msdl.microsoft.com/download/symbols .reload /f ; === 确认驱动加载 === lm m storport lm m bhtsddr ; === 定位关键函数 === x bhtsddr!BHTDispatchPnp x bhtsddr!BhtAllocateDmaBuffer x storport!StorPortAllocateDmaMemory ; === 下断点(等驱动加载后再执行)=== ;bp /p @$curprocess bhtsddr!BHTDispatchPnp+0xa0 ;bp /p @$curprocess bhtsddr!BhtAllocateDmaBuffer+0x50 ; === 如果驱动未加载,使用事件断点 === ;sxe ld:bhtsddr.sys ;g ; === 命中断点后抓 dump === ;kv ;r ;!process -1 0 ;.crash ; === 提示 === echo === 断点设置完成 === echo === 执行 'g' 让目标机继续运行 === echo === 驱动加载后如需下断点,执行上面被注释的 bp 命令 ===
8. 代码索引 以下是你驱动源码中与 WinDbg 断点对应的精确代码行号:
调试目的
代码位置
说明
0xC000009A 被返回的位置
winscsientry.c:2628
SavedDispatchPnp(DeviceObject, Irp) → storport 返回错误
PnP 分发总入口
winscsientry.c:2498
BHTDispatchPnp() — 断点打在这里的 return 前
PnP 分发表
winscsientry.c:2171
DispatchPnpAdapter() — 处理 IRP_MN_START_DEVICE
SavedDispatchPnp 全局变量
winscsientry.c:58
存储 storport.sys 原始 PnP 函数指针
DMA 缓冲分配
winapi.c:4101
BhtAllocateDmaBuffer() — 调用 StorPortAllocateDmaMemory
DMA 分配成功日志
winapi.c:4133
"DMAR_DIAG: BhtAllocateDmaBuffer: VA=%p IOVA=0x%08X"
DMA 分配失败日志
winapi.c:4138
"DMAR_DIAG: BhtAllocateDmaBuffer FAILED"
高层 DMA wrapper
winapi.c:2004
os_alloc_dma_buffer()
MapBuffers 配置
winscsientry.c:1256-1262
STOR_MAP_NON_READ_WRITE_BUFFERS vs STOR_MAP_ALL_BUFFERS_INCLUDING_READ_WRITE
10 秒超时保护
winscsientry.c:1215
util_is_timeout(&while_loop_wait)
UnitControl 回调
winscsientry.c:1454
scsi_HwUnitControl() — Storport 在 Start 时调用
驱动入口
winscsientry.c:2692
DriverEntry() — SavedDispatchPnp 初始化于 line 2932
DMAR 诊断宏
debug.h:148-152
#define DMAR_DEBUG,DmarDbg 日志宏
附录 A:WinDbg 常用命令速查
命令
说明
g
目标机继续运行
Ctrl+Break
中断目标机(WinDbg 端)
lm m modulename
查看模块加载地址和符号状态
x modulename!*func*
列出模块中匹配的函数符号
bp /p @$curprocess addr
在 System 进程上下文中下断点
bl
列出所有断点
bc *
清除所有断点
kv
显示栈回溯(带参数)
r
显示所有寄存器
!process -1 0
显示当前进程信息
!thread
显示当前线程信息
!irp poi(addr) 0
显示 IRP 结构
.crash
触发手动 bugcheck(生成 dump)
!analyze -v
自动分析 bugcheck
.dump /f filename.dmp
手动保存当前内存快照(不蓝屏)
uf addr
反汇编函数
ln addr
显示地址最近的符号
!poolused
查看池内存使用
!pte addr
查看页表条目
!vmap
虚拟内存映射
.logopen /t filepath
将命令输出记录到文件
.sympath+ path
添加符号路径
附录 B:WinDbg Preview vs 经典 WinDbg
特性
WinDbg Preview
经典 WinDbg (windbg.exe)
来源
Microsoft Store(自动更新)
Windows SDK 独立安装
KDNET 连接
GUI 向导更友好
命令行参数
命令行界面
两者相同
两者相同
颜色/主题
深色主题默认
默认浅色
命令扩展
相同
相同
推荐
新手优先用 Preview
老手或自动化脚本用经典版
KDNET 连接(Preview) :
WinDbg Preview -> File -> Launch executable
Kernel Debug -> Net -> 输入 Port Key 和目标机 IP -> OK
等连接建立
KDNET 连接(经典版) :
1 windbg.exe -k net:portkey=xxxxxxxxxxxxxxxx,targethost=192.168.1.101