第5章 调试
用宿主—目标架构搭建内核调试环境,通过符号、断点、调用栈与崩溃转储,把驱动故障从蓝屏定位到具体函数与指令。
学习目标
- 能配置目标机调试启动项并建立宿主—目标内核调试连接
- 能改出在 DriverEntry 命中断点并查看调用栈的效果,说出栈顶三条记录的含义
- 能回答:符号文件版本与驱动不匹配时,调用栈会变成什么样?为什么?
为什么写驱动必须先学会“隔着一堵墙观察”
想象你在观察一座运行中的核电站。你不能站在反应堆旁边看——太危险,机器一出事你也会搭进去。正确做法是坐在隔壁的监控室,透过仪表和摄像头观察:哪里温度异常、哪个阀门先动、报警从哪一路传来。监控室就是本章的主角。
没有监控室会怎样?你只能等电站炸了再冲进去看废墟——对应到驱动,就是等蓝屏后重启,对着重启前的一瞬猜测原因。猜中靠运气,猜不中只能反复炸、反复猜。
这一章教你搭监控室:把观察者与被观察者分开(两台机器)、装上仪表(调试工具)、给仪表标好刻度(符号)、设好报警点(断点)、学会看事故录像(转储)。搭好之后,驱动崩溃不再是玄学,而是一条可以回放的现场。
宿主—目标架构:监控室与车间
内核调试的基本架构是两台机器:↡运行调试器、负责观察和控制的机器,不运行被测驱动(host)运行 WinDbg,↡实际运行被测驱动的机器,被调试器暂停、断点和读写内存(target)运行驱动。两台机器通过串口、网络或 USB 相连,目标机启动时由引导配置开启调试支持,内核在启动早期就等待调试器接管。
为什么非要两台?因为内核崩溃时整个系统停摆,如果调试器在崩溃的机器上,连它一起停——等于监控室建在反应堆里。分开之后,目标机随便崩,宿主机完好无损,随时可以打断、检查、重启目标机。可丢弃的测试虚拟机就是最常用的目标机。
符号文件:照片上的坐标标注
调试器看到的是二进制地址,而你要的是函数名和行号——中间靠 ↡把二进制地址映射回函数名、变量名和源行号的文件,后缀 .pdb,必须与二进制同版本(.pdb)搭桥。WinDbg 加载符号后,调用栈从 MyDriver+0x14 变成 MyDriver!MyDeviceControl+0x14,可读性天差地别。
符号必须与 .sys 完全同版本。改了代码重新编译,就得重新加载新的 .pdb——用旧符号调试新二进制,函数名会错位,栈回溯全是“鬼影”。调试会话的第一步永远是核对符号版本,lm 命令列出模块时,符号状态一目了然。
断点:在关键路口设卡
↡让目标机在指定函数或地址暂停执行、把控制权交给调试器的机制(breakpoint)是调试的基本手段。bp MyDriver!MyDeviceControl 表示:目标机一旦进入这个函数就暂停,控制权回到宿主机。此时你可以查看参数、寄存器、内存,然后 g(继续)或 p(单步)。
断点比 DbgPrint 日志强在“暂停现场”:日志只能告诉你“来过”,断点能让你在“刚来”的时刻把现场翻个底朝天。断点打在驱动生命周期关键点(DriverEntry、分发例程、Unload),配合调用栈就能回答“谁调了我、参数是什么、栈上还有什么”。
崩溃转储:事故录像
目标机蓝屏后,如果配置了转储文件,系统把崩溃瞬间的内存镜像存成 .dmp——这就是 ↡蓝屏时保存的内存镜像文件,可在宿主机离线分析崩溃原因(crash dump)。它不需要目标机在线,宿主机随时打开分析。
分析命令是 !analyze -v:自动给出 BugCheck 代码(如 0xD1 表示在错误 IRQL 访问内存)、首错地址、责任模块和调用栈。!analyze -v 是崩溃定位的第一站——它把“哪里崩了”变成“大概是谁崩的”,剩下的是顺着调用栈找到具体那行指令。
单步执行:慢放每一帧
↡每按一次命令只执行一条指令或一个函数,逐帧观察程序状态(stepping)把“暂停”变成“慢放”:断点命中后,p 执行一条指令、t 跟进函数内部,每步都能看寄存器与内存变化。适合确认“这行到底有没有执行”“这个值是什么时候变的”。
单步在内核里要小心:在 DISPATCH_LEVEL 及以上单步可能让目标机挂起,某些路径(中断、DPC)不能随意慢放。实战中通常是断点定位大范围、单步精查小范围。
动手:搭建调试工作台
下面的可视化把宿主—目标架构画成工作台:左右两台机器、中间调试链路、下方断点选择、调用栈与转储分析面板。点任意区域看详情;切换断点位置观察命令与调用栈变化;关掉符号开关看“没有符号”的世界。
猜一猜:把“符号”开关关掉,调用栈第一行会变成什么样子?用符号文件的概念解释为什么调试器“不认识”你的函数名。
宿主机运行 WinDbg,是观察窗口所在。它不运行被调试的驱动,而是通过调试链路控制目标机:设置断点、查看寄存器、回溯调用栈、读取内存。宿主机的 WinDbg 需要正确加载符号文件,否则看到的只是地址不是函数名。
代码对照:从开机配置到崩溃定位
调试不是“点开 WinDbg”这么简单,它是一条完整的命令链:目标机配置 → 连接 → 断点 → 分析。四个代码段走完整条链。
段一:目标机开启内核调试
先在目标机上用 bcdedit 配置调试启动项。注意顺序:先开 debug,再设调试传输(这里是网络调试,hostip 指向宿主机),最后重启生效。配错了,目标机起来后 WinDbg 一直连不上。
:: 目标机上以管理员身份运行
bcdedit /debug on
bcdedit /dbgsettings net hostip:192.168.1.10 port:50000 key:1.2.3.4.5
bcdedit /enum {dbgsettings} :: 核对配置
:: 重启目标机,内核启动早期等待调试器连接
shutdown /r /t 0目标机 BIOS/EFI 启动
│
├─→ 引导器读取 {dbgsettings}
│ ├─→ /debug on 开启调试支持
│ └─→ net hostip/port/key 网络调试参数
│
├─→ 内核初始化早期:等待调试器
│ · 无调试器 → 正常启动(连接可后补)
│ · 有调试器 → 内核被托管,可随时打断
│
└─→ 系统就绪 → 可加载被测驱动段二:宿主机连接与断点
宿主机启动 WinDbg(内核模式),选择网络调试并填目标机地址。连接后先配符号路径再重载模块——顺序反了符号白配。然后设断点、g 放行,目标机跑到断点就停。
# WinDbg (内核模式) → 网络 → 目标 IP:端口 + 调试密钥
# 1. 配置符号路径(Microsoft 公共符号 + 自己的 .pdb 目录)
.sympath srv*C:\Symbols*https://msdl.microsoft.com/download/symbols;D:\MyDriver\x64
# 2. 重载模块符号(确认 MyDriver 已加载且符号匹配)
.reload
lm m MyDriver # 观察符号状态:deferred/loaded/mismatched
# 3. 设断点并放行
bp MyDriver!MyDeviceControl
g宿主机 WinDbg 目标机
│ 连接(网络 + 密钥) │
│ ←──────────────────────────→ │ 内核被托管
│ .sympath 配符号路径 │
│ .reload │ 核对每个模块的 .pdb 版本
│ bp MyDriver!MyDeviceControl │ 断点登记到目标机
│ g(继续) │ 正常运行
│ ←── 断点命中 ─────────────── │ 停在 MyDeviceControl 入口
│ k(看栈)→ 参数、内存、寄存器 │ 现场冻结段三:断点命中后的现场勘察
断点命中后,k 回溯调用栈,r 看寄存器,dc 看内存。栈顶是你断的函数,往下是“谁调了它”的完整链条。第 4 章的驱动断在 MyDeviceControl 时,栈里能看到 NtDeviceIoControlFile——证明请求来自用户模式的 DeviceIoControl。
kd> k
# Child-SP RetAddr Call Site
00 ffff8000`12345678 fffff800`2a1b4f51 MyDriver!MyDeviceControl+0x14
01 ffff8000`12345680 fffff800`2a2ba1a1 nt!IofCallDriver+0x51
02 ffff8000`123456c0 fffff800`2a2c302a nt!IopXxxControlFile+0xa1
03 ffff8000`12345740 fffff800`2a1d5f25 nt!NtDeviceIoControlFile+0x2a
04 ffff8000`12345740 00007ffa`12345678 nt!KiSystemServiceCopyEnd+0x25
kd> r rcx, rdx # 查看参数寄存器
kd> dc poi(MyDriver!g_ctx) # 查看设备扩展内容栈帧 00 MyDriver!MyDeviceControl ← 当前停在这里(断点位置)
栈帧 01 nt!IofCallDriver ← 内核把请求交给驱动的那一步
栈帧 02 nt!IopXxxControlFile ← DeviceIoControl 的内核实现
栈帧 03 nt!NtDeviceIoControlFile ← 系统服务入口
栈帧 04 nt!KiSystemServiceCopyEnd ← 从用户模式切进来的边界
从上往下读:用户模式调用 → 系统服务 → 派发 → 你的驱动。段四:蓝屏后的转储分析
目标机崩了不要慌——转储文件已经把现场保存好了。宿主机打开 .dmp,!analyze -v 自动给出首错信息。关键是看三行:BugCheck 代码(什么类型的错误)、Probably caused by(责任模块)、以及栈顶的模块+偏移(具体位置)。
# 打开转储:WinDbg → File → Open Crash Dump
kd> !analyze -v
BugCheck 0xD1, {0, 2, 0, 0}
DRIVER_IRQL_NOT_LESS_OR_EQUAL
*** WARNING: Unable to verify timestamp for MyDriver.sys
Probably caused by : MyDriver.sys ( MyDriver!MyDeviceControl+14 )
STACK_TEXT:
MyDriver!MyDeviceControl+0x14
nt!IofCallDriver+0x51
...
# 用 !irql 看崩溃时 IRQL,用 uf 反汇编定位偏移
kd> !irql
kd> uf MyDriver!MyDeviceControl目标机蓝屏
│ 崩溃瞬间内存写入 .dmp
▼
宿主机打开转储(离线分析,目标机可关机)
│
├─→ BugCheck 0xD1 = 在错误 IRQL 访问内存(类型)
├─→ Probably caused by = MyDriver.sys(责任模块)
├─→ MyDeviceControl+14 = 首错偏移(具体位置)
│
└─→ 对照规则:
DISPATCH_LEVEL 下访问分页内存?→ 第 3 章 IRQL 规则
缓冲区长度未校验? → 第 4 章协议校验容易踩的坑
小结
- 内核调试必须宿主—目标分离,目标机是可丢弃的测试环境
- 符号文件把地址映射回函数名,版本错配会让调用栈变裸地址
- 断点让目标机暂停在关键函数,配合 k/r 勘察现场
- 崩溃转储保存蓝屏现场,!analyze -v 给出首错起点
- 单步慢放适合低 IRQL 精查,高 IRQL 用日志插桩替代
练习
问题 1: 在 Demo 中关闭“符号”开关,观察调用栈变化。解释为什么符号版本不匹配时 lm 会显示 mismatched,以及 .reload /f 能解决什么问题。
问题 2: 你的驱动在 MyDriver!MyWriteDispatch+0x20 处蓝屏,BugCheck 是 0xD1。调用栈显示请求来自用户模式的 WriteFile。列出你下一步要做的三件事。
问题 3(独立实现题): 写出在宿主机上完成以下任务的全部命令:1)设置符号路径包含 D:\MyDriver\x64;2)在 MyDriver!DriverEntry 设断点;3)继续执行;4)命中后回溯调用栈并查看参数寄存器。要求按调试顺序写出命令并注释每一步的目的。
知识点对照
5.1 Debugging Tools for Windows
Debugging Tools for Windows 是微软官方调试工具集,包含 WinDbg、kd 与符号工具。
5.2 WinDbg简介
WinDbg简介:微软图形界面调试器,支持断点、内存查看、寄存器观察与源码级调试,适合交互排错。
5.3 内核调试
内核调试分本地内核调试与双机内核调试,通过调试接口连接目标系统,可设置断点并查看内核状态。
5.4 完整内核调试
完整内核调试需要分别配置目标机与宿主机,通过串口、网络或 USB 等通道建立稳定的调试连接。
5.5 内核驱动程序调试教程
内核驱动程序调试教程演示设置断点、查看变量与寄存器、单步执行以及查看调用栈的完整排错流程。
5.3.1 本地内核调试
本地内核调试在同一台机器上进行,用 bcdedit 开启本机的 debug 模式后连接调试器。
5.3.2 本地内核调试教程
本地内核调试教程演示在本机用 WinDbg 连接内核并查看系统信息的完整实践操作过程,这也是本章的核心要点。
5.4.1 配置目标机
配置目标机用 bcdedit 设置调试参数,包括调试端口、波特率与调试开关等三项关键内容。
5.4.2 配置宿主机
配置宿主机启动 WinDbg 并选择内核调试模式,设置与目标机完全匹配且一致的连接参数,这也是本章的核心要点。
5.6 总结
本章总结了调试工具选择、双机调试配置与驱动调试流程的核心要点、实战步骤和常见排查误区,这也是本章的核心要点。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 宿主机
运行 WinDbg 调试器的机器,负责观察和控制目标机。它不运行被测驱动,目标机蓝屏也不影响它。相当于监控室:人坐在里面,事故发生在车间。
- 目标机
实际运行被测驱动的机器,被调试器暂停、断点、读写内存。必须是可丢弃的测试环境(专用机器或虚拟机),因为内核调试随时可能把它弄蓝屏。相当于车间:出事的地方,但不是人待的地方。
- 符号文件
把二进制地址映射回函数名、变量名和源行号的文件(.pdb)。调试器加载符号后调用栈才有人话可读。必须与 .sys 完全同版本,错配时函数名变裸地址。相当于照片上的坐标标注:没有它,照片只是一堆色块。
- 断点
让目标机在指定函数或地址暂停执行、控制权交给调试器的机制。断点命中后可以看寄存器、内存、调用栈,然后继续或单步。相当于在关键路口设卡:车一到就拦下检查。
- 崩溃转储
蓝屏时保存的内存镜像文件(.dmp),可在宿主机离线分析。!analyze -v 自动给出 BugCheck 代码、责任模块和调用栈。相当于事故录像:现场已封存,随时可以回放。
- 单步执行
每按一次命令只执行一条指令或一个函数的调试方式,逐帧观察状态变化。适合低 IRQL 精查;高 IRQL 下单步可能挂死目标机,改用日志插桩。