
1. 先搞清楚这6个PDO是从哪个环节冒出来的调试Windows内核驱动ACPI.sys这类系统级总线驱动绕不开IRP_MN_QUERY_BUS_RELATIONS。我之前排查一个设备无故丢失的现象把断点打在ACPI!ACPIDetectPdoDevices上继续执行后返回一看这一轮枚举一共建立了6个设备PDO。这个数字不是随机的它直接对应DSDT里_SB_作用域下一批“在场且可用”的子节点。很多人第一次追ACPI的枚举流程会犯一个错以为ACPI驱动会把整台机器所有的设备都建出来。实际上ACPI根总线只负责_SB_下的直接子设备PCI总线下面的显卡、网卡、USB控制器都归PCI驱动管。ACPI驱动创建的PDO是设备树的“骨架层”——PCI0、LPCB、处理器对象、内嵌控制器这类节点真正挂满外设的是它们被启动之后各自的总线驱动继续枚举出来的子孙节点。所以“6个设备PDO”这句话严格说应该是“ACPI根总线在一次IRP_MN_QUERY_BUS_RELATIONS处理中向PnP管理器上报了6个直接子设备的PDO”。把这层关系理清楚再去看ACPIRootIrpQueryBusRelations和ACPIDetectPdoDevices这两个函数代码逻辑就顺了。这篇不是通篇贴反汇编而是把这两个函数在整个枚举链路里的职责、判定规则和可验证手段梳理一遍适合做内核驱动开发、系统底层调试或者固件兼容性排查的读者参考。1.1 ACPI根设备在设备树中的位置ACPI驱动不是给某个PCI设备服务的它是整个系统“根总线”的驱动。PnP管理器在启动早期加载ACPI.sys把它挂在一个名为“ACPI Fixed Feature Hardware”的根设备下面随后由这个根节点往下枚举。设备树大致是这样根设备ROOTACPI根设备ACPI.sys的FDO挂在这里PDO1PCI0PCI根总线PDO2LPCBLPC总线PDO3PR00处理器0PDO4PR01处理器1PDO5EC0内嵌控制器PDO6BAT0电池当PnP管理器想让ACPI根设备报告它的孩子时就发送IRP_MN_QUERY_BUS_RELATIONS。这个IRP的Minor Function Code是IRP_MN_QUERY_BUS_RELATIONS属于IRP_MJ_PNP大类。ACPI.sys里的分发函数收到之后最终把活儿交给ACPIRootIrpQueryBusRelations。1.2 为什么把观察点放在ACPIDetectPdoDevicesACPIRootIrpQueryBusRelations这个名字从符号表看是IRP的入口处理函数。它会做IRP参数校验、锁管理、调用实际的设备检测例程然后填充返回结构。真正决定“哪几个设备会出现”的核心逻辑在ACPIDetectPdoDevices里。打个比方前者是前台接待后者是仓库管理。前台把订单收进来仓库管理决定发哪几件货。调试时如果只想关心“最终上报了哪些PDO”断点打在ACPIRootIrpQueryBusRelations入口只能看到IRP进来看不到设备是怎么被选中的打在ACPIDetectPdoDevices上才能观察到每一轮枚举对namespace的扫描结果和PDO的创建动作。2. ACPIRootIrpQueryBusRelations的入口处理逻辑ACPIRootIrpQueryBusRelations的逻辑用符号文件加反汇编看下来大致分几步取当前IRP栈位置、确认MinorFunction、检查设备扩展的枚举状态、进入临界区然后把控制权转给设备检测例程。2.1 IRP栈位置与参数确认内核调试器里在ACPI!ACPIRootIrpQueryBusRelations下断命中的瞬间可以先看一下IRPkd bp ACPI!ACPIRootIrpQueryBusRelations kd g Breakpoint 0 hit ACPI!ACPIRootIrpQueryBusRelations: fffff8074a2a1b40 48894c2408 mov [rsp8], rcx kd k # Child-SP RetAddr Call Site 00 ffffae8012345678 fffff8074a2a0000 ACPI!ACPIRootIrpQueryBusRelations 01 ffffae8012345680 fffff8074a2a0100 ACPI!ACPIPnpCompletionRoutine用dt nt!_IRP看IRP重点看CurrentLocation和StackLocation确认是IRP_MN_QUERY_BUS_RELATIONS而不是IRP_MN_QUERY_REMOVAL_RELATIONS之类的兄弟IRP。参数确认完之后函数会检查设备扩展里的一个状态标记判断当前ACPI根设备是否已经完成了namespace扫描。如果还没扫描会先做一次静态扫描如果已经扫描过就直接走缓存路径。这里有个容易忽略的点ACPI.sys在驱动加载阶段也就是DriverEntry和AddDevice过程里已经解析过DSDT把namespace里的设备节点映射成了内部数据结构。ACPIDetectPdoDevices不是每次IRP来都重新解析DSDT而是在首次查询时建立PDO并缓存后续查询直接复用。2.2 委托给ACPIDetectPdoDevices的条件ACPIDetectPdoDevices不是无条件被调用的。反汇编里能看到调用前有一个分支判断大致逻辑是如果是首次枚举或PnP管理器标记了需要重新枚举Reenumerate才进入完整检测流程。如果只是常规查询且内部缓存已建立则直接遍历已有PDO列表填充返回结构。这也是为什么调试的时候重复发QUERY_BUS_RELATIONS不会每次都看到6个PDO被“重新建立”。第一次进来的那6个PDO是新建的后面再进来ACPIDetectPdoDevices可能压根儿不会被调用。2.3 IRP完成前的收尾工作检测完设备、确保每个设备都有PDO之后ACPIRootIrpQueryBusRelations要构造PNP_BUS_RELATIONS返回结构typedef struct _PNP_BUS_RELATIONS { ULONG Count; PDEVICE_OBJECT Child[1]; } PNP_BUS_RELATIONS;Count填6Child数组依次填6个PDO的指针。这个缓冲区由ACPI驱动分配PnP管理器使用完毕后会释放。最后设置IoStatus.Status STATUS_SUCCESS、IoStatus.Information 缓冲区大小调用IofCompleteRequest完成IRP。这里有一个实操技巧调试时如果想抓这6个PDO的具体地址可以对ACPI!ACPIRootIrpQueryBusRelations的返回指令下断比如bp ACPI!ACPIRootIrpQueryBusRelations0x1a0具体偏移按实际反汇编为准然后在返回前手动打印rcx或栈上保存的局部变量而不是在IRP完成之后再回头找。3. ACPIDetectPdoDevices如何逐个筛选并建立PDOACPIDetectPdoDevices是整条链路的“决策中心”。它做的事情可以拆成三块遍历namespace、检查设备状态、创建PDO。3.1 从_SB_开始的自顶向下遍历ACPI的DSDT里所有普通设备都挂在_SB_这个System Bus作用域下面。ACPIDetectPdoDevices从_SB_出发沿着命名空间树往下走。走到每个设备节点时解析它的类型。不同节点处理方式不一样处理器对象_PR下的PR00、PR01走处理器枚举路径。普通设备有_HID或_ADR走设备枚举路径。作用域_SB_.PCI0下的子设备不是直接遍历对象只作为容器继续深入。关键点是ACPIDetectPdoDevices只处理“直接挂在_SB_下”的设备。_SB_.PCI0本身是一个设备但它下面的_SB_.PCI0.NVMe不会在这一层被创建成PDO。PCI0的PDO建立之后PCI总线驱动自己会枚举_SB_.PCI0.NVMe。有个例外值得留意某些固件会把部分设备放在_SB_更深的层级比如_SB_.PCI0.LPCB.EC0。这种情况下ACPI根驱动仍然只枚举_SB_的直接子节点PCI0和LPCBEC0的PDO由LPCB的驱动通常是msisadrv或pci.sys的ACPI分支在枚举过程中场合式创建。所以“6个PDO”描述的都是直接子层级的数量。3.2 _STA状态位是真正的过滤闸门namespace里每个设备节点后面可能跟一个_STA方法ACPI规范要求这个方法返回一个32位整数Bit0到Bit4是常用状态位位含义说明Bit0Present设备物理上存在Bit1Enabled设备被使能、开始解码Bit2Show in UI是否在用户界面显示Bit3Functioning设备功能正常Bit4Battery Present仅电池设备有意义ACPIDetectPdoDevices对每个节点调用_STA返回值参与一个简单的位与判断。通常_STA 0x0F 0x0F才认为设备“在场且可用”才会创建PDO。某些设备没有_STA方法ACPI规范规定此时默认当作存在且使能也就是默认按0x0F处理。调试中常见的现象是DSDT里明明有某个设备但ACPIDetectPdoDevices没给它建PDO。多数情况下就是_STA返回的Present位或Enabled位是0。这时候直接去读_STA的返回值比改驱动判断条件更有效。3.3 创建设备对象时写入扩展结构的信息筛选通过之后ACPI驱动调用IoCreateDevice创建PDO。IoCreateDevice的原型是NTSTATUS IoCreateDevice( IN PDRIVER_OBJECT DriverObject, IN ULONG DeviceExtensionSize, IN PUNICODE_STRING DeviceName, IN DEVICE_TYPE DeviceType, IN ULONG DeviceCharacteristics, IN BOOLEAN Exclusive, OUT PDEVICE_OBJECT *DeviceObject );ACPI驱动传的DeviceExtensionSize通常是它自定义的扩展结构大小。这个扩展结构里会存几类关键信息ACPI namespace节点指针也就是这个PDO对应的DSDT节点在哪里。设备状态标志记录_STA的原始返回值。设备的_HID、_UID、_CID等标识符的解析结果。电源管理相关字段比如_PR0、_PR2等功率资源引用。设备扩展是把PDO和DSDT节点“绑定”起来的关键。内核里调试时拿到一个PDO地址用dt去看它的设备扩展就能反推出它对应namespace里哪个设备、_HID是什么、_STA当时的判定结果是多少。这一步对排查“设备为什么没枚举出来”特别有用。4. 这6个PDO到底是谁一份典型清单与建立顺序先声明不同主板、不同BIOSDSDT里_SB_的直接子设备不会一模一样。我这里给出一个非常典型的笔记本平台案例6个PDO分别对应序号Namespace节点常见_HID角色1_SB_.PCI0PNP0A03 / PNP0A08PCI根总线2_SB_.LPCBPNP0C01 / PNP0C02LPC/ISA桥3_SB_.PR00ACPI0007处理器04_SB_.PR01ACPI0007处理器15_SB_.EC0PNP0C09内嵌控制器6_SB_.BAT0PNP0C0A电池这只是其中一种组合。有的平台会出现AC0电源适配器挤进去、有的会有TZ00热区、有的四核处理器会给到PR00~PR03有的则用C000、C001这种命名。6这个数字本身不代表任何固定规则它就是“这一台机器上DSDT直接子节点里通过_STA筛选后剩下的数量”。4.1 _SB_下还有什么可能出现的设备为了帮助你排查时对号入座我把常见设备节点列一下节点典型_HID说明_SB_.PCI0PNP0A03PCI根总线几乎所有x86平台都有_SB_.LPCBPNP0C01LPC总线桥接传统设备_SB_.PR0xACPI0007处理器对象_SB_.EC0PNP0C09内嵌控制器通常含电池数据通道_SB_.BAT0PNP0C0A电池设备_SB_.AC0ACPI0003AC电源适配器_SB_.TZ00无标准_HID热区有时直接叫ThermalZone_SB_.LID0PNP0C0D笔记本盖子开关_SB_.BT0W不固定蓝牙等无线设备依赖固件_SB_.SL0PNP0C0E睡眠按钮_SB_.DPRSACPI000C显示器节电相关所以如果你调试的机器看到的是7个、8个PDO不用慌先确认DSDT原文件里_SB_下到底声明了几个直接子设备再对照_STA返回值逐一排除。4.2 建立顺序和DSDT声明顺序的关系ACPIDetectPdoDevices创建PDO的顺序基本遵循namespace遍历的顺序。ACPI的namespace是一棵树遍历通常是深度优先但实际顺序仍然由ASL源码里各设备的声明先后决定。观察到一个有用的规律处理器对象通常会被集中处理。很多平台的_PR作用域紧跟在_SB_后面于是PDO建立顺序经常是PCI0、LPCB、PR00、PR01、EC0、BAT0。如果你用!devnode看设备树子节点顺序和你断点单步看到的创建顺序应该一致这个可以用来交叉验证。另一个观察有些固件把EC0、BAT0、AC0放在_SB_.PCI0的下面比如_SB_.PCI0.LPCB.EC0、_SB_.PCI0.BAT0这种情况下A B C PIdetectPdoDevices不会直接创建它们这6个PDO就变成了PCI0、LPCB、PR00、PR01、TZ00、LID0之类。还是那句话PDO数量完全取决于DSDT对设备的挂载方式。4.3 为什么不是“所有设备”ACPI根驱动是“总线驱动”它只对自己的总线负责。对这个总线来说孩子就是_SB_的直接子节点。PCI0虽然是总线但它本身首先是ACPI总线的一个“终端设备”它的子设备属于PCI总线的枚举范围。这种层级关系是PnP管理器的基本模型一个设备只能属于一个父总线枚举责任在父总线对应的驱动身上。所以当你排查“某个设备怎么不在ACPI的6个PDO里”时先想清楚这个设备的父节点是谁而不是直接怀疑ACPI驱动漏了。大多数情况下漏枚举发生在PCI总线或某个桥驱动身上不在ACPI这一层。5. 用内核调试器完整复现一遍6个PDO的建立过程光看逻辑不够实际操作一遍才踏实。下面这组调试器操作是我常用的流程适用于WinDbg连接目标机内核调试的场景。5.1 下断点与准备观察命令首先符号要装对。确认ACPI.sys的符号能解析kd lm m acpi Browse full module list start end module name fffff8004a2a0000 fffff8004a2c0000 ACPI (pdb symbols) ACPI.pdb然后下断点kd bp ACPI!ACPIRootIrpQueryBusRelations kd bp ACPI!ACPIDetectPdoDevices如果机器已经启动了ACPI枚举可能已经发生过了断点不一定能再命中。这时有两个办法用bc *清掉断点在目标机上触发一次设备重新枚举比如在设备管理器里禁用再启用ACPI根设备或者执行pnputil /scan-devices让PnP管理器重新发一轮QUERY_BUS_RELATIONS。直接在断点列表里留ACPIDetectPdoDevices重启目标机在系统启动早期捕获。pnputil /scan-devices不用重启系统是最快的验证手段。5.2 断点命中后看什么ACPIDetectPdoDevices命中后先用k看调用栈确认它确实是ACPIRootIrpQueryBusRelations调过来的kd k # Child-SP RetAddr Call Site 00 ffffae801234f000 fffff8004a2a0000 ACPI!ACPIDetectPdoDevices 01 ffffae801234f100 fffff8004a2a0100 ACPI!ACPIRootIrpQueryBusRelations 02 ffffae801234f200 fffff8004a2a0200 ACPI!ACPIPnpDispatch然后单步跟踪观察每次IoCreateDevice调用前后的参数。如果嫌单步太慢也可以给IoCreateDevice下断点加条件当ACPI模块的驱动对象和当前的DriverObject相等时才中断。IoCreateDevice返回后DeviceObject参数会填上PDO地址。记下这6个地址然后在IRP完成之后用!devnode看设备树kd !devnode 0 1 Dumping IopRootDeviceNode ( DeviceTreeRoot) DeviceNode 0xffffae801234... InstancePath ACPI\ACPI_Fixed_Feature_Hardware\0 DeviceObjectList 0xffffae80... DeviceObject 0xffffae801111... ... ChildList 0xffffae801222... DeviceNode 0xffffae801333... InstancePath ACPI\PNP0A03\0输出里的ChildList就是ACPI根设备的直接子设备列表应该正好对应那6个PDO。对照之前记录的PDO地址把DeviceObject字段一一对上就能确认“这6个PDO”和“设备树里这6个节点”是同一个东西。5.3 三个容易误判的坑第一个坑断点打早了。DriverEntry阶段ACPI.sys已经解析了DSDT但PDO还没创建。如果你在驱动加载早期下断点会看到namespace节点已经建立、但ACPIDetectPdoDevices还没被调用的状态。这时候观察到的不是“建立PDO失败”而是“还没到时机”。第二个坑把缓存当成了新枚举。第一次扫描完成后PDO已经存在于ACPI驱动的内部链表里。后续再来QUERY_BUS_RELATIONSACPIDetectPdoDevices可能根本不执行。如果你用pnputil /scan-devices触发重新枚举看到PDO地址和第一次完全一样不要怀疑断点没生效ACPI驱动只是在复用已有PDO。第三个坑_STA的副作用。有些固件在_STA里不只是返回状态还会执行_PS0、_ON等操作方法去实际点亮设备。调试时如果断点停在_STA相关的ACPI方法执行路径上可能会改变设备的物理状态。遇到这种固件先记录状态再继续执行别反复单步避免对硬件造成意外操作。6. 这次追踪带来的几点实用体会把ACPIRootIrpQueryBusRelations到ACPIDetectPdoDevices这条链走通之后再看ACPI相关的设备问题思路会清晰很多。第一区分“枚举层级”是排查设备问题的第一件事。设备没出现先问它的父总线是什么别一股脑追ACPI。ACPI只管_SB_直接子节点这层PDO建好了剩下的枚举责任在PCI、LPC这些桥驱动身上。很多“设备丢失”的根源其实在更上层的某个桥驱动而不是ACPI。第二_STA是筛选设备的关键闸门但它不是唯一决定因素。ACPI驱动还会结合设备类型、平台配置做额外检查。调试时最有效的做法是先在调试器里找到对应的namespace节点手动执行ACPI方法拿_STA返回值再对照驱动是否建了PDO。两步一对比问题出在固件还是驱动立刻见分晓。第三6个PDO这个数字让很多人误以为ACPI枚举很“简单”恰恰相反ACPI根驱动的枚举只是整个设备树最上面的一层。真正复杂的枚举逻辑全部在层次更深的PCI、USB、I2C这些总线上。把ACPI这一层吃透了后面看任何“父总线枚举子设备”的驱动代码都能套用同一套思路判断入口函数、找筛选条件、验证PDO创建、对照设备树输出。如果你也在追ACPI相关的内核问题建议先用!devnode把设备树完整导一份标出每一层的父驱动是谁。这份笔记比任何函数分析都直观等设备出问题时拿着它按图索骥效率会高很多。