文章目錄
磁盤的過濾
10.1 磁盤過濾驅動的概念
10.1.1 設備過濾和類過濾
在前面學習過鍵盤過濾,建立一個過濾設備並將其綁定在一個有名字的設備上,這叫做設備過濾。但是在Windows中有一些情況,例如設備剛插入的時候,如何在這些設備剛加入時就自動的進行綁定呢?這就要用到類過濾了。這種類過濾驅動的驅動程序,能夠在某一類特定的設備建立時由PnP管理器調用指定的過濾驅動代碼,並且允許用戶在此時對這一類設備進行綁定。根據用戶設備在整個設備棧上相對系統本來存在設備的不同位置,可以分爲上層過濾和下層過濾。其中以上層過濾最爲常見,因爲這時過濾設備在設備棧上位於實際功能設備的上面,會首先獲得 Windows系統發下來的IRP請求,便於過濾設備的實現者進行處理。
10.1.2 磁盤設備和磁盤卷設備過濾驅動
在Windows的存儲系統中,最底層的是磁盤,而在磁盤上層的是卷,卷雖然只是邏輯上的一個概念,但是Window仍然爲其建立了設備。所以會有磁盤設備和磁盤卷設備兩種類型。
磁盤卷並不僅僅是隻對應一個磁盤,有時會出現跨磁盤的現象。所以我們必須將這兩種設備區分開來。
從驅動的角度上來說,這兩種設備收到的讀寫請求都是針對磁盤大小或者在卷大小範圍之內的請求,都是以扇區大小對齊的,處理起來沒有太大的區別。所以本章主要講解磁盤卷設備的上層過濾驅動,因爲對於用戶來說,卷是最直接看到的現象。而對於開發人員來說,使用捲過濾會在一定程度上減少工作量。因爲不需要處理磁盤設備中才會遇到的一些問題,但是同時也限制了一些功能的實現,原因同樣是不能處理磁盤設備上的問題,正所謂有利有弊!
10.1.3 註冊表和磁盤卷設備過濾驅動
在實際的系統運行過程中,一個普通的驅動程序是如何告知Windows操作系統它是一個類過濾驅動,並且如何和相應的設備聯繫起來呢?這就需要註冊表的幫忙了。
我們應該熟悉一個驅動程序作爲服務是如何在註冊表中存在的,在 HKEY\LOCAL\MACHINE\SYSTEM\CurrentSet\Services 下服務鍵的名字也就是這個服務的名字了。同時在HKEY\LOCAL\MACHINE\SYSTEM\Control\Class 下也會有許多類的名字,這些類的名字都是一長串數字,根據上一章的內容,這一長串數字實際上是一個ClassGUID,隨意選擇一個鍵,下面都會有一個叫做Class的值,這就是一個累。在這些鍵中,可以找到一個Class值爲“Volume”的鍵,這就是磁盤卷類。在本章中最關鍵的是其中一個叫做 UpperFilters 的值,這個值起了最關鍵的作用——說明這個類的上層過濾驅動都有哪些。到此爲止,我們應該明白,只需要在這個 UpperFilter 值中填入相應的驅動名,這個驅動就會作爲這一類設備的上層過濾驅動被Windows操作系統是被,實際上這樣也就完成了上層過濾驅動的安裝工作了。
10.2 具有還原功能的磁盤捲過濾驅動
10.2.1 簡介
這個過濾驅動爲講解而寫,需要使用 Windows XP系統,還有許多其他的限制。但是作爲學習,還是瞭解到大致的框架即可。後續需要的時候,再去查相關的文檔實現。這裏實現的功能爲,將磁盤還原到某一時刻的功能,在該時間點之後的所有操作將被抹去。
10.2.2 基本思想
爲了實現還原,一種簡單的思路如下:
- 在開啓還原之後,所有對還原卷的操作將被寫到另一個地方,而不會真正的寫在還原捲上。這裏所說的另一個地方也被稱爲轉存處。
- 在開啓還原之後,所有對還原卷的讀操作將分爲兩種情況處理。一種是讀了開啓還原之前就存在的內容,這種情況就按照正常的讀取方式從還原捲上讀取;另一種情況是讀了開啓還原卷之後還寫到還原捲上的內容,這種情況將會從轉存處把之前寫過的內容讀取出來。
- 上述讀寫必須建立在互斥的基礎上。
- 重啓之後,轉存的數據清零。
- 上述轉存同樣必須在卷設定還原之後立即起作用,而不能出現寫了一半纔開始轉存的情況;否則數據會在重啓之後不同步。
10.3 驅動分析
10.3.1 DriverEntry 函數
DriverEntry 函數作爲過濾函數的入口函數,主要負責初始化本驅動的各個分發函數。此外,它還指定了這個驅動的 AddDevice 和 Unload 函數。由於這個設備驅動被註冊成了磁盤卷設備的上層過濾驅動,PnP管理器將會載一個新的磁盤卷建立之後,首先調用本過濾驅動的 AddDevice 函數,然後再調用磁盤卷設備驅動中的 AddDevice 函數。而 Unload 函數會在過濾驅動結束時被調用。
在 DriverEntry 函數的最後,還註冊了一個 boot 類型驅動的完成回調函數。本驅動是作爲一個 boot 類型驅動存在的,這一點在可以註冊表的 HKEY\LOCAL\MACHINE\SYSTEM\CurrentSet\Services 下驅動服務的 start 值中指定,0爲 boot 類型。boot 類型的驅動是啓動最早的驅動程序,在系統引導時加載完畢;而對於註冊爲boot類型驅動的完成回調函數的函數,將會在所有的boot類型驅動執行完畢之後被調用一次,需要注意的是,這時候仍然是系統啓動過程比較早的時候。在這裏需要註冊這個回調函數,因爲驅動中有些工作需要等待到這個時間才能做,具體是什麼工作稍後會講到。
{
//用來做循環控制變量
int i;
//調式時候產生斷點
//KdBreakPonit();
for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;i++)
{
DriverObject->MajorFunction[i] = DPDispatchAny;
}
//下面將我們特殊關注的分發函數重新賦值爲我們自己的處理函數
DriverObject->MajorFunction[IRP_MJ_POWER] = DPDispatchPower;
DriverObject->MajorFunction[IRP_MJ_PNP] = DPDispatchPnp;
DriverObject->MajorFunction[IPR_MJ_DEVICE_CONTROL] = DPDispatchDeviceControl;
DriverObject->MajorFunction[IRP_MJ_READ] = DPDispatchReadWrite;
DriverObject->MajorFunction[IRP_MJ_WRITE] = DPDispatchReadWrite;
//將這個驅動的AddDevice函數初始化爲DPAddDevice函數
DriverObject->DriverExtension->AddDevice = DPAddDevice;
//同理初始化 Unload 函數
DriverObject->DriverExtension->Unload = DOUnload;
//註冊一個boot驅動結束回調,這個回調函數會在所有的boot類型驅動運行完畢後執行
IoRegisterBootDriverReinitialization(
DriverObject,
DPReinitializationRoutine.
NULL
);
//作爲一個過濾驅動,無論如何都要返回成功
return STATUS_SUCCESS;
}
10.3.2 AddDevice 函數
該函數是在任何磁盤卷設備建立的時候被調用。但是需要注意的是,該 DPAddDevice 函數被調用的時候,實際上磁盤卷已經建立起來了,只是還不能使用,也就是說這個設備的設備對象已經有了,但是不能相應大部分的IRP請求。
在DPAddDevice中建立一個過濾設備,這個設備將被綁定在真正的磁盤卷設備上。並且由於這是一個上層過濾設備。這個過濾設備將會位於磁盤卷設備的棧頂方向上,也就是先於磁盤卷收到IRP請求。在建立並綁定了這個過濾設備之後,需要對這個過濾設備做一些初始化,而過濾層設備的所有基本信息都會以 DP_FILTER_DEV_EXTENSION 結構的類型存儲在設備擴展中。下面介紹該結構
// 卷的名字 例如 C: D: 等卷名字中字母部分
WCHAR VolumeLetter;
//這個卷是否處於保護狀態
BOOL Protect;
//這個卷的總大小,以byte爲單位
LARGE_INTEGER TotalSizeInByte;
//這個捲上的文件系統的每簇大小,以Byte爲單位
DWORD ClusterSizeInByte;
//這個捲上每個扇區的大小,以Byte爲單位
DWORD SectorSizeInByte;
//這個卷對應的過濾設備的設備對象
PDEVICE_OBJECT FltDevObj;
//這個卷對應的過濾設備的下層設備對象
PDEVICE_OBJECT LowerDevObj;
//這個卷設備對應的物理設備的設備對象
PDEVICE_OBJECT PhyDevObj;
//這個數據結構是否已經初始完畢
BOOL InitializeCompleted;
//這個捲上的保護系統室友的位圖句柄
PDP_BITMAP BitMap;
//用來轉儲的文件句柄
HANDLE TempFile;
//這個捲上的保護系統使用的請求隊列
LIST_ENTRY ReqList;
//這個捲上的保護系統使用的請求隊列的鎖
KSPIN_LOCK R EQlOCK;
//這個捲上的保護系統使用的請求隊列的同步事件
KEVENT ReqEvent;
//這個捲上的保護系統使用的請求隊列的處理線程的線程句柄
PVOID ThreadHandle;
//這個捲上的保護系統使用的請求隊列的處理線程的結束標誌
BOOLEAN ThreadTermFlag;
//這個捲上的保護系統的關機分頁電源的計數事件
KEVENT PagingPathCountEvent;
//這個捲上的保護系統的關機分頁電源請求的計數
LONG PagingPathCount;
在上述的數據結構中可以看到三個設備對象: 過濾設備、物理設備和下層設備。其中過濾設備是本過濾驅動自己建立的;物理設備是通過AddDevice 函數的參數傳遞進來的設備,是真正的磁盤卷設備;而下層設備是將在過濾設備綁定到物理設備上之後,返回的綁定之前的物理設備上的最上層的設備。
在該數據結構中還可以看到,針對每個過濾設備都會建立一個處理線程和相應的請求隊列。這是因爲在這個驅動中採用了將所有的請求依次排隊,然後使用一個單獨的線程依次處理的方式。這樣做的好處是將所有的讀寫請求串行化,程序易於編寫而且不會出現同步問題。
在該函數中還會發現初始化了 PagingPathCountEvent 和 PagingPathCount 這兩個與分頁路徑相關的變量。它們將會在 PnP IRP 請求的處理中被用到。現在只需要知道初始化的值是多少即可。
NTSTATUS
DPAddDevice(
IN PDRIVER_OBJECT DriverObject,
IN PDEVICE_OBJECT PhysicalDeviceObject
)
{
//NTSTATUS類型的函數返回值
NTSTATUS ntStatus = STATUS_SUCCESS;
//用來指向過濾設備的設備擴展的指針
PDP_FILTER_DEV_EXTENSION DevExt = NULL;
//過濾設備的下層設備的指針對象
PDEVICE_OBJECT LowerDevObj = NULL;
//過濾設備的設備指針的指針對象
PDEVICE_OBJECT FltDevObj = NULL;
//過濾設備的處理線程的線程句柄
HANDLE ThreadHandle = NULL;
//建立一個過濾設備,這個設備是FILE_DEVICE_DISK類型的設備並且具有DP_FILTER_DEV_EXTENSION類型的設備擴展
ntStatus = IoCreateDevice(
DriverObject,
sizeof(DP_FILTER_DEV_EXTENSION),
NULL,
FILE_DEVICE_DISK,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&FltDevObj);
if (!NT_SUCCESS(ntStatus))
goto ERROUT;
//將DevExt指向過濾設備的設備擴展指針
DevExt = FltDevObj->DeviceExtension;
//清空過濾設備的設備擴展
RtlZeroMemory(DevExt,sizeof(DP_FILTER_DEV_EXTENSION));
//將剛剛建立的過濾設備附加到這個卷設備的物理設備上
LowerDevObj = IoAttachDeviceToDeviceStack(
FltDevObj,
PhysicalDeviceObject);
if (NULL == LowerDevObj)
{
ntStatus = STATUS_NO_SUCH_DEVICE;
goto ERROUT;
}
//初始化這個卷設備的分頁路徑計數的計數事件
KeInitializeEvent(
&DevExt->PagingPathCountEvent,
NotificationEvent,
TRUE);
//對過濾設備的設備屬性進行初始化,過濾設備的設備屬性應該和它的下層設備相同
FltDevObj->Flags = LowerDevObj->Flags;
//給過濾設備的設備屬性加上電源可分頁的屬性
FltDevObj->Flags |= DO_POWER_PAGABLE;
//對過濾設備進行設備初始化
FltDevObj->Flags &= ~DO_DEVICE_INITIALIZING;
//將過濾設備對應的設備擴展中的相應變量進行初始化
//卷設備的過濾設備對象
DevExt->FltDevObj = FltDevObj;
//卷設備的物理設備對象
DevExt->PhyDevObj = PhysicalDeviceObject;
//卷設備的下層設備對象
DevExt->LowerDevObj = LowerDevObj;
//初始化這個卷的請求處理隊列
InitializeListHead(&DevExt->ReqList);
//初始化請求處理隊列的鎖
KeInitializeSpinLock(&DevExt->ReqLock);
//初始化請求處理隊列的同步事件
KeInitializeEvent(
&DevExt->ReqEvent,
SynchronizationEvent,
FALSE
);
//初始化終止處理線程標誌
DevExt->ThreadTermFlag = FALSE;
//建立用來處理這個卷的請求的處理線程,線程函數的參數則是設備擴展
ntStatus = PsCreateSystemThread(
&ThreadHandle,
(ACCESS_MASK)0L,
NULL,
NULL,
NULL,
DPReadWriteThread,
DevExt
);
if (!NT_SUCCESS(ntStatus))
goto ERROUT;
//獲取處理線程的對象
ntStatus = ObReferenceObjectByHandle(
ThreadHandle,
THREAD_ALL_ACCESS,
NULL,
KernelMode,
&DevExt->ThreadHandle, //獲取到的線程對象
NULL
);
if (!NT_SUCCESS(ntStatus))
{
DevExt->ThreadTermFlag = TRUE;
KeSetEvent(
&DevExt->ReqEvent,
(KPRIORITY)0,
FALSE
);
goto ERROUT;
}
ERROUT:
if (!NT_SUCCESS(ntStatus))
{
//如果上面有不成功的地方,首先需要解除可能存在的附加
if (NULL != LowerDevObj)
{
IoDetachDevice(LowerDevObj);
DevExt->LowerDevObj = NULL;
}
//然後刪除可能建立的過濾設備
if (NULL != FltDevObj)
{
IoDeleteDevice(FltDevObj);
DevExt->FltDevObj = NULL;
}
}
//關閉線程句柄,我們今後不會用到它,所有對線程的引用都通過線程對象來進行了
if (NULL != ThreadHandle)
ZwClose(ThreadHandle);
//返回狀態值
return ntStatus;
}
與前面的過濾綁定差不多的操作,創建設備—>綁定設備->填充設備擴展->…但是複雜的是多了許多暫時還不確定用處的變量。但是帶着對該函數的作用去理解還是可以的,畢竟這個 AddDevice 函數的作用,主要是綁定過濾設備。其他的可以暫時先放着不看。
明日計劃
論文
繼續學習驅動編程下一小節 PnP請求的處理。