文章目錄
Windows驅動之IRP結構
IRP是數據請求包的一個簡稱,當應用程序發起CreateFile
或者 ReadFile
API操作設備的時候,就會將相關參數信息封裝成爲一個IRP數據包,通過IoCallDriver
傳遞給驅動程序。
下面具體來分析一下IRP的各個成員的意義。
1. 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掛入到自己的隊列中。
2. IO_STACK_LOCATION
任何內核模式程序在創建一個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)
: 是一個任意的與上下文相關的值,將作爲參數傳遞給完成例程。
3. IRP 和 IO_STACK_LOCATION 的交互
從上面我們知道,整個設備棧來處理這個IRP,但是每個設備都應該有自己的參數信息,這個參數信息就是通過IO_STACK_LOCATION
來保管的,那麼IRP是怎麼保管IO_STACK_LOCATION
的呢?下面我們來分析一下整個流程。
3.1 IoAllocateIrp
這個函數用來分配一個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
是從後往前來使用的。
3.2 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
。