本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Kernel-Mode Driver Framework (KMDF) 是微软为简化Windows内核模式驱动开发提供的现代编程框架,具备高可靠性与面向对象特性。本文详细讲解KMDF驱动的调用机制与核心知识点,涵盖驱动初始化、设备对象创建、I/O请求处理、电源管理、同步控制及调试方法等内容。通过系统性介绍DriverEntry入口点、IRP处理流程、关键回调函数和资源管理策略,帮助开发者掌握KMDF驱动的完整生命周期与实际开发技巧,适用于各类硬件驱动项目开发与系统级编程实践。
kmdf driver

1. KMDF驱动程序基本结构概述

概述与核心组件

KMDF(Kernel-Mode Driver Framework)通过面向对象的抽象机制,将传统WDM中复杂的驱动开发模型简化为“框架对象 + 事件回调”的编程范式。其核心由WDFDRIVER、WdfDevice和WdfIoQueue等对象构成,每个对象封装了特定系统交互逻辑,并通过引用计数实现安全生命周期管理。

// 典型KMDF驱动结构示意
WDF_DRIVER_CONFIG config;
WDFDRIVER driver;
WdfDriverCreate(&driverAttributes, &config, WDF_NO_HANDLE);

该框架运行于内核态,依赖 WdfLibrary 统一接口层与操作系统交互,自动处理IRP分发、电源管理与即插即用(PnP)状态迁移,使开发者聚焦业务逻辑实现。

2. DriverEntry入口点与驱动初始化实现

在Windows内核开发中, DriverEntry 是KMDF(Kernel-Mode Driver Framework)驱动程序的起点,是系统加载驱动时第一个被调用的函数。它承担着整个驱动生命周期初始化的核心职责,其执行质量直接决定驱动是否能正确注册到内核并进入后续运行阶段。与用户态程序的 main 函数类似, DriverEntry 是操作系统加载器在驱动映像映射进内存后主动调用的入口,标志着驱动从静态文件转变为可执行的内核实体。

该函数不仅需要完成基本的框架初始化和对象创建,还需设置关键回调、分配必要资源,并确保所有操作遵循严格的错误处理规范。由于运行于内核模式下的高特权级别,任何未捕获的异常或资源泄漏都可能导致系统崩溃(蓝屏),因此对 DriverEntry 的设计必须兼具健壮性、可调试性和结构清晰性。现代KMDF通过封装WDM的复杂性,将开发者注意力集中于业务逻辑而非底层调度细节,但理解 DriverEntry 的内部机制仍然是掌握驱动开发的关键一步。

本章将深入剖析 DriverEntry 的执行上下文、参数语义、调用流程以及在实际项目中的最佳实践。我们将从系统加载机制入手,逐步解析如何使用 WdfDriverCreate 创建框架驱动对象,配置全局行为回调,并管理驱动级状态与资源预分配策略。最终通过一个完整的最小可运行示例,展示如何利用WDK工具链构建出可在目标系统上成功加载的 .sys 驱动模块。

2.1 DriverEntry函数的作用与调用时机

DriverEntry 是每个Windows驱动程序必须实现的导出函数,其作用相当于应用程序的 main() WinMain() ,它是驱动在内核空间执行的第一段代码。当系统通过服务控制管理器(SCM)或即插即用管理器触发驱动加载请求时,内核的图像加载器会将驱动的 .sys 文件映射到非分页池内存中,并查找其导出表中的 DriverEntry 符号地址,随后跳转执行。

2.1.1 系统加载驱动时的执行流程分析

驱动加载过程始于用户态或系统策略发起的启动请求。例如,在注册表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\<DriverName> 下存在对应的服务项后,可通过 net start <servicename> 命令显式启动驱动。此时,服务控制管理器向内核发出加载请求,I/O管理器接管并执行以下步骤:

graph TD
    A[用户或系统发起加载请求] --> B{服务注册表项是否存在?}
    B -- 是 --> C[I/O管理器调用IoCreateDriver]
    C --> D[加载驱动镜像至内核空间]
    D --> E[解析PE头获取DriverEntry RVA]
    E --> F[调用DriverEntry(PDRIVER_OBJECT, PUNICODE_STRING)]
    F --> G[执行初始化逻辑]
    G -- 成功 --> H[返回STATUS_SUCCESS]
    G -- 失败 --> I[返回错误码,驱动卸载]

上述流程展示了从请求到入口执行的完整路径。值得注意的是, DriverEntry 运行在 DISPATCH_LEVEL (IRQL = PASSIVE_LEVEL),这意味着它可以安全地调用分页内存访问、等待同步对象以及调用大部分内核API。然而,仍需避免长时间阻塞,以免影响系统响应。

在整个加载过程中,操作系统为驱动分配了一个 DRIVER_OBJECT 结构体实例,用于维护该驱动的所有设备对象链表、驱动卸载例程指针、派遣函数数组等元信息。同时传入的 PUNICODE_STRING 参数指向注册表路径,通常形如 \Registry\Machine\System\CurrentControlSet\Services\MyKmdfDriver ,可用于读取自定义配置参数。

2.1.2 驱动入口函数原型解析: In PDRIVER_OBJECT, In PUNICODE_STRING

标准的 DriverEntry 函数签名如下所示:

NTSTATUS
DriverEntry(
    _In_ PDRIVER_OBJECT  DriverObject,
    _In_ PUNICODE_STRING RegistryPath
)
参数说明:
参数 类型 含义
DriverObject PDRIVER_OBJECT 指向由I/O管理器分配的驱动对象,包含设备链、派遣函数表、卸载函数指针等
RegistryPath PUNICODE_STRING 指向注册表路径字符串,用于访问INF安装时写入的配置数据

其中, PDRIVER_OBJECT 是一个核心结构体,部分关键字段包括:

typedef struct _DRIVER_OBJECT {
    ...
    ULONG DriverExtension;                // 扩展信息
    UNICODE_STRING DriverName;            // 驱动名称(如 \Driver\MyDriver)
    LIST_ENTRY DeviceObject;              // 设备对象链表头
    DRIVER_INITIALIZE *DriverInit;        // 初始化函数(通常是DriverEntry)
    PDRIVER_STARTIO StartIo;              // 启动I/O例程
    PDRIVER_UNLOAD DriverUnload;          // 可选卸载函数
    PDRIVER_DISPATCH MajorFunction[28];   // 派遣函数数组(IRP_MJ_READ等)
} DRIVER_OBJECT, *PDRIVER_OBJECT;

虽然KMDF自动管理大多数派遣函数和设备链,但在 DriverEntry 中仍可通过 DriverObject->DriverUnload 设置自定义卸载回调(尽管不推荐在KMDF中手动设置,应由框架管理)。

RegistryPath 参数常用于打开注册表键以读取初始化参数。示例如下:

NTSTATUS status;
HANDLE hKey;
OBJECT_ATTRIBUTES attr;
WCHAR keyPathBuffer[256];
UNICODE_STRING keyPath;

RtlCopyUnicodeString(&keyPath, RegistryPath);
// 构造子键路径:\Registry\Machine\System\CurrentControlSet\Services\YourDriver\Parameters
wcscpy(keyPathBuffer, keyPath.Buffer);
wcscat(keyPathBuffer, L"\\Parameters");
RtlInitUnicodeString(&keyPath, keyPathBuffer);

InitializeObjectAttributes(&attr, &keyPath, OBJ_CASE_INSENSITIVE, NULL, NULL);

status = ZwOpenKey(&hKey, KEY_READ, &attr);
if (NT_SUCCESS(status)) {
    // 读取MaxBufferSize等配置值
    ZwClose(hKey);
}

代码逻辑逐行解读
- 第1–2行:声明状态变量和句柄。
- 第3–4行:定义对象属性和缓冲区用于拼接完整注册表路径。
- 第6–7行:复制原始路径并追加 \Parameters 子键名。
- 第8行:初始化新的 UNICODE_STRING 指向拼接后的路径。
- 第10行:初始化 OBJECT_ATTRIBUTES ,启用大小写不敏感查找。
- 第12–16行:尝试打开 Parameters 键;若成功则进行配置读取,完成后关闭句柄。

此模式常见于传统WDM驱动,而在KMDF中更推荐使用 WPP_RECORDER WdfRegistryQuery* 系列辅助函数来简化操作。

2.2 WDF驱动对象的创建与配置

