Windows内核-7-IRP和派遣函数
作者:互联网
Windows内核-7-IRP和派遣函数
IRP以及派遣函数是Windows中非常重要的概念。IRP 是I/O Request Pocket的简称,意思是I/O操作的请求包,Windows中所有User和Kernel之间的交流都会被封装成一个IRP结构体,然后不同的IRP会被派遣到不同的派遣函数里面,通过派遣函数来实现I/O操作。
IRP
typedef struct _IRP {
CSHORT Type;
USHORT Size;
PMDL MdlAddress;
ULONG Flags;
union {
struct _IRP *MasterIrp;
__volatile LONG IrpCount;
PVOID SystemBuffer;
} AssociatedIrp;
LIST_ENTRY ThreadListEntry;
IO_STATUS_BLOCK IoStatus;
KPROCESSOR_MODE RequestorMode;
BOOLEAN PendingReturned;
CHAR StackCount;
CHAR CurrentLocation;
BOOLEAN Cancel;
KIRQL CancelIrql;
CCHAR ApcEnvironment;
UCHAR AllocationFlags;
union {
PIO_STATUS_BLOCK UserIosb;
PVOID IoRingContext;
};
PKEVENT UserEvent;
union {
struct {
union {
PIO_APC_ROUTINE UserApcRoutine;
PVOID IssuingProcess;
};
union {
PVOID UserApcContext;
#if ...
_IORING_OBJECT *IoRing;
#else
struct _IORING_OBJECT *IoRing;
#endif
};
} AsynchronousParameters;
LARGE_INTEGER AllocationSize;
} Overlay;
__volatile PDRIVER_CANCEL CancelRoutine;
PVOID UserBuffer;
union {
struct {
union {
KDEVICE_QUEUE_ENTRY DeviceQueueEntry;
struct {
PVOID DriverContext[4];
};
};
PETHREAD Thread;
PCHAR AuxiliaryBuffer;
struct {
LIST_ENTRY ListEntry;
union {
struct _IO_STACK_LOCATION *CurrentStackLocation;
ULONG PacketType;
};
};
PFILE_OBJECT OriginalFileObject;
} Overlay;
KAPC Apc;
PVOID CompletionKey;
} Tail;
} IRP;
IRP这种机制类似于Windows的消息机制,驱动在接受到IRP之后会根据IRP的不同类型分配给不同类型的派遣函数来处理IRP。
IRP不是单独的,只要创建了IRP就会跟着创建IRP的I/O栈,有一个栈是给内核驱动用的:
驱动需要调用IoGetCurrentIrpStackLocation函数来获取内驱驱动对应的I/O栈。
例如:
auto stack = IoGetCurrentIrpStackLocation(Irp)
该API返回一个IO_STACK_LOCATION 结构体:
typedef struct _IO_STACK_LOCATION {
UCHAR MajorFunction;
UCHAR MinorFunction;
UCHAR Flags;
UCHAR Control;
union {
...
...
...
...
...
} Parameters;
PDEVICE_OBJECT DeviceObject;
PFILE_OBJECT FileObject;
PIO_COMPLETION_ROUTINE CompletionRoutine;
PVOID Context;
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;
这个结构体有两个重要的属性,分别是MajorFunction和MinorFunction,分别记录了IRP的主类型和子类型,操作系统根据MajorFunction来将IRP派遣到不同的派遣函数里面去处理,而还可以继续利用MinorFunction来判断更多的内容,基本上MajorFunction用的比较多。
设置IRP和派遣函数
在DriverEntry驱动对象里面有一个函数指针数组MajorFunction就是来记录派遣函数的地址,来让派遣函数和IRP一一对应,这个数组里面采用宏定义来将每个 IRP和派遣函数通过数组的索引来一一对应:
#define IRP_MJ_CREATE 0x00
#define IRP_MJ_CREATE_NAMED_PIPE 0x01
#define IRP_MJ_CLOSE 0x02
#define IRP_MJ_READ 0x03
#define IRP_MJ_WRITE 0x04
#define IRP_MJ_QUERY_INFORMATION 0x05
#define IRP_MJ_SET_INFORMATION 0x06
#define IRP_MJ_QUERY_EA 0x07
#define IRP_MJ_SET_EA 0x08
#define IRP_MJ_FLUSH_BUFFERS 0x09
#define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a
#define IRP_MJ_SET_VOLUME_INFORMATION 0x0b
#define IRP_MJ_DIRECTORY_CONTROL 0x0c
#define IRP_MJ_FILE_SYSTEM_CONTROL 0x0d
#define IRP_MJ_DEVICE_CONTROL 0x0e
#define IRP_MJ_INTERNAL_DEVICE_CONTROL 0x0f
#define IRP_MJ_SHUTDOWN 0x10
#define IRP_MJ_LOCK_CONTROL 0x11
#define IRP_MJ_CLEANUP 0x12
#define IRP_MJ_CREATE_MAILSLOT 0x13
#define IRP_MJ_QUERY_SECURITY 0x14
#define IRP_MJ_SET_SECURITY 0x15
#define IRP_MJ_POWER 0x16
#define IRP_MJ_SYSTEM_CONTROL 0x17
#define IRP_MJ_DEVICE_CHANGE 0x18
#define IRP_MJ_QUERY_QUOTA 0x19
#define IRP_MJ_SET_QUOTA 0x1a
#define IRP_MJ_PNP 0x1b
这些每一个都有特定的意思,比较常用的有:
IRP_MJ_CREATE //创建 和CreateFile对应
IRP_MJ_READ //读取 和ReadFile对应
IRP_MJ_WRITE //写入 和WriteFile对应
IRP_MJ_CLOSE //关闭 和CloseFile对应
所有的派遣函数都有一个原型:
typedef NTSTATUS DRIVER_DISPATCH (
_In_ PDEVICE_OBJECT DeviceObject,
_Inout_ PIRP Irp);
//typedef可以省去,名字自己取
IRP传递流程
I/O系统是以设备对象为中心,而不是以驱动对象为中心的。IRP可以在设备对象中传来传去:
但是不管是在怎么传递,最后都必须把这个IRP请求结束,给它完成。
完成必备操作:
NTSTATUS SysMonRead(PDEVICE_OBJECT, PIRP Irp) {
Irp->IoStatus.Status = status;//设置状态
Irp->IoStatus.Information = count;//统计处理的字节数
IoCompleteRequest(Irp, IO_NO_INCREMENT);//完成IO操作的必须返回函数
return status;
}
IoCompleteRequest这个API需要返回一个IRP大家应该没问题,但是第二个参数就比较复杂了,第二个参数是返回之后的线程级,这是因为一个线程在执行IRP的时候会等待。
这里以ReadFile为例,ReadFile函数会调用ntdll中的NtReadFile函数,然后ntdll中的NtReadFile函数又调用内核的NtReadFile函数,然后内核的NtReadFile函数创建关于Read这个类型的IO_MJ_READ类型的IRP再将它发送到内核的对应IRP的派遣函数里面,然后等待IRP对应派遣函数执行完之后再返回。所以需要设置当返回后线程又重新执行了的线程级。
User操作设备对象
前面我们说了,User只能通过符号链接来操作设备对象进行I/O交互,所以User只需要在User的API下面把通常使用的文件路径改成符号链接就好了。
比如:
CreateFile(L"\\\\.\\test", GENERIC_READ, 0, nullptr, OPEN_EXISTING, 0, nullptr)
注意C语言是有转义字符的,所以这个第一个参数看起来怪怪的。
读写方式
IRP和派遣函数是用来进行I/O操作的,I/O操作就是读写,所以很自然的有了这个读写方式的环境。
一般读写方式有三种:缓冲I/O,直接I/O,和其它方式。三种方式对应的Flags分别是DO_BUFFERED_IO、DO_DIRECT_IO和0。都在DeviceObject设备对象里面添加。
例如:
DeviceObject->Flags |= 0;
注意:并不是直接赋值
缓冲I/O
读写操作通常是WriteFile和ReadFile这类API,这类API会要求添加缓冲区指针和缓冲区大小作为函数参数,然后WriteFile/ReadFile将这段内存的数据传递给驱动程序。由于这段缓冲区是用户模式的内存地址,所以直接使用会非常危险,因为Windows是多进程操作系统,有可能在你用的时候就被别的进程改了,所以直接使用非常危险。
而缓冲I/O的原理就是,在内核模式下开辟一个缓冲空间来存储该缓冲区内容,然后读取的时候先放到内核缓冲区里面,再来进行复制和赋值操作。
比如:当调用ReadFile/WriteFile时,操作系统提供内核模式下的一段地址来存放User在WriteFile里面配置的Buffer,然后当IRP请求结束,内核区域的Buffer就会被拷贝到User/Kernel里面。
1 I/O 管理器从非分页池中分配一个与用户缓冲区大小相同的缓冲区。它将指向这个新缓冲区的指针存储在 IRP 的 AssociatedIrp->SystemBuffer 成员中。 (缓冲区大小可以在当前 I/O 堆栈位置的 Parameters.Read.Length 或 Parameters.Write.Length 中找到。)
2 对于写请求,I/O 管理器将用户的缓冲区复制到系统缓冲区。
3 现在才调用驱动程序的调度例程。驱动程序可以直接使用系统缓冲区指针而无需任何检查,因为缓冲区在系统空间中(它的地址是绝对的 - 从任何进程上下文中都一样),并且在任何 IRQL 中,因为缓冲区是从非分页池分配的,所以它不能被调出。
4 一旦驱动完成IRP(IoCompleteRequest),I/O管理器(对于读请求)将系统缓冲区复制回用户的缓冲区(复制的大小由IRP中设置的IoStatus.Information字段决定)司机)。
5 最后,I/O 管理器释放系统缓冲区。
图文讲解一下ReadFile的整体流程:(WriteFile以此类推)
这里是User下一个Buffer缓冲区:
操作系统知道是缓冲I/O后开辟了一个内核缓冲区,然后由WriteFile/ReadFile创建的Irp->AssociatedIrp.SystemBuffer来存储记录
然后驱动访问系统空间往里面写东西
操作系统将系统空间的内容拷贝到User缓冲区里。
拷贝完了,释放系统空间内存。
派遣函数中可以通过Irp的Parameters.Read.Length来知道User请求多少字节,也可以通过Parameters.Write.Length来知道要写入多少字节,但是真正执行了多少是通过Irp的IoStatus.Information字段来返回,所以WriteFile/ReadFile函数中各个参数是意义就可以解释通透了。
但是这样的缺点是会造成内存空间开销,因为内核空间是公有的大家都得用,而且由于大量数据的复制也会影响效率,所以针对比较小的内容采用缓冲I/O可以采取这种办法,别的还是用直接I/O吧。
//缓冲I/O的例子:
NTSTATUS SysMonRead(PDEVICE_OBJECT, PIRP Irp) {
auto status = STATUS_SUCCESS;
auto stack = IoGetCurrentIrpStackLocation(Irp);//得到当前的IRP栈
auto ulReadLength = stack->Parameters.Read.Length;//得到要读取的字节数
//完成IRP操作
Irp->IoStatus.Status = status;//设置IRP完成状态
Irp->IoStatus.Information = ulReadLength;//设置实际完成的IRP字节数
memset(Irp->AssociatedIrp.SystemBuffer, 0xAA, ulReadLength);//拷贝内容到系统地址空间
IoCompleteRequest(Irp, IO_NO_INCREMENT);//完成IRP操作
return status;
}
User下的就不用写了吧,hh,偷个懒。
直接I/O
直接I/O用了另一个办法来规避风险。
1 I/O 管理器确保用户的缓冲区有效。
2 将其映射到物理内存中,然后将缓冲区锁定在内存中,因此在另行通知之前无法将其调出。这解决了缓冲区访问的问题之一——不会发生页面错误,因此在任何 IRQL 中访问缓冲区都是安全的。
3 I/O 管理器构建内存描述符列表 (MDL),这是一种知道缓冲区如何映射到 RAM 的数据结构。该数据结构的地址存储在 IRP 的 MdlAddress 字段中。
4 此时,驱动程序调用派遣函数。因为用户的缓冲区被锁定在 RAM 中,不能从任意线程访问。所以当驱动程序需要访问缓冲区时,它必须调用将同一用户缓冲区映射到系统地址的函数。由于该地址被锁进了系统中所以,该地址在任何进程上下文中都是有效的。所以本质上,我们得到了到同一个缓冲区的两个映射。一个来自原始地址(仅在请求者进程的上下文中有效),另一个来自系统空间,始终有效。要调用的 API 是 MmGetSystemAddressForMdlSafe,传递由 I/O 管理器构建的 MDL。返回值是系统地址。
5 User或Kernel进行缓冲区修改:
6结束: 一旦驱动程序完成请求,I/O 管理器删除第二个映射(到系统空间),释放 MDL 并解锁用户缓冲区,因此它可以像任何其他用户模式内存一样正常分页。
这里全程没有用到拷贝,完完全全就是通过地址映射User->内存,然后Kernel修改内存等同于直接修改User的缓冲区。
直接I/O在用户态下:
直接I/O操作系统会在User下用MDL这个数据结构来存储相关信息:
大小保存在MDL->ByteCount里,然后这段虚拟内存的首地址在MDL->StartVa里,实际缓冲区的首地址相当于起始地址的偏移地址在ByteOffset里,DDK里面封装了几个宏来方便用
#define MmGetMdlByteCount(Mdl) ((Mdl)->MyteCount)
#define MmGetMdlByteOffset(Mdl) ((Mdl)->ByteOffset)
#define MmGetMdlVirtualAddress(Mdl) (PVOID)((PCHAR)(Mdl)->StartVa)+(Mdl)->ByteOffset)
直接I/O在Kernel下
Kernel下比较简单粗暴直接用了一个宏来得到MDL在内核模式下的地址映射
MmGetSystemAddressForMdlSafe();
然后通过前面缓冲I/O用到的一些Read的length或者Write的Length直接通过这个地址拷贝就行了。
直接I/O例子
NTSTATUS TestRead(IN PDEVICE_OBJECT pDevObj,
IN PIRP pIrp)
{
KdPrint(("Enter HelloDDKRead\n"));
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
NTSTATUS status = STATUS_SUCCESS;
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
ULONG ulReadLength = stack->Parameters.Read.Length;
KdPrint(("ulReadLength:%d\n",ulReadLength));
ULONG mdl_length = MmGetMdlByteCount(pIrp->MdlAddress);
PVOID mdl_address = MmGetMdlVirtualAddress(pIrp->MdlAddress);
ULONG mdl_offset = MmGetMdlByteOffset(pIrp->MdlAddress);
KdPrint(("mdl_address:0X%08X\n",mdl_address));
KdPrint(("mdl_length:%d\n",mdl_length));
KdPrint(("mdl_offset:%d\n",mdl_offset));
if (mdl_length!=ulReadLength)
{
pIrp->IoStatus.Information = 0;
status = STATUS_UNSUCCESSFUL;
}else
{
//ÓÃMmGetSystemAddressForMdlSafe
PVOID kernel_address = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority);
KdPrint(("kernel_address:0X%08X\n",kernel_address));
memset(kernel_address,0XAA,ulReadLength);
pIrp->IoStatus.Information = ulReadLength; // bytes xfered
}
pIrp->IoStatus.Status = status;
IoCompleteRequest( pIrp, IO_NO_INCREMENT );
KdPrint(("Leave TestRead\n"));
return status;
}
其它
其它的I/O读写方式用得非常少,而且很麻烦,这里就不介绍了。
IO设备控制操作
除了常用的ReadFile,WriteFile,CreateFile,CloseFile这类操作外,还可以通过另一个API DeviceIoControl来操作设备。DeviceIoControl会创建一个IRP_MJ_DEVICE_CONTROL类型的IRP,别的和其它的IRP以及派遣函数是一样的。
DeviceIoControl和驱动交互
DeviceIoControl除了可以被用来读写还可以用在其它操作上。
BOOL DeviceIoControl(
HANDLE hDevice, //设备对象句柄
DWORD dwIoControlCode, //控制码
LPVOID lpInBuffer, //输入缓冲区
DWORD nInBufferSize, //输入缓冲区大小
LPVOID lpOutBuffer, //输出缓冲区
DWORD nOutBufferSize, //输出缓冲区大小
LPDWORD lpBytesReturned, //实际返回字节数 这个对应这Irp->IoStatus.Information
LPOVERLAPPED lpOverlapped //是否OVERLAP操作
);
dwIoControlCode是I/O控制码,也叫IOCTL值。
Windows有一些内置的I/O控制码可以选用:(在官方文档上可以获取:DeviceIoControl function (ioapiset.h) - Win32 apps | Microsoft Docs
同样的我们也可以自己定义,Windows提供了控制码的定义规定:
在ddk头文件里面有一个宏定义方便我们使用:
#define CTL_CODE( DeviceType, Function, Method, Access ) (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))
DeviceType(31-16):设备对象的类型,这个和IoCreateDevice时设置的设备对象类型一样。
Access(15-14):访问权限,如果没有特殊要求,一般采用FILE_ANY_ACCESS
Funciton:0X000~0X7FF由微软保留,0x800~0xFFF由程序员自己定义
Method:操作模式,以下四种之一:
1 METHOD_BUFFERED: 使用缓冲区方式操作
2 METHOD_IN_DIRECT: 使用直接写方式操作
3 METHOD_OUT_DIRECT: 使用直接读方式操作
4 METHOD_NEITHER: 使用其它方式操作
缓冲内存模式IOCTL
DeviceIoControl的缓冲读取和前面的缓冲I/O有一点不一样,前面的流程都一样,都是复制到系统进程的缓冲区里面,然后这个缓冲区地址可以由Irp->AssociatedIrp.SystemBuffer来获取。不一样的是DeviceIoControl会传入输入和输出两个缓冲区,但是两个缓冲区对应的是一个地址,因为如果是输入就是输出到Kernel里,Kernel可以进行操作后,再把这个缓冲区修改了然后作为输出,输出到User里。
首先定义一个自己的IOCTL码
#define IOCTL_TEST CTL_CODE(FILE_DEVICE_UNKNOWN,0X800,METHOD_BUFFERED,FILE_ANY_ACCESS)
在内核状态下要使用IOCTL需要添加ntddk头文件,在User下需要添加winioctl.h头文件
NTSTATUS DeviceIoControl(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
auto stack = IoGetCurrentIrpStackLocation(pIrp);
auto status = STATUS_SUCCESS;
//得到输入缓冲区大小
auto cbIn = stack->Parameters.DeviceIoControl.InputBufferLength;
//得到输出缓冲区大小
auto cbOut = stack->Parameters.DeviceIoControl.OutputBufferLength;
//得到IOCTL控制码
auto code = stack->Parameters.DeviceIoControl.IoControlCode;
ULONG info = 0;
switch (code)
{
case IOCTL_TEST1:
{
//处理输入给内核的缓冲区内容
UCHAR* InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
for (ULONG i = 0; i < cbIn; i++)
{
KdPrint(("%x\n", InputBuffer[i]));
}
//处理输出给User的缓冲区内容
UCHAR* OutputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
memset(OutputBuffer, 0xAA, cbOut);
info = cbOut;
break;
}
default:
{
status = STATUS_INVALID_VARIANT;
break;
}
}
pIrp->IoStatus.Information = info;
pIrp->IoStatus.Status = status;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
}
直接内存模式IOCTL
直接内存模式IOCTL也和直接I/O有稍许区别,直接IOCTL中的输入缓冲区就是前面的缓冲内存模式下的缓冲区,直接开辟一个系统缓冲区,但是输出缓冲区就是前面的直接I/O,通过地址映射来得到的地址。所以直接内存模式的IOCTL需要分两种来处理,一种是输入缓冲区当缓冲I/O处理,另一种是输出缓冲区当直接I/O来处理。
这里需要说明一下直接模式的
METHOD_IN_DIRECT: 使用直接写方式操作
METHOD_OUT_DIRECT: 使用直接读方式操作
这两种方式的区别,在调用DeviceIoControl的时候是会指定打开的模式是只读还是只写,还是可读可写,就对应着这两种,如果是只读,那么只有METHOD_IN_DIRECT编写的IOCTL控制代码才会识别才会有用,以此类推。
直接内存模式IOCTL的例子:
1 先创建IOCTL
#define IOCTL_TEST1 CTL_CODE(FILE_DEVICE_UNKNOWN,0X801,METHOD_IN_DIRECT,FILE_ANY_ACCESS)
2 编写对应IOCTL的派遣函数
NTSTATUS DeviceIoControl(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
auto stack = IoGetCurrentIrpStackLocation(pIrp);
auto status = STATUS_SUCCESS;
//得到输入缓冲区大小
auto cbIn = stack->Parameters.DeviceIoControl.InputBufferLength;
//得到输出缓冲区大小
auto cbOut = stack->Parameters.DeviceIoControl.OutputBufferLength;
//得到IOCTL控制码
auto code = stack->Parameters.DeviceIoControl.IoControlCode;
ULONG info = 0;
switch (code)
{
case IOCTL_TEST1:
{
//处理输入给内核的缓冲区内容
UCHAR* InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
for (ULONG i = 0; i < cbIn; i++)
{
KdPrint(("%x\n", InputBuffer[i]));
}
//处理输出给User的缓冲区内容
UCHAR* OutputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
memset(OutputBuffer, 0xAA, cbOut);
info = cbOut;
break;
}
case IOCTL_TEST2:
{
//当是IOCTL_TEST2的IOCTL时的代码逻辑
auto InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
for (int i = 0; i < cbIn; i++)
{
KdPrint(("%X\n", InputBuffer[i]));
}
auto OutputBuffer = (UCHAR*)MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);
memset(OutputBuffer, 0xAA, cbOut);
info = cbOut;
break;
}
default:
{
status = STATUS_INVALID_VARIANT;
break;
}
}
pIrp->IoStatus.Information = info;
pIrp->IoStatus.Status = status;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
}
其它内存模式IOCTL
其它模式用的很少,而且很麻烦,这里也不介绍了。
总结
不管是DeviceIoControl还是ReadFile,WriteFile其实都是用作User和Kernel交互的API,其中的IRP是自带的数据结构体,里面保存了要交互的东西的信息。DeviceIoControl更像输入东西给Kernel让Kernel通过根据输入的内容和控制码来执行命令的一个东西,通过I/O来控制Kernel执行一些代码流程;而ReadFille/WriteFile可能用得更多的是在单纯的交互数据上面。
标签:Windows,MJ,IRP,IOCTL,内核,缓冲区,pIrp,define 来源: https://www.cnblogs.com/Sna1lGo/p/15232112.html