从IRP说起
IRP(I/O request package)是操作系统内核的一个数据结构。应用程序与驱动程序进行通信需要通过IRP包。当上层应用程序需要与驱动通信的时候,通过调用一定的API函数,IO管理器针对不同的API产生不同的IRP,IRP被传递到驱动内部不同的分发函数进行处理。对于不会处理的IRP包需要提供一个默认的分发函数来处理。
现在我们来看一下IRP的结构:
typedef struct _IRP {
struct _IRP
…
LIST_ENTRY
…
} IRP, *PIRP;
MSDN 说IRP是一个半透明结构,开发者只能访问其中透明的部分。
其实数据结构 IRP 只是"I/O 请求包"IRP的头部,在 IRP 数据结构的后面还有一个IO_STACK_LOCATION 数据结构的数组,数组的大小则取决于 IRP 数据结构中的StackCount,其数值来自堆叠中顶层设备对象的 StackSize 字段。这样,就在 IRP 中为目标设备对象堆叠中的每一层即每个模块都准备好了一个 IO_STACK_LOCATION 数据结构。而CurrentLocation,则是用于该数组的下标,说明目前是在堆叠中的哪一层,因而正在使用哪一个 IO_STACK_LOCATION 数据结构。
第一个参数 PMDL
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)没有公开为驱动程序使用,但可以通过IoGetCurrentIrpStackLoca
说到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 {
PDEVICE_OBJECT
PFILE_OBJECT
} 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(
);
第二个参数一般设置为IO_NO_INCREMENT。具体可参见MSDN。
对缺省IRP我们可以这样编写函数来处理:
NTSTATUS xxxDispatchRoutine(IN PDEVICE_OBJECT
{
}
WDM驱动是分层的,经常需要将IRP包在各层驱动中传递,负责IRP传递的函数有下面几个:IoCallDriver()
函数分别的定义为(注意函数的参数):
NTSTATUS IoCallDriver(
);
通过该函数,将IRP送到指定设备(第一个参数)的驱动程序进行处理。
VOID IoSkipCurrentIrpStackLoc
);
#define IoSkipCurrentIrpStackLoc
该函数其实就是一个宏定义,设置IRP中IO_STACK_LOCATION的指针,上面两个函数一般在过滤驱动中配合使用:
IoSkipCurrentIrpStackLoc
IoCallDriver(deviceExtension->nextLower, Irp);//location-1
执行完上面两步之后,location正好跟调用者一样,IO_STACK_LOCATION中的内容也不变。Filter driver常用此种手段转发IRP:收到一个IRP,获取或者修改其数据,继续转发,因为location没变所以上层驱动设置的CompleteRoutine依然会被filter之下的那个驱动调用到,Filter driver 就像透明的一样。
VOID IoCopyCurrentIrpStackLoc
);
#define IoCopyCurrentIrpStackLoc
可以看出该函数是一个宏定义,注意这里拷贝的是IRP stack,并不会影响下层的IRP stack。该函数一般和IoSetCompletionRoutine连用,一般用来处理异步的IRP包。每次调用IoCopyCurrentStackLocati
VOID IoSetCompletionRoutine(
);
该函数设定一个CompletionRountine,当IRP处理完成逐层弹出到设定了CompletionRountine的堆栈的时候,则通过这个CompletionRountine再次进行处理。
最后再介绍一下获取IRP当前堆栈位置的函数:
IoGetCurrentIrpStackLoca
这其实是一个宏定义:
#define IoGetCurrentIrpStackLoca
( Irp ) ( (Irp)->Tail.Overlay.CurrentStackLocation )
还有一个可获得IRP下层堆栈:
IoGetNextIrpStackLocatio
#define IoGetNextIrpStackLocatio
(Irp)->Tail.Overlay.CurrentStackLocation - 1 )