KMDF的核心理念是“以对象为中心”,所有驱动行为围绕一系列框架对象展开。其中最顶层的是 WDFDRIVER 对象,代表整个驱动实例。它的创建依赖于 WdfDriverCreate 函数,该函数不仅注册驱动本身,还允许设置多个事件回调,从而控制驱动的全局行为。

2.2.1 WdfDriverCreate的参数详解与使用规范

函数原型如下:

NTSTATUS WdfDriverCreate(
    _In_ PDRIVER_OBJECT DriverObject,
    _In_ PUNICODE_STRING RegistryPath,
    _In_opt_ PWDF_OBJECT_ATTRIBUTES DriverAttributes,
    _In_ PWDF_DRIVER_CONFIG DriverConfig,
    _Out_opt_ WDFDRIVER* Driver
);
参数说明表:
参数 必需性 描述
DriverObject 来自DriverEntry的原始驱动对象
RegistryPath 注册表路径,用于配置读取
DriverAttributes 指定WDFDRIVER对象的属性(如上下文空间)
DriverConfig 驱动配置结构,含关键回调函数指针
Driver 接收创建成功的WDFDRIVER句柄

调用前必须先初始化 WDF_DRIVER_CONFIG 和可选的 WDF_OBJECT_ATTRIBUTES

典型用法如下:

WDF_DRIVER_CONFIG config;
WDF_OBJECT_ATTRIBUTES attrs;
WDFDRIVER hDriver;

WDF_DRIVER_CONFIG_INIT(&config, WdfDeviceWdmDispatchIrpToIoQueue);

config.EvtDriverDeviceAdd = OnDeviceAdd;
config.EvtDriverUnload = OnDriverUnload;

WDF_OBJECT_ATTRIBUTES_INIT(&attrs);
attrs.EvtCleanupCallback = OnDriverContextCleanup;

status = WdfDriverCreate(DriverObject, RegistryPath, &attrs, &config, &hDriver);
if (!NT_SUCCESS(status)) {
    return status;
}

代码逻辑逐行解读
- 第1–2行:声明配置和属性结构体。
- 第3行:使用宏初始化 WDF_DRIVER_CONFIG ,默认派遣方式为转发IRP到I/O队列。
- 第5行:设置设备添加回调,将在新设备发现时调用(如即插即用枚举)。
- 第6行:设置驱动卸载回调,用于释放全局资源。
- 第8–10行:初始化对象属性并指定清理回调函数。
- 第12–16行:调用创建函数;失败则立即返回错误码,防止继续执行。

此调用成功后,KMDF框架便接管了该驱动的生命周期管理,包括设备创建、电源管理、对象引用计数等。

2.2.2 WDF_DRIVER_CONFIG结构体中关键回调函数设置

WDF_DRIVER_CONFIG 定义了驱动的行为策略,其主要成员包括:

回调函数 用途
EvtDriverDeviceAdd 当系统检测到匹配硬件时调用,负责创建WDFDEVICE
EvtDriverUnload 驱动被显式卸载时调用(测试/调试场景)
EvtDriverDeviceListCreate 自定义PDO列表创建逻辑(高级用法)

特别地, EvtDriverDeviceAdd 是绝大多数KMDF驱动的核心入口点之一。它接收一个 WDFDEVICE_INIT* 参数,开发者需在此完成设备初始化、I/O队列设置及接口发布。

示例实现:

NTSTATUS
OnDeviceAdd(
    _In_ WDFDRIVER Driver,
    _Inout_ PWDFDEVICE_INIT DeviceInit
)
{
    WDFDEVICE device;
    NTSTATUS status;

    status = CreateDevice(DeviceInit, &device);
    if (!NT_SUCCESS(status)) {
        return status;
    }

    return STATUS_SUCCESS;
}

此处 CreateDevice 封装了 WdfDeviceCreate 调用,将在第三章详细展开。

此外, EvtDriverUnload 并非常规KMDF推荐使用的机制——因为生产环境中驱动极少被卸载,且KMDF默认不启用卸载支持。若需启用,应在配置中显式赋值,并确保所有子对象均已释放。

2.3 驱动全局状态管理与资源预分配

2.3.1 静态数据段与非分页池内存的应用场景

驱动可能需要维护跨设备共享的状态,如统计计数器、日志缓冲区或加密密钥池。这类数据不应存储在栈上或分页内存中,否则在 IRQL >= DISPATCH_LEVEL 时引发页面错误。

推荐做法是使用 非分页池(Non-paged Pool) 分配全局内存:

typedef struct _GLOBAL_EXTENSION {
    LONG ActiveDevices;
    ULONG TotalIoCount;
    KSPIN_LOCK Lock;
} GLOBAL_EXTENSION, *PGLOBAL_EXTENSION;

PGLOBAL_EXTENSION g_Globals = NULL;

// 在DriverEntry中分配
g_Globals = (PGLOBAL_EXTENSION)ExAllocatePool2(
    POOL_FLAG_NON_PAGED,
    sizeof(GLOBAL_EXTENSION),
    'GLBS'
);
if (!g_Globals) {
    return STATUS_INSUFFICIENT_RESOURCES;
}

RtlZeroMemory(g_Globals, sizeof(GLOBAL_EXTENSION));
KeInitializeSpinLock(&g_Globals->Lock);

参数说明
- POOL_FLAG_NON_PAGED :保证内存始终驻留物理RAM。
- 'GLBS' :标记标签,便于调试工具追踪内存分配。
- KeInitializeSpinLock :初始化自旋锁以保护并发访问。

此类全局变量应避免滥用,优先考虑使用 WDFDRIVER 上下文 实现封装:

WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attrs, GLOBAL_EXTENSION);
WdfDriverCreate(..., &attrs, ...);

// 获取上下文
PGLOBAL_EXTENSION ctx = WdfDriverGetContext(Driver);

这样更符合KMDF的对象模型,且能自动随驱动销毁而释放。

2.3.2 初始化失败时的清理逻辑设计原则

DriverEntry 必须遵循“单出口、逆序清理”原则。一旦某步失败,应立即释放此前已获取的所有资源。

NTSTATUS DriverEntry(...) {
    WDFDRIVER driver = NULL;
    PGLOBAL_EXTENSION globals = NULL;
    NTSTATUS status;

    globals = ExAllocatePool2(...);
    if (!globals) { goto Exit; }

    status = WdfDriverCreate(..., &driver);
    if (!NT_SUCCESS(status)) { goto Exit; }

    // 全部成功
    status = STATUS_SUCCESS;
    goto Exit;

Exit:
    if (driver) {
        // 框架会自动清理,无需手动删除
    }
    if (globals && !driver) {
        ExFreePool(globals);
    }
    return status;
}

注意:KMDF对象具有自动引用计数管理,只要未附加父对象, WdfDriverCreate 失败时框架会自动回收已创建的部分对象,无需手动调用 WdfObjectDelete

2.4 实战:构建最小可运行KMDF驱动模块

2.4.1 编写完整的DriverEntry并注册框架

完整示例代码如下:

#include <ntddk.h>
#include <wdf.h>

DRIVER_INITIALIZE DriverEntry;
EVT_WDF_DRIVER_DEVICE_ADD OnDeviceAdd;

NTSTATUS
DriverEntry(
    _In_ PDRIVER_OBJECT DriverObject,
    _In_ PUNICODE_STRING RegistryPath
)
{
    WDF_DRIVER_CONFIG config;
    NTSTATUS status;

    WDF_DRIVER_CONFIG_INIT(&config, WdfDeviceWdmDispatchIrpToIoQueue);
    config.EvtDriverDeviceAdd = OnDeviceAdd;

    status = WdfDriverCreate(DriverObject, RegistryPath, NULL, &config, NULL);
    if (!NT_SUCCESS(status)) {
        KdPrint(("Failed to create WDF driver: %x\n", status));
    }

    return status;
}

NTSTATUS
OnDeviceAdd(
    _In_ WDFDRIVER Driver,
    _Inout_ PWDFDEVICE_INIT DeviceInit
)
{
    WDFDEVICE hDevice;
    NTSTATUS status;

    UNREFERENCED_PARAMETER(Driver);

    status = WdfDeviceCreate(&DeviceInit, WDF_NO_OBJECT_ATTRIBUTES, &hDevice);
    if (!NT_SUCCESS(status)) {
        return status;
    }

    return STATUS_SUCCESS;
}

该驱动虽无实际功能,但满足KMDF基本要求,可编译加载。

2.4.2 使用WDK编译工具链生成.sys文件

