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 )