Windows驱动-IRP派遣函数与设备交互
Windows驱动 - IRP派遣函数与设备交互
IRP有两个基本的属性, 分别是 MajorFunction 和 MinorFunction , 分别记录IRP的主类型和次类型.
操作系统根据 MajorFunction 将IRP派遣到对应的派遣函数中, 在派遣函数中根据 MinorFunction 来判断具体的操作.
文件I/O的相关函数, 和内核中相关函数例如 CreateFile/ZwCreateFile , ReadFile/ZwReadFile , WriteFile/ZwWriteFile 等函数会使操作系统产出 IRP_MJ_CREATE , IRP_MJ_READ , IRP_MJ_WRITE 等IRP.
处理如下
pDriverObject->MajorFunction[IRP_MJ_CREATE] = MyCreate;
pDriverObject->MajorFunction[IRP_MJ_READ] = MyRead;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = MyWrite;一个简单的派遣函数如下
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 请求流程如下
用户模式调用
ReadFileReadFile调用 ntdllNtReadFilentdll
NtReadFile进入内核模式, 调用系统服务ntoskrnl.exe中的NtReadFileNtReadFile创建IRP_MJ_WRITE, 然后等待这个IRP完成, 此时线程进入睡眠状态派遣函数使用
IoCompleteRequest完成IRP请求
应用程序读取设备
设备可以通过符号链接对设备进行访问, 驱动通过 IoCreateSymbolicLink 创建符号链接
UNICODE_STRING uDevName, uSymLink;
RtlInitUnicodeString(&uDevName, L"\\Driver\\MyDevice");
RtlInitUnicodeString(&uSymLink, L"\\??\\MyDeviceLink");
NTSTATUS status = IoCreateSymbolicLink(&uSymLink, &uDevName);在用户模式下就变成了 \\.\MyDeviceLink , C语言字符串则是 \\\\.\\MyDeviceLink
则可以通过如下方式打开设备
HANDLE hDevice = CreateFile(L"\\\\.\\MyDeviceLink",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0,
NULL
);读写设备有三种方式, 分别为 缓冲区读写 DO_BUFFERED_IO , 直接读写 DO_DIRECT_IO , 其他方式 0
在创建设备对象时, 可以指定设备对象的读写方式
pDeviceObject->Flags |= DO_BUFFERED_IO;缓冲区读写设备
缓冲区读写是复制型 IO:用户态缓冲与内核中间缓冲分离,数据由 I/O 管理器在两者之间复制
设备创建时:在 IoCreateDevice 后,将设备对象的 Flags 设置为 DO_BUFFERED_IO。
应用层发起 IO:调用
ReadFile或WriteFile时提供用户态缓冲区及长度。写设备时,系统处理:
按写入长度分配内核态中间缓冲,将其指针写入 IRP 的
AssociatedIrp.SystemBuffer。将
WriteFile用户缓冲中的数据复制到该内核缓冲。
读设备时,系统处理:
按
ReadFile指定长度分配内核态中间缓冲,将其指针写入 IRP 的AssociatedIrp.SystemBuffer。IRP 完成后,将
AssociatedIrp.SystemBuffer中的数据复制回用户缓冲,并释放内核分配的中间缓冲。
驱动处理:只对
AssociatedIrp.SystemBuffer进行读写;用户缓冲与内核中间缓冲之间的复制及内核缓冲的分配、释放由系统完成。
一个读设备的派遣函数如下
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: 调用
ReadFile或WriteFile时提供缓冲区系统处理:
锁定用户态虚拟内存, 防止换页和释放
使用 MDL(内存描述符表) 记录这段内存的物理页信息 (虚拟地址, 偏移, 大小)
将用户态缓冲区重新映射到内核态地址空间,内核态地址在进程切换时保持不变。
驱动处理: 通过MDL获取内核态映射地址, 直接读写物理内存, 无需复制
一个读设备的派遣函数如下
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 控制码, 然后传递给驱动程序, 然后派遣函数会根据这个控制码来判断具体操作
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 值, 其规定如下
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 | 设备类型 | 和 |
Function | 功能码 | 自行定义: 0x800~0xFFF, 微软保留 0x0000到0x7FFF |
Method | 方法 |
|
Access | 访问权限 | 没有特殊要求一般使用 |