使用 Visual Studio + WDK 创建 KMDF 驱动项目后, sources 文件内容如下:

TARGETNAME=HelloKmdf
TARGETPATH=obj
TARGETTYPE=DRIVER
DRIVERTYPE=KMDF
KMDF_VERSION_MAJOR=1
KMDF_VERSION_MINOR=29

SOURCES=\
    driver.c \
    $(NULL)

执行 build -ceZ 即可在 objchk_wlh_x86\i386\ 目录下生成 HelloKmdf.sys

安装测试命令:

sc create HelloKmdf type= kernel binPath= C:\path\HelloKmdf.sys
sc start HelloKmdf

若无报错,则表明驱动成功加载。

该最小模块为后续扩展提供了坚实基础。

3. 设备对象创建与WdfDeviceCreate应用

在Windows内核模式驱动开发中,设备对象是整个驱动程序功能实现的核心载体。KMDF(Kernel-Mode Driver Framework)通过抽象和封装原始的WDM模型中的复杂性,为开发者提供了更高层次、更安全且易于维护的设备管理机制。其中, WdfDeviceCreate 是构建设备栈结构的关键函数,它不仅负责初始化底层 DEVICE_OBJECT ,还完成了对象生命周期管理、I/O队列注册、接口发布等一系列关键任务。本章将深入剖析设备对象在KMDF框架中的角色定位,解析 WdfDeviceCreate 的内部工作机制,并结合实际案例展示如何使用该API创建一个可被用户态访问的虚拟设备。

3.1 设备对象在KMDF中的角色定位

设备对象作为操作系统识别硬件或逻辑实体的基本单位,在即插即用(PnP)与电源管理(Power Management)体系中扮演着核心角色。在传统WDM模型中,开发者必须手动分配并初始化 DEVICE_OBJECT 结构体,设置派遣例程(Dispatch Routines),处理IRP转发逻辑,过程繁琐且容易出错。而KMDF引入了面向对象的设计理念,将设备抽象为 WDFDEVICE 对象,屏蔽了底层细节,使开发者能够以事件回调的方式专注于业务逻辑。

3.1.1 物理设备对象(PDO)与功能设备对象(FDO)的区别

在Windows PnP架构中,每个物理设备都会由总线驱动程序创建一个 物理设备对象 (Physical Device Object, PDO)。PDO代表系统中真实存在的硬件节点,通常由ACPI、PCI、USB等总线驱动生成,不直接参与I/O请求的处理。

相对地, 功能设备对象 (Functional Device Object, FDO)是由功能驱动(Function Driver)创建的,位于设备栈的上层,负责提供具体的功能服务。例如,一个USB存储设备的PDO由USB总线驱动创建,而其FDO则由磁盘类驱动创建,用于处理读写请求、IOCTL控制命令等。

属性 PDO FDO
创建者 总线驱动(如 PCI、ACPI) 功能驱动(如 KMDF 驱动)
所处位置 设备栈最底层 设备栈中间或上层
是否处理I/O 否(仅传递IRP) 是(主动响应IRP)
可见性 不对用户暴露 可发布接口供用户访问
典型用途 描述硬件资源(IRQ、内存地址等) 实现设备控制逻辑

在KMDF中,我们通常创建的是FDO级别的设备对象。通过调用 WdfDeviceCreate ,KMDF会自动在设备栈中插入对应的 DEVICE_OBJECT 并绑定到当前驱动上下文中。

WDFDEVICE hDevice;
NTSTATUS status;

// 初始化 DeviceInit 对象
PWDFDEVICE_INIT deviceInit = WdfControlDeviceInitAllocate(
    driver,
    &SDDL_DEVOBJ_SYS_ALL_ADM_RWX_WORLD_R
);

if (!deviceInit) {
    return STATUS_INSUFFICIENT_RESOURCES;
}

// 设置设备类型与特性
WdfDeviceInitSetDeviceType(deviceInit, FILE_DEVICE_UNKNOWN);
WdfDeviceInitSetIoType(deviceInit, WdfDeviceIoDirect);

// 创建设备对象
status = WdfDeviceCreate(&deviceInit, WDF_NO_OBJECT_ATTRIBUTES, &hDevice);
if (!NT_SUCCESS(status)) {
    return status;
}

代码逻辑逐行解读:

  • 第5行: WdfControlDeviceInitAllocate 创建一个用于控制设备的初始化结构 WDFDEVICE_INIT ,并设置默认安全描述符(允许系统和管理员完全访问)。
  • 第9–10行:指定设备类型为 FILE_DEVICE_UNKNOWN ,表示这是一个自定义设备;I/O类型设为 WdfDeviceIoDirect ,意味着缓冲区将以直接I/O方式映射进内核空间,适用于大块数据传输场景。
  • 第14行: WdfDeviceCreate 执行设备对象的实际创建,若成功返回句柄 hDevice ,后续可用于注册队列、发布接口等操作。

此段代码体现了KMDF对设备创建流程的高度封装——开发者无需关心 IoCreateDevice KeInitializeDeviceObject 等底层API,只需配置属性即可完成设备实例化。

3.1.2 WDFDEVICE对象对底层DEVICE_OBJECT的封装优势

KMDF中的 WDFDEVICE 是一个框架管理的对象句柄,其背后对应一个真实的 DEVICE_OBJECT 内核结构。但相比原生WDM编程,KMDF带来的主要优势体现在以下几个方面:

封装性增强

KMDF将 DEVICE_OBJECT 的字段访问封装成一系列安全的访问函数,避免直接操作可能导致的蓝屏风险。例如:
- 使用 WdfDeviceSetIoType() 而非直接修改 DeviceObject->Flags
- 使用 WdfDeviceAddQueryInterface() 注册接口能力,而非手动挂接派遣函数

自动化资源管理

KMDF基于引用计数机制自动管理对象生命周期。当所有子对象(如I/O队列、定时器、内存块)都被释放后,框架会自动销毁 WDFDEVICE ,无需显式调用 IoDeleteDevice

回调驱动模型

KMDF采用“注册回调”代替“派遣函数”的设计范式。开发者不再需要编写 DriverObject->MajorFunction[IRP_MJ_READ] 这样的派遣入口,而是通过配置 WDF_IO_QUEUE_CONFIG 来设置 EvtIoRead 回调函数,提升代码可读性和模块化程度。

graph TD
    A[WDFDEVICE] --> B[WDF_IO_QUEUE]
    A --> C[WDF_INTERRUPT]
    A --> D[WDF_TIMER]
    A --> E[WDF_MEMORY]
    B --> F[Read Queue]
    B --> G[Write Queue]
    B --> H[Control Queue]
    style A fill:#4CAF50,stroke:#388E3C,color:white
    style B fill:#2196F3,stroke:#1976D2,color:white

上图展示了 WDFDEVICE 作为容器对象,聚合多个子对象的关系结构。这种组合式设计符合现代软件工程原则,便于模块解耦与单元测试。

此外,KMDF支持父子对象依赖关系管理。例如,当 WDFDEVICE 被销毁时,所有附加在其上的 WDFQUEUE 也会被自动清理,极大降低了资源泄漏的风险。

综上所述, WDFDEVICE 不仅是对 DEVICE_OBJECT 的简单包装,更是KMDF框架实现高可靠性、易调试、可扩展驱动开发的基础构件。

3.2 WdfDeviceCreate函数深度解析

WdfDeviceCreate 是KMDF中最关键的设备创建API之一,其原型如下:

NTSTATUS WdfDeviceCreate(
    _Inout_ PWDFDEVICE_INIT *DeviceInit,
    _In_opt_ PWDF_OBJECT_ATTRIBUTES DeviceAttributes,
    _Out_ WDFDEVICE *DeviceHandle
);

该函数承担了从设备初始化参数解析到最终对象注册的全流程工作。理解其各参数含义及调用时机,对于构建稳定可靠的驱动至关重要。

3.2.1 参数列表详解:DeviceInit, DeviceAttributes, DeviceHandle

DeviceInit:设备初始化上下文

PWDFDEVICE_INIT 是一个指向内部结构的指针,包含设备创建所需的所有元信息。它并非由开发者手动构造,而是通过以下两类函数之一获取:

  • WdfDeviceInitAllocate :为物理设备(PDO)分配初始化结构
  • WdfControlDeviceInitAllocate :为控制设备(如全局设备对象)分配结构

一旦获得 DeviceInit ,便可调用一系列“设置”函数来配置设备行为:

