3. 派遣函数基础:IRP与派遣函数概念、MajorFunction与MinorFunction、派遣函数注册机制
3.1 IRP 与派遣函数概念
在 Windows 内核中,I/O 请求包(IRP, I/O Request Packet)是驱动与操作系统之间传递 I/O 操作的核心数据结构。当用户模式应用程序调用 ReadFile、WriteFile、DeviceIoControl 等 API 时,I/O 管理器会创建一个对应的 IRP,并将其发送到目标驱动对象。
派遣函数(Dispatch Function) 是驱动中处理特定类型 IRP 的回调例程。每个派遣函数对应一个 MajorFunction 代码,驱动通过注册这些函数来声明自己能够处理哪些 I/O 操作。
下图展示了 IRP 从用户态到驱动派遣函数的完整流转路径:
3.2 MajorFunction 与 MinorFunction
IRP 的主功能码 MajorFunction 标识 I/O 操作的大类,而 MinorFunction 则在大类下进一步细分操作。MinorFunction 通常用于 SCSI、键盘、鼠标、IR 等类驱动或总线驱动,用于区分同一大类下的不同子操作。
| MajorFunction 常量 | 数值 | 说明 | 常见 MinorFunction 示例 |
|---|---|---|---|
IRP_MJ_CREATE |
0x00 | 打开设备(CreateFile) | 无 |
IRP_MJ_CLOSE |
0x02 | 关闭设备(CloseHandle) | 无 |
IRP_MJ_READ |
0x03 | 从设备读取数据 | 无 |
IRP_MJ_WRITE |
0x04 | 向设备写入数据 | 无 |
IRP_MJ_DEVICE_CONTROL |
0x0E | 设备 I/O 控制(DeviceIoControl) | 无 |
IRP_MJ_INTERNAL_DEVICE_CONTROL |
0x0F | 内部设备 I/O 控制(驱动间通信) | SCSI、HID 等类驱动定义 |
IRP_MJ_PNP |
0x1B | 即插即用事件 | IRP_MN_START_DEVICE、IRP_MN_STOP_DEVICE、IRP_MN_REMOVE_DEVICE |
IRP_MJ_POWER |
0x1C | 电源管理事件 | IRP_MN_SET_POWER、IRP_MN_QUERY_POWER |
IRP_MJ_SYSTEM_CONTROL |
0x1F | WMI 系统控制 | IRP_MN_QUERY_ALL_DATA、IRP_MN_CHANGE_SINGLE_INSTANCE |
!irp <IRP 地址> 可以查看 IRP 的 MajorFunction 和 MinorFunction。对于 PnP 和 Power IRP,MinorFunction 是判断具体事件类型的关键字段。
下图展示了 MajorFunction 与 MinorFunction 在 IRP 中的层级关系:
3.3 派遣函数注册机制
在 KMDF 框架中,派遣函数的注册通过 WdfDeviceCreate 之后的 WdfDeviceInitAssignWdmIrpPreprocessCallback 或更常见的 WdfDeviceIoControl、WdfDeviceRead 等框架封装方法完成。但底层机制仍然基于 WDM 的 MajorFunction 表。
KMDF 驱动注册派遣函数的典型步骤如下:
- DriverEntry 入口: 调用
WdfDriverCreate创建 WDF 驱动对象。 - EvtDriverDeviceAdd 回调: 在此回调中创建 WDF 设备对象。
- 配置 WDFDEVICE_INIT: 通过
WdfDeviceInitAssignWdmIrpPreprocessCallback注册需要预处理的自定义 MajorFunction。 - 创建设备: 调用
WdfDeviceCreate。 - 注册 I/O 回调: 使用
WdfDeviceIoQueueCreate创建队列,并通过WdfIoQueueAssignForwardProgressPolicy或直接设置WDF_IO_QUEUE_CONFIG的EvtIoRead、EvtIoWrite、EvtIoDeviceControl等成员。
DEVICE_OBJECT->MajorFunction 数组。KMDF 框架会接管该数组,直接修改会导致不可预测的行为。应始终使用框架提供的注册接口。
以下代码片段展示了 KMDF 驱动中注册派遣函数的典型模式:
// DriverEntry 示例
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
WDF_DRIVER_CONFIG config;
WDFDRIVER driver;
WDF_DRIVER_CONFIG_INIT(&config, EvtDriverDeviceAdd);
return WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, &config, &driver);
}
// EvtDriverDeviceAdd 回调
NTSTATUS EvtDriverDeviceAdd(WDFDRIVER Driver, PWDFDEVICE_INIT DeviceInit)
{
WDF_IO_QUEUE_CONFIG queueConfig;
WDFQUEUE queue;
WDFDEVICE device;
// 1. 创建 WDF 设备
NTSTATUS status = WdfDeviceCreate(&DeviceInit, WDF_NO_OBJECT_ATTRIBUTES, &device);
if (!NT_SUCCESS(status)) return status;
// 2. 配置默认 I/O 队列
WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&queueConfig, WdfIoQueueDispatchSequential);
queueConfig.EvtIoRead = EvtIoRead;
queueConfig.EvtIoWrite = EvtIoWrite;
queueConfig.EvtIoDeviceControl = EvtIoDeviceControl;
// 3. 创建队列并关联设备
status = WdfIoQueueCreate(device, &queueConfig, WDF_NO_OBJECT_ATTRIBUTES, &queue);
return status;
}
// 派遣函数实现
VOID EvtIoRead(WDFQUEUE Queue, WDFREQUEST Request, size_t Length)
{
// 处理读请求
WdfRequestCompleteWithInformation(Request, STATUS_SUCCESS, Length);
}
VOID EvtIoWrite(WDFQUEUE Queue, WDFREQUEST Request, size_t Length)
{
// 处理写请求
WdfRequestCompleteWithInformation(Request, STATUS_SUCCESS, Length);
}
VOID EvtIoDeviceControl(WDFQUEUE Queue, WDFREQUEST Request, size_t OutputBufferLength, size_t InputBufferLength, ULONG IoControlCode)
{
// 处理设备 I/O 控制
WdfRequestComplete(Request, STATUS_SUCCESS);
}
下图展示了 KMDF 框架下派遣函数注册的完整架构:
3.4 派遣函数注册的底层原理
KMDF 框架在 WdfDeviceCreate 内部会执行以下操作:
- 调用
IoCreateDevice创建 WDMDEVICE_OBJECT。 - 将驱动提供的
EvtIoRead、EvtIoWrite等回调与框架内部的 WDM 派遣函数 绑定。 - 框架内部的派遣函数(如
kmdf!FxPkgIo::DispatchRead)会解析 IRP,然后调用对应的EvtIoXxx回调。
下表对比了 WDM 与 KMDF 在派遣函数注册上的差异:
| 特性 | WDM 驱动 | KMDF 驱动 |
|---|---|---|
| 注册方式 | 直接赋值 DriverObject->MajorFunction[IRP_MJ_READ] = MyRead; |
通过 WDF_IO_QUEUE_CONFIG 设置 EvtIoRead 等回调 |
| IRP 处理 | 手动处理 IRP 栈位置、完成状态 | 框架自动管理 IRP 生命周期 |
| 灵活性 | 完全控制,但易出错 | 受框架约束,但更安全 |
| 调试复杂度 | 需要跟踪 IRP 手动流转 | 框架封装后,调试需理解框架内部派遣 |
| 推荐场景 | 需要极致性能或特殊 IRP 处理 | 大多数通用驱动开发 |
!wdfdevice 0x<WDFDEVICE 句柄> 可以查看 KMDF 设备对象的详细信息,包括关联的 I/O 队列和已注册的 EvtIo 回调地址。使用 !wdfqueue 0x<WDFQUEUE 句柄> 可以查看队列的派遣函数配置。