3. WDM驱动入口与卸载例程:DriverEntry、DriverUnload的标准实现
WDM(Windows Driver Model)驱动的生命周期始于 DriverEntry 例程,终于 DriverUnload 例程。这两个例程是驱动与操作系统交互的契约入口,其实现质量直接决定了驱动的加载成功率、资源管理安全性和系统稳定性。本节将深入剖析这两个例程的标准实现模式、参数含义及常见陷阱。
3.1 DriverEntry:驱动的初始化入口
DriverEntry 是 WDM 驱动的第一个执行函数,由 I/O 管理器在驱动加载时调用。它的主要职责是:注册驱动支持的所有分发例程(MajorFunction)、创建必要的设备对象、初始化全局资源,并返回状态以指示加载是否成功。
3.1.1 函数原型与参数
NTSTATUS DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
);
| 参数 | 类型 | 说明 |
|---|---|---|
| DriverObject | PDRIVER_OBJECT | 指向刚创建的驱动对象,用于注册分发例程和卸载例程 |
| RegistryPath | PUNICODE_STRING | 指向注册表中该驱动的服务键路径(如 \Registry\Machine\System\CurrentControlSet\Services\MyDriver) |
关键提示: RegistryPath 指向的内存由 I/O 管理器管理,DriverEntry 返回后即失效。如果需要持久化使用该路径(例如在 DriverUnload 中读取配置),必须在 DriverEntry 中将其复制到驱动全局内存中。
3.1.2 标准实现步骤
- 注册分发例程: 为 DriverObject->MajorFunction 数组的每个索引赋值函数指针。
- 注册卸载例程: 设置 DriverObject->DriverUnload 为自定义的卸载函数。
- 创建设备对象: 调用 IoCreateDevice 创建功能设备对象(FDO)。
- 初始化资源: 分配自旋锁、内存池、定时器等全局资源。
- 返回状态: 成功返回 STATUS_SUCCESS,失败返回相应的错误码并清理已分配资源。
3.1.3 标准实现代码模板
NTSTATUS DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
NTSTATUS status = STATUS_SUCCESS;
PDEVICE_OBJECT deviceObject = NULL;
UNICODE_STRING deviceName;
// 1. 注册分发例程(所有 IRP 走默认处理)
for (ULONG i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
{
DriverObject->MajorFunction[i] = MyDispatchPassThrough;
}
// 2. 注册特定的分发例程
DriverObject->MajorFunction[IRP_MJ_CREATE] = MyDispatchCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = MyDispatchClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyDispatchDeviceControl;
DriverObject->MajorFunction[IRP_MJ_READ] = MyDispatchRead;
DriverObject->MajorFunction[IRP_MJ_WRITE] = MyDispatchWrite;
// 3. 注册卸载例程
DriverObject->DriverUnload = MyDriverUnload;
// 4. 创建设备对象
RtlInitUnicodeString(&deviceName, L"\\Device\\MyWDMDevice");
status = IoCreateDevice(
DriverObject,
sizeof(DEVICE_EXTENSION), // 设备扩展大小
&deviceName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&deviceObject
);
if (!NT_SUCCESS(status))
{
KdPrint(("DriverEntry: IoCreateDevice failed with status 0x%X\n", status));
return status;
}
// 5. 初始化设备扩展
PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)deviceObject->DeviceExtension;
devExt->DeviceObject = deviceObject;
KeInitializeSpinLock(&devExt->SpinLock);
devExt->InterruptObject = NULL;
devExt->IsStarted = FALSE;
// 6. 保存注册表路径(复制到全局内存)
// 此处省略具体复制逻辑,建议使用 ExAllocatePoolWithTag + RtlCopyUnicodeString
KdPrint(("DriverEntry: Driver loaded successfully.\n"));
return STATUS_SUCCESS;
}
常见错误: 在 DriverEntry 中忘记注册 DriverUnload 例程,或者注册了但卸载例程为空。这会导致驱动无法被安全卸载,系统在重启前会一直占用驱动资源。
3.2 DriverUnload:驱动的清理与卸载
DriverUnload 例程在驱动被卸载时由 I/O 管理器调用。它的核心职责是:撤销 DriverEntry 中执行的所有操作,释放所有已分配的资源,删除设备对象,确保不留下任何未清理的状态。
3.2.1 函数原型与参数
VOID DriverUnload(
_In_ PDRIVER_OBJECT DriverObject
);
| 参数 | 类型 | 说明 |
|---|---|---|
| DriverObject | PDRIVER_OBJECT | 指向正在被卸载的驱动对象,可通过它遍历设备链表 |
3.2.2 标准实现步骤
- 遍历并删除设备对象: 从 DriverObject->DeviceObject 链表头开始,逐个删除。
- 释放全局资源: 释放内存池、删除定时器、关闭句柄等。
- 清理符号链接: 如果创建了符号链接,调用 IoDeleteSymbolicLink 删除。
- 注销中断(如有): 调用 IoDisconnectInterrupt 断开中断连接。
- 返回: 函数无返回值,但必须确保所有资源已释放。
3.2.3 标准实现代码模板
VOID MyDriverUnload(
_In_ PDRIVER_OBJECT DriverObject
)
{
PDEVICE_OBJECT deviceObject = DriverObject->DeviceObject;
PDEVICE_OBJECT nextDevice = NULL;
KdPrint(("MyDriverUnload: Entering driver unload.\n"));
// 1. 遍历设备链表,删除所有设备对象
while (deviceObject != NULL)
{
nextDevice = deviceObject->NextDevice;
PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)deviceObject->DeviceExtension;
// 1.1 如果设备已启动,先停止并断开中断
if (devExt->IsStarted)
{
// 假设有中断对象需要断开
if (devExt->InterruptObject != NULL)
{
IoDisconnectInterrupt(devExt->InterruptObject);
devExt->InterruptObject = NULL;
}
}
// 1.2 删除符号链接(如果存在)
UNICODE_STRING symLink;
RtlInitUnicodeString(&symLink, L"\\DosDevices\\MyWDMDevice");
IoDeleteSymbolicLink(&symLink);
// 1.3 删除设备对象
IoDeleteDevice(deviceObject);
deviceObject = nextDevice;
}
// 2. 释放全局资源(如注册表路径副本、全局内存池等)
// 此处省略具体释放逻辑
KdPrint(("MyDriverUnload: Driver unloaded successfully.\n"));
}
最佳实践: 在 DriverUnload 中始终使用 while 循环遍历设备链表,而不是假设只有一个设备。WDM 驱动可能创建多个设备对象(如功能设备对象和附加过滤设备对象),漏删任何一个都会导致资源泄漏。
3.3 DriverEntry 与 DriverUnload 的对称性
WDM 框架要求 DriverEntry 和 DriverUnload 在资源管理上保持严格的对称性:每个在 DriverEntry 中分配的资源,都必须在 DriverUnload 中释放。下表列出了常见的对称操作对:
| DriverEntry 操作 | DriverUnload 对应操作 | 说明 |
|---|---|---|
| IoCreateDevice | IoDeleteDevice | 删除设备对象 |
| IoCreateSymbolicLink | IoDeleteSymbolicLink | 删除符号链接 |
| ExAllocatePoolWithTag | ExFreePoolWithTag | 释放内存池 |
| KeInitializeSpinLock | (无需显式销毁) | 自旋锁无需释放,但需确保无锁竞争 |
| IoConnectInterrupt | IoDisconnectInterrupt | 断开中断连接 |
| KeInitializeTimer | KeCancelTimer + 等待 | 取消并清理定时器 |
严重警告: 如果在 DriverEntry 中调用 IoCreateDevice 失败,必须在返回前释放之前已分配的所有资源。不要在失败路径上直接返回错误码而不做清理,否则会导致内存泄漏。
3.4 DriverEntry 与 DriverUnload 的调用流程
下图展示了从驱动加载到卸载的完整生命周期,突出 DriverEntry 和 DriverUnload 在其中的位置:
3.5 常见陷阱与避坑指南
- 未注册 DriverUnload: 如果 DriverObject->DriverUnload 为 NULL,系统将拒绝卸载该驱动,导致驱动常驻内存。
- 在 DriverUnload 中访问已释放的内存: 删除设备对象后,不能再访问其设备扩展。应先读取所需数据,再删除设备。
- 忽略设备链表遍历: 假设只有一个设备对象,使用 deviceObject->NextDevice 遍历所有设备。
- 在 DriverEntry 失败时未清理: 如果 IoCreateDevice 失败,之前分配的内存池必须释放,否则造成泄漏。
- 在 DriverUnload 中未断开中断: 如果驱动连接了中断,卸载前必须调用 IoDisconnectInterrupt,否则系统可能崩溃。
调试建议: 在 DriverEntry 和 DriverUnload 的入口和出口处添加 KdPrint 日志,记录关键操作和状态码。这可以帮助快速定位加载/卸载失败的原因。