4. 设备对象与设备栈:深入理解WDM驱动核心

设备对象,说白了就是内核里代表一个设备的"身份证"。每个设备在系统中都有一个DEVICE_OBJECT结构体。我刚开始接触WDM驱动时,总觉得这东西就是个抽象概念,直到有一次调试蓝屏,才发现设备对象里藏着大量关键信息。

4.1 DEVICE_OBJECT结构体剖析

先看看这个结构体的核心字段:

typedef struct _DEVICE_OBJECT {
    CSHORT Type;                    // 对象类型,必须是IO_TYPE_DEVICE
    USHORT Size;                    // 设备对象大小
    LONG ReferenceCount;            // 引用计数
    struct _DRIVER_OBJECT *DriverObject;  // 所属驱动对象
    struct _DEVICE_OBJECT *NextDevice;    // 同一驱动下的下一个设备
    struct _DEVICE_OBJECT *AttachedDevice; // 附加的设备(设备栈下一层)
    struct _IRP *CurrentIrp;        // 当前正在处理的IRP
    PDEVICE_QUEUE DeviceQueue;      // 设备队列
    ULONG Flags;                    // 设备标志
    ULONG Characteristics;          // 设备特性
    PVOID DeviceExtension;          // 设备扩展(重点!)
    DEVICE_TYPE DeviceType;         // 设备类型
    CCHAR StackSize;                // 设备栈深度
    // ... 还有不少字段
} DEVICE_OBJECT;

嗯,这里要注意StackSize字段。我在项目中遇到过一个问题:驱动加载后IRP下发总失败,查了半天发现是StackSize设小了。设备栈每层都需要一个IO_STACK_LOCATION,StackSize必须大于等于设备栈深度。

4.2 设备扩展(DeviceExtension)——你的私有数据区

设备扩展是每个设备对象自带的"小仓库"。我个人习惯把设备状态、硬件资源、同步对象都放这里。

核心要点:设备扩展的大小在创建设备时指定,之后不能动态改变。所以一开始就要想好要存什么。

创建设备对象的典型代码:

NTSTATUS CreateDevice(PDRIVER_OBJECT DriverObject) {
    PDEVICE_OBJECT deviceObject = NULL;
    UNICODE_STRING deviceName;
    
    // 设备扩展大小设为sizeof(MY_DEVICE_EXTENSION)
    RtlInitUnicodeString(&deviceName, L"\\Device\\MyDevice");
    
    NTSTATUS status = IoCreateDevice(
        DriverObject,
        sizeof(MY_DEVICE_EXTENSION),  // 设备扩展大小
        &deviceName,
        FILE_DEVICE_UNKNOWN,
        0,
        FALSE,
        &deviceObject
    );
    
    if (!NT_SUCCESS(status)) {
        return status;
    }
    
    // 获取设备扩展指针
    PMY_DEVICE_EXTENSION devExt = (PMY_DEVICE_EXTENSION)deviceObject->DeviceExtension;
    
    // 初始化设备扩展
    devExt->DeviceState = DEVICE_STOPPED;
    KeInitializeSpinLock(&devExt->SpinLock);
    devExt->HardwareBase = 0x300;  // 假设的IO端口基址
    
    return STATUS_SUCCESS;
}

我的经验:设备扩展里一定要放一个KSPIN_LOCK或FAST_MUTEX。为什么?因为IRP可能从多个线程同时进来,没有同步对象你会死得很惨。我曾经就因为这个,调试了整整两天才发现是竞态问题。

4.3 设备栈与IO请求传递

设备栈是WDM驱动最巧妙的设计之一。你想想看,一个USB设备从硬件到应用层,中间要经过USB总线驱动、Hub驱动、功能驱动、类驱动……这么多驱动怎么协同工作?答案就是设备栈。

每个设备对象都挂在某个设备栈上。IRP从栈顶往下传,每层驱动处理完后交给下一层。这个传递机制的核心就是IoCallDriver和IoCompleteRequest。

避坑指南:我曾经犯过一个低级错误——在Dispatch函数里直接完成了IRP,忘了往下传。结果上层驱动永远收不到完成通知,整个设备栈卡死了。记住:除非你确定这个IRP就该你处理,否则一定要调用IoSkipCurrentIrpStackLocation + IoCallDriver往下传。

IRP传递的标准模式:

