下圖是我們的IFS DDK在堆棧中的位置
I/O管理器 |
文件驅動 |
中間驅動層 |
設備驅動 |
硬件抽象層 |
可以看出IFS DDK是在我們的硬件和操作系統之間工作的。
那麼如何學好IFS DDK呢?
首先,要把 Windows 的底層基礎學紮實。
其次,就是要把英文閱讀能力練好。所有資料幾乎全是英文的,DDK開發工具也全是英文,而且你別指望會有中文開發工具面市,至少短期內是不會有的,我費了老大勁才從Microsoft偷出了WDK工具包,現在的文件系統驅動是要收費的,就是博士說的4000$。這種東西一般是不會有人共享的。
再次,還有附加的一個條件就是要有足夠的耐心,驅動程序設計將要花費的時間是你無法想象到的,沒有耐性建議趁早轉行。
最後,也是最重要的一點就是一定要親自動手做!看再多的資料不動手,那只是停留在理論上,實踐永遠是空白。
看了上面是不是已經做好了心理準備了?準備好了我們就繼續往下看。
開發文件系統驅動的目的
一是用於防病毒引擎。希望在系統讀寫文件的時候,捕獲讀寫的數據內容,然後檢測其中是否含有病毒代碼。
二是用於加密文件系統,希望在文件寫過程中對數據進行加密,在讀的過程中進行解密。
三是設計透明的文件系統加速。讀寫磁盤的時候,合適的cache算法是可以大大提高磁盤的工作效率。windows本身的cache算法未必適合一些特殊的讀寫磁盤操作(如流媒體服務器上讀流媒體文件)。
知道了開發目的,我們就來了解一下DDK裏面的具體東西
例程(Routine):簡單說例程就是函數。
接口(Api):編程開發接口,一個提供給你調用的函數。
流(Stream):實際上就是FileObject(文件對象),是一個文件中的物理數據流。
文件(File):一個文件可能有多個FileObject,因爲可能多次打開,多個FileObject可能對應一個文件。
域(Field):數據結構中的一個數據成員。數據庫中稱爲字段。面向對象中稱爲數據成員。
回調(Callback)函數:一個由系統調用而不能自己調用的函數。
入口函數(DriverEntry)和win32編程一樣,驅動也有一個入口函數 (相當於main WinMain),這裏注意他們之間的運行方式是不同的,win32入口函數後是進入消息循環,驅動DriverEntry的主要功能是註冊一些IRP的相當於回調處理方式的函數,也就是派遣函數。入口函數主要是創建設備對象及符號連接,以及其它初使化操作,如分配池內存等。
出口函數(DriverUnload):刪除符號連接與設備對象,並釋放已經分配的各種資源。
IRP(I/O請求包),驅動編程裏十分重要的概念,說白了它就是一個數據結構,裏面有對應操作需要的數據,比如我們在應用層調用了CreateFile函數,在驅動層就需要構建一個IRP(IRP_MJ_CREATE),通過這個IRP的處理完成對應操作,如果我們對用戶打開,創建文件等操作有興趣,就可以通過掛接的方式讓系統將這個IRP發送到我們指定的函數(DriverEntry裏面指定的)。
DriverObject->MajorFunction[IRP_MJ_CREATE]=FileDiskCreateClose
DriverObject->MajorFunction[IRP_MJ_CLOSE]=FileDiskCreateClose;響應建立關閉DriverObject->MajorFunction[IRP_MJ_READ]=FileDiskReadWrite; DriverObject->MajorFunction[IRP_MJ_WRITE]=FileDiskReadWrite;響應一般讀寫DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = FileDiskDeviceControl; 設置IRP控制函數
DriverObject->DriverUnload = FileDiskUnload; 設置卸載規則
CDO(control Device Object),控制設備對象,這個對象代表這個驅動,它是供我們應用層的程序使用的,在DriverEntry裏面創建它,它是沒有設備擴展的。我們一般見到說的DO(device object)是用來綁定到文件系統驅動的FDO上的,和CDO不同。他們都使用IoCreateDevice函數創建。
Device Extension(設備擴展),其實它就是一塊保存有用數據的內存,這裏一般就是一個結構體。它有一個好處就是它可以隨DO一起傳遞,方便又節省資源。
訪問輸入輸出參數:
Buffered方式:使用IoGetCurrentIrpStackLocation得到調用者堆棧區域指針IrpStack(PIO_STACK_LOCATION類型)
Irp->AssociatedIrp.SystemBuffer;輸入輸出緩衝
IrpStack->Parameters.DeviceIoControl.InputBufferLength;輸入緩衝長度
IrpStack->Parameters.DeviceIoControl.OutputBufferLength;輸出緩衝長度
IrpStack->Parameters.DeviceIoControl.IoControlCode;設備控制代碼
如果需要輸出參數,在填寫完SystemBuffer後要設置IRP的IoStatus成員的Information指示輸出數據的長度.
Direct方式:MmGetSystemAddressForMdlSafe映射IRP->MdlAddress地址到內核空間
MmGetMdlByteCount,得到MDL大小,以字節爲單位.通常應該在IRQL<=DISPATCH_LEVEL的情況下使用MDL,
Neither方式:這個方式比恐怖,除非確定驅動不是分層的並且運行在PASSIVE_LEVEL級,一般不使用這種方式.比如寫一個簡單的Dump核心數據結構的驅動,該驅動只由我們的一個程序控制,那麼可以直接把用戶模式的地址傳給驅動使用。
前面提到了IRP在驅動裏是個非常重要的概念,那我們下面就着重看一下I/O請求包數據結構中的具體東東
Flags(ULONG)域 包含一些對驅動程序只讀的標誌。
AssociatedIrp(union)域 是一個三指針聯合。其中,與WDK驅動程序相關的指針是AssociatedIrp.SystemBuffer。SystemBuffer指針指向一個數據緩衝區,該緩衝區位於內核模式的非分頁內存中。對於IRP_MJ_READ和IRP_MJ_WRITE操作,如果頂級設備指定DO_BUFFERED_IO標誌,則I/O管理器就創建這個數據緩衝區。對於IRP_MJ_DEVICE_CONTROL操作,如果 I/O控制功能代碼指出需要緩衝區,則I/O管理器就創建這個數據緩衝區。I/O管理器把用戶模式程序發送給驅動程序的數據複製到這個緩衝區,並對數據進行管理,這也是創建IRP過程的一部分。這些數據可以是與WriteFile調用有關的數據,或者是DeviceIoControl調用中所謂的輸入數據。對於讀請求,設備驅動程序把讀出的數據填到這個緩衝區,然後I/O管理器再把緩衝區的內容複製到用戶模式緩衝區。對於指定了 METHOD_BUFFERED的I/O控制操作,驅動程序把所謂的輸出數據放到這個緩衝區,然後I/O管理器再把數據複製到用戶模式的輸出緩衝區。
IoStatus(IO_STATUS_BLOCK) 是一個僅包含兩個域的結構,驅動程序在最終完成請求時設置這個結構。
RequestorMode 等於一個枚舉常量UserMode或KernelMode,指定原始I/O請求的來源。驅動程序有時需要查看這個值來決定是否要信任某些參數。
PendingReturned(BOOLEAN) 如果爲TRUE,則表明處理該IRP的最低級派遣例程返回了STATUS_PENDING。完成例程通過參考該域來避免自己與派遣例程間的潛在競爭。
Cancel(BOOLEAN) 如果爲TRUE,則表明IoCancelIrp已被調用,該函數用於取消這個請求。如果爲FALSE,則表明沒有調用IoCancelIrp函數。
CancelIrql(KIRQL) 是一個IRQL值,表明那個專用的取消自旋鎖是在這個IRQL上獲取的。當你在取消例程中釋放自旋鎖時應參考這個域。
IRQL是Interrupt ReQuest Level,中斷請求級別。
CancelRoutine(PDRIVER_CANCEL),是驅動程序取消例程的地址。可以使用IoSetCancelRoutine函數設置這個域但儘量不要直接修改該域。
任何內核模式程序在創建一個IRP時,同時還創建了一個與之關聯的 IO_STACK_LOCATION結構數組:數組中的每個堆棧單元都對應一個將處理該IRP的驅動程序,另外還有一個堆棧單元供IRP的創建者使用。堆棧單元中包含該IRP的類型代碼和參數信息以及完成函數的地址。
MajorFunction |
MinorFunction |
Flags |
Control |
Parameters |
|||
DeviceObject |
|||
FileObject |
|||
CompletionRoutine |
|||
Context |
上圖是I/O堆棧單元數據結構的示例圖,下面對圖中的函數做一下解釋。
MajorFunction(UCHAR) 是IRP的主功能碼。這個代碼應該爲類似 IRP_MJ_READ一樣的值,並與驅動程序對象中MajorFunction表的某個派遣函數指針相對應。如果該代碼存在於某個特殊驅動程序的I/O 堆棧單元中,它有可能一開始是,例如IRP_MJ_READ,而後被驅動程序轉換成其它代碼,並沿着驅動程序堆棧發送到低層驅動程序。我將在第十一章 (USB總線)中舉一個這樣的例子,USB驅動程序把標準的讀或寫請求轉換成內部控制操作,以便向USB總線驅動程序提交請求。
MinorFunction(UCHAR) 是IRP的副功能碼。它進一步指出該IRP屬於哪個主功能類。例如,IRP_MJ_PNP請求就有約一打的副功能碼,如IRP_MN_START_DEVICE、IRP_MN_REMOVE_DEVICE,等等。
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函數,該函數知道如何參考下一層驅動程序的堆棧單元。設備堆棧的最低一級驅動程序並不需要完成例程,因爲它們必須直接完成請求。然而,請求的發起者有時確實需要一個完成例程,但通常沒有自己的堆棧單元。這就是爲什麼每一級驅動程序都使用下一級驅動程序的堆棧單元保存自己完成例程指針的原因。
Context(PVOID) 是一個任意的與上下文相關的值,將作爲參數傳遞給完成例程。你絕對不要直接設置該域;它由IoSetCompletionRoutine函數自動設置,其值來自該函數的某個參數。
過濾/掛鉤IRP請求
對於過濾/掛鉤IRP請求我也不是十分理解,只是覺得比較有用,在這裏拿出來與大家一起探討,如果有知道的可以聯繫我。
掛鉤某個IRP處理函數
1 調用IoGetDeviceObjectPointer返回的設備對象(PDEVICE_OBJECT)
2 由設備對象得到驅動對象PDEVICE_OBJECT->DriverObject(PDRIVER_OBJECT),這裏應該調用ObReferenceObjectByPointer函數增加驅動對象的引用計數,以防止該驅動程序在我們的驅動程序前被卸載。
3 再由驅動對象得到中斷請求派遣函數表(PDRIVER_OBJECT->MajorFunction)
4 保存PDRIVER_OBJECT->MajorFunction[IRP_MJ_XXXXXXX]值(原中斷請求派遣函數地址)
5 修改PDRIVER_OBJECT->MajorFunction[IRP_MJ_XXXXXXX]值(讓它指向我們自定義的函數),使用鎖總線前綴lock的xchg指令進行賦值操作(讓代碼多線程和多處理器安全)
6 結束處理同System Service Hook
過濾某個設備的IRP請求:
1 初使化IRP請求派遣函數表MajorFunction,將它們全都指向一個派遣函數DispatchAny
2 再爲MajorFunction指定幾個我們感興趣的IRP請求派遣函數
3 得到設備對象指針:IoGetDeviceObjectPointer
4 將設備加到設備堆棧上:IoAttachDeviceToDeviceStack,並保存下層設備對象,以供IoCallDriver時使用.
5 在DispatchAny中將IRP傳給下層驅動IoSkipCurrentIrpStackLocation,IoCallDriver.
6 在指定的幾個請求派遣函數中對IRP進行處理.
說了這麼多是不是有點迷糊了,下面我們來看一個驅動框架的創建過程
NTSTATUS DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath )
{
NTSTATUS status;
DriverObject->DriverUnload = DriverUnload;
Device_object * ourCDO = NULL; //控制設備對象,這裏默認爲空
UNICODE_STRING Name_2k
RtlInitUnicodeString(&Name_2k,L"//FileSystem//OurFilter_Cdo");
status = IoCreateDevice(DriverObject, 0, &Name_orc);
if(!NT_SUCCESS(status))
return status; 如果創建控制設備對象失敗,返回status
UNICODE_STRING Link_orc; 創建一個Win32可見的符號連接
RtlInitUnicodeString(&Link_orc,L"//DosDevices//OurFilter");
status = IoCreateSymbolicLink(&Link_orc ,&Name_orc);
if(!NT_SUCCESS(status))
return status;
}
void DriverUnload(IN PDRIVER_OBJECT pDriverObject)
{
UNICODE_STRING uniNameString;
RtlInitUnicodeString(&Name_orc, L"//FileSystem//OurFilter_Cdo");
IoDeleteSymbolicLink(&Name_orc);//刪除win32可見的連接
IoDeleteDevice(pDriverObject->DeviceObject);//刪除設備
return;
}
DriverObject擁有一組函數指針,稱爲dispatch functions.開發驅動的主要任務就是親手撰寫這些dispatch functions.當系統用到你的驅動時,會向你的DO發送IRP(這是windows所有驅動的共同工作方式)。你的任務是在dispatch function中處理這些請求。你可以讓irp失敗,也可以成功返回,也可以修改這些irp,甚至可以自己發出irp。
這樣一個最基本的驅動框架就出來了,再加入DbgPrint()調試信息,編譯並調試後就可以看到輸出的調試信息。大體框架是這樣,裏面有很多東西我也不理解,大概也有些錯誤。至於調試我現在還在學習中,DDK工具中的好多東西還沒有弄明白。