《Windows內核安全與驅動編程》-第九章-磁盤的虛擬-day-1

磁盤的虛擬

9.1 虛擬的磁盤

​ 虛擬磁盤,是我們可以創造的一個行爲完全可控的磁盤驅動器。舉一個例子: 製作一個虛擬的磁盤,對這個磁盤的所有寫入操作都通過某種算法以口令加密,同時對這個磁盤的讀取操作使用同樣的算法和口令解密,那麼在該程序運行時,操作系統和用戶自己都可以看到正確的數據,而在這個程序不工作時,無論誰企圖讀這個磁盤都只能看到被加密過的數據。

9.2 一個具體的例子

WDK6001.18000 裏自帶的一個例子——Ramdisk,具體的路徑爲 $(WDK 安裝目錄) \src\kmdf\ramdisk 。這個例子實現了一個使用非分頁內存 (nonpaged memory) 做的磁盤存儲空間,並將其以一個獨立的磁盤形式暴露給用戶,用戶可以將其格式化成一個 Windows 能夠使用的卷,並且像操作一般的磁盤卷一樣對它進行操作。由於使用了內存作爲虛擬的存儲介質,使這個磁盤具有的一個顯著特點是性能的提高。由於這個優勢,使它非常適合作爲各種軟件的緩衝盤。

​ 這個例子使用了微軟的 WDF 驅動開發框架,所以這個驅動和普通的 WDM 驅動是不太一樣的。下面討論中所有的文件行號和文件內容均來自微軟提供的 WDK 6001.18000 中安裝目錄下的 src\kmdf\ramdisk 目錄。

入口函數

9.3.1 入口函數的定義

​ 任何一個驅動程序,都會有一個 DriverEntry 入口函數,就像應用程序裏的main一樣。這個函數的聲音是這樣的:

NTSTATUS DriverEntry(
	IN PDRIVER_OBJECT DrvierObject;
    IN PUNICODE_STRING RegisitryPath;
);

​ 這個函數的返回值是 NTSTATUS ,一般是用來確定程序錯誤時的原因。這些返回值的定義位於 WinDDK 安裝目錄下的 \inc\api\instatus.h 中。一般從英文意思就可以判斷返回值含義。

​ 這個函數具有兩個參數。其中第一個參數是一個 PDRIVER_OBJECT 類型的指針。它代表了 Windows 系統爲這個驅動程序所分配的一個驅動對象。這個驅動對象是系統中對該驅動的唯一標識,裏面包含了該驅動的各種信息、各個功能函數的入口地址等重要信息。

​ 第二個參數是一個 Unicode 字符串,它代表了驅動在註冊表中的參數所存放的位置。它代表了驅動在註冊表中的參數所存放的位置。由於每一個驅動都是一個類似服務的形式存在,熬字系統註冊表的 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services 樹下總有一個和驅動名字相同的子樹用來描述這個驅動的一些基本信息,並體哦那個一個可使用的存儲空間供驅動存放自己的特有信息。之所以會有這樣的一個空間,是因爲許多驅動加載的非常早,甚至僅僅晚於內核,這時候文件系統還沒有建立,驅動程序如果需要在此時享有自己可操作的存儲空間(例如用來記錄一些參數),除了註冊表外沒有其他任何地方可以使用。Windows 內核在啓動時加載了一個最小的文件系統,分析磁盤並將註冊表的 HKEY_LOCAL_MACHINE\SYSTEM 樹下的所有內容讀入到內存裏,這樣就保證了這一部分的註冊表內容在 Windows 內核剛加載之後就可以使用。而後 Windows 在啓動的過程中將這棵子樹的內容和磁盤上的註冊表進行同步,之後的註冊表操作就和一般的註冊表操作一致了。在 Windows 內核加載之後,這個同步發生之前,任何加載的驅動程序都可以操作註冊表並確保操作結果被最終留在了磁盤上,這就是 Windows 系統確保任何驅動都可以使用它提供的註冊表路徑來存儲信息的技術所在,也是這個參數的意義。

9.3.2 Ramdisk 驅動入口函數