NTSTATUS DispatchRead(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    // 获取当前堆栈位置
    PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);
    
    // 处理自己的逻辑
    // ...
    
    // 设置下一层的堆栈位置
    IoSkipCurrentIrpStackLocation(Irp);
    
    // 传给下一层设备
    return IoCallDriver(DeviceObject->AttachedDevice, Irp);
}

4.4 AttachDevice与IoGetDeviceObjectPointer

这两个函数是设备栈操作的核心工具。

函数 用途 典型场景
IoAttachDeviceToDeviceStack 将当前设备附加到目标设备栈 过滤驱动挂载到功能驱动上
IoGetDeviceObjectPointer 通过设备名称获取设备对象指针 跨驱动通信,获取目标设备引用

AttachDevice的使用:

NTSTATUS AttachToDeviceStack(PDRIVER_OBJECT DriverObject, PUNICODE_STRING targetDeviceName) {
    PDEVICE_OBJECT filterDevice = NULL;
    PDEVICE_OBJECT targetDevice = NULL;
    
    // 先创建自己的过滤设备
    IoCreateDevice(DriverObject, sizeof(FILTER_EXTENSION), 
                   NULL, FILE_DEVICE_UNKNOWN, 0, FALSE, &filterDevice);
    
    // 附加到目标设备栈
    targetDevice = IoAttachDeviceToDeviceStack(filterDevice, targetDeviceName);
    if (targetDevice == NULL) {
        IoDeleteDevice(filterDevice);
        return STATUS_UNSUCCESSFUL;
    }
    
    // 保存目标设备指针到设备扩展
    PFILTER_EXTENSION filterExt = (PFILTER_EXTENSION)filterDevice->DeviceExtension;
    filterExt->TargetDeviceObject = targetDevice;
    
    return STATUS_SUCCESS;
}

IoGetDeviceObjectPointer的使用:

NTSTATUS GetTargetDevice(PCWSTR deviceName) {
    UNICODE_STRING name;
    RtlInitUnicodeString(&name, deviceName);
    
    PFILE_OBJECT fileObject = NULL;
    PDEVICE_OBJECT deviceObject = NULL;
    
    // 获取设备对象指针和文件对象
    NTSTATUS status = IoGetDeviceObjectPointer(
        &name,
        FILE_ALL_ACCESS,  // 需要的访问权限
        &fileObject,      // 返回的文件对象
        &deviceObject     // 返回的设备对象
    );
    
    if (NT_SUCCESS(status)) {
        // 使用deviceObject...
        // 注意:用完要释放文件对象引用
        ObDereferenceObject(fileObject);
    }
    
    return status;
}

关键区别:

  • IoAttachDeviceToDeviceStack:把自己挂到别人的设备栈上,成为中间层
  • IoGetDeviceObjectPointer:拿到别人的设备对象指针,用于发送IRP或查询状态

4.5 设备栈的完整工作流程

我画了一张图,帮你理解设备栈的全貌:

设备栈与IRP传递流程 应用层(User Application) 文件系统驱动(可选) 过滤驱动(Filter Driver) 功能驱动(Function Driver) 总线驱动(Bus Driver) 硬件设备(Hardware) IRP传递方向 IRP从栈顶往下传 每层可处理或修改 完成时从下往上返回 AttachedDevice指向 下一层设备对象

这张图展示了典型的设备栈结构。IRP从应用层发起,经过文件系统驱动(如果有的话)、过滤驱动、功能驱动,最后到达总线驱动与硬件交互。每层驱动都可以选择处理IRP或者直接往下传。

4.6 实战中的注意事项

  • 设备扩展大小:我建议至少分配128字节以上,留点余量。后期加功能时不用重建设备。
  • StackSize设置:创建设备时默认是1,如果你要附加到别的设备栈上,记得把StackSize设大。我一般设成5,够用。
  • 设备对象引用计数:每次IoGetDeviceObjectPointer都会增加引用计数,用完一定要ObDereferenceObject,否则设备对象泄漏。
  • 同步问题:设备扩展里的数据访问一定要加锁。别问我怎么知道的——我调试过不下10次因为忘记加锁导致的蓝屏。

个人小技巧:调试设备栈时,可以用WinDbg的!devstack命令查看当前设备栈的完整结构。这个命令会列出从顶层到底层的所有设备对象,非常直观。我每次遇到IRP传递异常,第一件事就是跑这个命令。

设备对象和设备栈是WDM驱动的基石。理解了它们,你就掌握了驱动开发的一半精髓。剩下的就是各种IRP的处理细节了——那些我们后面再聊。


可可精品资料库