----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------CherryYue--------
一、IRP 简介
IRP是I/O Request Packet 的缩写,即I/O请求包。驱动与驱动之间通过 IRP 进行通信。而使用驱动的应用层调用的 CreateFile,ReadFile,WriteFile,DeviceIoControl 等函数,说到底也是使用 IRP 和驱动进行通信。IRP由I/O管理器根据用户态程序提出的请求创建并传给相应的驱动程序。在分层的驱动程序中,这个过程很复杂,一个IRP常常要穿越几层驱动程序。
二、IRP结构
IRP功能的复杂性也决定了IRP结构的复杂性。正确理解IRP的结构是理解驱动程序开发的基础。另外,IRP的创建是由I/O管理器在非分页内存进行的。
一个IRP有两部分组成:头部区域和I/O堆栈位置。
1)IRP的头部区域是一个固定的部分,起始就是一个IRP结构。
2)在这个头部的后面是一个I/O stack locations,这是一个IO_STACK_LOCATIONS的结构体数组,这个数组中元素的个数视具体情况而定。由 IoAllocateIrp( IN CCHAR StackSize , IN BOOLEAN ChargeQuota ) 时的参数 StackSize 决定。而 StackSize 通常由 IRP 发往的目标 DEVICE_OBJECT 的 +30 char StackSize 决定。而这个 StackSize 是由设备对象连入所在的设备栈时,根据在设备栈中位置决定的。
下面看看IRP结构(头部区域)和IO_STACK_LOCATIONS(I/O堆栈)的结构的定义
1. IRP结构介绍,结构图如下,其中灰色部分为不可见区域,这里主要讲解一下可见区域。
1.1 PMDL MdlAddress : 设备执行直接I/O时,指向用户空间的内存描述表
1.2 ULONG Flags: 包含一些对驱动程序只读的标志。但这些标志与WDM驱动程序无关
1.3 AssociatedIrp.SystemBuffer : SystemBuffer指针指向一个数据缓冲区,该缓冲区位于内核模式的非分页内存中I/O管理器把用户模式程序发送给驱动程序的数据复制到这个缓冲区,这也是创建IRP过程的一部分。对于读请求,设备驱动程序把读出的数据填到这个缓冲区,然后I/O管理器再把缓冲区的内容复制到用户模式缓冲区。
1.4 IoStatus : 是一个结构体IO_STATUS_BLOCK, 这个结构体仅包含两个域,驱动程序在最终完成请求时设置这个结构。
IoStatus.Status : 将收到一个NTSTATUS代码。
IoStatus.Information 的类型为ULONG_PTR,它将收到一个信息值,该信息值的确切含义要取决于具体的IRP类型和请求完成的状态。Information域的一个公认用法是用于保存数据传输操作。某些PnP请求把这个域作为指向另外一个结构的指针,这个结构通常包含查询请求的结果。
1.5 RequestorMode将等于一个枚举常量UserMode或KernelMode,指定原始I/O请求的来源。驱动程序有时需要查看这个值来决定是否要信任某些参数。
1.6 PendingReturned(BOOLEAN)如果为TRUE,则表明处理该IRP的最低级派遣例程返回了STATUS_PENDING。完成例程通过参考该域来避免自己与派遣例程间的潜在竞争。
1.7 Cancel(BOOLEAN)如果为TRUE,则表明IoCancelIrp已被调用,该函数用于取消这个请求。如果为FALSE,则表明没有调用IoCancelIrp函数。取消IRP是一个相对复杂的主题,我将在本章的最后详细描述它。
1.8 CancelIrql(KIRQL)是一个IRQL值,表明那个专用的取消自旋锁是在这个IRQL上获取的。当你在取消例程中释放自旋锁时应参考这个域。
1.9 CancelRoutine(PDRIVER_CANCEL)是驱动程序取消例程的地址。你应该使用IoSetCancelRoutine函数设置这个域而不是直接修改该域。
2.0 UserBuffer(PVOID) 对于METHOD_NEITHER方式的IRP_MJ_DEVICE_CONTROL请求,该域包含输出缓冲区的用户模式虚拟地址。该域还用于保存读写请 求缓冲区的用户模式虚拟地址,但指定了DO_BUFFERED_IO或DO_DIRECT_IO标志的驱动程序,其读写例程通常不需要访问这个域。当处理一个METHOD_NEITHER控制操作时,驱动程序能用这个地址创建自己的MDL。
2. IO堆栈
MajorFunction(UCHAR)是该IRP的主功能码
MinorFunction(UCHAR)是该IRP的副功能码
Parameters(UNION)是几个子结构的联合,每个请求类型都有自己专用的参数,而每个子结构就是一种参数。这些子结构包括Create(IRP_MJ_CREATE请求)、Read(IRP_MJ_READ请求)、StartDevice(IRP_MJ_PNP的IRP_MN_START_DEVICE子类型),等等。
DeviceObject(PDEVICE_OBJECT)是与该堆栈单元对应的设备对象的地址。该域由IoCallDriver函数负责填写。
FileObject(PFILE_OBJECT)是内核文件对象的地址,IRP的目标就是这个文件对象。驱动程序通常在处理清除请求(IRP_MJ_CLEANUP)时使用FileObject指针,以区分队列中与该文件对象无关的IRP。
CompletionRoutine(PIO_COMPLETION_ROUTINE)是一个I/O完成例程的地址,该地址是由与这个堆栈单元对应的驱动程序的更上一层驱动程序设置的。你绝对不要直接设置这个域,应该调用IoSetCompletionRoutine函 数,该函数知道如何参考下一层驱动程序的堆栈单元。设备堆栈的最低一级驱动程序并不需要完成例程,因为它们必须直接完成请求。然而,请求的发起者有时确实 需要一个完成例程,但通常没有自己的堆栈单元。这就是为什么每一级驱动程序都使用下一级驱动程序的堆栈单元保存自己完成例程指针的原因。
IO堆栈总结:
1)I/O堆栈位置的主要目的是,保存一个I/O请求的函数代码和参数。
2)I/O堆栈数量实际上就是参与I/O请求的I/O层的数量。
3)在一个IRP中,上层驱动负责为下层驱动设置堆栈位置指针。
i)驱动程序可以为每个IRP调用IoGetCurrentStackLocation来获得指向其自身堆栈位置的指针,
ii)上层驱动程序必须调用IoGetNextIrpStackLocation来获得指向下层驱动程序堆栈位置的指针。
因此,上层驱动可以在传送IRP给下层驱动之前设置堆栈位置的内容。
4)上层驱动调用IoCallDriver,将DeviceObject成员设置成下层驱动目标设备对象。当上层驱动完成IRP时,IoCompletion 函数被调用,I/O管理器传送给IoCompletion函数一个指向上层驱动的设备对象的指针。
=========================================================================================
假设某过滤驱动的分层结构如下:
1 FIDO <-- 此时你在这里调用IoAllocateIrp()创建一个先的IRP往下层驱动FDO传送
2 FDO
3 PDO
假设IO堆栈单元有3个,第3个IO堆栈单元表示new_IRP当前IO堆栈单元 第2个表示FDO的IO堆栈单元 第1个表示PDO的堆栈单元那么如果要发送此IRP到下层驱动FDO,则预先初始化new_IRP的FDO的IO堆栈单元,也就是第二个IO堆栈单元。此时new_IRP 的 StackCount = 2, CurrentLocation = 3 这里的3表示new_IRP的当前IO堆栈单元 如果你需要把此IRP往FDO驱动传递,那么不用初始化这个IO堆栈单元,为什么呢?
因为IoCallDriver() 内部会调用 类似 IoSetNextIrpStackLocation() 的操作来调整 CurrentLocation的索引。也就是 索引-1 ;所以要调用IoCallDriver() 把new_IRP传递到FDO这个下层驱动,需要先调用IoGetNextIrpStackLocation() 获取FDO对应的IO堆栈单元,并进行初始化。初始化完成后才能调用IoCallDriver()此时new_IRP的 CurrentLocation = 2 这个索引才是指向FDO的IO堆栈单元
下面是来自WMD一书的例子:我上面的话就是为了理解下面的代码片断
发往派遣例程。
创建完IRP后,你可以调用IoGetNextIrpStackLocation函数获得该IRP第一个堆栈单元的指针。然后初始化这个堆栈单元。在初始化过程的最后,你需要填充MajorFunction代码。堆栈单元初始化完成后,就可以调用IoCallDriver函数把IRP发送到设备驱动程序:
PDEVICE_OBJECT DeviceObject; //something gives you this
PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp);
stack->MajorFunction = IRP_MJ_Xxx;
NTSTATUS status = IoCallDriver(DeviceObject, Irp);
IRP中的第一个堆栈单元指针被初始化成指向该堆栈单元之前的堆栈单元,因为I/O堆栈实际上是IO_STACK_LOCATION结构数组,你可以认为这个指针被初始化为指向一个不存在的“-1”元素,因此当我们要初始化第一个堆栈单元时我们实际需要的是“下一个”堆栈单元。IoCallDriver将沿着这个堆栈指针找到第0个表项,并提取我们放在那里的主功能代码,在上例中为IRP_MJ_Xxx。然后IoCallDriver函数将利用DriverObject指针找到设备对象中的MajorFunction表。IoCallDriver将使用主功能代码索引这个表,最后调用找到的地址(派遣函数)。
你可以把IoCallDriver函数想象为下面代码:
NTSTATUS IoCallDriver(PDEVICE_OBJECT device, PIRP Irp)
{
IoSetNextIrpStackLocation(Irp);
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
stack->DeviceObject = device;
ULONG fcn = stack->MajorFunction;
PDRIVER_OBJECT driver = device->DriverObject;
return (*driver->MajorFunction[fcn])(device, Irp);
}
IRP(I/O request package)是操作系统内核的一个数据结构。应用程序与驱动程序进行通信需要通过IRP包。当上层应用程序需要与驱动通信的时候,通过调用一定的API函数,IO管理器针对不同的API产生不同的IRP,IRP被传递到驱动内部不同的分发函数进行处理。对于不会处理的IRP包需要提供一个默认的分发函数来处理。
现在我们来看一下IRP的结构:
typedef struct _IRP {
…
PMDL MdlAddress;
ULONG Flags;
union {
struct _IRP *MasterIrp;
…
PVOID SystemBuffer;
} AssociatedIrp;
LIST_ENTRY ThreadListEntry; //用来将 IRP挂入某个线程的 IrpList队列
IO_STATUS_BLOCK IoStatus; //用来返回操作的完成状况
KPROCESSOR_MODE RequestorMode;
BOOLEAN PendingReturned;
CHAR StackCount;
CHAR CurrentLocation;
…
BOOLEAN Cancel;
KIRQL CancelIrql;
…
PDRIVER_CANCEL CancelRoutine;
PVOID UserBuffer;
union {
struct {
…
union {
KDEVICE_QUEUE_ENTRY DeviceQueueEntry;
struct {
PVOID DriverContext[4];
};
};
…
PETHREAD Thread;
…
LIST_ENTRY ListEntry;
…
} Overlay;
…
} Tail;
} IRP, *PIRP;
MSDN 说IRP是一个半透明结构,开发者只能访问其中透明的部分。
其实数据结构 IRP 只是"I/O 请求包"IRP的头部,在 IRP 数据结构的后面还有一个IO_STACK_LOCATION 数据结构的数组,数组的大小则取决于 IRP 数据结构中的StackCount,其数值来自堆叠中顶层设备对象的 StackSize 字段。这样,就在 IRP 中为目标设备对象堆叠中的每一层即每个模块都准备好了一个 IO_STACK_LOCATION 数据结构。而CurrentLocation,则是用于该数组的下标,说明目前是在堆叠中的哪一层,因而正在使用哪一个 IO_STACK_LOCATION 数据结构。
先来对IRP结构进行说明。
第一个参数 PMDL MdlAddress:
MdlAddress域指向一个内存描述符表(MDL),描述了一个与该IO请求关联的用户模式缓冲区。如果顶级设备对象的Flags域为DO_DIRECT_IO,则I/O管理器为 IRP_MJ_READ或 IRP_MJ_WRITE请求创建这个MDL。如果一个IRP_MJ_DEVICE_CONTROL请求的控制代码指定METHOD_IN_DIRECT或METHOD_OUT_DIRECT操作方式,则I/O管理器为该请求使用的输出缓冲区创建一个MDL。
下一个参数:AssociatedIrp
我们WDM驱动会用到AssociatedIrp.SystemBuffer,这是一个指向系统空间的缓冲区。当使用直接IO的时候,这个缓冲区的用途由与IRP相关的Majorfunction决定。对于IRP_MJ_READ和IRP_MJ_WRITE,则不会用到这个缓冲区。对于IRP_MJ_DEVICE_CONTROL 或 IRP_MJ_INTERNAL_DEVICE_CONTROL这两类IRP,该缓冲区被作为DeviceIoControl函数的输入缓冲区。该缓冲区的长度由IO_STACK_LOCATION结构(后面会讲到该结构)中的Parameters.DeviceIoControl.InputBufferLength 成员来确定。
IoStatus(IO_STATUS_BLOCK)是一个仅包含两个域的结构,驱动程序在最终完成请求时设置这个结构。IoStatus.Status 表示IRP完成状态,IoStatus.information的值与请求相关,如果是数据传输请求,则将该域设置为传输的字节数。
CurrentLocation(CHAR)和Tail.Overlay.CurrentStackLocation(PIO_STACK_LOCATION)没有公开为驱动程序使用,但可以通过IoGetCurrentIrpStackLocation函数获取这些信息。
说到IRP结构的CurrentLocation,我们可以来看一下IO_STACK_LOCATION结构了。
任何内核模式程序在创建一个IRP时,同时还创建了一个与之关联的 IO_STACK_LOCATION 结构数组:数组中的每个堆栈单元都对应一个将处理该IRP的驱动程序,堆栈单元中包含该IRP的类型代码和参数信息以及完成函数的地址。
说简单些就是在分层驱动中使用CurrentLocation来记录IRP到达了哪一层,在不同的层有对应的处理函数(通过IO_STACK_LOCATION关联),对IRP进行特定的处理。
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指示驱动程序应该使用哪个函数来处理IO请求。
MinorFunction 进一步指出该IRP属于哪个主功能类
Flags 表明IO请求类型。
DeviceObject(PDEVICE_OBJECT)是与该堆栈单元对应的设备对象的地址。该域由IoCallDriver函数负责填写。
CompletionRoutine(PIO_COMPLETION_ROUTINE)是一个I/O完成例程的地址,该地址是由与这个堆栈单元对应的驱动程序的更上一层驱动程序设置的。通过调用IoSetCompletionRoutine函数来设置。设备堆栈的最低一级驱动程序并不需要完成例程,因为它们必须直接完成请求。然而,请求的发起者有时确实需要一个完成例程,但通常没有自己的堆栈单元。这就是为什么每一级驱动程序都使用下一级驱动程序的堆栈单元保存自己完成例程指针的原因。
现在对IRP和IO_STACK_LOCATION都有了一个初步的认识。当驱动程序对IRP完成了操作(对各个域的读写)之后,需要调用IoCompleteRequest表明IRP处理已经结束,并将IRP交还给IO管理器。
VOID IoCompleteRequest(
__in PIRP Irp,
__in CCHAR PriorityBoost
);
第二个参数一般设置为IO_NO_INCREMENT。具体可参见MSDN。
对缺省IRP我们可以这样编写函数来处理:
NTSTATUS xxxDispatchRoutine(IN PDEVICE_OBJECT do,IN PIRP Irp)
{
PAGED_CODE();
KdPrint(("Enter xxxDispatchRoutine\n"));
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0; // no bytes xfered
IoCompleteRequest( Irp, IO_NO_INCREMENT );
KdPrint(("Leave xxxDispatchRoutine\n"));
return STATUS_SUCCESS;
}
WDM驱动是分层的,经常需要将IRP包在各层驱动中传递,负责IRP传递的函数有下面几个:IoCallDriver() IoSkipCurrentIrpStackLocation() IoCopyCurrentIrpStackLocationToNext()。
函数分别的定义为(注意函数的参数):
NTSTATUS IoCallDriver(
__in PDEVICE_OBJECT DeviceObject,
__inout PIRP Irp
);
通过该函数,将IRP送到指定设备(第一个参数)的驱动程序进行处理。
VOID IoSkipCurrentIrpStackLocation(
[in, out] PIRP Irp
);
#define IoSkipCurrentIrpStackLocation( Irp ) { \
(Irp)->CurrentLocation++; \
(Irp)->Tail.Overlay.CurrentStackLocation++; }
该函数其实就是一个宏定义,设置IRP中IO_STACK_LOCATION的指针,上面两个函数一般在过滤驱动中配合使用:
IoSkipCurrentIrpStackLocation(Irp);//location+1
IoCallDriver(deviceExtension->nextLower, Irp);//location-1
执行完上面两步之后,location正好跟调用者一样,IO_STACK_LOCATION中的内容也不变。Filter driver常用此种手段转发IRP:收到一个IRP,获取或者修改其数据,继续转发,因为location没变所以上层驱动设置的CompleteRoutine依然会被filter之下的那个驱动调用到,Filter driver 就像透明的一样。
VOID IoCopyCurrentIrpStackLocationToNext(
__inout PIRP Irp
);
#define IoCopyCurrentIrpStackLocationToNext( Irp ) { \
PIO_STACK_LOCATION __irpSp; \
PIO_STACK_LOCATION __nextIrpSp; \
__irpSp = IoGetCurrentIrpStackLocation( (Irp) ); \
__nextIrpSp = IoGetNextIrpStackLocation( (Irp) ); \
RtlCopyMemory( __nextIrpSp, __irpSp, FIELD_OFFSET(IO_STACK_LOCATION, CompletionRoutine)); \
__nextIrpSp->Control = 0; }
可以看出该函数是一个宏定义,注意这里拷贝的是IRP stack,并不会影响下层的IRP stack。该函数一般和IoSetCompletionRoutine连用,一般用来处理异步的IRP包。每次调用IoCopyCurrentStackLocationToNext()函数,就将本层的IRP stack 放当下层的IRP stack顶端,当IoCompleteRequest函数被调用也就是IRP包被处理完成之后,IRP stack 会一层层堆栈向上弹出,如果遇到IO_STACK_LOCATION的CompletionRoutine非空,则调用这个函数,另外传进这个完成例程的是IO_STACK_LOCATION的子域Context。
VOID IoSetCompletionRoutine(
__in PIRP Irp,
__in_opt PIO_COMPLETION_ROUTINE CompletionRoutine,
__in_opt PVOID Context,
__in BOOLEAN InvokeOnSuccess,
__in BOOLEAN InvokeOnError,
__in BOOLEAN InvokeOnCancel
);
该函数设定一个CompletionRountine,当IRP处理完成逐层弹出到设定了CompletionRountine的堆栈的时候,则通过这个CompletionRountine再次进行处理。
最后再介绍一下获取IRP当前堆栈位置的函数:
IoGetCurrentIrpStackLocation(PIRP Irp);
这其实是一个宏定义:
#define IoGetCurrentIrpStackLocation \
( Irp ) ( (Irp)->Tail.Overlay.CurrentStackLocation )
还有一个可获得IRP下层堆栈:
IoGetNextIrpStackLocation(PIRP Irp);
#define IoGetNextIrpStackLocation( Irp ) (\
(Irp)->Tail.Overlay.CurrentStackLocation - 1 )