IRP是数据请求包的一个简称,当应用程序发起CreateFile
或者 ReadFile
API操作设备的时候,就会将相关参数信息封装成为一个IRP数据包,通过IoCallDriver
传递给驱动程序。
下面具体来分析一下IRP的各个成员的意义。
IRP的数据结构如下:
typedef struct _IRP {
PMDL MdlAddress;
ULONG Flags;
union {
struct _IRP* MasterIrp;
PVOID SystemBuffer;
} AssociatedIrp;
IO_STATUS_BLOCK IoStatus;
KPROCESSOR_MODE RequestorMode;
BOOLEAN PendingReturned;
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;
kd> dt nt!_IRP
+0x000 Type : Int2B
+0x002 Size : Uint2B
+0x004 MdlAddress : Ptr32 _MDL
+0x008 Flags : Uint4B
+0x00c AssociatedIrp : <unnamed-tag>
+0x010 ThreadListEntry : _LIST_ENTRY
+0x018 IoStatus : _IO_STATUS_BLOCK
+0x020 RequestorMode : Char
+0x021 PendingReturned : UChar
+0x022 StackCount : Char
+0x023 CurrentLocation : Char
+0x024 Cancel : UChar
+0x025 CancelIrql : UChar
+0x026 ApcEnvironment : Char
+0x027 AllocationFlags : UChar
+0x028 UserIosb : Ptr32 _IO_STATUS_BLOCK
+0x02c UserEvent : Ptr32 _KEVENT
+0x030 Overlay : <unnamed-tag>
+0x038 CancelRoutine : Ptr32 void
+0x03c UserBuffer : Ptr32 Void
+0x040 Tail : <unnamed-tag>
除去一些简单的成员,例如Type,Size等,我们来看一下各个成员的具体意义:
MdlAddress
: 是一个MDL的指针,当内核层和用户层采用共享内存的结构传递数据的时候,这个MDL就代表共享的内存信息(共享物理内存,通过MDL映射)。这个成员生效的标记为:DO_DIRECT_IO
, METHOD_IN_DIRECT
或者METHOD_OUT_DIRECT
.AssociatedIrp
: 这个成员是个联合体,其中存在一个SystemBuffer
程序;当内核层使用用户层的数据的时候是通过拷贝数据的方式来实现的话,那么拷贝后的数据就放在了AssociatedIrp.SystemBuffer
中了。这个成员生效的标记是DO_BUFFERED_IO
或者METHOD_BUFFERED
。IoStatus
: 返回的状态信息。RequestorMode
: UserMode
或KernelMode
,指定原始I/O请求的来源。驱动程序有时需要查看这个值来决定是否要信任某些参数。PendingReturned
: Pending 状态,如果为TRUE,则表明处理该IRP的最低级派遣例程返回了STATUS_PENDING
。StackCount
: 设备栈的数目。CurrentLocation
: 当前处于哪个设备栈的索引。Cancel
: IRP是否被取消,如果为TRUE,则表明IoCancelIrp
已被调用(该函数用于取消这个请求)。如果为FALSE,则表明没有调用IoCancelIrp
函数。CancelIrql(KIRQL)
: 是一个IRQL值,表明那个专用的取消自旋锁是在这个IRQL上获取的.CancelRoutine(PDRIVER_CANCEL)
: 是驱动程序取消例程的地址。你应该使用IoSetCancelRoutine
函数设置这个域而不是直接修改该域(因为可以原子修改)。UserBuffer(PVOID)
: 用户层参数的直接地址,设置标记METHOD_NEITHER
时候有效。Tail.Overlay
是Tail联合中的一种联合结构,如下:在这个图中,以水平方向从左到右是这个联合的三个可选成员,在垂直方向是每个结构的成员描述:
Tail.Overlay.DeviceQueueEntry(KDEVICE_QUEUE_ENTRY)
和Tail.Overlay.DriverContext(PVOID[4])
是Tail.Overlayare
内一个未命名联合的两个可选成员(只能出现一个)。I/O管理器把DeviceQueueEntry
作为设备标准请求队列中的连接域。当IRP还没有进入某个队列时,如果你拥有这个IRP你可以使用这个域,你可以任意使用DriverContext
中的四个指针;Tail.Overlay.DeviceQueueEntry
主要用在StartIo相关例程上面。Tail.Overlay.ListEntry(LIST_ENTRY)
仅能作为你自己实现的私有队列的连接域;这个成员比较重要,因为如果需要自己实现IRP异步执行的队列,那么就需要使用这个成员将IRP挂入到自己的队列中。任何内核模式程序在创建一个IRP时,同时还创建了一个与之关联的IO_STACK_LOCATION
结构数组:数组中的每个堆栈单元都对应一个将处理该IRP的驱动程序,另外还有一个堆栈单元供IRP的创建者使用。堆栈单元中包含该IRP的类型代码和参数信息以及完成函数的地址。例如如下设备关系:
IO_STACK_LOCATION
结构如下:
typedef struct _IO_STACK_LOCATION {
UCHAR MajorFunction;
UCHAR MinorFunction;
UCHAR Flags;
UCHAR Control;
union {
struct {
PIO_SECURITY_CONTEXT SecurityContext;
ULONG Options;
USHORT POINTER_ALIGNMENT FileAttributes;
USHORT ShareAccess;
ULONG POINTER_ALIGNMENT EaLength;
} Create;
struct {
ULONG Length;
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset;
} Read;
struct {
ULONG Length;
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset;
} Write;
... ...
} Parameters;
PDEVICE_OBJECT DeviceObject;
PFILE_OBJECT FileObject;
PIO_COMPLETION_ROUTINE CompletionRoutine;
PVOID Context;
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;
kd> dt nt!_IO_STACK_LOCATION
+0x000 MajorFunction : UChar
+0x001 MinorFunction : UChar
+0x002 Flags : UChar
+0x003 Control : UChar
+0x004 Parameters : <unnamed-tag>
+0x014 DeviceObject : Ptr32 _DEVICE_OBJECT
+0x018 FileObject : Ptr32 _FILE_OBJECT
+0x01c CompletionRoutine : Ptr32 long
+0x020 Context : Ptr32 Void
这个结构的主要成员意义为:
MajorFunction(UCHAR)
: 是该IRP的主功能码。这个代码应该为类似IRP_MJ_READ
一样的值,并与驱动程序对象中MajorFunction
表的某个派遣函数指针相对应。如果该代码存在于某个特殊驱动程序的I/O堆栈单元中,它有可能一开始是,例如IRP_MJ_READ
,而后被驱动程序转换成其它代码,并沿着驱动程序堆栈发送到低层驱动程序。MinorFunction(UCHAR)
: 是该IRP的副功能码。它进一步指出该IRP属于哪个主功能类。例如,IRP_MJ_PNP
请求就有约一打的副功能码,如IRP_MN_START_DEVICE
、IRP_MN_REMOVE_DEVICE
,等等。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
函数,该函数知道如何参考下一层驱动程序的堆栈单元。设备堆栈的最低一级驱动程序并不需要完成例程,因为它们必须直接完成请求。然而,请求的发起者有时确实需要一个完成例程,但通常没有自己的堆栈单元。这就是为什么每一级驱动程序都使用下一级驱动程序的堆栈单元保存自己完成例程指针的原因。Context(PVOID)
: 是一个任意的与上下文相关的值,将作为参数传递给完成例程。从上面我们知道,整个设备栈来处理这个IRP,但是每个设备都应该有自己的参数信息,这个参数信息就是通过IO_STACK_LOCATION
来保管的,那么IRP是怎么保管IO_STACK_LOCATION
的呢?下面我们来分析一下整个流程。
这个函数用来分配一个IRP,我们看一下IRP的分配过程:
PIRP
IopAllocateIrpPrivate(
IN CCHAR StackSize,
IN BOOLEAN ChargeQuota
)
{
//...
packetSize = IoSizeOfIrp(StackSize);
allocateSize = packetSize;
//...
irp = ExAllocatePoolWithTag(NonPagedPool, allocateSize, ' prI');
//...
IopInitializeIrp(irp, packetSize, StackSize);
//...
}
这个函数中,我们通过packetSize = IoSizeOfIrp(StackSize);
来计算整个IRP的大小,这个声明如下:
#define IoSizeOfIrp( StackSize ) \
((USHORT) (sizeof( IRP ) + ((StackSize) * (sizeof( IO_STACK_LOCATION )))))
也就是说是IRP的大小 + 设备栈个数个IO_STACK_LOCATION
.
IopInitializeIrp
: 用来初始化IRP,这个是个宏定义如下:
#define IopInitializeIrp( Irp, PacketSize, StackSize ) { \
RtlZeroMemory( (Irp), (PacketSize) ); \
(Irp)->Type = (CSHORT) IO_TYPE_IRP; \
(Irp)->Size = (USHORT) ((PacketSize)); \
(Irp)->StackCount = (CCHAR) ((StackSize)); \
(Irp)->CurrentLocation = (CCHAR) ((StackSize) + 1); \
(Irp)->ApcEnvironment = KeGetCurrentApcEnvironment(); \
InitializeListHead (&(Irp)->ThreadListEntry); \
(Irp)->Tail.Overlay.CurrentStackLocation = \
((PIO_STACK_LOCATION) ((UCHAR *) (Irp) + \
sizeof( IRP ) + \
( (StackSize) * sizeof( IO_STACK_LOCATION )))); }
这个IRP和IO_STACK_LOCATION
交互得有三个成员:
(Irp)->StackCount = (CCHAR) ((StackSize))
设备栈的大小:(Irp)->CurrentLocation = (CCHAR) ((StackSize) + 1)
: 当前设备栈,放到了最顶端的下一个。(Irp)->Tail.Overlay.CurrentStackLocation
: 指向最顶层的IO_STACK_LOCATION
, 从这里发现IO_STACK_LOCATION
是从后往前来使用的。在使用IO_STACK_LOCATION
的时候,我们先要获取这个这个程序,看下Windows是怎么获取的:
NTSTATUS
FASTCALL
IopfCallDriver(
IN PDEVICE_OBJECT DeviceObject,
IN OUT PIRP Irp
)
{
PIO_STACK_LOCATION irpSp;
PDRIVER_OBJECT driverObject;
NTSTATUS status;
ASSERT( Irp->Type == IO_TYPE_IRP );
Irp->CurrentLocation--;
if (Irp->CurrentLocation <= 0) {
KeBugCheckEx( NO_MORE_IRP_STACK_LOCATIONS, (ULONG_PTR) Irp, 0, 0, 0 );
}
irpSp = IoGetNextIrpStackLocation( Irp );
Irp->Tail.Overlay.CurrentStackLocation = irpSp;
irpSp->DeviceObject = DeviceObject;
driverObject = DeviceObject->DriverObject;
PERFINFO_DRIVER_MAJORFUNCTION_CALL(Irp, irpSp, driverObject);
status = driverObject->MajorFunction[irpSp->MajorFunction]( DeviceObject,
Irp );
PERFINFO_DRIVER_MAJORFUNCTION_RETURN(Irp, irpSp, driverObject);
return status;
}
#define IoGetNextIrpStackLocation( Irp ) (\
(Irp)->Tail.Overlay.CurrentStackLocation - 1 )
在调用设备的分发函数之前,先使用IoGetNextIrpStackLocation
是的IRP中的(Irp)->Tail.Overlay.CurrentStackLocation
指向正确位置,因为初始化的时候是指向最后一个的末尾的,因此这里要前移一个。
在分发函数中,我们可以使用IoGetCurrentIrpStackLocation
获取当前的IO_STACK_LOCATION
,这个宏定义如下:
#define IoGetCurrentIrpStackLocation( Irp ) ( (Irp)->Tail.Overlay.CurrentStackLocation )
直接返回了IRP中的成员。
当设备栈中的设备完成处理之后,需要向下传递IRP,因此需要调用IoCallDriver
,在上面我们看到过,IoCallDriver
会调用IoGetNextIrpStackLocation
下移设备栈的指针,因此我们需要对设备栈做如下之一的操作:
#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; }
#define IoSkipCurrentIrpStackLocation( Irp ) \
(Irp)->CurrentLocation++; \
(Irp)->Tail.Overlay.CurrentStackLocation++;
IoCopyCurrentIrpStackLocationToNext
拷贝IO_STACK_LOCATION
成员到下一层。IoSkipCurrentIrpStackLocation
上移一层,是的下次使用的时候仍旧使用当前的IO_STACK_LOCATION
。