4. WDM设备扩展:设备扩展结构体的定义、初始化与使用
在WDM驱动框架中,设备扩展(Device Extension)是驱动开发者自定义的一块内存区域,与每个设备对象(DEVICE_OBJECT)一一绑定。它相当于驱动为每个设备实例维护的“私有数据仓库”,用于存储设备状态、硬件资源、中断上下文、I/O缓冲区指针等关键信息。正确使用设备扩展是编写可重入、多实例安全WDM驱动的基础。
核心概念:设备扩展不是系统预定义的结构,而是由驱动开发者自行定义的结构体类型。驱动在创建设备对象时,通过
IoCreateDevice 的 DeviceExtensionSize 参数指定其大小,系统自动分配并清零。
4.1 设备扩展结构体的定义
设备扩展结构体通常定义在驱动的主头文件中,其内容完全由驱动的功能需求决定。以下是一个典型的中断驱动型WDM设备扩展结构体定义:
// 定义设备扩展结构体
typedef struct _DEVICE_EXTENSION {
// 设备对象指针(反向引用)
PDEVICE_OBJECT Self;
// 硬件资源
PHYSICAL_ADDRESS MemoryBase;
ULONG MemoryLength;
UCHAR InterruptVector;
KINTERRUPT_MODE InterruptMode;
// 中断对象与DPC
PKINTERRUPT InterruptObject;
KDPC Dpc;
KTIMER Timer;
// I/O缓冲区管理
PUCHAR ReadBuffer;
ULONG ReadBufferSize;
PUCHAR WriteBuffer;
ULONG WriteBufferSize;
// 设备状态标志
ULONG DeviceState; // 0=停止, 1=启动, 2=运行
BOOLEAN InterruptEnabled;
BOOLEAN DeviceRemoved;
// 同步锁
KSPIN_LOCK SpinLock;
// IOCTL参数缓存
ULONG IoctlCode;
PVOID IoctlInputBuffer;
ULONG IoctlInputLength;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
设计要点:
- Self指针: 存储设备对象地址,便于从扩展反向获取设备对象。
- 硬件资源字段: 在
IRP_MN_START_DEVICE处理中填充,供中断和I/O例程使用。 - 中断对象指针: 保存
IoConnectInterrupt返回的PKINTERRUPT,用于中断连接/断开。 - DPC与Timer: 用于中断下半部处理或超时操作。
- 同步锁: 保护多线程/多中断并发访问的共享数据。
4.2 设备扩展的初始化
设备扩展的初始化发生在驱动处理 IRP_MJ_CREATE 或 IRP_MN_START_DEVICE 时。初始化流程分为三个阶段:
| 阶段 | 触发IRP | 初始化内容 | 关键函数 |
|---|---|---|---|
| 阶段1:基础清零 | IRP_MJ_CREATE |
系统已自动清零,驱动填充Self指针、锁、默认状态 | KeInitializeSpinLock |
| 阶段2:资源分配 | IRP_MN_START_DEVICE |
解析CM_RESOURCE_LIST,填充MemoryBase、InterruptVector等 | IoGetDeviceProperty |
| 阶段3:中断连接 | IRP_MN_START_DEVICE(资源分配后) |
调用IoConnectInterrupt,保存InterruptObject | IoConnectInterrupt |
以下展示阶段1和阶段2的典型初始化代码:
// 阶段1:在AddDevice或IRP_MJ_CREATE中初始化
NTSTATUS InitializeDeviceExtension(PDEVICE_OBJECT DeviceObject)
{
PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
// 1. 反向引用
devExt->Self = DeviceObject;
// 2. 初始化同步锁
KeInitializeSpinLock(&devExt->SpinLock);
// 3. 初始化DPC对象(中断下半部)
KeInitializeDpc(&devExt->Dpc, DpcRoutine, devExt);
// 4. 设置默认状态
devExt->DeviceState = 0; // 停止状态
devExt->InterruptEnabled = FALSE;
devExt->DeviceRemoved = FALSE;
// 5. 清零硬件资源字段(系统已清零,但显式赋值更安全)
devExt->MemoryBase.QuadPart = 0;
devExt->InterruptVector = 0;
return STATUS_SUCCESS;
}
// 阶段2:在IRP_MN_START_DEVICE中填充硬件资源
NTSTATUS StartDeviceResources(PDEVICE_OBJECT DeviceObject, PCM_RESOURCE_LIST ResourceList)
{
PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
// 解析ResourceList,提取Memory和Interrupt资源
// ...(省略具体解析代码)
devExt->MemoryBase = memoryDescriptor->u.Port.Start;
devExt->MemoryLength = memoryDescriptor->u.Port.Length;
devExt->InterruptVector = interruptDescriptor->u.Interrupt.Vector;
devExt->InterruptMode = interruptDescriptor->u.Interrupt.Mode;
return STATUS_SUCCESS;
}
常见错误: 在
IRP_MN_START_DEVICE 之前就尝试访问硬件资源字段(如MemoryBase),此时资源尚未分配,访问会导致系统崩溃。必须严格遵循“资源分配 → 资源使用 → 资源释放”的生命周期。
4.3 设备扩展的使用模式
设备扩展在WDM驱动的各个例程中通过设备对象指针访问。以下展示三种典型使用场景:
4.3.1 在中断服务例程(ISR)中访问
BOOLEAN InterruptServiceRoutine(PKINTERRUPT Interrupt, PDEVICE_EXTENSION DevExt)
{
// 读取硬件状态寄存器(通过DevExt->MemoryBase映射的地址)
UCHAR status = READ_PORT_UCHAR((PUCHAR)DevExt->MemoryBase.QuadPart);
if (status & INTERRUPT_PENDING) {
// 记录中断数据到扩展缓冲区
DevExt->ReadBuffer[0] = status;
// 请求DPC(下半部处理)
KeInsertQueueDpc(&DevExt->Dpc, NULL, NULL);
return TRUE; // 中断已处理
}
return FALSE; // 不是本设备的中断
}
4.3.2 在IOCTL分发例程中访问
NTSTATUS IoctlDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);
ULONG ioctlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;
// 使用自旋锁保护IOCTL参数缓存
KIRQL oldIrql;
KeAcquireSpinLock(&devExt->SpinLock, &oldIrql);
devExt->IoctlCode = ioctlCode;
devExt->IoctlInputBuffer = Irp->AssociatedIrp.SystemBuffer;
devExt->IoctlInputLength = irpStack->Parameters.DeviceIoControl.InputBufferLength;
KeReleaseSpinLock(&devExt->SpinLock, oldIrql);
// 根据IOCTL码执行具体操作
switch (ioctlCode) {
case IOCTL_GET_DEVICE_STATUS:
// 从设备扩展读取状态
break;
case IOCTL_SET_INTERRUPT_MASK:
// 修改设备扩展中的中断掩码
break;
}
// ...
}
4.3.3 在DPC例程中访问
VOID DpcRoutine(PKDPC Dpc, PVOID DeferredContext, PVOID SystemArgument1, PVOID SystemArgument2)
{
PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)DeferredContext;
// 从设备扩展读取中断累积数据
// 执行耗时的数据处理(如拷贝到用户缓冲区)
// 注意:DPC运行在DISPATCH_LEVEL,不能访问分页内存
if (devExt->ReadBufferSize > 0) {
// 处理数据...
}
}
4.4 设备扩展的生命周期与清理
设备扩展的内存由系统管理,但驱动必须在 IRP_MN_REMOVE_DEVICE 中执行清理操作:
NTSTATUS RemoveDevice(PDEVICE_OBJECT DeviceObject)
{
PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
// 1. 断开中断(如果已连接)
if (devExt->InterruptObject != NULL) {
IoDisconnectInterrupt(devExt->InterruptObject);
devExt->InterruptObject = NULL;
}
// 2. 释放硬件资源映射
if (devExt->MemoryBase.QuadPart != 0) {
MmUnmapIoSpace((PVOID)devExt->MemoryBase.QuadPart, devExt->MemoryLength);
}
// 3. 释放驱动分配的额外缓冲区
if (devExt->ReadBuffer != NULL) {
ExFreePoolWithTag(devExt->ReadBuffer, 'BufT');
devExt->ReadBuffer = NULL;
}
// 4. 删除设备对象(系统自动释放设备扩展内存)
IoDeleteDevice(DeviceObject);
}
最佳实践: 设备扩展的内存由
IoDeleteDevice 自动释放,驱动不要在扩展上调用 ExFreePool。但扩展中指向的额外分配内存(如ReadBuffer)必须由驱动显式释放。
4.5 设备扩展结构图
下图展示了设备扩展在WDM驱动框架中的位置及其与各系统组件的关系:
4.6 多设备实例与设备扩展
WDM驱动支持多个设备实例(如多个相同硬件),每个实例拥有独立的 DEVICE_OBJECT 和 DEVICE_EXTENSION。设备扩展的独立性保证了多实例安全:
| 场景 | 设备实例1 | 设备实例2 | 说明 |
|---|---|---|---|
| 设备对象 | DeviceObject1 |
DeviceObject2 |
由 IoCreateDevice 分别创建 |
| 设备扩展 | DevExt1 |
DevExt2 |
内存地址不同,互不干扰 |
| 硬件资源 | MemoryBase=0xD0000 | MemoryBase=0xD1000 | PnP管理器分配不同资源 |
| 中断对象 | Interrupt1 |
Interrupt2 |
可共享同一中断向量 |
| ISR上下文 | DevExt1 |
DevExt2 |
中断服务例程通过上下文区分 |
多实例优势: 设备扩展机制使得驱动代码可以完全重入。ISR、DPC、Dispatch例程只需通过传入的
PDEVICE_EXTENSION 指针操作对应实例的数据,无需全局变量或静态数组。这是WDM驱动支持热插拔和多设备的核心设计。
4.7 设备扩展使用避坑指南
- 不要跨IRP级别访问: 在DISPATCH_LEVEL及以上不能访问分页内存,设备扩展本身在非分页池中,但扩展内的指针(如ReadBuffer)可能指向分页内存,需用
ProbeForRead/ProbeForWrite验证。 - 不要假设扩展已清零: 虽然
IoCreateDevice会清零扩展,但IoCreateDeviceSecure等变体可能行为不同,建议在初始化时显式赋值所有字段。 - 不要存储IRP指针: 设备扩展中不应长期保存IRP指针,IRP可能被取消或超时,应使用
IoCancelIrp或取消例程处理。 - 同步访问保护: 多线程/多中断并发访问设备扩展时,必须使用自旋锁(
KSPIN_LOCK)或互斥体(FAST_MUTEX)保护共享字段。 - 资源释放顺序: 在
IRP_MN_REMOVE_DEVICE中,先断开中断、释放映射、释放额外内存,最后调用IoDeleteDevice。
严重警告: 不要在设备扩展中存储指向
IRP 或 MDL 的指针并在ISR/DPC中访问。IRP可能在任意时刻被完成或取消,访问已释放的IRP会导致系统崩溃(BugCheck 0xA)。正确的做法是在ISR中记录状态,在DPC中通过 IoGetCurrentIrpStackLocation 重新获取当前IRP。