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 标准实现步骤

  1. 注册分发例程: 为 DriverObject->MajorFunction 数组的每个索引赋值函数指针。
  2. 注册卸载例程: 设置 DriverObject->DriverUnload 为自定义的卸载函数。
  3. 创建设备对象: 调用 IoCreateDevice 创建功能设备对象(FDO)。
  4. 初始化资源: 分配自旋锁、内存池、定时器等全局资源。
  5. 返回状态: 成功返回 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 标准实现步骤

  1. 遍历并删除设备对象: 从 DriverObject->DeviceObject 链表头开始,逐个删除。
  2. 释放全局资源: 释放内存池、删除定时器、关闭句柄等。
  3. 清理符号链接: 如果创建了符号链接,调用 IoDeleteSymbolicLink 删除。
  4. 注销中断(如有): 调用 IoDisconnectInterrupt 断开中断连接。
  5. 返回: 函数无返回值,但必须确保所有资源已释放。

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 在其中的位置:

WDM 驱动生命周期:DriverEntry 与 DriverUnload 调用流程 系统启动 / 动态加载 I/O 管理器调用 DriverEntry 注册分发例程、创建设备、初始化资源 驱动正常运行 处理 IRP、中断、IOCTL 请求 卸载触发 服务停止 / 设备移除 / 系统关闭 I/O 管理器调用 DriverUnload 删除设备、释放资源、断开中断 驱动卸载完成 关键原则 对称性:每个分配 对应一个释放 失败路径必须清理

3.5 常见陷阱与避坑指南

  • 未注册 DriverUnload: 如果 DriverObject->DriverUnload 为 NULL,系统将拒绝卸载该驱动,导致驱动常驻内存。
  • 在 DriverUnload 中访问已释放的内存: 删除设备对象后,不能再访问其设备扩展。应先读取所需数据,再删除设备。
  • 忽略设备链表遍历: 假设只有一个设备对象,使用 deviceObject->NextDevice 遍历所有设备。
  • 在 DriverEntry 失败时未清理: 如果 IoCreateDevice 失败,之前分配的内存池必须释放,否则造成泄漏。
  • 在 DriverUnload 中未断开中断: 如果驱动连接了中断,卸载前必须调用 IoDisconnectInterrupt,否则系统可能崩溃。
调试建议: 在 DriverEntry 和 DriverUnload 的入口和出口处添加 KdPrint 日志,记录关键操作和状态码。这可以帮助快速定位加载/卸载失败的原因。