ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Windows设备树调试:用!devnode解析CmResourceList等资源列表排查驱动冲突

Windows设备树调试:用!devnode解析CmResourceList等资源列表排查驱动冲突 “设备树”这三个字做安卓 BSP 和嵌入式 Linux 的人首先想到的是 DTS/DTB 那一套做 Windows 驱动的人想到的却是 PnP 管理器内存里那棵设备节点树。我这次要聊的是用调试器里的 !devnode 扩展把 Windows 设备树里某个节点的相关配置摊开来看尤其是那个节点下的 CmResourceList、BootResourcesList 和 IoResList 三份资源列表。驱动开发的朋友遇到设备启动失败、IRQ 冲突、资源分配异常时这三份列表比什么日志都好使前提是你真看得懂。文章后面附了完整的实战排查过程按步骤走就能复现整套思路。1. 先别急着敲命令设备树到底是什么树1.1 Windows 设备树和 Linux 设备树是两码事在正式开始之前先做一次概念清扫因为两个领域的工程师经常因为同一个词发生误会。做嵌入式 Linux 的同事说的设备树是 Petalinux 里导出的 dts/dtsi是瑞芯微 RK3568 平台板级目录下那堆描述 CPU、内存、UART、I2C、GPIO 的设备树文件。它们以文本形式存在经过 dtc 编译器生成 dtb由 bootloader 在启动时传给内核。你要改一个串口的寄存器地址、中断号就要去改设备树文件把它当成普通文本处理。Windows 里的设备树则完全不同。它是 PnP 管理器在系统启动过程中通过 ACPI、总线驱动枚举、驱动加载等机制在内存中构建出来的一棵动态树。根节点是 HTREE\ROOT\0下面挂着 ACPI 枚举的系统设备、PCI 总线、USB 总线每个物理设备都有一个设备节点DevNode每个 DevNode 内部记录着它的状态、设备对象PDO/FDO、资源列表、以及和其他节点的父子关系。这棵树不是给你读的文本而是密密麻麻的链表和结构体直接翻内存不现实所以才有了 !devnode 这类调试器扩展。它的作用就是把这棵二进制树导成可读文本。你在 WinDbg 里敲 !devnode 看到的输出就是设备树的一棵子树或者某个节点的详细状态。同样叫“设备树”但一个解决的是“板级硬件怎么描述”的问题另一个解决的是“系统运行时资源怎么分配”的问题。搞明白这一层再看 !devnode 就不会再有先入为主的偏见。1.2 !devnode 的基本操作和 flag 语义!devnode 命令的用法本身不复杂但很多人第一次上手会被它那堆 flag 数字搞迷糊。我常用的组合就几个!devnode 0 0从根节点开始打印整棵设备树。!devnode 0 DevNode地址只打印指定节点不带资源信息。!devnode 1 DevNode地址打印指定节点并且带资源信息。!devnode 4 DevNode地址打印指定节点的全量信息包含资源、需求、状态机等。从 0 到 4信息量是递进的。flag 是 bitmask0 是最基本的节点信息1 会把已翻译的资源列表加进来4 基本是能显示的都显示。我建议第一次排查问题时直接上 4省得一次一次试探如果只需要看资源就用 1因为 4 的输出里噪音很多比如一长串状态字段、调试用的数值反而干扰判断。这里有一个新手最容易踩的坑DevNode 地址不是稳定的设备标识。同一台机器这次启动和下次启动同一个设备的 DevNode 指针可能就是另一个地址。所以我在排查时习惯先把!devnode 0 0的整体输出导到日志里然后用实例路径比如PCI\VEN_8086DEV_153ASUBSYS_00008086REV_03在文本里定位找到当次会话的 DevNode 地址再去单独 dump。直接拿上次记录的地址去查很容易扑空。还有符号问题。!devnode 这类扩展命令对系统符号的依赖比较强如果你发现输出内容里大量字段显示???或者命令执行报错先检查符号路径。!symfix和.reload先跑一遍确保系统调试符号能加载再重新执行 !devnode。符号齐不齐决定你看到的是一张解释良好的资源表还是一堆十六进制天书。2. 三份资源列表三个不同的时态2.1 CmResourceList设备此刻真正拿到手的资源CmResourceList 全称 Configuration Manager Resource List由配置管理器维护表示系统当前已经翻译并分配给这个设备节点的资源。所谓“翻译”是指经过总线驱动转换后的系统内存空间视角比如 PCI 设备的 BAR 地址会被映射成系统内存地址中断会被转换成 IRQ 形式。你在设备管理器的“资源”选项卡里看到的就是这份列表的友好展示版。看 CmResourceList 主要看几个字段Type资源类型常见的是 PortIO 端口、Interrupt中断、Memory内存范围、DmaDMA 通道。Range具体值域。Memory 一般给起始地址和长度Port 是 IO 端口范围Interrupt 给 Level/Vector或者消息中断信息。Flags是不是 Shareable、LevelSensitive 之类的属性。我在调试时有一个习惯先数 CmResourceList 里有多少条资源。如果设备驱动一个正常的 PCIe 网卡一般会看到一条 MemoryMMIO BAR、一条 InterruptMSI-X 对应的消息中断或传统中断如果硬件还有 IO BAR 就再多一条 Port。如果该有的条目没出现说明 PnP 分配阶段就没把资源给全后面的设备启动失败大概率是资源不足造成的连锁反应而不是驱动逻辑问题。2.2 BootResourcesList开机那一下的分配快照BootResourcesList 是系统启动早期PnP 管理器还没开始重新仲裁资源之前直接继承固件BIOS/UEFI分配的资源列表。大家可以把它看成一张“启动快照”。在传统 BIOS 系统里固件会在 POST 阶段为很多设备分配好资源系统启动后 PnP 管理器可以选择保留这些分配也可以在检测到冲突、资源不足、或某个设备请求了不同资源时重新做资源仲裁和分配。BootResourcesList 记录的就是这个初始状态。设备节点如果保留了启动资源你会发现 CmResourceList 和 BootResourcesList 里的内容基本一样这代表系统信任固件的分配结果没有做任何调整。反过来如果两者不一致说明 PnP 管理器对资源动了手脚。排查“资源重平衡失败”“插拔设备后资源变化导致驱动异常”这类问题重点就要盯 BootResourcesList 和 CmResourceList 的差异。我遇到过一台设备BIOS 分配的中断是 15PnP 为了给另一个设备腾位置把它的中断重新分配成 17结果驱动里死死写死了 IRQ 15设备直接失败。这种情况从代码层面看驱动逻辑没毛病但 BootResourcesList 一对比问题马上就暴露了。2.3 IoResList设备对 PnP 管理器提出的资源需求书IoResList 是 IO Resource List有些资料也叫 Resource Requirements List。它描述的不是设备当前拿到了什么而是设备驱动或 ACPI 固件向 PnP 管理器申报的“我希望用什么资源、需要多大范围、可不可以共享”的需求清单。这条列表往往包含多个 option。比如一块 PCI 设备可能说“我的中断有多个候选第一个是 LevelSensitive 共享中断第二个是消息中断”也可能说“我的内存资源最小 4K、最大 128K”。PnP 管理器会结合系统仲裁器里的空闲资源池尽量从中挑一个满足条件、且不和其他设备冲突的方案最后落实成 CmResourceList。所以 IoResList 的价值在于逆向判断当设备拿到的资源和需求明显不匹配时问题可能出在驱动申报需求太死板。比如驱动声明某个资源必须 DriverExclusive驱动独占而系统里这个资源已经被另一个设备占用了仲裁器怎么都调不出来设备就起不来。这时候别急着怪 Windows先在 IoResList 里看 ShareDisposition 和资源范围往往能发现是驱动自身把路堵死了。为了方便记忆我用生活里的类比BootResourcesList 是婆婆固件帮你拟好的旧工作计划CmResourceList 是老板PnP 管理器最终批下来的排班表IoResList 则是你自己提交的排班需求表。老板会参考你的需求表再结合部门现状其他设备的资源占用做调整。如果最后排班和你想的差太远要么是你需求提得不合理要么是部门里资源确实紧张。3. 实战一次网卡启动失败和一次中断冲突排查3.1 准备条件与输出保存在进入案例之前先说清楚我每次定位问题的固定准备动作。第一确认目标机处于内核调试会话。可以本地内核调试也可以通过网络做 target 侧调试。我用 WinDbg 连接目标机后第一步总是让符号就位.symfix .reload第二打开日志记录。因为整棵设备树的 dump 输出非常长WinDbg 窗口不一定能滚动到合适位置。我习惯.logopen C:\debug\devnode_dump.txt后续所有输出都会写进文件在编辑器里搜索要比在调试器里翻页高效太多。第三明确本次要查的设备实例路径。以设备管理器为准把“硬件 ID”或“实例路径”抄下来。这个路径在设备树输出里就是 InstancePath 字段拿它做搜索关键词最准确。比如PCI\VEN_8086DEV_153ASUBSYS_00008086REV_03\41a2b3c4d000E0下面两个案例都是围绕这套流程展开的。3.2 案例一网卡报错代码 10资源到底有没有分下来现象目标机器上的千兆网卡在设备管理器里显示黄色感叹号属性里错误代码 10也就是设备无法启动。启动调试器符号就位之后先看整棵树的根kd !devnode 0 0 Dumping IopRootDeviceNode ( 0x84a9b000) DevNode 0x84a9b000 for PDO 0x84a9b000 InstancePath HTREE\ROOT\0 State DeviceNodeStarted (0x308) Previous State DeviceNodeEnumerateCompletion (0x30d) ...日志里搜索网卡的实例路径找到对应 DevNodeDevNode 0x8543f008 for PDO 0x8543f008 InstancePath PCI\VEN_8086DEV_153ASUBSYS_00008086REV_03\41a2b3c4d000E0 State DeviceNodeFailed (0x310) Previous State DeviceNodeStarted (0x308)State 已经明确是 DeviceNodeFailed说明这个节点没有成功启动。接着用 flag 4 看全量信息kd !devnode 4 0x8543f008输出里三份资源列表被展开。先看 CmResourceListCmResourceList at 0x847E0AC8 (7 entries) PaddedDecode: Entry 0: 4-byte Memory Flags: ReadOnly, Shareable Range: 0xF7D00000 - 0xF7DFFFFF Entry 1: 2-byte Interrupt Flags: LevelSensitive, Shareable Level: 0x10, Vector: 0x10, Affinity: 0xFFFFFFFF看起来资源是分配了的一条 Memory、一条 Interrupt。但设备还是失败那就继续往下看事件状态同时去确认这些资源是否与其他设备冲突。我在完整输出里找到另一块设备CmResourceList at 0x847E21F0 (4 entries) Entry 1: 2-byte Interrupt Flags: LevelSensitive, Shareable Level: 0x10, Vector: 0x10, Affinity: 0xFFFFFFFF一样的 Level 0x10。共享中断本身不是问题但注意两块设备如果一个是 Shareable另一个显示 DeviceExclusive那 Windows 仲裁理论上是不会让它们同时存在的。如果共享声明没问题下一步查的是设备驱动加载了什么、有没有 bind 失败。资源列表只是把你带到一个明确的分支路口它告诉你“PnP 层面没问题 / 有问题”不会直接告诉你驱动内部的过错。这里我还做了个额外动作用!devnode 1分别看网卡和它父节点 PCI 桥的资源确认父节点 Memory 窗口有没有覆盖子设备 BAR。PCI 体系里上游桥的窗口和下游设备的 BAR 必须匹配。输出里网卡 Memory 范围是 0xF7D00000-0xF7DFFFFF而桥节点如果窗口只到 0xF0000000那这个范围根本不可达设备当然起不来。这类问题只看设备节点列表是看不出来的一定要顺着树往上看几级。3.3 案例二中断共享与 DMA 通道的确认第二个场景是排查两个串口设备之间的“资源打架”嫌疑。16550 兼容的串口设备通常需要 IO 端口和中断两个串口如果硬件设计上允许共享中断但驱动不声明 Shareable系统就会强制给它们分不同的 IRQ极端情况下可能不够用。这时候用 flag 1 分别 dump 两个串口节点kd !devnode 1 0x8543f008先看 IoResList 里第一个串口申报的中断 ShareDispositionIoResList at 0x847E5000 (2 entries) Option 0: Interrupt: Type: Interrupt, ShareDisposition: Shareable Level: 0x3, Vector: 0x3, Affinity: 0xFFFFFFFF再看第二个串口节点。如果第二个节点申报的是 DeviceExclusive而系统里只有一条空闲中断PnP 分配器无论怎么排第二个设备都是停摆。处理办法不是硬调中断而是回到驱动代码把中断的 ShareDisposition 改成 Shareable前提是硬件方案允许共享。这算是 IoResList 在判断“资源需求声明是否合理”上的典型应用。顺带提一下 DMA 通道。传统 ISA 设备在 IoResList 里还会申报 DMA 通道。看 CmResourceList 的 DMA 条目时留意 Channel 是几号通道、是否 Shareable。很多老硬件调试问题就出在 DMA 通道没有正确释放导致后续设备分不到通道。4. 高频问题与避坑心得4.1 CmResourceList 为空设备绝对起不来CmResourceList 空代表设备在当前状态下没有任何可用资源。这里可以记一条经验如果 CmResourceList 完全为空状态基本不会是 DeviceNodeStarted大概率停在 DeviceNodeResourcesAssigned 或 DeviceNodeFailed。至于为什么空常见有几种情况驱动没在 AddDevice 里正确申报资源需求导致 IoResList 空PnP 无从分配。设备枚举阶段就出问题总线驱动没有获得资源。系统地址空间不足仲裁器无法分配。怎么区分先看 IoResList如果 IoResList 也是空说明设备没有提出需求相当于你都不告诉我你要什么我怎么给你如果 IoResList 有内容而 CmResourceList 空说明需求提了但没满足这时候去查仲裁器的可用资源池。4.2 BootResourcesList 与 CmResourceList 不一致时注意啥比较两表以后发现内存范围被改了、中断号被换了第一反应不是“系统坏了”而是 PnP 在启动后执行了资源重平衡或者当前分配与固件分配发生了冲突。这种情况下驱动如果自行记忆了旧资源位置就很容易埋雷。在 Windows 10/11 时代资源重平衡比以前更常见特别是热插拔 USB4/Thunderbolt 设备会触发 PCI 资源重排。排查这类问题我建议把状态机也一起打出来多看几级父节点的资源窗口。资源的移动往往是链式的一个设备动了父桥的窗口变了底下所有设备都要跟着变。4.3 调试输出里容易被忽略的字段实际 dump 里的字段远比我上面写的多。下面几个我认为最容易被忽略、但价值极高InterfaceType资源的接口类型。比如 Internal、Isa、Eisa、Bus 等代表这条资源来源的总线类型不是所有设备都是 PCI。Bus Number总线号。多总线系统里这是必看的。两个设备在同一 Device Number 但不同 Bus Number资源可以各自独立别把本来不冲突的两份资源看成冲突了。Affinity中断亲和性。对单核旧系统无所谓多核系统里中断亲和性影响性能如果两个设备的中断亲和性处理器集合完全一样又不均摊负载就可能出现某个核中断负载特别高的问题。Flags 里的 BootOnly、System 之类属性表示这条资源是否只在启动时使用。看到 BootOnly 的资源就不必指望运行时它能被驱动正常使用。这些字段和具体硬件强相关保持警觉度如果你发现自己对某几个字段的含义拿不准尽早去看内核结构体定义。调试器里用 dt 命令可以直接列出结构kd dt nt!CM_PARTIAL_RESOURCE_DESCRIPTOR kd dt nt!IO_RESOURCE_DESCRIPTOR4.4 输出太长怎么办日志和搜索最后分享一个提升效率的习惯。设备树一旦大起来比如服务器主板带了几十张卡!devnode 0 0一次输出轻松数千行。此时硬靠眼睛找 InstancePath 属于自我折磨。我的流程是.logopen记录到文件。执行!devnode 0 0把整棵树导出。在文本编辑器里搜InstancePath和设备的硬件 ID。拿到 DevNode 地址后再回到调试器单独打这个节点的 flag 4 或 1。这样一来全量输出只产生一次后续所有针对性 dump 都很快。配合编辑器的多标签甚至可以把几个疑似冲突的节点 dump 摆在一起做资源对比效率完全不一样。做驱动和系统调试这些年我个人的体会是设备树资源列表不是“查一下看个结果”的静态信息而是记录了一套完整的动态协商过程。BootResourcesList 是起点IoResList 是诉求CmResourceList 是结果三份列表对上了设备正常启动对不上那就是问题所在。调试器只负责把内存里的结构摊开给你看真正判断意义还是要回到硬件和驱动的逻辑上。比如你是做 PCIe 网卡驱动的看到 Memory 资源范围就必须知道它对应 BAR 空间是做传统 ISA 串口驱动的看到 Port 资源就必须能换算成 IO 地址。把资源和你的驱动代码一一对上!devnode 才能从“输出工具”变成“定位利器”。
返回列表