​ 在 Ramdisk 驅動代碼的 DriverEntry 函數中只做了幾件簡單的事情,下面將這個函數的所有代碼列出並加以說明。

WDF_DRIVER_CONFIG config;

WDF_DRIVER_CONFIG_INIT (&config,RamDiskEvtDeviceAdd);
return WdfDriverCreate(
	DriverObject,
	RegistryPath,
	WDF_NO_OBJECT_ATTRIBUTES,
	&config,
	WDF_NO_HANDLE
);

​ 首先聲明變量 config ,然後進行初始化。WDF_DRIVER_CONFIG 結構通常用來說明這個驅動程序的一些可配置項,其中包括了這個驅動程序的 EvtDriverDeviceAddEvtDriverUnload 回調函數的入口地址和這個驅動在初始化時的一些標誌和分配內存時所使用的 tag 值。 WDF_DRIVER_CONFIG_INIT 函數將用戶提供的 EvtDriverDeviceAdd 函數填入其中,並初始化這個變量的其他部分。 EvtDriverDeviceAdd 回調函數是 WDF 驅動框架中的一個重要的回調函數,它可以用來在即插即拔管理器發現一個新設備的時候對這個新設備進行初始化操作。

EvtDriverDeviceAdd 回調函數,類似於 WDM 驅動程序中的 AddDevice 回調函數的翻版。這個回調函數在 PnP 類型的驅動中起着很重要的最用,任何一個支持 PnP 操作的驅動都應該有這麼一個 EvtDriverDeviceAdd 函數。這裏 PnP 是即插即拔的意思,在前面有學過。但是俺沒有過開發經驗,暫時還不太懂這個回調函數具體的意思。只理解爲,剛剛發現新設備時的初始化函數。

​ 在初始化好 config 變量後,後續直接調用了 WdfDriverCreate 函數並返回。 WdfDriverCreate 函數是使用任何 WDF 框架提供的函數之前必須調用的一個函數,在原作者看來這僅僅是一次對原本驅動程序開發方式的一次包裝。作用就是一些初始化工作。前兩個參數我們都知道,第三個參數是用來說明這個 WDF 驅動的驅動對象不需要一些特殊屬性。第四個參數是初始化後的 config 變量。最後一個參數作爲該函數的輸出結果——WDF 驅動的驅動對象。調用了這個函數之後,前面初始化過的 conigf 和 EvtDriverDeviceAdd 函數就和這個驅動掛鉤起來。在今後的系統運行過程中,一旦發現了此類設備,RamDiskEvtDeviceAdd 就會被 Windows 的 PnP 管理器調用,這個驅動自己的處理流程也就要上演了。

​ WDM驅動模型和WDF驅動模型的最大的區別是:

  • wdf驅動框架對WDM進行了一次封裝,WDF框架就好像C++中的基類一樣,且這個基類中的model,IO model ,pnp和電源管理模型;且提供了一些與操作系統相關的處理函數,這些函數好像C++中的虛函數一樣,WDF驅動中能夠對這些函數進行override;特別是Pnp管理和電源管理!基本上都由WDF框架做了,而WDF的功能驅動幾乎不要對它進行特殊的處理;

  • WDF驅動採用隊列進行IO處理,而WDM中將所有的IO操作都用默認的隊列進行處理,如果要進行IRp同步,必須使用StartIO;

  • WDF是面向對象的,而WDM是面向過程的,WDF提供對象的封裝,如將IRP封裝成WDFREQUEST,對象提供方法和Event。

9.4 EvtDriverDeviceAdd 函數

9.4.1 EvtDriverDetivceAdd 的定義

​ 在本驅動中, RamDiskEvtDeviceAdd 作爲一個 EvtDriverDeviceAdd 函數在 DriverEntry 中被註冊,在 DriverEntry 函數執行完畢之後,這個驅動就只依靠 RamDiskEvtDriverDevicAdd 函數和系統保持聯繫了。正如上一節所說的,系統在運行過程中一旦發現了這種類型的設備,就會調用RamDiskEvtDriverDevicAdd 函數。下面進行更仔細的分析:

NTSTATUS RamDiskEvtDeviceAdd(
	IN WDFDRIVER Driver,
	IN PWDFDEVICT_INIT DeviceInit
);

