Windows驱动-IRP派遣函数与设备交互

Windows驱动 - IRP派遣函数与设备交互

IRP有两个基本的属性, 分别是 MajorFunctionMinorFunction , 分别记录IRP的主类型和次类型.

操作系统根据 MajorFunction 将IRP派遣到对应的派遣函数中, 在派遣函数中根据 MinorFunction 来判断具体的操作.

文件I/O的相关函数, 和内核中相关函数例如 CreateFile/ZwCreateFile , ReadFile/ZwReadFile , WriteFile/ZwWriteFile 等函数会使操作系统产出 IRP_MJ_CREATE , IRP_MJ_READ , IRP_MJ_WRITE 等IRP.

处理如下

C
pDriverObject->MajorFunction[IRP_MJ_CREATE] = MyCreate;
pDriverObject->MajorFunction[IRP_MJ_READ] = MyRead;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = MyWrite;

一个简单的派遣函数如下

C
NTSTATUS DispatchRoutine(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
{
    KdPrint(("Enter Dispatch Routine\n"));
    pIrp->IoStatus.Status = STATUS_SUCCESS;                        // 设置IRP完成状态
    pIrp->IoStatus.Information = 0;                                // 设置IRP操作字节
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    KdPrint(("Exit Dispatch Routine\n"));
    return STATUS_SUCCESS;
}

一个 ReadFile 请求流程如下

  1. 用户模式调用 ReadFile

  2. ReadFile 调用 ntdll NtReadFile

  3. ntdll NtReadFile 进入内核模式, 调用系统服务 ntoskrnl.exe 中的 NtReadFile

  4. NtReadFile 创建 IRP_MJ_WRITE , 然后等待这个IRP完成, 此时线程进入睡眠状态

  5. 派遣函数使用 IoCompleteRequest 完成IRP请求

应用程序读取设备

设备可以通过符号链接对设备进行访问, 驱动通过 IoCreateSymbolicLink 创建符号链接

C
UNICODE_STRING uDevName, uSymLink;
RtlInitUnicodeString(&uDevName, L"\\Driver\\MyDevice");
RtlInitUnicodeString(&uSymLink, L"\\??\\MyDeviceLink");
NTSTATUS status = IoCreateSymbolicLink(&uSymLink, &uDevName);

在用户模式下就变成了 \\.\MyDeviceLink , C语言字符串则是 \\\\.\\MyDeviceLink

则可以通过如下方式打开设备

C
HANDLE hDevice = CreateFile(L"\\\\.\\MyDeviceLink",
                            GENERIC_READ | GENERIC_WRITE,
                            0,
                            NULL,
                            OPEN_EXISTING,
                            0,
                            NULL
);

读写设备有三种方式, 分别为 缓冲区读写 DO_BUFFERED_IO , 直接读写 DO_DIRECT_IO , 其他方式 0

在创建设备对象时, 可以指定设备对象的读写方式

C
pDeviceObject->Flags |= DO_BUFFERED_IO;

缓冲区读写设备

缓冲区读写是复制型 IO:用户态缓冲与内核中间缓冲分离,数据由 I/O 管理器在两者之间复制

  • 设备创建时:在 IoCreateDevice 后,将设备对象的 Flags 设置为 DO_BUFFERED_IO。

  • 应用层发起 IO:调用 ReadFileWriteFile 时提供用户态缓冲区及长度。

  • 写设备时,系统处理:

    1. 按写入长度分配内核态中间缓冲,将其指针写入 IRP 的 AssociatedIrp.SystemBuffer

    2. WriteFile 用户缓冲中的数据复制到该内核缓冲。

  • 读设备时,系统处理:

    1. ReadFile 指定长度分配内核态中间缓冲,将其指针写入 IRP 的 AssociatedIrp.SystemBuffer

    2. IRP 完成后,将 AssociatedIrp.SystemBuffer 中的数据复制回用户缓冲,并释放内核分配的中间缓冲。

  • 驱动处理:只对 AssociatedIrp.SystemBuffer 进行读写;用户缓冲与内核中间缓冲之间的复制及内核缓冲的分配、释放由系统完成。

一个读设备的派遣函数如下

C
NTSTATUS ReadDispatchRoutine(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
{
    KdPrint(("Enter Read Dispatch Routine\n"));
    PIO_STACK_LOCATION pStack = IoGetCurrentIrpStackLocation(pIrp);  // 获取当前IRP堆栈位置
    ULONG readLength = pStack->Parameters.Read.Length;                    // 获取读取长度
    RtlFillMemory(pIrp->AssociatedIrp.SystemBuffer, readLength, 0xAA);    // 填充IRP缓冲区
    pIrp->IoStatus.Status = STATUS_SUCCESS;                               // 设置IRP完成状态
    pIrp->IoStatus.Information = readLength;                              // 设置IRP操作字节
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);          // 完成IRP
    KdPrint(("Exit Read Dispatch Routine\n"));
    return STATUS_SUCCESS;
}

直接读写设备

直接读写设备是一种 零拷贝 IO方式, 核心是让用户态缓冲区和内核态缓冲区共享同一段物理内存

  • 设备创建时: 在IoCreateDevice后, 将设备对象的Flags设置为DO_DIRECT_IO(而非DO_BUFFERED_IO)。

  • 应用层发起IO: 调用 ReadFileWriteFile 时提供缓冲区

  • 系统处理:

    1. 锁定用户态虚拟内存, 防止换页和释放

    2. 使用 MDL(内存描述符表) 记录这段内存的物理页信息 (虚拟地址, 偏移, 大小)

    3. 将用户态缓冲区重新映射到内核态地址空间,内核态地址在进程切换时保持不变。

  • 驱动处理: 通过MDL获取内核态映射地址, 直接读写物理内存, 无需复制

一个读设备的派遣函数如下

C
NTSTATUS ReadDirectDispatchRoutine(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
{
    KdPrint(("Enter Read Direct Dispatch Routine\n"));

    PIO_STACK_LOCATION pStack = IoGetCurrentIrpStackLocation(pIrp); // 获取当前IRP堆栈位置
    ULONG readLength = pStack->Parameters.Read.Length;              // 获取读取长度
    ULONG mdlLen = MmGetMdlByteCount(pIrp->MdlAddress);             // 获取MDL长度
    if (mdlLen != readLength)
    {
        pIrp->IoStatus.Information = 0; // 长度不同, 判定为失败
        pIrp->IoStatus.Status = STATUS_UNSUCCESSFUL;
    }
    else
    {
        PVOID kernalAddress = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);  // 获取内核地址
        RtlFillMemory(kernalAddress, readLength, 0xAA);
        pIrp->IoStatus.Status = STATUS_SUCCESS;
        pIrp->IoStatus.Information = readLength;
    }
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    KdPrint(("Exit Read Direct Dispatch Routine\n"));
    return pIrp->IoStatus.Status;
}

其他方式读写

如果设置为0则为其他方式读写, 此时派遣函数将会直接读写用户模式下提供的缓冲地址

Caution
只有在同线程上下文中才能使用, 而且需要过滤空指针和无效地址, 以及地址的可读可写性

IO设备控制操作

除了读写设备外, 应用程序还可以通过 DeviceIoControl 函数对设备进行控制操作, 它会创建一个 IRP_MJ_DEVICE_CONTROL 类型的IRP.

通过自定义一个 I/O 控制码, 然后传递给驱动程序, 然后派遣函数会根据这个控制码来判断具体操作

C
BOOL DeviceIoControl(
  [in]                HANDLE       hDevice,           // 设备句柄
  [in]                DWORD        dwIoControlCode,   // 控制码
  [in, optional]      LPVOID       lpInBuffer,        // 输入缓冲区
  [in]                DWORD        nInBufferSize,     // 输入缓冲区大小
  [out, optional]     LPVOID       lpOutBuffer,       // 输出缓冲区
  [in]                DWORD        nOutBufferSize,    // 输出缓冲区大小
  [out, optional]     LPDWORD      lpBytesReturned,   // 返回字节数
  [in, out, optional] LPOVERLAPPED lpOverlapped       // 是否OVERLAP操作
);

其中 dwIoControlCode 也称 IOCTL 值, 其规定如下

C
typedef struct _IO_CONTROL_CODE
{
    ULONG Method      : 2;    // Bit 0~1   传输方式 (2位)
    ULONG Function    : 12;   // Bit 2~13  功能码   (12位)
    ULONG Access      : 2;    // Bit 14~15 访问权限 (2位)
    ULONG DeviceType  : 16;   // Bit 16~31 设备类型 (16位)
} IO_CONTROL_CODE, *PIO_CONTROL_CODE;

#define CTL_CODE( DeviceType, Function, Method, Access ) (                 \
    ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)
类型描述备注

DeviceType

设备类型

IoCreateDeviceDeviceType 相同, 形如 FILE_DEVICE_XX

Function

功能码

自行定义: 0x800~0xFFF, 微软保留 0x0000到0x7FFF

Method

方法

METHOD_BUFFERED , METHOD_IN_DIRECT , METHOD_OUT_DIRECT , METHOD_NEITHER

Access

访问权限

没有特殊要求一般使用 FILE_ANY_ACCESS


Windows驱动-IRP派遣函数与设备交互
https://simonkimi.githubio.io/posts/20260325080337/
作者
simonkimi
发布于
2026年3月25日
许可协议