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