​ 這個函數的第一個參數在這個例子中並不會使用到;第二個參數是一個 WDFDEVICE_INIT 類型的指針,這個參數是 WDF 驅動模型中自動分配出來的一個數據結構,專門傳遞給 EvtDriverrDeviceAdd 類函數用來建立一個新設備。下面分段具體看這個驅動的 EvtDriverDeviceAdd 類函數是如何工作的。

9.4.2 局部變量的聲明

​ 這裏具體說一下每個變量的作用,方便在後面的函數分析中作爲參考。

//將要建立的設備對象的屬性描述變量
WDF_OBJECT_ATTRIBUTES deviceAttributes;
//將要調用的各種函數的狀態返回值
NTSTATUS status;
//將要建立的設備
WDFDEVICE device;
//將要建立的隊列對象的屬性描述變量
WDF_OBJECT_ATTRIBUTES queueAttributes;
//將要建立的隊列配置變量
WDF_IO_QUEUE_CONFIG ioQueueConfig;
//這個設備對應的設備擴展域的指針
PDEVICE_EXTENSION pDeviceExtension;
//將要建立的隊列擴展域的指針
PQUEUE_EXTENSION pQueueContext = NULL;
//將要建立的隊列
WDFQUEUE queue;
//聲名一個 UNICODE_STRING 類型的變量,並將它初始化爲 NT_DEVICE_NAME 宏所聲名的字符串,這裏實際上是 \\Device\\Ramdisk
DECLARE_CONST_UNICODE_STRING(ntDevictName,NT_DEVICE_NAME);
//保證這個函數可以操作paged內存
PAGRD_CODE();
//由於我們不適用 Driver 這個參數,爲了避免警告,加入這句
UNREFFERENCED_PARAMETER(Driver);

9.4.3 磁盤設備的創建

EvtDriverDeviceAdd 類函數的一個重要任務是創建設備,而它的 WDFDEVICE_INIT 類型的參數就是用來做這件事的。首先,設備需要一個名字,這是因爲這個設備將會通過這個名字暴露給應用層並且被應用層所使用,一個沒有名字的設備是不可以用的。

​ 另外,需要將這個設備的類型設置爲 FILE_DEVICE_DISK ,這是因爲所有的磁盤設備都需要使用這個設備類型。將這個設備的 IO 類型設置爲 Direct 方式,這樣將在讀寫和 DeviceIoControlIRP 發送到這個設備時, IRP 所攜帶的緩衝區將可以直接被使用。將 Exclusive 設置爲 FALSE ,這說明這個設備可以被多次打開。這裏還需要將這個設備對象關聯一個設備擴展的上下文,這是開發人員指定類型的一塊內存區域,這塊內存區域被這個設備的設備擴展指針所指,開發人員可以在這塊內存區域裏存儲一些自定義的信息。

​ 針對本驅動建立出的任何一個設備,這塊內存區域的結構都是相同的。但是隨設備對象的不同,可能具有的內容不同。這些內容將會在接下來的編程中作爲針對設備對象的處理函數的參數。除此之外,還需要爲設備的一些功能指定相應的處理函數,例如設備在銷燬時調用哪個函數。WDF 驅動模型框架已經爲開發人員做了許多事情,基本上實現了所有功能的標準處理過程,在大部分情況下這些標準處理過程就足夠了,只需要我們根據自己的要求進行很少的功能處理即可。

// 首先需要爲這個設備指定一個名詞
// "\\Device\\Ramdisk"
status = WdfDeviceInitAssignName(DeviceInit,&neDeviceName);
if(!NT_SUCCESS(status))
{
    return status;
}

//接下來需要對這個設備進行一些屬性的設置,包括設備類型和IO操作類型和設備的排他方式
WdfDeviceInitSetDeviceType(DeviceInit,FILE_DEVICE_DISK);
WdfDeviceInitSetIoType(DeviceInit,WdfDeviceIoDirect);
WdfDeviceInitSetExclusive(DeviceInit,FALSE);