函数 作用
WdfDeviceInitSetDeviceType 设置设备类型(如 FILE_DEVICE_DISK)
WdfDeviceInitSetIoType 定义I/O处理方式(Buffered/ Direct/ Neither)
WdfDeviceInitSetExclusive 控制是否独占访问(TRUE 表示一次只能一个句柄打开)
WdfDeviceInitAssignName 指定设备名称(\Device\MyVirtualDev)
WdfDeviceInitSetCharacteristics 添加设备特性标志(如 FILE_AUTO_DELETE)

这些函数内部会对 DEVICE_OBJECT 相关字段进行安全赋值,并记录状态以便后续验证。

DeviceAttributes:对象属性配置

PWDF_OBJECT_ATTRIBUTES 允许开发者指定新创建设备对象的附加属性,主要包括:

  • ParentObject :设置父对象,形成引用依赖链
  • ExecutionLevel :设定执行级别(Passive/LowLevel)
  • SynchronizationScope :同步范围(None/Channel/Object)
  • EvtCleanupCallback :对象销毁前的清理回调

示例代码如下:

WDF_OBJECT_ATTRIBUTES attr;
WDF_OBJECT_ATTRIBUTES_INIT(&attr);
attr.EvtCleanupCallback = MyDeviceCleanup;

status = WdfDeviceCreate(&deviceInit, &attr, &hDevice);

hDevice 最终被释放时, MyDeviceCleanup 将被调用,可用于释放私有上下文数据。

DeviceHandle:输出设备句柄

成功创建后, WdfDeviceCreate 返回一个有效的 WDFDEVICE 句柄,可用于后续操作:

  • 注册I/O队列: WdfIoQueueCreate(hDevice, ...)
  • 发布设备接口: WdfDeviceCreateSymbolicLink(hDevice, ...)
  • 分配上下文空间: WdfObjectAllocateContext(...)

注意: DeviceHandle 必须是非空有效指针,否则函数将返回 STATUS_INVALID_PARAMETER

3.2.2 DEVICE_INIT初始化策略与安全属性设置

在调用 WdfDeviceCreate 前,正确初始化 DEVICE_INIT 至关重要。错误的配置可能导致设备无法加载、权限不足或安全漏洞。

安全描述符(Security Descriptor)

KMDF允许通过 SDDL(Security Descriptor Definition Language)字符串设置设备的安全访问策略。例如:

const PCWSTR sddl = L"D:P(A;;GA;;;SY)(A;;GRGW;;;WD)";
PWDFDEVICE_INIT deviceInit = WdfControlDeviceInitAllocate(driver, sddl);

上述SDDL含义如下:

组件 解释
D: 开始声明自主访问控制列表(DACL)
P DACL保护标志(不继承)
(A;;GA;;;SY) 允许本地系统(SY)完全访问(GA)
(A;;GRGW;;;WD) 允许所有人(WD)读取和写入权限(GRGW)

这确保了普通用户也能打开设备句柄,适合大多数通用驱动需求。

I/O类型选择策略

KMDF支持三种I/O处理模式:

模式 宏定义 适用场景
缓冲I/O WdfDeviceIoBuffered 小数据量控制命令(IOCTL)
直接I/O WdfDeviceIoDirect 大数据传输(DMA兼容)
无缓冲I/O WdfDeviceIoNeither 特殊协议通信(需自定义拷贝)

推荐优先使用 WdfDeviceIoDirect ,因其既保证性能又具备内存安全性。

WdfDeviceInitSetIoType(deviceInit, WdfDeviceIoDirect);

此设置会影响后续 WDFREQUEST 中缓冲区的访问方式。例如,在 EvtIoRead 回调中可通过 WdfRequestRetrieveOutputMemory 获取MDL(Memory Descriptor List),进而执行锁定操作。

设备命名与符号链接分离设计

建议遵循微软最佳实践: 设备名仅供内核内部引用,用户访问应通过符号链接实现隔离

UNICODE_STRING devName;
RtlInitUnicodeString(&devName, L"\\Device\\MyVirtualDevice");
WdfDeviceInitAssignName(deviceInit, &devName);

WdfDeviceCreate(&deviceInit, WDF_NO_OBJECT_ATTRIBUTES, &hDevice);

WdfDeviceCreateSymbolicLink(hDevice, L"\\DosDevices\\MyDev");

这样做的好处是:
- 设备名称可以包含反斜杠路径而不暴露给用户
- 符号链接可动态启用/禁用,不影响设备存在性
- 支持多别名映射(如 COM3、LPT1)

3.3 设备接口与符号链接发布

为了让用户态应用程序能够访问KMDF驱动创建的设备,必须通过发布设备接口或符号链接建立通信通道。

3.3.1 WdfDeviceCreateSymbolicLink创建用户访问通道

WdfDeviceCreateSymbolicLink 是最简单的暴露设备路径的方法,常用于调试或旧式应用程序兼容。

NTSTATUS status;
status = WdfDeviceCreateSymbolicLink(hDevice, L"\\DosDevices\\MyVirtualDev");
if (!NT_SUCCESS(status)) {
    KdPrint(("Failed to create symbolic link\n"));
    return status;
}

成功后,用户可通过 CreateFile("\\\\.\\MyVirtualDev", ...) 打开设备。

然而,这种方法缺乏标准化,不利于即插即用管理和多设备枚举。因此,微软推荐使用 设备接口类(Device Interface Class)

3.3.2 WdfDeviceSetDeviceInterfaceState启用接口可见性

设备接口类是一组GUID标识的逻辑类别,如 {a5dcbf10-6530-11d2-901f-00c04fb951ed} 表示显示适配器。我们可为自定义设备定义专属GUID:

DEFINE_GUID(MY_DEVICE_INTERFACE,
    0x12345678, 0x1234, 0x1234, 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF);

NTSTATUS RegisterInterface(WDFDEVICE hDevice)
{
    NTSTATUS status;
    UNICODE_STRING symbolicLinkName;

    status = WdfDeviceCreateDeviceInterface(
        hDevice,
        &MY_DEVICE_INTERFACE,
        NULL  // 默认符号链接名
    );
    if (!NT_SUCCESS(status)) {
        return status;
    }

    RtlInitUnicodeString(&symbolicLinkName, L"\\??\\MyVirtualDev");
    status = WdfDeviceSetDeviceInterfaceState(
        hDevice,
        &MY_DEVICE_INTERFACE,
        TRUE,
        &symbolicLinkName
    );

    return status;
}

WdfDeviceCreateDeviceInterface 注册接口类; WdfDeviceSetDeviceInterfaceState 控制其可见性。

函数 说明
WdfDeviceCreateDeviceInterface 在PnP管理器中注册接口类
WdfDeviceQueryProperty 查询设备接口路径
SetupDiGetClassDevs + SetupDiEnumDeviceInterfaces 用户态枚举设备接口

表格:设备访问方式对比

方式 安全性 易用性 推荐度
Symbolic Link 中等 ⭐⭐☆
Device Interface 高(标准API) ⭐⭐⭐⭐⭐
Named Pipe 中(需命名管道支持) ⭐⭐⭐

推荐始终使用设备接口类配合GUID进行发布,以支持现代Windows应用模型(如UWP、Win32 App via SetupAPI)。

sequenceDiagram
    participant UserApp
    participant Kernel
    participant KMDFDriver

    UserApp->>Kernel: SetupDiGetClassDevs(GUID)
    Kernel-->>UserApp: 返回设备句柄
    UserApp->>Kernel: SetupDiEnumDeviceInterfaces
    Kernel-->>UserApp: 返回接口路径 \\?\abcd...
    UserApp->>KMDFDriver: CreateFile(path, ...)
    KMDFDriver-->>UserApp: 返回文件句柄
    UserApp->>KMDFDriver: DeviceIoControl(h, IOCTL_X, ...)
    KMDFDriver-->>UserApp: 处理并返回结果

上图为用户态通过设备接口发现并通信的完整流程。

3.4 实践案例:实现一个支持用户态通信的虚拟设备

3.4.1 定义设备名称与GUID接口类

首先定义唯一GUID:

// mydriver.h
DEFINE_GUID(BT_VIRTUAL_DEVICE_GUID,
    0x87654321, 0x1234, 0x5678, 0x90, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67);

并在 .inf 文件中声明:

[Version]
Signature="$WINDOWS NT$"
Class={87654321-1234-5678-90AB-CDEFF0123456}
ClassGuid={87654321-1234-5678-90AB-CDEFF0123456}
Provider=%ManufacturerName%

