IRP IO_STACK_LOCATION 《寒江独钓》内核学习笔记(1)
在學(xué)習(xí)內(nèi)核過濾驅(qū)動的過程中,遇到了大量的涉及IRP操作的代碼,這里有必要對IRP的數(shù)據(jù)結(jié)構(gòu)和與之相關(guān)的API函數(shù)做一下筆記。
?
1. 相關(guān)閱讀資料
《深入解析 windows 操作系統(tǒng)(第4版,中文版)》 --- 9章
《windows driver kit 幫助文檔》
http://support.microsoft.com/kb/115758/zh-cn? IRP 結(jié)構(gòu)中各地址字段的含義
http://www.programlife.net/io_stack_location-irp.html??? 代碼瘋子對IRP的研究
?
?
?
?
2. IRP的數(shù)據(jù)結(jié)構(gòu)
IRP是一個數(shù)據(jù)結(jié)構(gòu),其中包含了用來描述一個IO請求的完整信息。
IO管理器創(chuàng)建一個IRP來代表一個IO操作,并且將該IRP傳遞給正確的驅(qū)動程序,當(dāng)此IO操作完成時再處理該請求包。相對的,驅(qū)動程序(上層的虛擬設(shè)備驅(qū)動或者底層的真實設(shè)備驅(qū)動)接收一個IRP,執(zhí)行該IRP指定的操作,然后將IRP傳回給IO管理器,告訴它,該操作已經(jīng)完成,或者應(yīng)該傳給另一個驅(qū)動以進行進一步處理。
?
談到IRP,IRP是個總的概念,本質(zhì)上IRP由IRP Header和IRP Sub-Request組成
從數(shù)據(jù)結(jié)構(gòu)的角度上來說,其實數(shù)據(jù)結(jié)構(gòu) IRP 只是"I/O 請求包"IRP的頭部,在 IRP 數(shù)據(jù)結(jié)構(gòu)的后面還有一個IO_STACK_LOCATION 數(shù)據(jù)結(jié)構(gòu)的數(shù)組,數(shù)組的大小則取決于 IRP 數(shù)據(jù)結(jié)構(gòu)中的StackCount(我們之后會詳細分析),其數(shù)值來自設(shè)備堆棧中頂層設(shè)備對象的 StackSize 字段。
這樣,就在 IRP 中為目標(biāo)設(shè)備對象設(shè)備堆棧中的每一層即每個模塊(每個驅(qū)動)都準(zhǔn)備好了一個 IO_STACK_LOCATION 數(shù)據(jù)結(jié)構(gòu)。而CurrentLocation,則是用于該數(shù)組的下標(biāo),說明目前是在堆疊中的哪一層,因而正在使用哪一個 IO_STACK_LOCATION 數(shù)據(jù)結(jié)構(gòu)。
?
那這兩個結(jié)構(gòu)是怎么來的呢?有兩種渠道:
1. 程序員在代碼中手工地創(chuàng)建一個IRP(廣義的IRP)
PIRP IoAllocateIrp(IN CCHAR StackSize,IN BOOLEAN ChargeQuota);任何內(nèi)核模式程序在創(chuàng)建一個IRP時,同時還創(chuàng)建了一個與之關(guān)聯(lián)的 IO_STACK_LOCATION 結(jié)構(gòu)數(shù)組:數(shù)組中的每個堆棧單元都對應(yīng)一個將處理該IRP的驅(qū)動程序,堆棧單元中包含該IRP的類型代碼和參數(shù)信息以及完成函數(shù)的地址。
2. I/O管理器在接收到應(yīng)用層的設(shè)備讀寫請求后,將請求封裝為一個IRP請求(包括IRP頭部和IRP STACK_LOCATINO數(shù)組)發(fā)往對應(yīng)的設(shè)備的設(shè)備棧的最頂層的那個設(shè)備驅(qū)動。
?
我們先從IRP的頭結(jié)構(gòu)開始學(xué)起:
下面是WDK上搬下來的解釋,我們來一條一條的學(xué)習(xí)。
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是一個半透明結(jié)構(gòu),開發(fā)者只能訪問其中透明的部分,所以其中的..代表是我們不能訪問的部分,所以我們在編程中基本上也不會用到它們,我們集中精力來觀察這些暴露出來的數(shù)據(jù)結(jié)構(gòu)。
?
?
?
1. IRP中的三種緩沖區(qū)
PMDL MdlAddress; union {struct _IRP *MasterIrp;..PVOID SystemBuffer;} AssociatedIrp; PVOID UserBuffer;IRP這個數(shù)據(jù)結(jié)構(gòu)中有3個地方可以描述緩沖區(qū)。?
1) irp->AssociatedIrp.SystemBuffer 2) irp->MdlAddress 3) irp->UserBuffer不同的IO類別,IRP的緩沖區(qū)不同。
1) AssociatedIrp.SystemBuffer
一般用于比較簡單且不追求效率情況下的解決方案 把R3層中的內(nèi)存中的緩沖數(shù)據(jù)拷貝到內(nèi)核空間中。注意,是直接拷貝過來,有的書上會說這是"直接方式",不過我們要重點記住的是它是直接拷貝過來。
2) MdlAddress
通過構(gòu)造MDL就能實現(xiàn)這個R3到R0的地址映射功能。MDL可以翻譯為"內(nèi)存描述符鏈",本質(zhì)上就是一個指針,從這個MDL中可以讀出一個內(nèi)核空間的虛擬地址。這就彌補了UserBuffer的不足,同時比SystemBuffer的完全拷貝方法要輕量,因為這個內(nèi)存實際上還是在老地方,沒有拷貝。
3) UserBuffer
最追求效率的解決方案 R3的緩沖區(qū)地址直接放在UserBuffer里,在內(nèi)核空間中直接訪問。在當(dāng)前進程和發(fā)送進程一致的情況下,內(nèi)核訪問應(yīng)用層的內(nèi)存空間當(dāng)然是沒錯的。但是一 旦內(nèi)核進程已經(jīng)切換,這個訪問就結(jié)束了,訪問UserBuffer當(dāng)然是跳到其他進程空間去了(我們還是訪問同一個地址,但是這個時候因為進程的上下文切換了,同一個地址對應(yīng)的內(nèi)容自然不同了)。因為在windows中,內(nèi)核空間是所有進程共享的,而應(yīng) 用層空間則是各個進程隔離的。當(dāng)然還有一個更簡單的做法是把應(yīng)用層的地址空間映射到內(nèi)核空間,這需要在頁表中增加一個映射。
在驅(qū)動在獲取這三種緩沖區(qū)的方法。
//獲得緩沖區(qū) 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; }看到這里,就產(chǎn)生另一個問題了,這三種緩沖區(qū)是操作系統(tǒng)幫我們自動填好的嗎?那在什么樣的IRP請求會使用到不同的緩沖區(qū)類型呢?
這里就要涉及到兩種內(nèi)存訪問方式: 直接方式DO_DIRECT_IO / 非直接方式(緩沖方式)DO_BUFFERD_IO
1) 在buffered(AssociatedIrp.SystemBuffer)方式中,I/O管理器先創(chuàng)建一個與用戶模式數(shù)據(jù)緩沖區(qū)大小相等的系統(tǒng)緩沖區(qū)。而你的驅(qū)動程序?qū)⑹褂眠@個系統(tǒng)緩沖區(qū)工作。I/O管理器負責(zé)在系統(tǒng)緩沖區(qū)和用戶模式緩沖區(qū)之間復(fù)制數(shù)據(jù)。
2) 在direct(MdlAddress)方式中,I/O管理器鎖定了包含用戶模式緩沖區(qū)的物理內(nèi)存頁,并創(chuàng)建一個稱為MDL(內(nèi)存描述符表)的輔助數(shù)據(jù)結(jié)構(gòu)來描述鎖定頁。因此你的驅(qū)動程序?qū)⑹褂肕DL工作。
3) 在neither(UserBuffer)方式中,I/O管理器僅簡單地把用戶模式的虛擬地址傳遞給你。而使用用戶模式地址的驅(qū)動程序應(yīng)十分小心。
?
繼續(xù)思考我們之前的問題,在IRP中具體是使用哪種緩沖方式呢?由誰來決定?
答案是在增加設(shè)備的時候就決定的了。即我們在新增一個設(shè)備的時候就要決定這個設(shè)備的緩沖區(qū)讀寫方式。
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; }總結(jié)一下,在新增設(shè)備的時候這個物理設(shè)備的數(shù)據(jù)緩沖方式就被決定了。這之后你不能該變緩沖方式的設(shè)置,因為過濾器驅(qū)動程序?qū)?fù)制這個標(biāo)志設(shè)置,并且,如果你改變了設(shè)置,過濾器驅(qū)動程序沒有辦法知道這個改變。
接下來解決目前為止的最后一個疑問: 系統(tǒng)是如何來根據(jù)設(shè)備對象中的緩沖類型碼來進行不同緩沖區(qū)類型的的緩沖區(qū)數(shù)據(jù)的實際填充過程的(即實際的緩沖區(qū)填充過程是什么)?
1) Buffered方式(有一個復(fù)制過程)
當(dāng)I/O管理器創(chuàng)建IRP_MJ_READ或IRP_MJ_WRITE請求時(讀寫請求中會用到數(shù)據(jù)緩沖區(qū),普通的文件屬性查詢請求或設(shè)置中最多用到一個指定的數(shù)據(jù)結(jié)構(gòu)大小的內(nèi)存空間即可),它探測設(shè)備的緩沖標(biāo)志(在創(chuàng)建設(shè)備時就決定的了)以決定如何描述新IRP中的數(shù)據(jù)緩沖區(qū)。如果DO_BUFFERED_IO(UserBuffer)標(biāo)志設(shè)置,I/O管理器將分配與用戶緩沖區(qū)大小相同的"非分頁"內(nèi)存(不會產(chǎn)生缺頁中斷的內(nèi)存空間)。
注意,它把緩沖區(qū)的地址和長度保存到兩個十分不同的地方,下面是一段模擬代碼。你可以假定I/O管理器執(zhí)行下面代碼(注意這并不是Windows NT的源代碼):
可以看出,系統(tǒng)緩沖區(qū)地址被放在IRP的AssociatedIrp.SystemBuffer域中,而數(shù)據(jù)的長度被放到stack->Parameters聯(lián)合中。I/O管理器把用戶模式虛擬地址(uva變量)保存到IRP的UserBuffer域中,這樣一來內(nèi)核驅(qū)動代碼就可以找到這個地址。
?
2) Direct方式
如果你在設(shè)備對象中指定DO_DIRECT_IO(MDL)方式,I/O管理器將創(chuàng)建一個MDL用來描述包含該用戶模式數(shù)據(jù)緩沖區(qū)的鎖定內(nèi)存頁(本質(zhì)上還是一種映射,創(chuàng)建映射到同一內(nèi)存位置的內(nèi)核模式虛擬地址)。MDL結(jié)構(gòu)的聲明如下:
?
?StartVa成員給出了用戶緩沖區(qū)的虛擬地址,這個地址僅在擁有數(shù)據(jù)緩沖區(qū)的用戶模式進程上下文中才有效(即用戶模式中的虛擬地址空間)。ByteOffset是緩沖區(qū)起始位置在一個頁幀中的偏移值,ByteCount是緩沖區(qū)的字節(jié)長度。Pages數(shù)組沒有被正式地聲明為MDL結(jié)構(gòu)的一部分,在內(nèi)存中它跟在MDL的后面,包含用戶模式虛擬地址映射為物理頁幀的個數(shù)。
要注意的是,我們不可以直接訪問MDL的任何成員。應(yīng)該使用宏或訪問函數(shù):
宏或函數(shù) 描述 IoAllocateMdl 創(chuàng)建MDL(在文件系統(tǒng)驅(qū)動的透明加密中你可能需要創(chuàng)建自己的臨時MDL以暫時替換系統(tǒng)的原始的MDL)IoBuildPartialMdl 創(chuàng)建一個已存在MDL的子MDLIoFreeMdl 銷毀MDLMmBuildMdlForNonPagedPool 修改MDL以描述內(nèi)核模式中一個非分頁內(nèi)存區(qū)域MmGetMdlByteCount 取緩沖區(qū)字節(jié)大小MmGetMdlByteOffset 取緩沖區(qū)在第一個內(nèi)存頁中的偏移MmGetMdlVirtualAddress 取虛擬地址MmGetSystemAddressForMdl 創(chuàng)建映射到同一內(nèi)存位置的內(nèi)核模式虛擬地址MmGetSystemAddressForMdlSafe 與MmGetSystemAddressForMdl相同,但Windows 2000首選MmInitializeMdl (再)初始化MDL以描述一個給定的虛擬緩沖區(qū)MmPrepareMdlForReuse 再初始化MDLMmProbeAndLockPages 地址有效性校驗后鎖定內(nèi)存頁MmSizeOfMdl 取為描述一個給定的虛擬緩沖區(qū)的MDL所占用的內(nèi)存大小MmUnlockPages 為該MDL解鎖內(nèi)存頁對于I/O管理器執(zhí)行的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管理器首先創(chuàng)建一個描述用戶緩沖區(qū)的MDL。IoAllocateMdl的第三個參數(shù)(FALSE)指出這是一個主數(shù)據(jù)緩沖區(qū)。第四個參數(shù)(TRUE)指出內(nèi)存管理器應(yīng)把該內(nèi)存充入進程配額。最后一個參數(shù)(Irp)指定該MDL應(yīng)附著的IRP。在內(nèi)部,IoAllocateMdl把Irp->MdlAddress設(shè)置為新創(chuàng)建MDL的地址,以后你將用到這個成員,并且I/O管理器最后也使用該成員來清除MDL。
這段代碼的關(guān)鍵地方是調(diào)用MmProbeAndLockPages。該函數(shù)校驗?zāi)莻€數(shù)據(jù)緩沖區(qū)是否有效,是否可以按適當(dāng)模式訪問。如果我們向設(shè)備寫數(shù)據(jù),我們必須能讀緩沖區(qū)。如果我們從設(shè)備讀數(shù)據(jù),我們必須能寫緩沖區(qū)。另外,該函數(shù)鎖定了包含數(shù)據(jù)緩沖區(qū)的物理內(nèi)存頁,并在MDL的后面填寫了頁號數(shù)組。在效果上,一個鎖定的內(nèi)存頁將成為非分頁內(nèi)存池的一部分,直到所有對該頁內(nèi)存加鎖的調(diào)用者都對其解了鎖。
在Direct方式的讀寫操作中,對MDL你最可能做的事是把它作為參數(shù)傳遞給其它函數(shù)。例如,DMA傳輸?shù)腗apTransfer步驟需要一個MDL。另外,在內(nèi)部,USB讀寫操作總使用MDL。所以你應(yīng)該把讀寫操作設(shè)置為DO_DIRECT_IO方式,并把結(jié)果MDL傳遞給USB總線驅(qū)動程序。
順便提一下,I/O管理器確實在stack->Parameters聯(lián)合中保存了讀寫請求的長度,但驅(qū)動程序應(yīng)該直接從MDL中獲得請求數(shù)據(jù)的長度
?
?
3) Neither方式
如果你在設(shè)備對象中同時忽略了DO_DIRECT_IO和DO_BUFFERED_IO標(biāo)志設(shè)置,你將得到默認的neither方式。對于這種方式,I/O管理器將簡單地把用戶模式虛擬地址和字節(jié)計數(shù)直接交給你,其余的工作由你去做。這種情況下程序員將自己去解決因為進程的切換導(dǎo)致的用戶模式地址失效問題。
至此,我們目前的疑問就全部解決了,我們知道了IRP中的三種不同的緩沖區(qū)是怎么來的(設(shè)備添加的時候決定的),是由誰填充的(操作系統(tǒng)自動地把用戶模式地址空間的數(shù)據(jù)填充到IRP中的緩沖區(qū)中/或者直接給出用戶空間地址)。
接下來就可以引出IRP中(準(zhǔn)備說是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:在文件系統(tǒng)驅(qū)動程序的編程中將使用到這個數(shù)據(jù)域,這對所有的驅(qū)動程序來說是只讀的,它指示了這個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關(guān)于這個Flags字段,我們常常要注意的是,我們?nèi)绻谧鲞^濾/綁定/捕獲類型的驅(qū)動類型的編程中,我們新創(chuàng)建的上層過濾設(shè)備的Flags字段的值一定要和下層的真實設(shè)備或底層驅(qū)動的Flags保持一致。
比如我在做文件系統(tǒng)卷的過濾設(shè)備編程的時候就遇到這樣的代碼:
//設(shè)備標(biāo)志的復(fù)制 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)是一個僅包含兩個域的結(jié)構(gòu),驅(qū)動程序在最終完成請求時設(shè)置這個結(jié)構(gòu)。
IoStatus.Status 表示IRP完成狀態(tài)
ntdef.h
ntstatus.h
WDK的這兩個頭文件中定了所有的系統(tǒng)級的返回信息,在驅(qū)動中,函數(shù)的返回值大多數(shù)情況下就是這樣結(jié)果狀態(tài)信息,當(dāng)然也有通過引用的方式獲得函數(shù)執(zhí)行的結(jié)果的,但是函數(shù)還是返回個執(zhí)行結(jié)果。
IoStatus.information的值與請求相關(guān),如果是數(shù)據(jù)傳輸請求,則將該域設(shè)置為傳輸?shù)淖止?jié)數(shù)(在windows編程中,基本上涉及到數(shù)據(jù)讀寫的函數(shù)一般都是返回這一類的結(jié)果,即操作的字節(jié)數(shù))。
?
?
?
?
4. KPROCESSOR_MODE? RequestorMode
RequestorMode將等于一個枚舉常量UserMode或KernelMode, 指定原始I/O請求的來源。驅(qū)動程序有時需要查看這個值來決定是否要信任某些參數(shù)。
?
?
?
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.這段話是什么意思呢?這和內(nèi)核驅(qū)動中的多層設(shè)備棧有關(guān)系。為了解釋這個問題,我們先來看一段代碼demo:
.... Kevent event; KeInitializeEvent(&event, NotificatinoEvent, FALSE); IoCopyCurrentIrpStackLocationToNext(Irp); //設(shè)置完成回調(diào)函數(shù) IoSetCompletionRoutine(Irp,IrpComplete, //回調(diào)函數(shù)&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完成回調(diào)函數(shù)的原型 NTSTATUS IrpComplete(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp,IN PVOID Context ) {...if(Irp->PendingReturned){//這個函數(shù)等價于: Irp->IoStatus.Status = STATUS_PENDING 即表名這個IRP處理流程依舊沒有結(jié)束 IoMarkIrpPending(Irp);}return Irp->IoStatus.Status; }請原諒我沒頭沒腦的給這出這段看起來不知所云的代碼。這是因為IRP機制是一種基礎(chǔ)機制,往往是配合一些具體的過濾驅(qū)動的編程而使用的,如果要給出完整的例程那這篇文章的篇幅就會無窮無盡了。
所以接下來我盡我最大的能力來解釋這段代碼的意思并給出它的利用場景。
IRP作為一個線程無關(guān)的調(diào)用棧
進行一個設(shè)備的I/O操作通常需要調(diào)用這一設(shè)備相關(guān)的不止一個驅(qū)動。每一個和這個設(shè)備相關(guān)的驅(qū)動都會創(chuàng)建一個設(shè)備對象(Device Object),并且這些設(shè)備對象會垂直壓入(排列進)一個設(shè)備棧(Device Stack)。IRP會在設(shè)備棧中從上到下的一個個被傳遞進去(從頂層的設(shè)備驅(qū)動一直往下到底層的真實設(shè)備)。對于棧中的每一個驅(qū)動,IRP都會用一個指針標(biāo)識一個棧位置(IO_STACK_LOCATION和設(shè)備棧上的設(shè)備驅(qū)動的一一對應(yīng)關(guān)系用這個指針來綁定)。由于驅(qū)動可以異步地處理請求,因此IRP就像是一個線程無關(guān)的調(diào)用棧一樣
仔細看這張圖,每一級驅(qū)動程序都使用下一級驅(qū)動程序的堆棧單元保存自己完成例程指針。最底層的驅(qū)動程序不應(yīng)該安裝一個完成例程,它應(yīng)該總是返回一個真實的硬件操作結(jié)果。即我們在當(dāng)前位置的設(shè)備驅(qū)動中設(shè)置一個下層驅(qū)動的完成回調(diào)函數(shù)時,本質(zhì)上是在下層驅(qū)動的棧空間中布置一個了一個回調(diào)函數(shù)地址。
將IRP傳遞到下一級驅(qū)動程序(又被稱作轉(zhuǎn)發(fā)IRP)是指IRP等價于一個子例程調(diào)用。當(dāng)驅(qū)動轉(zhuǎn)發(fā)一個IRP,這個驅(qū)動程序必須向IRP參數(shù)組增加下一個I/O棧位置,告知這一IRP棧的指針,然后調(diào)用下一驅(qū)動的分發(fā)例程(dispatch routine)。基本來說,就是驅(qū)動向下調(diào)用IRP棧(calling down the IRP stack)
傳遞一個IRP,驅(qū)動通常會采取以下幾種步驟
1) . 建立下一個I/O棧位置的參數(shù)。
1.1) 調(diào)用IoGetNextIrpStackLocation例程來得到一個指針指向下一個I/O棧位置,然后將請求參數(shù)數(shù)組復(fù)制到那個得到的位置
1.2) 調(diào)用CopyCurrentIrpStackLocationToNext例程(如果驅(qū)動設(shè)置了IoCompletion例程)
或者
1.3) 調(diào)用IoSkipCurrentIrpStackLocation例程(沒有設(shè)置IoCompletion例程)來傳遞當(dāng)前位置所使用的同樣的參數(shù)組。
2). 如果需要的話,調(diào)用IoSetCompletionRoutine例程,為后期處理(post-processing)設(shè)置一個IoCompletion例程。如果驅(qū)動設(shè)置了IoCompletion例程,那么他在上一步中必須使用IoCopyCurrentIrpStackLocationToNext。
3). 通過調(diào)用IoCallDriver例程將請求傳遞到下一個驅(qū)動。這個例程會自動通告IRP棧指針,并且調(diào)用下一個驅(qū)動的分發(fā)例程。
理解這句話非常重要,這個IRP中的核心思想,也就是說,一旦你調(diào)用了IoCallDriver()把IRP傳遞給了下層的驅(qū)動,這個IRP就和你沒關(guān)系了。
如果驅(qū)動需要訪問一個已經(jīng)在棧里傳下去的IRP,那么這個驅(qū)動必須實現(xiàn)(設(shè)置)IoCompletion例程。當(dāng)I/O管理器(I/O Manager)調(diào)用IoCompletion例程時(當(dāng)下層完成處理后,自動調(diào)用了回調(diào)函數(shù)),這個驅(qū)動(之前把IRP下發(fā)的那個上層驅(qū)動)就能夠在IoCompletion例程執(zhí)行期間重新獲得對這一IRP的所有權(quán)。如此,IoCompletion例程就能夠訪問IRP中的域。
設(shè)置異步完成回調(diào)函數(shù)的方法上面的代碼已給出,這是一個經(jīng)典的模型,即創(chuàng)建一個事件對象->初始化這個事件對象->上層對這個事件進行阻塞等 待->將IRP下發(fā)給下層驅(qū)動->下層驅(qū)動完成處理邏輯后設(shè)置設(shè)置這個事件(即觸發(fā)一個完成信號,解除這個事件的互斥)->上層驅(qū)動獲 得這個事件的釋放->上層驅(qū)動繼續(xù)代碼邏輯,并根據(jù)下層驅(qū)動的返回結(jié)構(gòu)來做進一步的操作。
若是驅(qū)動的分發(fā)例程(上層驅(qū)動)也必須在IRP被后面的驅(qū)動(下層驅(qū)動)處理完成之后再處理它,這個IoCompletion例程(上層驅(qū)動設(shè)置的完成回調(diào)函數(shù))必須返回STATUS_MORE_PROCESSING_REQUIRED,以將IRP的所有權(quán)返回給分發(fā)例程(上層驅(qū)動)。如此依賴,I/O管理器會停止IRP的處理(這指回卷處理,之后會解釋),將最終完成IRP的任務(wù)(調(diào)用IoCompleteRequest來完成這個IRP)留給分發(fā)例程。分發(fā)例程能夠在之后調(diào)用IoCompleteRequest來完成這個IRP,或者還能將這個IRP標(biāo)記為等待進一步處理,繼續(xù)回傳給它之上的驅(qū)動。
當(dāng)輸入、輸出操作(I/O)完成時,完成這個I/O操作的驅(qū)動會調(diào)用IoCompleteRequest例程,這個例程將IRP棧指針移到指向IRP棧的前一個(更上面)的位置。
如果一個驅(qū)動在設(shè)備棧中向下傳遞IRP時設(shè)定了IoCompletion例程,I/O管理器就會在IRP棧指針再次指向這一驅(qū)動的這個I/O棧位置的時候調(diào)用此例程,IoCompletion例程就表現(xiàn)為: 當(dāng)IRP在設(shè)備棧中傳遞時,操作IRP的那些驅(qū)動的返回地址。
當(dāng)每一個驅(qū)動都完成了它對應(yīng)的子請求,I/O請求就完成了。I/O管理器從Irp->IoStatus.Status域取回請求的狀態(tài)信息,并且從Irp->IoStatus.Information域取回傳輸?shù)淖止?jié)數(shù)。
(這段話是我們自己根據(jù)MSDN和《寒江獨釣》的研究后總結(jié)的,說心理話,不敢保證100%正確,這塊內(nèi)容確實很復(fù)雜,如果看到這篇文章的牛牛知道真實的詳細細節(jié)的話,希望不吝賜教,分享一些好的思路)
至此,我們知道了PendingReturned是用來判斷下層的驅(qū)動返回的處理狀態(tài)的。那它的利用場景是什么呢?這通常見于一些文件系統(tǒng)驅(qū)動過濾的應(yīng)用中: 如磁盤透明加密, NTFS透明加密的編程中,我們的上層過濾驅(qū)動要先捕獲到這個IRP_MJ_READ請求,然后下放這個IRP,讓它去調(diào)用磁盤驅(qū)動讀取數(shù)據(jù),然后在完成回調(diào)函數(shù)中對剛才讀取到的數(shù)據(jù)進行加解密,這是一個典型的應(yīng)用。
?
?
?
?
6. IRP操作取消機制
BOOLEAN Cancel; KIRQL CancelIrql; . . PDRIVER_CANCEL CancelRoutine;這三個字段同屬于IRP取消機制的范湊,我們一并研究。
Cancel(BOOLEAN)如果為TRUE,則表明IoCancelIrp已被調(diào)用,該函數(shù)用于取消這個請求。如果為FALSE,則表明沒有調(diào)用IoCancelIrp函數(shù)。
CancelIrql(KIRQL)是一個IRQL值,表明那個專用的取消自旋鎖是在 這個IRQL上獲取的。當(dāng)你在取消例程中釋放自旋鎖時應(yīng)參考這個域。
CancelRoutine(PDRIVER_CANCEL)是驅(qū)動程序取消例程的地 址。你應(yīng)該使用IoSetCancelRoutine函數(shù)設(shè)置這個域而不是直接修改該域
IRP請求的最終結(jié)局無非有兩個:要么被完成了,要么被取消了。完成IRP請求的過程已經(jīng)在前面講過了,這里仔細講一個IRP請求的取消。
為什么要取消IRP請求呢?一般來講,原因不外乎是本請求操作超時或設(shè)備故障導(dǎo)致的。具體理解,可以考慮如下兩種情形:
情形1:驅(qū)動發(fā)送一個請求到下級驅(qū)動,下級驅(qū)動由于忙,將它放到自己的請求隊中去,下級驅(qū)動一直忙,請求一直沒有得到處理,而這個請求又比較重要,如果一直得不到處理就會造成系統(tǒng)處于死鎖。于是,驅(qū)動就會給這個請求加上超時機制,若超過一定的時間還沒有得到處理結(jié)果,就通知下級驅(qū)動直接取消該請求。
情形1:驅(qū)動發(fā)送很多請求到下級驅(qū)動去處理,下級驅(qū)動返回了一個請求的結(jié)果。可是,這個結(jié)果是個錯誤,而且是個很嚴重的錯誤,比如設(shè)備出故障了。這時,就要將設(shè)備進行錯誤恢復(fù),如重啟設(shè)備,同時,其它送下去的請求都要同時取消掉。
驅(qū)動如何被取消? 一般來講,取消的發(fā)起者一定是上層的驅(qū)動,而取消的實際執(zhí)行者,則是下層的驅(qū)動。
?
?
?
?
7. Tail(一個很大的聯(lián)合體)
union {struct {..union {KDEVICE_QUEUE_ENTRY DeviceQueueEntry;struct {PVOID DriverContext[4];};};..PETHREAD Thread;..LIST_ENTRY ListEntry;..} Overlay;.. } Tail;Tail.Overlay是Tail聯(lián)合中的一種 結(jié)構(gòu),它含有幾個對WDM驅(qū)動程序有潛在用途的成員。
在這個圖中,以水平方向從左到右是這個聯(lián)合的三個可 選成員,在垂直方向是每個結(jié)構(gòu)的成員描述。
Tail.Overlay.DeviceQueueEntry(KDEVICE_QUEUE_ENTRY)
和
Tail.Overlay.DriverContext(PVOID[4])是Tail.Overlayare內(nèi) 一個未命名聯(lián)合的兩個可選成員(只能出現(xiàn)一個)。
I/O管理器把DeviceQueueEntry作為設(shè)備標(biāo)準(zhǔn)請求隊列中的連接域。當(dāng)IRP還沒有進入某 個隊列時,如果你擁有這個IRP你可以使用這個域,你可以任意使用DriverContext中的四個指針。
Tail.Overlay.ListEntry(LIST_ENTRY) 僅能作為你自己實現(xiàn)的私有隊列的連接域。我們在做文件系統(tǒng)驅(qū)動過濾的時候往往對讀寫請求進行串行化處理,這個時候就需要這個ListEntry來構(gòu)建一個IO請求的隊列,以解決并行請求的串行化問題。
?
?
?
至此,我們把IRP頭部的數(shù)據(jù)結(jié)構(gòu)分析完了,接下繼續(xù)學(xué)習(xí)下IRP Sub-Requst子請求部分的數(shù)據(jù)結(jié)構(gòu)(即設(shè)備棧中的每層驅(qū)動對應(yīng)的IO_STACK_LOCATION結(jié)構(gòu))
我們知道,在IRP頭部后面跟有很多個相同的結(jié)構(gòu)。
PIO_STACK_LOCATION IoGetCurrentIrpStackLocation(IN PIRP Irp);使用這個函數(shù)可以獲得"本層"所對應(yīng)的那個IO_STACK_LOCATION。
我們來分析一下這個IO_STACK_LOCATION的數(shù)據(jù)結(jié)構(gòu)。
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;在每個驅(qū)動的入口函數(shù)DriverEntry中,我們經(jīng)常要做的是就是當(dāng)前驅(qū)動的分發(fā)函數(shù)進行賦值。即上層應(yīng)用會很多種不同的調(diào)用請求,這些請求被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已經(jīng)在接口中實現(xiàn)了一個分發(fā)例程的原型,并以數(shù)組的形式把接口(即函數(shù)地址)放了出來,我們在寫代碼的時候,如果想在本驅(qū)動中對指定的IRP類型進行處理,就必須去對"分發(fā)例程(就是這個數(shù)組)"進行賦值,并對我們提供的例程函數(shù)進行代碼實現(xiàn)。
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(文件系統(tǒng))中需要通過子功能號來進一步對IRP的操作類型進行區(qū)分。所以每個類型的IRP操作(PnP/Power/filesystem)的子功能號都是不一樣的。我們以文件系統(tǒng)的子功能號為例子。
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請求的類型進行區(qū)分的,它們只是一些代號而已。
?
?
?
2. UNION Parameters
接下來是一個很大的聯(lián)合體,我們仔細觀察,其實這個聯(lián)合體還是很有規(guī)律的,而且也很簡單,因為有規(guī)律的東西往往會相對簡單。
里面包含了每種IRP請求所需要的參數(shù),可以參考MSDN上的解釋。
http://msdn.microsoft.com/en-us/library/ff550659
?
?
?
3. PDEVICE_OBJECT? DeviceObject
指向這個IO_STACK_LOCATINO所對應(yīng)的設(shè)備對象。可能是中間的過濾設(shè)備,也可能是底層的真實設(shè)備。
?
?
?
4. PFILE_OBJECT? FileObject
指向一個這個IRP對應(yīng)的文件對象,這個文件對象是一個廣義的概念,在內(nèi)核中,磁盤/文件/目錄都算是一種文件。
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;在文件系統(tǒng)的過濾驅(qū)動的編程中,我們經(jīng)常要使用到這個參數(shù),來獲取這次操作所涉及到的文件對象,以此得到這個文件的相關(guān)信息。由于本次筆記重點是數(shù)據(jù)結(jié)構(gòu)的學(xué)習(xí),相關(guān)的使用場景打算在后續(xù)的學(xué)習(xí)筆記中進行應(yīng)用。
?
總結(jié)
以上是生活随笔為你收集整理的IRP IO_STACK_LOCATION 《寒江独钓》内核学习笔记(1)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Element-Ui 复选框动态改变绑定
- 下一篇: 2019招商银行信用卡中心秋招IT笔试编