IRP
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------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 )