3.4.2 在DriverEntry中完成设备实例化全流程

NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
    WDF_DRIVER_CONFIG config;
    WDFDRIVER hDriver;
    NTSTATUS status;

    WDF_DRIVER_CONFIG_INIT(&config, WdfNoOpDriverUnload);
    status = WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, &config, &hDriver);
    if (!NT_SUCCESS(status)) {
        return status;
    }

    PWDFDEVICE_INIT devInit = WdfControlDeviceInitAllocate(hDriver, &SDDL_DEVOBJ_SYS_ALL_ADM_RWX_WORLD_RW);
    WdfDeviceInitSetDeviceType(devInit, FILE_DEVICE_UNKNOWN);
    WdfDeviceInitSetIoType(devInit, WdfDeviceIoDirect);

    WDFDEVICE hDevice;
    WDF_OBJECT_ATTRIBUTES attr;
    WDF_OBJECT_ATTRIBUTES_INIT(&attr);

    status = WdfDeviceCreate(&devInit, &attr, &hDevice);
    if (!NT_SUCCESS(status)) {
        return status;
    }

    // 发布设备接口
    status = WdfDeviceCreateDeviceInterface(hDevice, &BT_VIRTUAL_DEVICE_GUID, NULL);
    if (NT_SUCCESS(status)) {
        WdfDeviceSetDeviceInterfaceState(hDevice, &BT_VIRTUAL_DEVICE_GUID, TRUE, NULL);
    }

    // 创建默认I/O队列(处理读/写/IOCTL)
    WDF_IO_QUEUE_CONFIG queueConfig;
    WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&queueConfig, WdfIoQueueDispatchSequential);
    queueConfig.EvtIoRead = OnRead;
    queueConfig.EvtIoWrite = OnWrite;
    queueConfig.EvtIoDeviceControl = OnControl;

    status = WdfIoQueueCreate(hDevice, &queueConfig, WDF_NO_OBJECT_ATTRIBUTES, WDF_NO_HANDLE);
    if (!NT_SUCCESS(status)) {
        return status;
    }

    return STATUS_SUCCESS;
}

此驱动可在用户态通过 CreateFile DeviceIoControl 进行交互,完整实现了设备创建 → 接口发布 → 请求处理的闭环流程。

4. I/O队列管理与IRP处理机制详解

在现代Windows内核驱动开发中,I/O请求的高效、安全处理是衡量驱动程序质量的重要标准。KMDF(Kernel-Mode Driver Framework)通过高度抽象化的对象模型,将传统WDM中复杂的IRP(I/O Request Packet)派遣与完成机制封装为更直观、可维护的事件回调模式。本章聚焦于KMDF框架下的I/O队列管理机制,深入剖析其如何将底层IRP转化为高阶的WDFREQUEST对象,并通过灵活的队列调度策略实现对读写、控制命令等各类I/O请求的精细化控制。理解这一机制不仅有助于构建响应迅速的设备驱动,也为后续实现异步通信、多线程并发访问以及电源管理协同提供了坚实基础。

4.1 I/O请求包(IRP)在KMDF中的抽象演化

KMDF并非绕开Windows内核原有的I/O子系统,而是对其进行了一层面向对象的封装与行为重定向。传统的WDM驱动需要手动解析IRP结构、判断主功能代码(MajorFunction)、设置完成例程并管理派遣锁,而KMDF则将这些低层次操作统一交由框架处理,开发者只需关注“请求来了怎么办”,而非“怎么知道请求来了”。

4.1.1 IRP到WDFREQUEST的转换机制

当用户态应用程序调用如 ReadFile WriteFile DeviceIoControl 等API时,I/O管理器会创建一个IRP,并将其发送至目标设备栈。在KMDF驱动中,该IRP并不会直接暴露给开发者,而是由框架自动捕获并转换为 WDFREQUEST 对象。这个过程发生在KMDF内部的派遣函数中,其核心逻辑如下图所示:

flowchart TD
    A[User Application Calls ReadFile()] --> B[I/O Manager Creates IRP_MJ_READ]
    B --> C[KMDF Internal Dispatch: WdfIoQueueDispatchPrepared]
    C --> D{Is Queue Ready?}
    D -- Yes --> E[Convert IRP to WDFREQUEST]
    D -- No --> F[Queue IRP for Later Processing]
    E --> G[Invoke EvtIoRead Callback]
    G --> H[Driver Processes Data via WDFMEMORY]
    H --> I[WDF Framework Completes IRP]

上述流程展示了从应用层发起读请求,到KMDF完成处理并回传结果的完整生命周期。关键点在于: IRP的生存周期被WDFREQUEST代理 ,开发者不再需要调用 IoCompleteRequest 或操作 Irp->IoStatus 字段,所有状态更新均由框架自动同步。

WDFREQUEST 作为IRP的高层抽象,具备以下优势:
- 支持引用计数管理,防止过早释放;
- 可绑定上下文数据(Context),便于跨回调传递状态;
- 提供统一接口访问输入/输出缓冲区(如 WdfRequestRetrieveInputMemory );
- 允许延迟完成(Forward and Set Completion Routine),支持复杂异步场景。

例如,在收到读请求后,可通过如下方式提取请求参数:

VOID OnRead(
    WDFQUEUE Queue,
    WDFREQUEST Request,
    size_t Length
)
{
    NTSTATUS status;
    WDFMEMORY outputMem;
    void* buffer;

    // 获取输出内存对象
    status = WdfRequestRetrieveOutputMemory(Request, &outputMem);
    if (!NT_SUCCESS(status)) {
        WdfRequestComplete(Request, status);
        return;
    }

    // 映射内存以便写入数据
    buffer = WdfMemoryGetBuffer(outputMem, NULL);
    if (buffer == NULL) {
        WdfRequestComplete(Request, STATUS_INSUFFICIENT_RESOURCES);
        return;
    }

    // 填充模拟数据
    RtlFillMemory(buffer, Length, 0xAA);

    // 设置完成长度
    WdfRequestSetInformation(Request, Length);
    WdfRequestComplete(Request, STATUS_SUCCESS);
}
代码逻辑逐行分析:
  1. WdfRequestRetrieveOutputMemory :尝试获取与请求关联的输出内存块。此函数适用于 IRP_MJ_READ ,因为数据应写入输出缓冲区。
  2. 检查返回状态,若失败则立即完成请求并返回错误码。
  3. WdfMemoryGetBuffer :获取可写指针,无需额外锁定即可安全访问用户缓冲区(KMDF已确保页面驻留)。
  4. 使用 RtlFillMemory 填充测试数据(此处为0xAA)。
  5. WdfRequestSetInformation :设置实际传输字节数,等价于 Irp->IoStatus.Information
  6. WdfRequestComplete :通知框架请求已完成,状态和信息将自动提交回I/O管理器。
参数说明:
  • Queue :接收请求的I/O队列对象,可用于区分多个队列来源。
  • Request :当前正在处理的WDFREQUEST句柄,代表一次I/O操作。
  • Length :请求的数据长度(由I/O管理器根据缓冲区大小计算得出)。

这种封装极大降低了内存访问风险——开发者无需使用 ProbeForWrite MmProbeAndLockPages 等易出错的API,KMDF已在内部完成安全性检查与缓冲区锁定。

4.1.2 框架自动派遣机制与手动队列控制对比

KMDF默认采用“自动派遣”(Automatic Dispatching)模式,即一旦请求到达设备队列,框架立即调用预设的事件回调(如 EvtIoRead ),并在回调返回后自动完成请求。这适用于大多数同步、快速处理的场景。

然而,在某些情况下,驱动可能需要延迟处理请求,比如等待硬件就绪、执行DMA传输或与其他线程同步。此时需切换至“手动派遣”(Manual Dispatching)模式:

WDF_IO_QUEUE_CONFIG queueConfig;
WDFQUEUE readQueue;

WDF_IO_QUEUE_CONFIG_INIT(&queueConfig, WdfIoQueueDispatchManual);
queueConfig.EvtIoRead = OnDeferredRead;

status = WdfIoQueueCreate(device, &queueConfig, WDF_NO_OBJECT_ATTRIBUTES, &readQueue);
if (!NT_SUCCESS(status)) {
    return status;
}

在此配置下,即使有新的读请求到达,KMDF也不会自动调用 OnDeferredRead ,必须显式调用 WdfIoQueueRetrieveNextRequest 来拉取请求:

