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从应用层发起,经过文件系统驱动(如果有的话)、过滤驱动、功能驱动,最后到达总线驱动与硬件交互。每层驱动都可以选择处理IRP或者直接往下传。
4.6 实战中的注意事项
- 设备扩展大小:我建议至少分配128字节以上,留点余量。后期加功能时不用重建设备。
- StackSize设置:创建设备时默认是1,如果你要附加到别的设备栈上,记得把StackSize设大。我一般设成5,够用。
- 设备对象引用计数:每次IoGetDeviceObjectPointer都会增加引用计数,用完一定要ObDereferenceObject,否则设备对象泄漏。
- 同步问题:设备扩展里的数据访问一定要加锁。别问我怎么知道的——我调试过不下10次因为忘记加锁导致的蓝屏。
个人小技巧:调试设备栈时,可以用WinDbg的!devstack命令查看当前设备栈的完整结构。这个命令会列出从顶层到底层的所有设备对象,非常直观。我每次遇到IRP传递异常,第一件事就是跑这个命令。
设备对象和设备栈是WDM驱动的基石。理解了它们,你就掌握了驱动开发的一半精髓。剩下的就是各种IRP的处理细节了——那些我们后面再聊。
可可精品资料库