IRP IO_STACK_LOCATION 《寒江独钓》内核学习笔记(1)
在學習內核過濾驅動的過程中,遇到了大量的涉及IRP操作的代碼,這里有必要對IRP的數據結構和與之相關的API函數做一下筆記。
?
1. 相關閱讀資料
《深入解析 windows 操作系統(第4版,中文版)》 --- 9章
《windows driver kit 幫助文檔》
http://support.microsoft.com/kb/115758/zh-cn? IRP 結構中各地址字段的含義
http://www.programlife.net/io_stack_location-irp.html??? 代碼瘋子對IRP的研究
?
?
?
?
2. IRP的數據結構
IRP是一個數據結構,其中包含了用來描述一個IO請求的完整信息。
IO管理器創建一個IRP來代表一個IO操作,并且將該IRP傳遞給正確的驅動程序,當此IO操作完成時再處理該請求包。相對的,驅動程序(上層的虛擬設備驅動或者底層的真實設備驅動)接收一個IRP,執行該IRP指定的操作,然后將IRP傳回給IO管理器,告訴它,該操作已經完成,或者應該傳給另一個驅動以進行進一步處理。
?
談到IRP,IRP是個總的概念,本質上IRP由IRP Header和IRP Sub-Request組成
從數據結構的角度上來說,其實數據結構 IRP 只是"I/O 請求包"IRP的頭部,在 IRP 數據結構的后面還有一個IO_STACK_LOCATION 數據結構的數組,數組的大小則取決于 IRP 數據結構中的StackCount(我們之后會詳細分析),其數值來自設備堆棧中頂層設備對象的 StackSize 字段。
這樣,就在 IRP 中為目標設備對象設備堆棧中的每一層即每個模塊(每個驅動)都準備好了一個 IO_STACK_LOCATION 數據結構。而CurrentLocation,則是用于該數組的下標,說明目前是在堆疊中的哪一層,因而正在使用哪一個 IO_STACK_LOCATION 數據結構。
?
那這兩個結構是怎么來的呢?有兩種渠道:
1. 程序員在代碼中手工地創建一個IRP(廣義的IRP)
PIRP IoAllocateIrp(IN CCHAR StackSize,IN BOOLEAN ChargeQuota);任何內核模式程序在創建一個IRP時,同時還創建了一個與之關聯的 IO_STACK_LOCATION 結構數組:數組中的每個堆棧單元都對應一個將處理該IRP的驅動程序,堆棧單元中包含該IRP的類型代碼和參數信息以及完成函數的地址。
2. I/O管理器在接收到應用層的設備讀寫請求后,將請求封裝為一個IRP請求(包括IRP頭部和IRP STACK_LOCATINO數組)發往對應的設備的設備棧的最頂層的那個設備驅動。
?
我們先從IRP的頭結構開始學起:
下面是WDK上搬下來的解釋,我們來一條一條的學習。
IRPtypedef 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;MSDN 說IRP是一個半透明結構,開發者只能訪問其中透明的部分,所以其中的..代表是我們不能訪問的部分,所以我們在編程中基本上也不會用到它們,我們集中精力來觀察這些暴露出來的數據結構。
?
?
?
1. IRP中的三種緩沖區
PMDL MdlAddress; union {struct _IRP *MasterIrp;..PVOID SystemBuffer;} AssociatedIrp; PVOID UserBuffer;IRP這個數據結構中有3個地方可以描述緩沖區。?
1) irp->AssociatedIrp.SystemBuffer 2) irp->MdlAddress 3) irp->UserBuffer不同的IO類別,IRP的緩沖區不同。
1) AssociatedIrp.SystemBuffer
一般用于比較簡單且不追求效率情況下的解決方案 把R3層中的內存中的緩沖數據拷貝到內核空間中。注意,是直接拷貝過來,有的書上會說這是"直接方式",不過我們要重點記住的是它是直接拷貝過來。
2) MdlAddress
通過構造MDL就能實現這個R3到R0的地址映射功能。MDL可以翻譯為"內存描述符鏈",本質上就是一個指針,從這個MDL中可以讀出一個內核空間的虛擬地址。這就彌補了UserBuffer的不足,同時比SystemBuffer的完全拷貝方法要輕量,因為這個內存實際上還是在老地方,沒有拷貝。
3) UserBuffer
最追求效率的解決方案 R3的緩沖區地址直接放在UserBuffer里,在內核空間中直接訪問。在當前進程和發送進程一致的情況下,內核訪問應用層的內存空間當然是沒錯的。但是一 旦內核進程已經切換,這個訪問就結束了,訪問UserBuffer當然是跳到其他進程空間去了(我們還是訪問同一個地址,但是這個時候因為進程的上下文切換了,同一個地址對應的內容自然不同了)。因為在windows中,內核空間是所有進程共享的,而應 用層空間則是各個進程隔離的。當然還有一個更簡單的做法是把應用層的地址空間映射到內核空間,這需要在頁表中增加一個映射。
在驅動在獲取這三種緩沖區的方法。
//獲得緩沖區 PUCHAR buf = NULL; if(irp->MdlAddress != NULL) {buffer = (PUCHAR)MmGetSystemAddressForMdlSafe(irp->MdlAddress, NormalPagePriority); } else {buffer = (PUCHAR)irp->UserBuffer; } if(buffer = NULL) {buffer = (PUCHAR)irp->AssociatedIrp.SystemBuffer; }看到這里,就產生另一個問題了,這三種緩沖區是操作系統幫我們自動填好的嗎?那在什么樣的IRP請求會使用到不同的緩沖區類型呢?
這里就要涉及到兩種內存訪問方式: 直接方式DO_DIRECT_IO / 非直接方式(緩沖方式)DO_BUFFERD_IO
1) 在buffered(AssociatedIrp.SystemBuffer)方式中,I/O管理器先創建一個與用戶模式數據緩沖區大小相等的系統緩沖區。而你的驅動程序將使用這個系統緩沖區工作。I/O管理器負責在系統緩沖區和用戶模式緩沖區之間復制數據。
2) 在direct(MdlAddress)方式中,I/O管理器鎖定了包含用戶模式緩沖區的物理內存頁,并創建一個稱為MDL(內存描述符表)的輔助數據結構來描述鎖定頁。因此你的驅動程序將使用MDL工作。
3) 在neither(UserBuffer)方式中,I/O管理器僅簡單地把用戶模式的虛擬地址傳遞給你。而使用用戶模式地址的驅動程序應十分小心。
?
繼續思考我們之前的問題,在IRP中具體是使用哪種緩沖方式呢?由誰來決定?
答案是在增加設備的時候就決定的了。即我們在新增一個設備的時候就要決定這個設備的緩沖區讀寫方式。
DRIVER_ADD_DEVICE AddDevice;NTSTATUSAddDevice(__in struct _DRIVER_OBJECT *DriverObject,__in struct _DEVICE_OBJECT *PhysicalDeviceObject ){...}NTSTATUS IoCreateDevice(IN PDRIVER_OBJECT DriverObject,IN ULONG DeviceExtensionSize,IN PUNICODE_STRING DeviceName OPTIONAL,IN DEVICE_TYPE DeviceType,IN ULONG DeviceCharacteristics,IN BOOLEAN Exclusive,OUT PDEVICE_OBJECT *DeviceObject);typedef struct _DEVICE_OBJECT {CSHORT Type;USHORT Size;LONG ReferenceCount;PDRIVER_OBJECT DriverObject;PDEVICE_OBJECT NextDevice;PDEVICE_OBJECT AttachedDevice;PIRP CurrentIrp;PIO_TIMER Timer;ULONG Flags;//notice ULONG Characteristics;__volatile PVPB Vpb;PVOID DeviceExtension;DEVICE_TYPE DeviceType;CCHAR StackSize;union {LIST_ENTRY ListEntry;WAIT_CONTEXT_BLOCK Wcb;} Queue;ULONG AlignmentRequirement;KDEVICE_QUEUE DeviceQueue;KDPC Dpc;ULONG ActiveThreadCount;PSECURITY_DESCRIPTOR SecurityDescriptor;KEVENT DeviceLock;USHORT SectorSize;USHORT Spare1;PDEVOBJ_EXTENSION DeviceObjectExtension;PVOID Reserved; } DEVICE_OBJECT, *PDEVICE_OBJECT;NTSTATUS AddDevice(DriverObject, PhysicalDeviceObject) { PDEVICE_OBJECT fdo; IoCreateDevice(..., &fdo); fdo->Flags |= DO_BUFFERED_IO; //或者是下面的代碼,這三句任取其一fdo->Flags |= DO_DIRECT_IO; //<or> fdo->Flags |= 0; }總結一下,在新增設備的時候這個物理設備的數據緩沖方式就被決定了。這之后你不能該變緩沖方式的設置,因為過濾器驅動程序將復制這個標志設置,并且,如果你改變了設置,過濾器驅動程序沒有辦法知道這個改變。
接下來解決目前為止的最后一個疑問: 系統是如何來根據設備對象中的緩沖類型碼來進行不同緩沖區類型的的緩沖區數據的實際填充過程的(即實際的緩沖區填充過程是什么)?
1) Buffered方式(有一個復制過程)
當I/O管理器創建IRP_MJ_READ或IRP_MJ_WRITE請求時(讀寫請求中會用到數據緩沖區,普通的文件屬性查詢請求或設置中最多用到一個指定的數據結構大小的內存空間即可),它探測設備的緩沖標志(在創建設備時就決定的了)以決定如何描述新IRP中的數據緩沖區。如果DO_BUFFERED_IO(UserBuffer)標志設置,I/O管理器將分配與用戶緩沖區大小相同的"非分頁"內存(不會產生缺頁中斷的內存空間)。
注意,它把緩沖區的地址和長度保存到兩個十分不同的地方,下面是一段模擬代碼。你可以假定I/O管理器執行下面代碼(注意這并不是Windows NT的源代碼):
可以看出,系統緩沖區地址被放在IRP的AssociatedIrp.SystemBuffer域中,而數據的長度被放到stack->Parameters聯合中。I/O管理器把用戶模式虛擬地址(uva變量)保存到IRP的UserBuffer域中,這樣一來內核驅動代碼就可以找到這個地址。
?
2) Direct方式
如果你在設備對象中指定DO_DIRECT_IO(MDL)方式,I/O管理器將創建一個MDL用來描述包含該用戶模式數據緩沖區的鎖定內存頁(本質上還是一種映射,創建映射到同一內存位置的內核模式虛擬地址)。MDL結構的聲明如下:
?
?StartVa成員給出了用戶緩沖區的虛擬地址,這個地址僅在擁有數據緩沖區的用戶模式進程上下文中才有效(即用戶模式中的虛擬地址空間)。ByteOffset是緩沖區起始位置在一個頁幀中的偏移值,ByteCount是緩沖區的字節長度。Pages數組沒有被正式地聲明為MDL結構的一部分,在內存中它跟在MDL的后面,包含用戶模式虛擬地址映射為物理頁幀的個數。
要注意的是,我們不可以直接訪問MDL的任何成員。應該使用宏或訪問函數:
宏或函數 描述 IoAllocateMdl 創建MDL(在文件系統驅動的透明加密中你可能需要創建自己的臨時MDL以暫時替換系統的原始的MDL)IoBuildPartialMdl 創建一個已存在MDL的子MDLIoFreeMdl 銷毀MDLMmBuildMdlForNonPagedPool 修改MDL以描述內核模式中一個非分頁內存區域MmGetMdlByteCount 取緩沖區字節大小MmGetMdlByteOffset 取緩沖區在第一個內存頁中的偏移MmGetMdlVirtualAddress 取虛擬地址MmGetSystemAddressForMdl 創建映射到同一內存位置的內核模式虛擬地址MmGetSystemAddressForMdlSafe 與MmGetSystemAddressForMdl相同,但Windows 2000首選MmInitializeMdl (再)初始化MDL以描述一個給定的虛擬緩沖區MmPrepareMdlForReuse 再初始化MDLMmProbeAndLockPages 地址有效性校驗后鎖定內存頁MmSizeOfMdl 取為描述一個給定的虛擬緩沖區的MDL所占用的內存大小MmUnlockPages 為該MDL解鎖內存頁對于I/O管理器執行的Direct方式的讀寫操作,其過程可以想象為下面代碼:
KPROCESSOR_MODE mode; // either KernelMode or UserMode PMDL mdl = IoAllocateMdl(uva, length, FALSE, TRUE, Irp); MmProbeAndLockPages(mdl, mode, reading ? IoWriteAccess : IoReadAccess); <code to send and await IRP>MmUnlockPages(mdl); ExFreePool(mdl);I/O管理器首先創建一個描述用戶緩沖區的MDL。IoAllocateMdl的第三個參數(FALSE)指出這是一個主數據緩沖區。第四個參數(TRUE)指出內存管理器應把該內存充入進程配額。最后一個參數(Irp)指定該MDL應附著的IRP。在內部,IoAllocateMdl把Irp->MdlAddress設置為新創建MDL的地址,以后你將用到這個成員,并且I/O管理器最后也使用該成員來清除MDL。
這段代碼的關鍵地方是調用MmProbeAndLockPages。該函數校驗那個數據緩沖區是否有效,是否可以按適當模式訪問。如果我們向設備寫數據,我們必須能讀緩沖區。如果我們從設備讀數據,我們必須能寫緩沖區。另外,該函數鎖定了包含數據緩沖區的物理內存頁,并在MDL的后面填寫了頁號數組。在效果上,一個鎖定的內存頁將成為非分頁內存池的一部分,直到所有對該頁內存加鎖的調用者都對其解了鎖。
在Direct方式的讀寫操作中,對MDL你最可能做的事是把它作為參數傳遞給其它函數。例如,DMA傳輸的MapTransfer步驟需要一個MDL。另外,在內部,USB讀寫操作總使用MDL。所以你應該把讀寫操作設置為DO_DIRECT_IO方式,并把結果MDL傳遞給USB總線驅動程序。
順便提一下,I/O管理器確實在stack->Parameters聯合中保存了讀寫請求的長度,但驅動程序應該直接從MDL中獲得請求數據的長度
?
?
3) Neither方式
如果你在設備對象中同時忽略了DO_DIRECT_IO和DO_BUFFERED_IO標志設置,你將得到默認的neither方式。對于這種方式,I/O管理器將簡單地把用戶模式虛擬地址和字節計數直接交給你,其余的工作由你去做。這種情況下程序員將自己去解決因為進程的切換導致的用戶模式地址失效問題。
至此,我們目前的疑問就全部解決了,我們知道了IRP中的三種不同的緩沖區是怎么來的(設備添加的時候決定的),是由誰填充的(操作系統自動地把用戶模式地址空間的數據填充到IRP中的緩沖區中/或者直接給出用戶空間地址)。
接下來就可以引出IRP中(準備說是IRP頭部的另一個成員域)
?
?
?
?
2. ULONG? Flags
File system drivers use this field, which is read-only for all drivers. Network and, possibly, highest-level device drivers also might read this field, which can be set with one or more of the following system-defined masks:在文件系統驅動程序的編程中將使用到這個數據域,這對所有的驅動程序來說是只讀的,它指示了這個IRP的操作類型。
IRP_NOCACHE IRP_PAGING_IO IRP_MOUNT_COMPLETION IRP_SYNCHRONOUS_API IRP_ASSOCIATED_IRP IRP_BUFFERED_IO IRP_DEALLOCATE_BUFFER IRP_INPUT_OPERATION IRP_SYNCHRONOUS_PAGING_IO IRP_CREATE_OPERATION IRP_READ_OPERATION IRP_WRITE_OPERATION IRP_CLOSE_OPERATION IRP_DEFER_IO_COMPLETION關于這個Flags字段,我們常常要注意的是,我們如果在做過濾/綁定/捕獲類型的驅動類型的編程中,我們新創建的上層過濾設備的Flags字段的值一定要和下層的真實設備或底層驅動的Flags保持一致。
比如我在做文件系統卷的過濾設備編程的時候就遇到這樣的代碼:
//設備標志的復制 if (FlagOn( DeviceObject->Flags, DO_BUFFERED_IO )) {SetFlag( SFilterDeviceObject->Flags, DO_BUFFERED_IO ); } if (FlagOn( DeviceObject->Flags, DO_DIRECT_IO )) {SetFlag( SFilterDeviceObject->Flags, DO_DIRECT_IO ); }?
?
?
?
?
3.? IO_STATUS_BLOCK? IoStatus
typedef struct _IO_STATUS_BLOCK {union {NTSTATUS Status;PVOID Pointer;};ULONG_PTR Information; } IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;IoStatus(IO_STATUS_BLOCK)是一個僅包含兩個域的結構,驅動程序在最終完成請求時設置這個結構。
IoStatus.Status 表示IRP完成狀態
ntdef.h
ntstatus.h
WDK的這兩個頭文件中定了所有的系統級的返回信息,在驅動中,函數的返回值大多數情況下就是這樣結果狀態信息,當然也有通過引用的方式獲得函數執行的結果的,但是函數還是返回個執行結果。
IoStatus.information的值與請求相關,如果是數據傳輸請求,則將該域設置為傳輸的字節數(在windows編程中,基本上涉及到數據讀寫的函數一般都是返回這一類的結果,即操作的字節數)。
?
?
?
?
4. KPROCESSOR_MODE? RequestorMode
RequestorMode將等于一個枚舉常量UserMode或KernelMode, 指定原始I/O請求的來源。驅動程序有時需要查看這個值來決定是否要信任某些參數。
?
?
?
5. BOOLEAN PendingReturned
PendingReturned(BOOLEAN)如果為TRUE,則表明處理該 IRP的最低級派遣例程返回了STATUS_PENDING。完成例程通過參考該域來避免自己與派遣例程間的潛在競爭。
If set to TRUE, a driver has marked the IRP pending. Each IoCompletion routine should check the value of this flag. If the flag is TRUE, and if the IoCompletion routine will not return STATUS_MORE_PROCESSING_REQUIRED, the routine should call IoMarkIrpPending to propagate the pending status to drivers above it in the device stack.這段話是什么意思呢?這和內核驅動中的多層設備棧有關系。為了解釋這個問題,我們先來看一段代碼demo:
.... Kevent event; KeInitializeEvent(&event, NotificatinoEvent, FALSE); IoCopyCurrentIrpStackLocationToNext(Irp); //設置完成回調函數 IoSetCompletionRoutine(Irp,IrpComplete, //回調函數&event,TRUE,TRUE,TRUE ); status = IoCallDriver(DeviceObject, Irp); if(status == STATUS_PENDING) {//code to handle asynchronous response//異步 status = KeWaitForSingleObject( &waitEvent,Executive,KernelMode,FALSE,NULL );} ...//這是一個IRP完成回調函數的原型 NTSTATUS IrpComplete(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp,IN PVOID Context ) {...if(Irp->PendingReturned){//這個函數等價于: Irp->IoStatus.Status = STATUS_PENDING 即表名這個IRP處理流程依舊沒有結束 IoMarkIrpPending(Irp);}return Irp->IoStatus.Status; }請原諒我沒頭沒腦的給這出這段看起來不知所云的代碼。這是因為IRP機制是一種基礎機制,往往是配合一些具體的過濾驅動的編程而使用的,如果要給出完整的例程那這篇文章的篇幅就會無窮無盡了。
所以接下來我盡我最大的能力來解釋這段代碼的意思并給出它的利用場景。
IRP作為一個線程無關的調用棧
進行一個設備的I/O操作通常需要調用這一設備相關的不止一個驅動。每一個和這個設備相關的驅動都會創建一個設備對象(Device Object),并且這些設備對象會垂直壓入(排列進)一個設備棧(Device Stack)。IRP會在設備棧中從上到下的一個個被傳遞進去(從頂層的設備驅動一直往下到底層的真實設備)。對于棧中的每一個驅動,IRP都會用一個指針標識一個棧位置(IO_STACK_LOCATION和設備棧上的設備驅動的一一對應關系用這個指針來綁定)。由于驅動可以異步地處理請求,因此IRP就像是一個線程無關的調用棧一樣
仔細看這張圖,每一級驅動程序都使用下一級驅動程序的堆棧單元保存自己完成例程指針。最底層的驅動程序不應該安裝一個完成例程,它應該總是返回一個真實的硬件操作結果。即我們在當前位置的設備驅動中設置一個下層驅動的完成回調函數時,本質上是在下層驅動的棧空間中布置一個了一個回調函數地址。
將IRP傳遞到下一級驅動程序(又被稱作轉發IRP)是指IRP等價于一個子例程調用。當驅動轉發一個IRP,這個驅動程序必須向IRP參數組增加下一個I/O棧位置,告知這一IRP棧的指針,然后調用下一驅動的分發例程(dispatch routine)。基本來說,就是驅動向下調用IRP棧(calling down the IRP stack)
傳遞一個IRP,驅動通常會采取以下幾種步驟
1) . 建立下一個I/O棧位置的參數。
1.1) 調用IoGetNextIrpStackLocation例程來得到一個指針指向下一個I/O棧位置,然后將請求參數數組復制到那個得到的位置
1.2) 調用CopyCurrentIrpStackLocationToNext例程(如果驅動設置了IoCompletion例程)
或者
1.3) 調用IoSkipCurrentIrpStackLocation例程(沒有設置IoCompletion例程)來傳遞當前位置所使用的同樣的參數組。
2). 如果需要的話,調用IoSetCompletionRoutine例程,為后期處理(post-processing)設置一個IoCompletion例程。如果驅動設置了IoCompletion例程,那么他在上一步中必須使用IoCopyCurrentIrpStackLocationToNext。
3). 通過調用IoCallDriver例程將請求傳遞到下一個驅動。這個例程會自動通告IRP棧指針,并且調用下一個驅動的分發例程。
理解這句話非常重要,這個IRP中的核心思想,也就是說,一旦你調用了IoCallDriver()把IRP傳遞給了下層的驅動,這個IRP就和你沒關系了。
如果驅動需要訪問一個已經在棧里傳下去的IRP,那么這個驅動必須實現(設置)IoCompletion例程。當I/O管理器(I/O Manager)調用IoCompletion例程時(當下層完成處理后,自動調用了回調函數),這個驅動(之前把IRP下發的那個上層驅動)就能夠在IoCompletion例程執行期間重新獲得對這一IRP的所有權。如此,IoCompletion例程就能夠訪問IRP中的域。
設置異步完成回調函數的方法上面的代碼已給出,這是一個經典的模型,即創建一個事件對象->初始化這個事件對象->上層對這個事件進行阻塞等 待->將IRP下發給下層驅動->下層驅動完成處理邏輯后設置設置這個事件(即觸發一個完成信號,解除這個事件的互斥)->上層驅動獲 得這個事件的釋放->上層驅動繼續代碼邏輯,并根據下層驅動的返回結構來做進一步的操作。
若是驅動的分發例程(上層驅動)也必須在IRP被后面的驅動(下層驅動)處理完成之后再處理它,這個IoCompletion例程(上層驅動設置的完成回調函數)必須返回STATUS_MORE_PROCESSING_REQUIRED,以將IRP的所有權返回給分發例程(上層驅動)。如此依賴,I/O管理器會停止IRP的處理(這指回卷處理,之后會解釋),將最終完成IRP的任務(調用IoCompleteRequest來完成這個IRP)留給分發例程。分發例程能夠在之后調用IoCompleteRequest來完成這個IRP,或者還能將這個IRP標記為等待進一步處理,繼續回傳給它之上的驅動。
當輸入、輸出操作(I/O)完成時,完成這個I/O操作的驅動會調用IoCompleteRequest例程,這個例程將IRP棧指針移到指向IRP棧的前一個(更上面)的位置。
如果一個驅動在設備棧中向下傳遞IRP時設定了IoCompletion例程,I/O管理器就會在IRP棧指針再次指向這一驅動的這個I/O棧位置的時候調用此例程,IoCompletion例程就表現為: 當IRP在設備棧中傳遞時,操作IRP的那些驅動的返回地址。
當每一個驅動都完成了它對應的子請求,I/O請求就完成了。I/O管理器從Irp->IoStatus.Status域取回請求的狀態信息,并且從Irp->IoStatus.Information域取回傳輸的字節數。
(這段話是我們自己根據MSDN和《寒江獨釣》的研究后總結的,說心理話,不敢保證100%正確,這塊內容確實很復雜,如果看到這篇文章的牛牛知道真實的詳細細節的話,希望不吝賜教,分享一些好的思路)
至此,我們知道了PendingReturned是用來判斷下層的驅動返回的處理狀態的。那它的利用場景是什么呢?這通常見于一些文件系統驅動過濾的應用中: 如磁盤透明加密, NTFS透明加密的編程中,我們的上層過濾驅動要先捕獲到這個IRP_MJ_READ請求,然后下放這個IRP,讓它去調用磁盤驅動讀取數據,然后在完成回調函數中對剛才讀取到的數據進行加解密,這是一個典型的應用。
?
?
?
?
6. IRP操作取消機制
BOOLEAN Cancel; KIRQL CancelIrql; . . PDRIVER_CANCEL CancelRoutine;這三個字段同屬于IRP取消機制的范湊,我們一并研究。
Cancel(BOOLEAN)如果為TRUE,則表明IoCancelIrp已被調用,該函數用于取消這個請求。如果為FALSE,則表明沒有調用IoCancelIrp函數。
CancelIrql(KIRQL)是一個IRQL值,表明那個專用的取消自旋鎖是在 這個IRQL上獲取的。當你在取消例程中釋放自旋鎖時應參考這個域。
CancelRoutine(PDRIVER_CANCEL)是驅動程序取消例程的地 址。你應該使用IoSetCancelRoutine函數設置這個域而不是直接修改該域
IRP請求的最終結局無非有兩個:要么被完成了,要么被取消了。完成IRP請求的過程已經在前面講過了,這里仔細講一個IRP請求的取消。
為什么要取消IRP請求呢?一般來講,原因不外乎是本請求操作超時或設備故障導致的。具體理解,可以考慮如下兩種情形:
情形1:驅動發送一個請求到下級驅動,下級驅動由于忙,將它放到自己的請求隊中去,下級驅動一直忙,請求一直沒有得到處理,而這個請求又比較重要,如果一直得不到處理就會造成系統處于死鎖。于是,驅動就會給這個請求加上超時機制,若超過一定的時間還沒有得到處理結果,就通知下級驅動直接取消該請求。
情形1:驅動發送很多請求到下級驅動去處理,下級驅動返回了一個請求的結果。可是,這個結果是個錯誤,而且是個很嚴重的錯誤,比如設備出故障了。這時,就要將設備進行錯誤恢復,如重啟設備,同時,其它送下去的請求都要同時取消掉。
驅動如何被取消? 一般來講,取消的發起者一定是上層的驅動,而取消的實際執行者,則是下層的驅動。
?
?
?
?
7. Tail(一個很大的聯合體)
union {struct {..union {KDEVICE_QUEUE_ENTRY DeviceQueueEntry;struct {PVOID DriverContext[4];};};..PETHREAD Thread;..LIST_ENTRY ListEntry;..} Overlay;.. } Tail;Tail.Overlay是Tail聯合中的一種 結構,它含有幾個對WDM驅動程序有潛在用途的成員。
在這個圖中,以水平方向從左到右是這個聯合的三個可 選成員,在垂直方向是每個結構的成員描述。
Tail.Overlay.DeviceQueueEntry(KDEVICE_QUEUE_ENTRY)
和
Tail.Overlay.DriverContext(PVOID[4])是Tail.Overlayare內 一個未命名聯合的兩個可選成員(只能出現一個)。
I/O管理器把DeviceQueueEntry作為設備標準請求隊列中的連接域。當IRP還沒有進入某 個隊列時,如果你擁有這個IRP你可以使用這個域,你可以任意使用DriverContext中的四個指針。
Tail.Overlay.ListEntry(LIST_ENTRY) 僅能作為你自己實現的私有隊列的連接域。我們在做文件系統驅動過濾的時候往往對讀寫請求進行串行化處理,這個時候就需要這個ListEntry來構建一個IO請求的隊列,以解決并行請求的串行化問題。
?
?
?
至此,我們把IRP頭部的數據結構分析完了,接下繼續學習下IRP Sub-Requst子請求部分的數據結構(即設備棧中的每層驅動對應的IO_STACK_LOCATION結構)
我們知道,在IRP頭部后面跟有很多個相同的結構。
PIO_STACK_LOCATION IoGetCurrentIrpStackLocation(IN PIRP Irp);使用這個函數可以獲得"本層"所對應的那個IO_STACK_LOCATION。
我們來分析一下這個IO_STACK_LOCATION的數據結構。
typedef struct _IO_STACK_LOCATION {UCHAR MajorFunction;UCHAR MinorFunction;UCHAR Flags;UCHAR Control;union {//// Parameters for IRP_MJ_CREATE // struct {PIO_SECURITY_CONTEXT SecurityContext;ULONG Options;USHORT POINTER_ALIGNMENT FileAttributes;USHORT ShareAccess;ULONG POINTER_ALIGNMENT EaLength;} Create;//// Parameters for IRP_MJ_READ // struct {ULONG Length;ULONG POINTER_ALIGNMENT Key;LARGE_INTEGER ByteOffset;} Read;//// Parameters for IRP_MJ_WRITE // struct {ULONG Length;ULONG POINTER_ALIGNMENT Key;LARGE_INTEGER ByteOffset;} Write;//// Parameters for IRP_MJ_QUERY_INFORMATION // struct {ULONG Length;FILE_INFORMATION_CLASS POINTER_ALIGNMENT FileInformationClass;} QueryFile;//// Parameters for IRP_MJ_SET_INFORMATION // struct {ULONG Length;FILE_INFORMATION_CLASS POINTER_ALIGNMENT FileInformationClass;PFILE_OBJECT FileObject;union {struct {BOOLEAN ReplaceIfExists;BOOLEAN AdvanceOnly;};ULONG ClusterCount;HANDLE DeleteHandle;};} SetFile;//// Parameters for IRP_MJ_QUERY_VOLUME_INFORMATION // struct {ULONG Length;FS_INFORMATION_CLASS POINTER_ALIGNMENT FsInformationClass;} QueryVolume;//// Parameters for IRP_MJ_DEVICE_CONTROL and IRP_MJ_INTERNAL_DEVICE_CONTROL // struct {ULONG OutputBufferLength;ULONG POINTER_ALIGNMENT InputBufferLength;ULONG POINTER_ALIGNMENT IoControlCode;PVOID Type3InputBuffer;} DeviceIoControl;//// Nonsystem service parameters.//// Parameters for IRP_MN_MOUNT_VOLUME // struct {PVOID DoNotUse1;PDEVICE_OBJECT DeviceObject;} MountVolume;//// Parameters for IRP_MN_VERIFY_VOLUME // struct {PVOID DoNotUse1;PDEVICE_OBJECT DeviceObject;} VerifyVolume;//// Parameters for Scsi using IRP_MJ_INTERNAL_DEVICE_CONTROL // struct { struct _SCSI_REQUEST_BLOCK *Srb;} Scsi;//// Parameters for IRP_MN_QUERY_DEVICE_RELATIONS // struct {DEVICE_RELATION_TYPE Type;} QueryDeviceRelations;//// Parameters for IRP_MN_QUERY_INTERFACE // struct {CONST GUID *InterfaceType;USHORT Size;USHORT Version;PINTERFACE Interface;PVOID InterfaceSpecificData;} QueryInterface;//// Parameters for IRP_MN_QUERY_CAPABILITIES // struct {PDEVICE_CAPABILITIES Capabilities;} DeviceCapabilities;//// Parameters for IRP_MN_FILTER_RESOURCE_REQUIREMENTS // struct {PIO_RESOURCE_REQUIREMENTS_LIST IoResourceRequirementList;} FilterResourceRequirements;//// Parameters for IRP_MN_READ_CONFIG and IRP_MN_WRITE_CONFIG // struct {ULONG WhichSpace;PVOID Buffer;ULONG Offset;ULONG POINTER_ALIGNMENT Length;} ReadWriteConfig;//// Parameters for IRP_MN_SET_LOCK // struct {BOOLEAN Lock;} SetLock;//// Parameters for IRP_MN_QUERY_ID // struct {BUS_QUERY_ID_TYPE IdType;} QueryId;//// Parameters for IRP_MN_QUERY_DEVICE_TEXT // struct {DEVICE_TEXT_TYPE DeviceTextType;LCID POINTER_ALIGNMENT LocaleId;} QueryDeviceText;//// Parameters for IRP_MN_DEVICE_USAGE_NOTIFICATION // struct {BOOLEAN InPath;BOOLEAN Reserved[3];DEVICE_USAGE_NOTIFICATION_TYPE POINTER_ALIGNMENT Type;} UsageNotification;//// Parameters for IRP_MN_WAIT_WAKE // struct {SYSTEM_POWER_STATE PowerState;} WaitWake;//// Parameter for IRP_MN_POWER_SEQUENCE // struct {PPOWER_SEQUENCE PowerSequence;} PowerSequence;//// Parameters for IRP_MN_SET_POWER and IRP_MN_QUERY_POWER // struct {ULONG SystemContext;POWER_STATE_TYPE POINTER_ALIGNMENT Type;POWER_STATE POINTER_ALIGNMENT State;POWER_ACTION POINTER_ALIGNMENT ShutdownType;} Power;//// Parameters for IRP_MN_START_DEVICE // struct {PCM_RESOURCE_LIST AllocatedResources;PCM_RESOURCE_LIST AllocatedResourcesTranslated;} StartDevice;//// Parameters for WMI Minor IRPs // struct {ULONG_PTR ProviderId;PVOID DataPath;ULONG BufferSize;PVOID Buffer;} WMI;//// Others - driver-specific// struct {PVOID Argument1;PVOID Argument2;PVOID Argument3;PVOID Argument4;} Others;} Parameters;PDEVICE_OBJECT DeviceObject;PFILE_OBJECT FileObject;... } IO_STACK_LOCATION, *PIO_STACK_LOCATION;?
1. IRP請求類型
UCHAR MajorFunction; UCHAR MinorFunction;在每個驅動的入口函數DriverEntry中,我們經常要做的是就是當前驅動的分發函數進行賦值。即上層應用會很多種不同的調用請求,這些請求被windows以IRP_MJ_XX這樣的主功能號進行了分類。例如下面的代碼。
NTSTATUS DriverEntry (IN PDRIVER_OBJECT DriverObject,IN PUNICODE_STRING RegistryPath) {...DriverObject->MajorFunction[IRP_MJ_CREATE] = SfCreate;DriverObject->MajorFunction[IRP_MJ_CREATE_NAMED_PIPE] = SfCreate;DriverObject->MajorFunction[IRP_MJ_CREATE_MAILSLOT] = SfCreate;DriverObject->MajorFunction[IRP_MJ_FILE_SYSTEM_CONTROL] = SfFsControl;DriverObject->MajorFunction[IRP_MJ_CLEANUP] = SfCleanupClose;DriverObject->MajorFunction[IRP_MJ_CLOSE] = SfCleanupClose;... }從面向接口編程的角度來理解,windows已經在接口中實現了一個分發例程的原型,并以數組的形式把接口(即函數地址)放了出來,我們在寫代碼的時候,如果想在本驅動中對指定的IRP類型進行處理,就必須去對"分發例程(就是這個數組)"進行賦值,并對我們提供的例程函數進行代碼實現。
IRP Major Function Codes
IRP_MJ_CREATE IRP_MJ_PNP IRP_MJ_POWER IRP_MJ_READ IRP_MJ_WRITE IRP_MJ_FLUSH_BUFFERS IRP_MJ_QUERY_INFORMATION IRP_MJ_SET_INFORMATION IRP_MJ_DEVICE_CONTROL IRP_MJ_INTERNAL_DEVICE_CONTROL IRP_MJ_SYSTEM_CONTROL IRP_MJ_CLEANUP IRP_MJ_CLOSE IRP_MJ_SHUTDOWN除了主功能號之外,還有子功能號,這是在一些PnP manager(PnP類型操作的IRP), the power manager(電源管理), file system drivers(文件系統)中需要通過子功能號來進一步對IRP的操作類型進行區分。所以每個類型的IRP操作(PnP/Power/filesystem)的子功能號都是不一樣的。我們以文件系統的子功能號為例子。
switch (irpSp->MinorFunction) { //磁盤卷掛載case IRP_MN_MOUNT_VOLUME: return SfFsControlMountVolume( DeviceObject, Irp ); //磁盤卷加載case IRP_MN_LOAD_FILE_SYSTEM: return SfFsControlLoadFileSystem( DeviceObject, Irp );//磁盤的請求case IRP_MN_USER_FS_REQUEST:{switch (irpSp->Parameters.FileSystemControl.FsControlCode) { case FSCTL_DISMOUNT_VOLUME:{..}}break;}}總之,這個功能號是用來對IRP請求的類型進行區分的,它們只是一些代號而已。
?
?
?
2. UNION Parameters
接下來是一個很大的聯合體,我們仔細觀察,其實這個聯合體還是很有規律的,而且也很簡單,因為有規律的東西往往會相對簡單。
里面包含了每種IRP請求所需要的參數,可以參考MSDN上的解釋。
http://msdn.microsoft.com/en-us/library/ff550659
?
?
?
3. PDEVICE_OBJECT? DeviceObject
指向這個IO_STACK_LOCATINO所對應的設備對象。可能是中間的過濾設備,也可能是底層的真實設備。
?
?
?
4. PFILE_OBJECT? FileObject
指向一個這個IRP對應的文件對象,這個文件對象是一個廣義的概念,在內核中,磁盤/文件/目錄都算是一種文件。
typedef struct _FILE_OBJECT {CSHORT Type;CSHORT Size;PDEVICE_OBJECT DeviceObject;PVPB Vpb;PVOID FsContext;PVOID FsContext2;PSECTION_OBJECT_POINTERS SectionObjectPointer;PVOID PrivateCacheMap;NTSTATUS FinalStatus;struct _FILE_OBJECT *RelatedFileObject;BOOLEAN LockOperation;BOOLEAN DeletePending;BOOLEAN ReadAccess;BOOLEAN WriteAccess;BOOLEAN DeleteAccess;BOOLEAN SharedRead;BOOLEAN SharedWrite;BOOLEAN SharedDelete;ULONG Flags;UNICODE_STRING FileName;LARGE_INTEGER CurrentByteOffset;ULONG Waiters;ULONG Busy;PVOID LastLock;KEVENT Lock;KEVENT Event;PIO_COMPLETION_CONTEXT CompletionContext;KSPIN_LOCK IrpListLock;LIST_ENTRY IrpList;PVOID FileObjectExtension; } FILE_OBJECT, *PFILE_OBJECT;在文件系統的過濾驅動的編程中,我們經常要使用到這個參數,來獲取這次操作所涉及到的文件對象,以此得到這個文件的相關信息。由于本次筆記重點是數據結構的學習,相關的使用場景打算在后續的學習筆記中進行應用。
?
總結
以上是生活随笔為你收集整理的IRP IO_STACK_LOCATION 《寒江独钓》内核学习笔记(1)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Element-Ui 复选框动态改变绑定
- 下一篇: 2019招商银行信用卡中心秋招IT笔试编