VOID ProcessPendingReads(WDFQUEUE Queue)
{
    WDFREQUEST request;
    NTSTATUS status;

    while ((status = WdfIoQueueRetrieveNextRequest(Queue, &request)) == STATUS_SUCCESS) {
        // 手动启动处理
        HandleSingleRead(request);
    }
}
对比表格:自动 vs 手动派遣模式
特性 自动派遣 手动派遣
调度时机 请求到达即触发回调 需主动调用 Retrieve 系列函数
并发控制 可设置最大并发数( PowerManaged 影响) 完全由驱动控制处理节奏
适用场景 快速同步处理(<1ms) 异步、分阶段处理
资源占用 可能频繁进入内核 更好地整合中断/DPC处理
复杂度 低,推荐初学者使用 中高,需注意死锁与资源泄漏

值得注意的是,手动队列并不意味着完全脱离框架管理。 WDFREQUEST 仍受引用计数保护,且可在任意合法执行级别(PASSIVE_LEVEL 或 DISPATCH_LEVEL)完成,只要遵循KMDF规则即可。

4.2 WdfIoQueue的配置与使用

I/O队列是KMDF中组织和管理传入请求的核心组件。每一个 WDFQUEUE 对象都绑定到特定设备,并可针对不同类型的I/O操作(读、写、控制)进行定制化配置。正确设计队列结构不仅能提升性能,还能增强系统的稳定性与可预测性。

4.2.1 创建默认队列与命名队列的方法

KMDF支持两种主要类型的I/O队列: 默认队列(Default Queue) 命名队列(Named Queue)

  • 默认队列 :每个设备最多有一个,默认接收所有未明确路由的I/O请求。通常用于通用设备接口。
  • 命名队列 :可创建多个,配合I/O控制代码(IOCTL)或其他筛选条件使用,实现请求分流。

创建默认队列的典型代码如下:

WDFQUEUE defaultQueue;
WDF_IO_QUEUE_CONFIG queueConfig;

WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(
    &queueConfig,
    WdfIoQueueDispatchSequential
);

queueConfig.EvtIoRead = MyEvtIoRead;
queueConfig.EvtIoWrite = MyEvtIoWrite;
queueConfig.EvtIoDeviceControl = MyEvtIoDeviceControl;

status = WdfIoQueueCreate(device, &queueConfig, WDF_NO_OBJECT_ATTRIBUTES, &defaultQueue);
if (!NT_SUCCESS(status)) {
    return status;
}

// 将其设为设备的默认队列
WdfDeviceSetDefaultQueue(device, defaultQueue);

而对于需要分离控制通道的高级设备(如网络适配器或加密模块),可以创建专用的命名队列:

WDFQUEUE controlQueue;
WDF_IO_QUEUE_CONFIG ctrlConfig;

WDF_IO_QUEUE_CONFIG_INIT(&ctrlConfig, WdfIoQueueDispatchParallel);
ctrlConfig.EvtIoDeviceControl = MySpecialIoctlHandler;

status = WdfIoQueueCreate(device, &ctrlConfig, WDF_NO_OBJECT_ATTRIBUTES, &controlQueue);
if (!NT_SUCCESS(status)) {
    return status;
}

然后在 EvtDeviceAdd 或其他初始化阶段注册该队列。注意:命名队列不会自动接收请求,必须通过 WdfDeviceConfigureRequestDispatching 显式指定哪些类型请求应导向该队列:

WdfDeviceConfigureRequestDispatching(
    device,
    controlQueue,
    WdfRequestTypeDeviceControl
);

这意味着所有 IOCTL 请求都将被送往 controlQueue ,而其他请求继续走默认队列。

表格:队列类型选择指南
设备类型 推荐队列结构 说明
字符设备(串口、虚拟文件) 单一默认队列 简单有序处理即可满足需求
存储设备(磁盘仿真) 默认 + 命名(用于格式化命令) 分离管理命令与数据流
网络接口卡 多个并行命名队列 发送/接收/控制独立队列提升吞吐
多通道采集设备 每通道一个命名队列 实现通道级隔离与QoS控制

4.2.2 设置队列调度策略:顺序、并行或自定义

KMDF提供三种基本的调度模式,通过 WDF_IO_QUEUE_DISPATCH_TYPE 枚举设定:

  1. WdfIoQueueDispatchSequential
    请求按FIFO顺序逐个处理,前一个完成后才开始下一个。适合共享资源或寄存器访问类设备。

  2. WdfIoQueueDispatchParallel
    允许多个请求同时活跃,框架不保证顺序。适用于高吞吐场景,但需自行实现同步(如自旋锁)。

  3. WdfIoQueueDispatchManual
    不自动派遣,完全由驱动决定何时处理请求。

此外,还可结合 AllowZeroLengthRequests PowerManaged 等属性进一步微调行为:

WDF_IO_QUEUE_CONFIG_INIT(&queueConfig, WdfIoQueueDispatchParallel);
queueConfig.Settings.Parallel.NumberOfPresentRequests = 8; // 限制并发请求数
queueConfig.PowerManaged = WdfFalse; // 避免因Dx状态切换暂停队列
示例:构建高性能并行写队列

假设我们正在开发一个高速日志记录设备,要求支持多线程并发写入:

VOID SetupParallelWriteQueue(WDFDEVICE device)
{
    WDF_IO_QUEUE_CONFIG queueConfig;
    WDFQUEUE writeQueue;
    WDF_OBJECT_ATTRIBUTES attrs;

    WDF_IO_QUEUE_CONFIG_INIT(&queueConfig, WdfIoQueueDispatchParallel);
    queueConfig.EvtIoWrite = OnConcurrentWrite;
    queueConfig.AllowZeroLengthRequests = WdfTrue;

    // 设置最大并发请求数(可选)
    queueConfig.Settings.Parallel.NumberOfPresentRequests = 16;

    WDF_OBJECT_ATTRIBUTES_INIT(&attrs);
    attrs.ExecutionLevel = WdfExecutionLevelPassive;

    NTSTATUS status = WdfIoQueueCreate(device, &queueConfig, &attrs, &writeQueue);
    if (NT_SUCCESS(status)) {
        WdfDeviceConfigureRequestDispatching(device, writeQueue, WdfRequestTypeWrite);
    }
}

OnConcurrentWrite 中,必须使用同步机制保护共享资源:

WDFSPINLOCK g_WriteLock;

EVT_WDF_IO_QUEUE_IO_WRITE OnConcurrentWrite(
    WDFQUEUE Queue,
    WDFREQUEST Request,
    size_t Length
)
{
    UNREFERENCED_PARAMETER(Queue);

    WdfSpinLockAcquire(g_WriteLock);
    // 写入共享缓冲区或硬件FIFO
    LogDataFromRequest(Request, Length);
    WdfSpinLockRelease(g_WriteLock);

    WdfRequestComplete(Request, STATUS_SUCCESS);
}

⚠️ 注意:在 DISPATCH_LEVEL 下调用 WdfSpinLockAcquire 是安全的,但应避免长时间持有锁,以防系统僵死。

4.3 I/O请求处理回调函数实战

KMDF通过一系列 EvtIoXxx 回调函数将不同类型的I/O请求分发至对应的处理程序。掌握这些回调的语义与使用方法,是实现功能性驱动的关键。

4.3.1 EvtIoRead/EvtIoWrite的数据读写实现

这两个回调分别对应 IRP_MJ_READ IRP_MJ_WRITE 操作。它们最常见的用途是在用户空间与设备之间传输数据。

以一个虚拟内存设备为例,其实现读操作如下:

#define MAX_BUFFER_SIZE 4096
static UCHAR g_VirtualBuffer[MAX_BUFFER_SIZE] = {0};

VOID OnIoRead(
    WDFQUEUE Queue,
    WDFREQUEST Request,
    size_t Length
)
{
    NTSTATUS status;
    PVOID readBuffer;
    WDFMEMORY mem;

    status = WdfRequestRetrieveOutputMemory(Request, &mem);
    if (!NT_SUCCESS(status)) goto Done;

    readBuffer = WdfMemoryGetBuffer(mem, NULL);
    if (!readBuffer) {
        status = STATUS_INVALID_DEVICE_STATE;
        goto Done;
    }

    // 限制最大读取量
    if (Length > MAX_BUFFER_SIZE) {
        Length = MAX_BUFFER_SIZE;
    }

    RtlCopyMemory(readBuffer, g_VirtualBuffer, Length);
    WdfRequestSetInformation(Request, Length);

Done:
    WdfRequestComplete(Request, status);
}