//下面來指定這個設備的設備對象擴展
//DEVICE_EXTENSION 是一個在頭文件中聲名的結構體數據類型
//我們使用一個 WDF_OBJECT_ATTRIBUTES 類型的變量並用其設置好 DEVICE_EXTENSION
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(
	&deviceAttributes;
    DEVICE_EXTENSION
);
//下面用這個這個 WDF_OBJECT_ATTRIBUTES 類型的變量來指定這個設備的清除回調函數
//這個個 WDF_OBJECT_ATTRIBUTES 類型的變量將會在下面建立設備時作爲一個參數傳進去
deviceAttributes.EvtCleanupCallBack = RamDiskEvtDeviceContextCleanup;

//到這裏,所有的準備工作做好。我們開始建立設備
//device 保存我們建立的設備
status = WdfDeviceCreate(&DeviceInit,&deviceAttributes,&device);
//這個 pDeviceExtension 是我們聲名的一個局部指針變量,將其指向新建立的設備拓展區域
pDeviceExtension = DeviceGetExtension(device);

9.4.4 如何處理髮往設備的請求

​ 在 WDM 開發中,常用的方式是設置這個設備哥哥請求的分發函數爲自己實現的回調函數。比如,在這個例子裏,可以將所有的讀寫請求都實現爲去讀寫內存,這就是最簡單的內存盤。處理方式實現起來需要一些技巧,一種常用的方式是建立一個或多個隊列,將所有發送到這個設備的請求都插入到隊列中,由另一個線程去處理隊列。這就是一個典型的生產者——消費者模型。這樣做的好處是有一個小小的緩衝,同時還不用擔心由於緩衝帶來的同步問題。因爲所有的請求都被排成了隊列。這裏, WDF 驅動框架中,微軟直接提供了這種處理隊列,這樣我們就不用自己再去實現了。

​ 爲了實現爲驅動製作一個處理隊列這一目標,我們需要初始化一個隊列配置變量 ioQueueConfig ,這個變量會說明隊列的各種屬性,一種簡單的方式是將它配置爲初始化默認狀態,之後再對一些具有特殊屬性的請求註冊回調函數,例如爲讀請求註冊回調函數。在初始化完成之後,再爲指定的設備建立這個隊列。 WDF 驅動框架會自動將所有發往這個設備的請求都放入隊列中等待處理,同時發現符合我們感興趣的屬性(讀寫操作)時,調用之前註冊過的處理函數去處理。對每個設備可以建立多個隊列,在隊列中也有類似設備的擴展。

WDF_IO_QUEUE_CONFIG_INIT_QUEUE(
	&ioQueueConfig,
	WdfIoQueueDispatchSequential
);
//	我們對讀寫和 DeviceControl 請求的處理設置爲自己的函數,其餘的使用默認值
ioQueueConfig.EvtIoDeviceControl = RamDiskEvtIoDeviceControl
ioQueueConfig.EvtIoRead = RamDiskEvtRead;
ioQueueConfig.EvtIoWrite = RamDiskEvtWrite;

//指定這個隊列的隊列對象擴展,這裏的QUEUE_EXTENSION 是一個在頭文件中
//聲名好的結構體數據類型
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(
	&queueAttributes,
	QUEUE_EXTENSION
);
//與之前類似,我們開始創建隊列。將之前創建的設備作爲這個隊列的父對象
//在這個設備被銷燬的同時,該設備也會被銷燬
status = WdfIoQueueCreate(
	device,
	&ioQueueConfig,
	queueAttributes,
	&queue
);
if(!NT_SUCCESS(status))
{
    return status;
}
// 將指針 pQueueContext 指向剛剛生成的隊列的擴展
pQueueContext = QueueGetExtension(queue);

//這裏初始化隊列擴展裏的 DeviceExtension 項,將其
//設置爲剛建立的設備拓展指針,這樣以後在有隊列的地方都
//可以輕鬆的獲得這個隊列對應的設備的設備擴展了
pQueueContext->DeviceExtension = pDeviceExtension;

​ 這裏今天就先結束學習吧,體會到 WDF 就是一層對 WDM的封裝。類似於現在的很多框架,爲編程節省了很多精力。

明日計劃:

翻譯論文

繼續學習驅動編程-磁盤的虛擬

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