驅動理解備忘錄

驅動設備層次結構

NT式以及WDM驅動都可以分層,即設備創建時,先創建底層PDO(物理設備對象,檢測到設備時由PNP管理器創建),隨後創建高層FDO(功能設備對象,IoCreateDevice創建)。每個PDO上必須有一個FDO來操作。可以通過IoAttachDeviceToDeviceStack將FDO附加到其他設備對象上。

垂直遍歷設備對象: 底層設備對象尋找上一層設備對象,通過對象結構體內AttachedDevice成員從下往上尋找上一層設備對象。若想從上向下尋找,可以在自定義設備擴展中記錄下層設備對象地址。

水平遍歷設備對象: 同一驅動程序創建出的所有設備對象就是同一水平的設備對象。通過成員NextDevice可遍歷所有同一個驅動創建出的所有設備對象。同一水平的設備對象的DriverObject指向同一驅動對象。

驅動設備IRP處理

驅動設備通過驅動中的MajorFunction數組設置對應的IRP主類型的派遣例程。在進入DriverEntry前,該數組所有元素被設置爲函數_IopInvalidDeviceRequest(該例程會轉發電源管理相關IRP,隨後設置IRP錯誤碼0xC0000010【指定的請求不是目標設備的有效操作】完成並返回該IRP的錯誤碼)。IRP的常用處理方式有:

完成IRP: 通過IoCompleteRequest完成IRP,完成前需要設置IRP完成狀態(pIrp->IoStatus.Status)以及IRP信息(pIrp->IoStatus.Information)。該函數第二個參數爲調用線程恢復時的優先級別。通常設置爲IO_NO_INCREMENT。

在同步操作時,用戶層會創建一個事件傳到派遣函數中並等待,IoCompleteRequest在完成該IRP時,會設置該事件。
異步操作時,若通過事件進行異步,IoCompleteRequest完成時會設置該事件,若通過完成例程進行異步操作時(例如ReadFileEx),IoCompleteRequest在完成IRP時會將該例程插入到用戶APC隊列中。當調用線程進入警惕模式後,該例程執行。

掛起IRP: 當異步讀寫設備時,設備例程首先要做一些不耗時的簡單處理,隨後需要將IRP通過IoMarkIrpPending函數掛起,最後返回STATUS_PENDING。

轉發IRP: 分層驅動對IRP做完處理後,如果需要繼續下發,則需要通過IoCallDriver函數,該函數會將當前設備堆棧下移。若不做任何操作,可以使用IoSkipCurrentIrpStackLocation上移堆棧繼續重複使用當前堆棧。

若進行過操作,則需要使用內核宏IoCopyCurrentIrpStackLocationToNext將當前設備棧複製到下一層。

調用IoCallDriver後驅動會失去對IRP的控制,所以返回值當前過濾層需要返回最終層返回的結果,即該函數的返回值。

驅動設備IRP例程

取消例程: IRP取消例程通過IoSetCancelRoutine設置,第二個參數爲例程地址,爲NULL時刪除原先例程。驅動中可以調用IoCancelIrp函數觸發取消例程,該函數內部通過一個自旋鎖進行同步,所以在取消例程中需要釋放自旋鎖。通過IoAcquireCancelSpinLock獲取自旋鎖,IoReleaseCancelSpinLock函數釋放自旋鎖。

應用層通過CancelIo取消IRP請求,函數內部會枚舉所有未完成的IRP並依次調用IoCancelIrp,應用程序在關閉設備時也會調用CancelIo。

完成例程: IRP完成例程是通過在當前設備堆棧CompletionRoutine成員中註冊實現的。當IRP完成後,一層層堆棧向上彈出,依次查詢該成員是否爲空,非空則會調用。使用完成例程時,只能使用IoCopyCurrentIrpStackLocationToNext複製設備棧而不能跳過。可通過IoSetCompletionRoutine設置完成例程。

一般完成例程返回STATUS_SUCCESS或STATUS_CONTINUE_COMPLETION(等價),此時對IRP任何設置都會引發崩潰。另一種情況是返回STATUS_MORE_PROCESSING_REQUIRED,會解除IRP完成狀態並重新獲得IRP控制權。之後在註冊完成例程的函數中需要重新處理該IRP,完成或者繼續下發。

設置完成例程是通過設置IRP、複製當前堆棧到下一層、最後轉發實現的。如果沒有設置完成例程,底層在返回Pending時,會自動將堆棧Control成員的SL_PENDING_RETURNED設置到當前堆棧。但如果設置完成例程後,系統則不會進行該操作。所以當底層返回Pending時,完成例程中需要對Irp->PendingReturned進行檢測,如果爲真,則需要調用IoMarkIrpPending設置當前堆棧。

驅動設備IO棧

驅動設備的IRP請求會從上層設備依次下發到底層設備。每一層設備都會有一個IO_STACK_LOCATION結構,該結構記錄着對應設備的操作。獲取本層設備IO棧可以通過IoGetCurrentIrpStackLocation函數獲取。IO設備棧中可以獲取IRP的類型、請求的參數等數據。

驅動設備讀寫

驅動設備創建後,可通過設備結構成員Flags設置讀寫方式。讀寫主要有三種方式:緩衝區方式、直接方式和其他方式:

緩衝區方式讀寫: DO_BUFFERED_IO。緩衝區模式下,系統會分配內核模式下的內存,大小爲Write/Read寫入讀取的字節數,該內存地址可從IRP的AssociatedIrp.SystemBuffer獲取。無論讀寫操作,都會發生用戶模式與內核模式地址數據複製。通過當前IO棧成員Parameters.Read/Write.Length可獲取讀寫字節數,操作完成後分配的內存會把數據複製回去並銷燬。

直接方式讀寫: DO_DIRECT_IO。直接模式下,系統會將用戶模式下的緩衝區鎖定,隨後通過內存描述符表(MDL,pIrp->MdlAddress)獲取虛擬內存地址(MmGetMdlVirtualAddress(mdl))和大小(MmGetMdlByteCount(mdl)),最後通過MmGetSystemAddressForMdlSafe得到MDL在內核模式下的映射。

其他方式: 即不設置Flag的默認方式,該模式下內存地址爲用戶模式地址,即pIrp->UserBuffer。其他與DO_BUFFERED_IO相似。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章