该函数实现了从内核缓冲区向用户空间的安全复制。由于KMDF已验证用户缓冲区有效性,因此无需额外 __try/__except 块。

对于写操作,原理类似,但使用 WdfRequestRetrieveInputMemory 获取输入数据:

VOID OnIoWrite(
    WDFQUEUE Queue,
    WDFREQUEST Request,
    size_t Length
)
{
    NTSTATUS status;
    PVOID writeBuffer;
    WDFMEMORY mem;

    status = WdfRequestRetrieveInputMemory(Request, &mem);
    if (!NT_SUCCESS(status)) goto Done;

    writeBuffer = WdfMemoryGetBuffer(mem, NULL);
    if (!writeBuffer) {
        status = STATUS_INVALID_DEVICE_STATE;
        goto Done;
    }

    if (Length <= MAX_BUFFER_SIZE) {
        RtlCopyMemory(g_VirtualBuffer, writeBuffer, Length);
        WdfRequestSetInformation(Request, Length);
    } else {
        status = STATUS_INVALID_PARAMETER;
    }

Done:
    WdfRequestComplete(Request, status);
}

4.3.2 EvtIoDeviceControl处理IOCTL命令

这是最常用的控制接口,用于执行设备专属命令,如启动采集、查询状态、设置参数等。

首先定义一组IOCTL代码:

#define IOCTL_SET_MODE \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_GET_STATS \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_OUT_DIRECT, FILE_READ_DATA)

然后注册处理函数:

queueConfig.EvtIoDeviceControl = OnDeviceControl;

具体实现:

VOID OnDeviceControl(
    WDFQUEUE Queue,
    WDFREQUEST Request,
    size_t OutputBufferLength,
    size_t InputBufferLength,
    ULONG IoControlCode
)
{
    NTSTATUS status = STATUS_SUCCESS;
    PVOID inputBuffer = NULL, outputBuffer = NULL;
    ULONG inLen = (ULONG)InputBufferLength;
    ULONG outLen = (ULONG)OutputBufferLength;

    switch (IoControlCode) {
    case IOCTL_SET_MODE:
        if (inLen < sizeof(ULONG)) {
            status = STATUS_BUFFER_TOO_SMALL;
            break;
        }
        status = WdfRequestRetrieveInputBuffer(Request, inLen, &inputBuffer, NULL);
        if (NT_SUCCESS(status)) {
            ULONG mode = *(PULONG)inputBuffer;
            // 更新设备模式
            g_CurrentMode = mode;
        }
        break;

    case IOCTL_GET_STATS:
        if (outLen < sizeof(DEVICE_STATS)) {
            status = STATUS_BUFFER_TOO_SMALL;
            break;
        }
        status = WdfRequestRetrieveOutputBuffer(Request, outLen, &outputBuffer, NULL);
        if (NT_SUCCESS(status)) {
            PDEVICE_STATS stats = (PDEVICE_STATS)outputBuffer;
            stats->ReadCount = g_ReadCount;
            stats->WriteCount = g_WriteCount;
            WdfRequestSetInformation(Request, sizeof(DEVICE_STATS));
        }
        break;

    default:
        status = STATUS_INVALID_DEVICE_REQUEST;
        break;
    }

    WdfRequestComplete(Request, status);
}

💡 提示: METHOD_BUFFERED 使用内核缓冲区复制数据; METHOD_OUT_DIRECT 允许直接映射用户缓冲区用于DMA输出。

4.4 同步与异步IRP处理模式对比

4.4.1 同步模式下的阻塞等待与资源占用

同步处理是最直观的方式:请求进入后立即处理并完成。优点是逻辑清晰,调试方便;缺点是在耗时操作中会长时间占用线程栈与CPU资源。

// 同步处理模拟延迟写
VOID OnSyncWrite(WDFQUEUE q, WDFREQUEST r, size_t len)
{
    LARGE_INTEGER delay;
    delay.QuadPart = -10 * 1000 * 10; // 10ms
    KeDelayExecutionThread(KernelMode, FALSE, &delay); // 阻塞
    WdfRequestComplete(r, STATUS_SUCCESS);
}

此类操作应在 PASSIVE_LEVEL 执行,否则会引发系统崩溃。

4.4.2 异步模式中延迟处理与CompletionRoutine的应用

真正的生产级驱动往往采用异步模式,将请求放入工作项(Work Item)或DPC中处理:

WDFWORKITEM workItem;

EVT_WDF_WORKITEM OnDeferredProcessing;

VOID OnAsyncRead(WDFQUEUE q, WDFREQUEST r, size_t l) {
    WdfWorkItemEnqueue(workItem);
    // 不完成请求!稍后在WorkItem中完成
}

在WorkItem中:

VOID OnDeferredProcessing(WDFWORKITEM item) {
    // 模拟异步完成
    WdfRequestComplete(savedRequest, STATUS_SUCCESS);
}

也可使用 WdfRequestMarkCancelable + CancelRoutine 实现可取消的长时间操作。

综上,合理运用KMDF的I/O队列机制,是构建高性能、稳定可靠驱动的基础。

5. KMDF驱动生命周期与系统集成部署

5.1 关键事件回调函数的设计与实现

在KMDF驱动开发中,驱动的生命周期由一系列预定义的事件回调函数控制。这些回调是框架在特定系统状态变化时自动调用的入口点,开发者通过注册并实现这些回调来响应设备准备、电源管理、I/O处理等关键行为。

5.1.1 EvtDevicePrepareHardware:硬件资源映射与初始化

EvtDevicePrepareHardware 是设备从低功耗状态进入D0(全工作状态)前被调用的关键回调之一,其主要职责是 分配和映射硬件资源 ,例如内存映射I/O(MMIO)、中断向量、DMA通道等。

该回调原型如下:

NTSTATUS
EvtDevicePrepareHardware(
    _In_ WDFDEVICE Device,
    _In_ WDFCMRESLIST ResourcesRaw,
    _In_ WDFCMRESLIST ResourcesTranslated
)
{
    UNREFERENCED_PARAMETER(ResourcesRaw);

    NTSTATUS status = STATUS_SUCCESS;
    PHW_DEVICE_CONTEXT devCtx = GetDeviceContext(Device);
    PCM_PARTIAL_RESOURCE_DESCRIPTOR desc;

    // 遍历已翻译的资源列表
    for (ULONG i = 0; i < WdfCmResourceListGetCount(ResourcesTranslated); i++) {
        desc = WdfCmResourceListGetDescriptor(ResourcesTranslated, i);
        if (!desc) continue;

        switch (desc->Type) {
            case CmResourceTypeMemory:
                if (!devCtx->MemoryAddress) {
                    devCtx->MemoryAddress = (PUCHAR)MmMapIoSpaceEx(
                        desc->u.Memory.Start,
                        desc->u.Memory.Length,
                        PAGE_READWRITE | PAGE_NOCACHE
                    );
                    if (!devCtx->MemoryAddress) {
                        return STATUS_INSUFFICIENT_RESOURCES;
                    }
                }
                break;

            case CmResourceTypeInterrupt:
                status = WdfInterruptCreate(
                    Device,
                    &interruptConfig,
                    WDF_NO_OBJECT_ATTRIBUTES,
                    &devCtx->WdfInterrupt
                );
                if (!NT_SUCCESS(status)) {
                    return status;
                }
                break;

            default:
                break;
        }
    }

    return STATUS_SUCCESS;
}

参数说明
- ResourcesRaw :原始未经翻译的资源(如PCI BAR),通常不用于直接操作。
- ResourcesTranslated :经HAL翻译后的系统物理地址,可用于实际映射。

此阶段不允许执行耗时操作或阻塞调用,否则可能导致系统超时。

5.1.2 EvtDeviceD0Entry与电源状态切换协同

EvtDeviceD0Entry 在设备即将进入D0状态时调用,负责启动设备功能逻辑,如使能寄存器、初始化内部状态机、启动定时器等。

典型结构如下:

NTSTATUS
EvtDeviceD0Entry(
    _In_ WDFDEVICE Device,
    _In_ WDF_POWER_DEVICE_STATE PreviousState
)
{
    PHW_DEVICE_CONTEXT ctx = GetDeviceContext(Device);

    if (PreviousState == WdfPowerDeviceD3Final) {
        // 设备冷启动,需完全初始化
        InitializeHardware(ctx);
    } else {
        // 热恢复,仅恢复上下文
        RestoreDeviceContext(ctx);
    }

    // 启动周期性任务
    WdfTimerStart(ctx->MonitorTimer, WDF_REL_TIMEOUT_IN_MS(100));

    return STATUS_SUCCESS;
}

对应的 EvtDeviceD0Exit 则用于保存状态、停止活动操作,并确保设备安全进入低功耗模式。

5.1.3 EvtIoDefault未匹配请求的兜底处理

当没有明确路由到具体队列的I/O请求到达时,KMDF会将其发送至默认队列的 EvtIoDefault 回调。这常用于处理未知IOCTL或调试命令。

示例代码:

VOID
EvtIoDefault(
    _In_ WDFQUEUE Queue,
    _In_ WDFREQUEST Request,
    _In_ size_t OutputBufferLength,
    _In_ size_t InputBufferLength,
    _In_ ULONG IoControlCode
)
{
    WDFDEVICE device = WdfIoQueueGetDevice(Queue);
    PREQUEST_CONTEXT reqCtx = GetRequestContext(Request);

    reqCtx->Operation = L"Unknown IOCTL";
    reqCtx->Timestamp = KeQueryInterruptTime();

    TraceLoggingWrite(
        g_hProvider,
        "UnhandledIoctl",
        TLG_STRING("Operation", reqCtx->Operation),
        TLG_UINT64("Time", reqCtx->Timestamp),
        TLG_UINT32("IoCode", IoControlCode)
    );

    WdfRequestComplete(Request, STATUS_INVALID_DEVICE_REQUEST);
}

该机制增强了驱动健壮性,避免因遗漏请求而导致系统挂起。

5.2 驱动资源清理与对象安全释放

KMDF采用引用计数机制管理对象生命周期,但开发者仍需主动干预以确保资源正确释放。

5.2.1 WdfObjectDelete在引用计数管理中的作用

虽然多数对象会在父对象销毁时自动析构,但对于独立创建的对象(如WDF_TIMER、WDF_MEMORY),应显式调用 WdfObjectDelete 或设置自动删除属性。

WDF_OBJECT_ATTRIBUTES attr;
WDF_TIMER_CONFIG timerCfg;

WDF_TIMER_CONFIG_INIT(&timerCfg, TimerCallback);
WDF_OBJECT_ATTRIBUTES_INIT(&attr);
attr.ParentObject = Device;  // 绑定生命周期至设备

status = WdfTimerCreate(&timerCfg, &attr, &timer);

若未设置父对象,则必须手动删除:

if (timer) {
    WdfTimerStop(timer, TRUE);  // 强制停止
    WdfObjectDelete(timer);     // 显式释放
}

5.2.2 清理定时器、内存句柄与事件对象的最佳实践

资源类型 释放方式 注意事项
WDF_TIMER WdfTimerStop + WdfObjectDelete 停止后等待回调结束
WDF_MEMORY WdfObjectDelete 若使用非分页池,注意PoolTag一致性
WDF_INTERRUPT 框架自动清理 确保所有DPC已完成
自定义缓冲区 ExFreePoolWithTag 匹配分配时使用的Tag
KMUTEX/KSEMAPHORE WdfWaitLockRelease + WdfObjectDelete 不可在Dispatch级持有锁

此外,在 EvtDriverDeviceAdd 失败时也必须回滚已分配资源:

if (!NT_SUCCESS(status)) {
    if (buffer) ExFreePoolWithTag(buffer, 'BUF0');
    if (mutex) WdfObjectDelete(mutex);
    return status;
}

5.3 调试技术与运行时问题追踪

5.3.1 WinDbg + KDNET内核调试环境搭建

使用以太网进行KDNET调试已成为现代Windows驱动调试的标准方式。

步骤如下

  1. 在目标机启用网络内核调试:
    cmd bcdedit /debug on bcdedit /dbgsettings NET HOSTIP:192.168.1.100 PORT:50000 KEY:1.a2b3c4d5.e6f7g8h9

  2. 主机端WinDbg Preview连接:
    - 调试 > 内核调试 > Net
    - 输入 Port:50000, Key:1.a2b3c4d5.e6f7g8h9

  3. 加载符号文件:
    .sympath SRV*C:\Symbols*https://msdl.microsoft.com/download/symbols .reload

常见调试命令:

命令 功能
!drvobj MyDriver 查看驱动对象信息
!wdfhandle 列出所有WDF句柄
!poolfind 'TAG0' 查找指定Tag的内存池块
lm m MyDriver 定位模块加载基址

5.3.2 使用TraceLogging API输出结构化驱动日志

TraceLogging 提供高效、低开销的日志记录能力,支持 ETW(Event Tracing for Windows)。

初始化提供者:

TRACELOGGING_DEFINE_PROVIDER(
    g_hProvider,
    "MyDriverProvider",
    (0x12345678, 0xabcd, 0xef01, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0),
    TraceLoggingOptionMicrosoftTelemetry()
);

// 注册
TraceLoggingRegister(g_hProvider);

写入日志:

TraceLoggingWrite(
    g_hProvider,
    "DeviceStarted",
    TraceLoggingLevel(WINEVENT_LEVEL_INFO),
    TraceLoggingKeyword(MYDRIVER_KEYWORD_BASE),
    TraceLoggingString(UuidToString(&guid), "InterfaceGuid"),
    TraceLoggingUInt32(threadId, "ThreadId")
);

可用工具 Windows Performance Analyzer (WPA) 分析 .etl 日志文件,实现可视化追踪。

5.4 INF文件配置与驱动安装部署流程

5.4.1 INF文件结构解析:[Version], [Manufacturer], [Models]节含义

INF 是文本格式的安装指令文件,指导PnP管理器如何加载和配置驱动。

[Version]
Signature="$WINDOWS NT$"
Class=System
ClassGuid={4d36e97d-e325-11ce-bfc1-08002be10318}
Provider=%ManufacturerName%
CatalogFile=MyDriver.cat
DriverVer=2024/04/01,1.0.0.0

[Manufacturer]
%ManufacturerName%=Standard,NTamd64

[Standard.NTamd64]
"My Virtual Device" = MyDevice_Install, ROOT\MyVirtualDevice

[MyDevice_Install]
CopyFiles=MyDriver_CopyFiles

[MyDevice_Install.Services]
AddService=MyDriver,,MyDriver_Service_Inst

[MyDriver_Service_Inst]
ServiceType=1
StartType=3
ErrorControl=1
ServiceBinary=%12%\MyDriver.sys

[SourceDisksNames]
1=%DiskName%

[SourceDisksFiles]
MyDriver.sys=1

[Strings]
ManufacturerName="Contoso Ltd."
DiskName="Driver Installation Disk"

%12% 表示 \System32\drivers 目录。

5.4.2 利用pnputil.exe进行测试签名驱动的注册与加载

对于未正式签署的测试驱动,可通过 pnputil 注册:

# 添加驱动包(生成.cab)
makecab MyDriver.inf MyDriver.cab

# 安装驱动到驱动存储
pnputil /add-driver MyDriver.cab /install

# 查看已安装驱动
pnputil /enum-drivers

# 删除指定OEM条目
pnputil /delete-driver oemX.inf

注意:系统需处于“测试签名模式”:
cmd bcdedit /set testsigning on

驱动安装成功后,可在设备管理器中查看“System devices”下的虚拟设备实例。

flowchart TD
    A[编写INF文件] --> B[打包为.cab]
    B --> C[pnputil /add-driver]
    C --> D[系统验证签名策略]
    D --> E{测试签名?}
    E -- 是 --> F[bcdedit启用testsigning]
    E -- 否 --> G[需EV证书签名]
    F --> H[重启生效]
    G --> H
    H --> I[设备管理器识别设备]

完整的部署流程还包括数字签名、WHQL认证以及CI/CD自动化测试集成。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Kernel-Mode Driver Framework (KMDF) 是微软为简化Windows内核模式驱动开发提供的现代编程框架,具备高可靠性与面向对象特性。本文详细讲解KMDF驱动的调用机制与核心知识点,涵盖驱动初始化、设备对象创建、I/O请求处理、电源管理、同步控制及调试方法等内容。通过系统性介绍DriverEntry入口点、IRP处理流程、关键回调函数和资源管理策略,帮助开发者掌握KMDF驱动的完整生命周期与实际开发技巧,适用于各类硬件驱动项目开发与系统级编程实践。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