這篇文章來介紹一下WDK中提供的一個案例源碼--Ramdisk虛擬磁盤。這個例子實現了一個非分頁內存做的磁盤儲存空間,並將其以一個獨立磁盤的形式暴露給用戶,用戶可以將它格式化成一個Windows能夠使用卷,並且像操作一般的磁盤卷一樣對它進行操作。由於使用了內存作為虛擬的存儲介質,使這個磁盤具有一個顯著的特點,性能的提高。這個例子所使用的微軟WDF驅動框架。
入口函數
1.入口函數的定義
任何一個驅動程序,不論它是一個標准的WDM驅動程序,還是使用WDF驅動程序框架,都會有一個叫做DriverEntry的入口函數,就好像普通控制台程序中的main函數一樣。這個函數是這樣聲明的:
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
);
這個函數具有兩個參數,其中一個是PDRIVER_OBJECT類型的指針。它代表了Windows系統為這個驅動程序所分配的一個驅動對象。這個驅動對象是Windows系統中對某個驅動的唯一標示。
DriverEntry的第二個參數是一個UNICODE字符串,它代表了驅動在注冊表中的參數所存放的位置。由於每個驅動都是以一個類似服務的形式存在的,在系統注冊表的HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services樹下總有一個和驅動名字相同的子樹用來描述驅動的一些基本信息,並提供一個可使用的存儲空間供驅動存放自己的特有信息。
2.Ramdisk驅動的入口函數
在Ramdisk驅動代碼的DriverEntry函數中只做了幾件簡單的事情,下面用代碼加以說明:
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
WDF_DRIVER_CONFIG config;
KdPrint(("Windows Ramdisk Driver - Driver Framework Edition.\n"));
KdPrint(("Built %s %s\n", __DATE__, __TIME__));
WDF_DRIVER_CONFIG_INIT( &config, RamDiskEvtDeviceAdd );
return WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE);
}
DriverEntry做的第一件事情是聲明了一個WDF_DRIVER_CONFIG類型的變量config,並且在兩句無關痛癢的輸出語句之后,很快的使用WDF_DRIVER_CONFIG_INT初始化config變量。WDF_DRIVER_CONFIG_INIT結構通常用來說明這個驅動程序的一些可配置項,其中包括了這個驅動程序的EvtDriverDeviceAdd和EvtDriverUnload回調函數的入口地址,這個驅動程序在初始化時的一些標志和這個驅動程序在分配內存時所使用的tag值,WDF_DRIVER_CONFIG_INIT這個宏在初始化WDF_DRIVER_CONFIG類型的變量時會把用戶提供的EvtDriverDeviceAdd回調函數的入口地址存入其中,並且初始化這個變量的其他部分。EvtDriverDeviceAdd回調函數是WDF驅動框架中的一個重要的回調函數,它用來在當即插即用管理器發現一個新的設備的時候對這個設備進行初始化操作,在這里我們可以將自己編寫的RamDiskEvtDeviceAdd函數提供給系統作為本驅動的EvtDriverDeviceAdd回調函數。
在設置好了config變量后,DriverEntry直接調用了WdfDriverCreate並返回。WdfDriverCreate函數是使用任何WDF框架提供的函數之前必須調用的一個函數,用來建立WDF驅動對象。WdfDriverCreate函數的前兩個參數就是DriverEntry傳入的驅動對象(DriverObject)和注冊表路徑(RegisterPath),第三個參數用來說明這個WDF驅動對象的屬性,這里簡單的用WDF_NO_OBJECT_ATTRIBUTES說明不需要特殊的屬性。第四個變量是之前初始化過的WDF_DRIVER_CONFIG變量,第四個參數是一個輸出結果。
調用這個函數之后,前面初始化過的config變量中的EvtDriverDeviceAdd回調函數--RamDiskEvtDeviceAdd就和這個驅動掛起鈎來,在今后的系統運行中,一旦發現了此類設備,RamDiskEvtDeviceAdd就會被Windows的Pnp manager調用,這個驅動自己的處理流程也就要上演了。
EvtDriverDeviceAdd函數
這里所說的EvtDriverDeviceAdd函數是WDF驅動模型中的名詞,對應到傳統的WDM驅動模型中就是WDM中的AddDevice函數。在本驅動中RamDiskEvtDeviceAdd作為一EvtDriverDeviceAdd函數在DriverEntry中被注冊,在DriverEntry函數執行完畢之后,這個驅動就只依靠RamDiskEvtDeviceAdd函數和系統保持聯系了。正如上一節所說的,系統在運行過程中一旦發現了這種類型的設備,就會調用RamDiskEvtDeviceAdd函數。下面對這個函數進行分析。
首先來看RamDiskEvtDeviceAdd的定義
NTSTATUS RamDiskEvtDeviceAdd(
IN WDFDRIVER Driver,
IN PWDFDRIVER_INIT DeviceInit
);
這個函數的返回值是NTSTATUS類型,可以根據實際函數的執行結果選擇返回表示正確的STATUS_SUCCESS或者其他代表錯誤的返回值。這個函數的第一個參數是一個WDFDRIVER類型的參數,在這個例子中不會使用這個參數;第二個參數是一個WDFDRIVER_INIT類型的指針,這個參數是WDF驅動模型自動分配出來的一個數據結構,專門傳遞給EvtDriverDeviceAdd類函數用來建立一個新設備。下面具體來看代碼:
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;
//設備名稱
DECLARE_CONST_UNICODE_STRING(ntDeviceName, NT_DEVICE_NAME);
//確保函數可以使用分頁內存
PAGED_CODE();
//避免編譯警告
UNREFERENCED_PARAMETER(Driver);
3.磁盤設備的創建
EvtDriverDeviceAdd類函數的一個重要任務是創建設備,而它的WDFDEVICE_INIT類型參數就是用來做這樣的事情,在創建設備之前需要按照開發人員的思想對WDFDEVICE_INIT變量進行進一步的加工,使創建的設備能夠達到想要的效果。由於這里的設備首先需要一個名字,這是因為這個設備將會通過這個名字暴露給應用層並且被應用層所使用,一個沒有名字的設備是無法在應用層使用的。另外需要將這個設備的類型設置為FILE_DEVICE_DISK,這是因為所有的磁盤設備都需要使用這個設備類型。將這個設備的I/O類型設置為Direct方式,這樣在將讀,寫和DeviceIoControl的IRP發送到這個設備時,IRP所攜帶的緩沖區將可以直接被使用。將Exclusive設置為FALSE這說明這個設備可以被多次打開。
//首先需要為這個設備指定一個名稱,這里使用剛才聲明的UNICODE_STRING
status = WdfDeviceInitAssignName(DeviceInit, &ntDeviceName);
if (!NT_SUCCESS(status)) {
return status;
}
//接下來需要對這個設備進行一些屬性設置,包括設備類型,IO操作類型和設備的排他方式
WdfDeviceInitSetDeviceType(DeviceInit, FILE_DEVICE_DISK);
WdfDeviceInitSetIoType(DeviceInit, WdfDeviceIoDirect);
WdfDeviceInitSetExclusive(DeviceInit, FALSE);
//下面來指定這個設備的設備擴展
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttributes, DEVICE_EXTENSION);
//下面我們還將利用這個WDF_OBJECT_ATTRIBUTES類型的變量來指定這個設備的清除回調函數,
//這個WDF_OBJECT_ATTRIBUTES類型的變量將會在下面建立設備時作為一個參數傳進去
deviceAttributes.EvtCleanupCallback = RamDiskEvtDeviceContextCleanup;
//到這里所有的准備工作都已就緒,我們可以開始真正的建立這個設備了,
//建立出的設備被保存在device這個局部變量中
status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);
if (!NT_SUCCESS(status)) {
return status;
}
//這個pDeviceExtension是我們聲明的一個局部指針變量,將其指向新建立的設備擴展
pDeviceExtension = DeviceGetExtension(device);
4.如何處理發往設備的請求
在設備創建好之后,如何處理所有可能發送給設備的請求是需要考慮的下一個問題。在以往的WDM開發中,常用的方式是設置這個設備各個請求的分發函數為自己實現回調函數,並且將特殊處理放置在這些函數中。例如在這個例子中,可以將所有讀寫請求都實現為讀寫內存,這就是最簡單的內存盤。上面的方式說起來很簡單,但是實現時還是需要一定的技巧的,一種常用的方式是建立一個或多個隊列,將所有發送到這個設備的請求都插入隊列中,由另一個線程去處理隊列。這是一個典型的生產者-消費者問題,這樣做的好處是有了一個小小的緩沖,同時還不用擔心由於緩沖帶來的同步問題,因為所有的請求都被隊列排隊了。而WDF驅動框架,微軟直接提供了這種隊列。
為了實現為驅動制作一個處理隊列這一目標,在WDF驅動框架中需要初始化一個隊列配置變量ioQueueConfig,這個變量會說明隊列的各種屬性。一個簡單的初始化方法是將這個配置變量初始化為默認狀態,之后再對一些具有特殊屬性的請求注冊回調函數,例如為請求注冊回調函數等。在這樣的初始化之后再為指定設備建立這個隊列,WDF驅動框架會自動將所有發往這個指定設備的請求都放到這個隊列中去處理,同時當請求符合感興趣的屬性是會調用之前注冊過的處理函數去處理。對每個設備可以建立多個隊列,但是在本例中只有一個隊列。
//將隊列的配置變量初始化為默認值
WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE (
&ioQueueConfig,
WdfIoQueueDispatchSequential
);
//由於我們對發往這個設備的DeviceIoControl請求和讀/寫請求感興趣,
//所以將這個個請求的處理函數設置為自己的函數,其余的請求使用默認值
ioQueueConfig.EvtIoDeviceControl = RamDiskEvtIoDeviceControl;
ioQueueConfig.EvtIoRead = RamDiskEvtIoRead;
ioQueueConfig.EvtIoWrite = RamDiskEvtIoWrite;
//指明這個隊列的隊列對象擴展,這里的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);
pQueueContext->DeviceExtension = pDeviceExtension;
5.用戶配置的初始化
在設備和用來處理設備請求的隊列都建立好之后,接下來就需要初始化與內存盤相關的一些數據結構了。對於內存盤來說,在驅動層中就是以剛才建立的那個設備作為代表的,那么自然而然的內存盤相應的數據結構也應該和這個設備相聯系。這里就使用了這個設備的設備擴展來存放這些數據結構的內容,具體而言,這個數據結構就是之前代碼中的DEVICE_EXTENSION數據結構。同時為了給用戶提供一些可配置的參數,在注冊表中還開辟了一個鍵用來存放這些可配置參數,這些參數對應到驅動中就成為了一個DISK_INFO類型的數據結構,在DEVICE_EXTENSION中會有一個成員來標識它,下面先來認識一下這兩個數據結構
typedef struct _DEVICE_EXTENSION {
//用來指向一塊內存區域,作為內存的實際數據儲存空間
PUCHAR DiskImage;
//用來儲存內存盤的磁盤Geometry
DISK_GEOMETRY DiskGeometry;
//我們自己定義的磁盤信息結構,在安裝時放在注冊表中
DISK_INFO DiskRegInfo;
//這個磁盤的符號鏈接名,這是真正的符號鏈接
UNICODE_STRING SymbolicLink;
//DiskRegInfo中的DriverLetter的儲存空間,這是用戶在注冊表中指定的盤符
WCHAR DriveLetterBuffer[DRIVE_LETTER_BUFFER_SIZE];
//SymboLink的存儲空間
WCHAR DosDeviceNameBuffer[DOS_DEVNAME_BUFFER_SIZE];
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
typedef struct _DISK_INFO {
//磁盤大小,以byte(字節)計算,所以我們的磁盤最大只有G
ULONG DiskSize;
//磁盤上根文件系統的進入結點
ULONG RootDirEntries;
//磁盤的每個簇由多少個扇區組成
ULONG SectorsPerCluster;
//磁盤的盤符
UNICODE_STRING DriveLetter;
} DISK_INFO, *PDISK_INFO;
在了解了數據結構中的各個成員對象的用處之后,就可以開始進行這些數據結構的初始化工作了。首先需要去注冊表中獲取用戶指定的信息,在這里是通過自己實現的一個函數RamDiskQueryRegParamters去獲取的,這個函數的第一個參數是注冊表的路徑,為了獲取這個路徑,首先通過WdfDriverGetRegisterPath從這個驅動對象中獲取到想應得注冊表路徑,在這里使用的WdfDeviceGetDriver和WdfDriverGetRegistryPath都是WDF庫提供的函數,他們的使用非常簡單,作用也可以直接從函數名中看出來,一個通過WDF驅動的設備獲取相應的驅動對象,而另一個是通過WDF驅動的驅動對象來獲取注冊表路徑。
//將生成設備的設備擴展中相應的UNICODE_STRING初始化
pDeviceExtension->DiskRegInfo.DriveLetter.Buffer =
(PWSTR) &pDeviceExtension->DriveLetterBuffer;
pDeviceExtension->DiskRegInfo.DriveLetter.MaximumLength =
sizeof(pDeviceExtension->DriveLetterBuffer);
//從系統為本驅動提供的注冊表鍵中獲取我們需要的信息
RamDiskQueryDiskRegParameters(
WdfDriverGetRegistryPath(WdfDeviceGetDriver(device)),
&pDeviceExtension->DiskRegInfo
);
在從注冊表中獲取了相應的參數只是初始化工作的第一步。由於這是一個使用內存來作為存儲介質的模擬磁盤,因此需要分配出一定大小的內存空間來模擬磁盤空間,這個大小是由注冊表的磁盤大小參數所指定的,在以后的內容中,這片空間被稱為Ramdisk鏡像。這里需要特別說明的是,在Windows內存中,可以分配兩種內存:一種是分頁內存,一種是非分頁內存。這里為了簡單起見,全部使用非分頁內存。
在分配好內存之后,磁盤就是了存儲空間,但是就好像任何剛買來的磁盤一樣,這個磁盤還沒有被分區,更沒有格式化。在這里需要自己做格式化操作,因為在內核是沒有調用format命令的。現在我們只要知道RamDiskFormatDisk所起的作用就是把內存介質的磁盤格式化。
6.鏈接給應用程序
到現在為止,程序建立了設備,建立了處理發往這個設備的的隊列,讀取了用戶的配置,並按照這個配置初始化所需的內存空間和其他一些相關參數。到此為止,磁盤設備已經具備了所有的部件,最后所需要做的事情就是將他們暴露給用戶層以供使用。在Windows中的各個盤符,例如“C: D:”實際上都是一個叫做符號鏈接的東西,應用層的代碼不能直接訪問在內核中建立的設備,但是可以訪問符號鏈接,所以在這里只需要用符號鏈接指向這個設備,便可以對符號鏈接的訪問指向這個設備了。這里所需要做的是根據用戶配置中選定的盤符去建立符號鏈接,將這個盤符和在這一節最開始建立的符號鏈接聯系起來。
//分配用戶指定大小的分頁內存。並使用我們自己的內存TAG值
pDeviceExtension->DiskImage = ExAllocatePoolWithTag(
NonPagedPool,
pDeviceExtension->DiskRegInfo.DiskSize,
RAMDISK_TAG
);
//初始化一個內容為“\\DosDevice\\”的UNICODE_STRING變量
RtlInitUnicodeString(&win32Name, DOS_DEVICE_NAME);
//初始化一個內容為“\\Device\Ramdisk\\”的UNICODE_STRING變量,這個
//變量沒有用處,只是為了在這里保持源文檔的完整性
RtlInitUnicodeString(&deviceName, NT_DEVICE_NAME);
//我們首先准備好用來儲存符號鏈接名的UNICODE_STRING變量
pDeviceExtension->SymbolicLink.Buffer = (PWSTR)
&pDeviceExtension->DosDeviceNameBuffer;
pDeviceExtension->SymbolicLink.MaximumLength =
sizeof(pDeviceExtension->DosDeviceNameBuffer);
pDeviceExtension->SymbolicLink.Length = win32Name.Length;
//將符號鏈接名的一開始設置為“\\DosDevice”,這是所有符號鏈接共有的前綴
RtlCopyUnicodeString(&pDeviceExtension->SymbolicLink, &win32Name);
//在上面賦值好的前綴后面連接我們從用戶配置讀出來的用戶指定盤符
RtlAppendUnicodeStringToString(&pDeviceExtension->SymbolicLink,
&pDeviceExtension->DiskRegInfo.DriveLetter);
//現在符號鏈接已經准備好,現在建立符號鏈接
status = WdfDeviceCreateSymbolicLink(device,
&pDeviceExtension->SymbolicLink);
FAT12/16磁盤卷初始化
1.磁盤卷結構簡介
Windows磁盤卷首先繼承了它所在磁盤的特性,這些特性是有硬件決定的,不可設置,不可更改。這些特性包括:
*每扇區的字節數
*每磁道扇區數
*每柱面磁道數
*柱面數
磁盤結構是由制作過程中的物理結構決定的,而操作系統對磁盤的管理主要通過文件系統來實現,這是一種邏輯上的結構。文件系統是建立在軟件能夠讀/寫磁盤任意扇區的基礎上的一種數據結構。在微軟,常見的文件系統包括FAT12,FAT16,FAT32,NTFS。
在FAT12/16文件系統中,有這么幾個參數需要解釋一下:
*MBR:Master Boot Record(主引導記錄)。
*DBR:DOS Boot Record(操作系統引導記錄)
*FAT區:FILE Allocation Table(文件分配表)
*根目錄入口點
2.Ramdisk對磁盤的初始化
Ramdisk驅動中的EvtDriverDeviceAdd類函數里會調用RamDiskFormatDisk函數對所分配的用於做磁盤磁盤映像的內存空間進行初始化。
首先來看一下這個函數的本地變量聲明
//一個指向磁盤啟動扇區的指針
PBOOT_SECTOR bootSector = (PBOOT_SECTOR) devExt->DiskImage;
//一個指向第一個FAT表的指針
PUCHAR firstFatSector;
//用於記錄有多少個根目錄入口點
ULONG rootDirEntries;
//由於記錄每個簇由多少個扇區組成
ULONG sectorsPerCluster;
//用於記錄FAT文件系統的類型
USHORT fatType; // Type FAT 12 or 16
//用於記錄在FAT表里面一共有多少個表項
USHORT fatEntries; // Number of cluster entries in FAT
//用於記錄一個FAT表需要占用多少個扇區來存儲
USHORT fatSectorCnt; // Number of sectors for FAT
//用於指向第一個根目錄入口點
PDIR_ENTRY rootDir; // Pointer to first entry in root dir
//用於確定這個函數可以存取分頁內存
PAGED_CODE();
//用於確定這個磁盤引導扇區的大小確實是一個扇區
ASSERT(sizeof(BOOT_SECTOR) == 512);
//用於確認我們操作的磁盤鏡像不是一個不可用的指針
ASSERT(devExt->DiskImage != NULL);
//清空磁盤鏡像
RtlZeroMemory(devExt->DiskImage, devExt->DiskRegInfo.DiskSize);
接下來格式化函數開始初始化一個保存在磁盤設備的設備擴展中的數據結構DiskGeometry。
typedef struct _DISK_GEOMETRY
{
//有多少個柱面
LARGE_INTEGER Cylinders;
//磁盤介質類型
MEDIA_TYPE MediaType;
//每個柱面有多少個磁道,也就是有多少個次盤面
ULONG TracksPerCylinder;
//每個磁道有多少個扇區
ULONG SetorsPerTrack;
//每個扇區由多少個字節
ULONG BytesPerSector;
}DISK_GEOMETRY,*PDISK_GEOMETRY;
這個數據結構被放在了磁盤設備擴展中,在今后的很多場合都被作為磁盤的參數被訪問。下面來看在格式化函數中是如何初始化這個數據結構的。
//住面數由磁盤的總容量計算得到
devExt->DiskGeometry.Cylinders.QuadPart = devExt->DiskRegInfo.DiskSize / 512 / 32 / 2;
//餐盤的介質是我們自己定義的RAMDISK_MEDIA_TYPE
devExt->DiskGeometry.MediaType = RAMDISK_MEDIA_TYPE;
在初始化了磁盤的物理參數之后,這里初始化一個文件系統和磁盤相關的參數——根目錄入口點數,這個參數決定了根目錄中能夠存在多少個文件和子目錄。同時初始化的還有每個簇由多少個扇區組成,這是根據用戶指定的數目來初始化的。
//根據用戶的指定值對根目錄的數目進行初始化
rootDirEntries = devExt->DiskRegInfo.RootDirEntries;
//根據用戶的指定值對每個簇有多少個扇區進行初始化
sectorsPerCluster = devExt->DiskRegInfo.SectorsPerCluster;
//由於根目錄入口點只使用字節,但是最少占用一個扇區,這里為了充分利用
//空間,在用戶指定的數目不合適時,會修正這個數目,以使扇區空間得到充分的利用
if (rootDirEntries & (DIR_ENTRIES_PER_SECTOR - 1)) {
rootDirEntries =
(rootDirEntries + (DIR_ENTRIES_PER_SECTOR - 1)) &
~ (DIR_ENTRIES_PER_SECTOR - 1);
}
在格式化函數一開始可以看到,bootSector指針直接指向了磁盤映像的首地址,聯系之前講過的磁盤和文件系統結構,通過之前的說明可以發現在磁盤映像最前面存儲的應該是這個分區DBR,也就是說,bootSector指針指向的是這個磁盤卷的DBR。下面看一下bootSector這個結構體的實際數據結構,值得注意的是,這也是標准DBR的結構。
//這是一個跳轉指令,跳轉到DBR中的引導程序
UCHAR bsJump[3]; // x86 jmp instruction, checked by FS
//這個卷的OEM名稱
CCHAR bsOemName[8]; // OEM name of formatter
//每個扇區有多少字節
USHORT bsBytesPerSec; // Bytes per Sector
//每個簇有多少個扇區
UCHAR bsSecPerClus; // Sectors per Cluster
//保留扇區數目,指的是第一個FAT表開始之前的扇區數,也包括DBR本身
USHORT bsResSectors; // Reserved Sectors
//這個卷有多少個FAT表
UCHAR bsFATs; // Number of FATs - we always use 1
//這個卷的根入口點有幾個
USHORT bsRootDirEnts; // Number of Root Dir Entries
//這個卷一共有多少個扇區,對大於個扇區的卷,這個字段為
USHORT bsSectors; // Number of Sectors
//這個卷的介質類型
UCHAR bsMedia; // Media type - we use RAMDISK_MEDIA_TYPE
//每個FAT表占用多少個扇區
USHORT bsFATsecs; // Number of FAT sectors
//每個磁道有多少個扇區
USHORT bsSecPerTrack; // Sectors per Track - we use 32
//有多少個磁頭
USHORT bsHeads; // Number of Heads - we use 2
//有多少個隱藏扇區
ULONG bsHiddenSecs; // Hidden Sectors - we set to 0
//一個卷超過個扇區,會使用這個字段來說明扇區總數
ULONG bsHugeSectors; // Number of Sectors if > 32 MB size
//驅動器編號
UCHAR bsDriveNumber; // Drive Number - not used
//保留字段
UCHAR bsReserved1; // Reserved
//磁盤擴展引導區標簽,Windows要求這個標簽為x28或者x29
UCHAR bsBootSignature; // New Format Boot Signature - 0x29
//磁盤卷ID
ULONG bsVolumeID; // VolumeID - set to 0x12345678
//磁盤卷標
CCHAR bsLabel[11]; // Label - set to RamDisk
//磁盤上的文件系統類型
CCHAR bsFileSystemType[8];// File System Type - FAT12 or FAT16
//保留字段
CCHAR bsReserved2[448]; // Reserved
//DBR結束簽名
UCHAR bsSig2[2]; // Originial Boot Signature - 0x55, 0xAA
在說明了DBR的結構之后需要看一下格式化函數是如何初始化這個數據結構的。初始化這個數據結構是通過向磁盤鏡像的起始位置填充指定數據來完成的。在下面的程序段中可以看到對於FAT12和FAT16相同的結構體成員是如何初始化的
//對於一開始的跳轉指令成員填入硬編碼指令,這里是Windows系統指定的
bootSector->bsJump[0] = 0xeb;
bootSector->bsJump[1] = 0x3c;
bootSector->bsJump[2] = 0x90;
//對於EOM成員,本驅動的作者填入了他的名字,讀者可以填寫任意名稱
bootSector->bsOemName[0] = 'R';
bootSector->bsOemName[1] = 'a';
bootSector->bsOemName[2] = 'j';
bootSector->bsOemName[3] = 'u';
bootSector->bsOemName[4] = 'R';
bootSector->bsOemName[5] = 'a';
bootSector->bsOemName[6] = 'm';
bootSector->bsOemName[7] = ' ';
//每個扇區有多少字節這個成員的數值直接取自之前初始化的磁盤信息數據結構
bootSector->bsBytesPerSec = (SHORT)devExt->DiskGeometry.BytesPerSector;
//這個卷只有一個保留扇區,姐DBR本身
bootSector->bsResSectors = 1;
//和正常的卷不同,為了節省空間,我們只存放一份FAT表,而不是通常的兩份
bootSector->bsFATs = 1;
//根目錄入口點數目之前的計算得知
bootSector->bsRootDirEnts = (USHORT)rootDirEntries;
//這個磁盤的總扇區數由磁盤大小和每個扇區的字節數計算得到
bootSector->bsSectors = (USHORT)(devExt->DiskRegInfo.DiskSize /
devExt->DiskGeometry.BytesPerSector);
//這個磁盤介質類型由之前初始化的磁盤信息得到
bootSector->bsMedia = (UCHAR)devExt->DiskGeometry.MediaType;
//每個簇有多少個扇區由之前的計算值初始化得到
bootSector->bsSecPerClus = (UCHAR)sectorsPerCluster;
接下來開始計算這個磁盤FAT表所占用的空間。前面已經說過,FAT表里面存儲的是一個將很多簇串聯起來的鏈表,那么FAT表的表項的數量就是磁盤上實際用來存儲數據的簇的數量,而這個簇的數量又是由磁盤總扇區數減去用來存儲其他數據的扇區數之后除以每個簇的扇區數得到的。下面可以看一下實際程序中是怎么計算的。
//FAT表的表項數目是總扇區數減去保留扇區數,再減去根目錄入口點所占用的扇區數
//然后除以每簇的扇區數,最后的結果需要加,因為FAT表中第項和第項是保留的
fatEntries =
(bootSector->bsSectors - bootSector->bsResSectors -
bootSector->bsRootDirEnts / DIR_ENTRIES_PER_SECTOR) /
bootSector->bsSecPerClus + 2;
至此已經計算出了FAT表的表項數量,根據這個表項數量首先可以決定到底使用FAT12還是FAT16文件系統。在決定了使用哪種文件系統之后,就可以算出整個FAT表所占用的扇區數。在實際的計算過程中還需要做一些小修正,正是因為在考慮了FAT標占用的空間之后,總的FAT表的表項數目可能有一些小出入。
//如果FAT表的表項大於,就使用FAT16文件系統,反之使用FAT32文件系統
if (fatEntries > 4087) {
fatType = 16;
fatSectorCnt = (fatEntries * 2 + 511) / 512;
fatEntries = fatEntries + fatSectorCnt;
fatSectorCnt = (fatEntries * 2 + 511) / 512;
}
else {
fatType = 12;
fatSectorCnt = (((fatEntries * 3 + 1) / 2) + 511) / 512;
fatEntries = fatEntries + fatSectorCnt;
fatSectorCnt = (((fatEntries * 3 + 1) / 2) + 511) / 512;
}
在上面的運算過程之后獲得了文件系統的類型和FAT表需要占用的扇區數目。下面可以接着初始化DBR的數據結構了。
//初始化FAT表所占用的分區數
bootSector->bsFATsecs = fatSectorCnt;
//初始化DBR中每個磁道的扇區數
bootSector->bsSecPerTrack = (USHORT)devExt->DiskGeometry.SectorsPerTrack;
//初始化磁頭數,也就是每個柱面的磁道數
bootSector->bsHeads = (USHORT)devExt->DiskGeometry.TracksPerCylinder;
//初始化啟動簽名,Windows要求是x28或者x29
bootSector->bsBootSignature = 0x29;
//隨便填寫一個卷的ID
bootSector->bsVolumeID = 0x12345678;
//將卷標設置成“Ramdisk”
bootSector->bsLabel[0] = 'R';
bootSector->bsLabel[1] = 'a';
bootSector->bsLabel[2] = 'm';
bootSector->bsLabel[3] = 'D';
bootSector->bsLabel[4] = 'i';
bootSector->bsLabel[5] = 's';
bootSector->bsLabel[6] = 'k';
bootSector->bsLabel[7] = ' ';
bootSector->bsLabel[8] = ' ';
bootSector->bsLabel[9] = ' ';
bootSector->bsLabel[10] = ' ';
//根據我們之前計算得出的結果來選則到底是FAT12還是FAT16文件系統
bootSector->bsFileSystemType[0] = 'F';
bootSector->bsFileSystemType[1] = 'A';
bootSector->bsFileSystemType[2] = 'T';
bootSector->bsFileSystemType[3] = '1';
bootSector->bsFileSystemType[4] = '?';
bootSector->bsFileSystemType[5] = ' ';
bootSector->bsFileSystemType[6] = ' ';
bootSector->bsFileSystemType[7] = ' ';
bootSector->bsFileSystemType[4] = ( fatType == 16 ) ? '6' : '2';
//簽署DBR最后標志,x55AA
bootSector->bsSig2[0] = 0x55;
bootSector->bsSig2[1] = 0xAA;
到此為止,DBR就算是初始化完畢了。在FAT12/16文件系統中,DBR之后緊接着的是FAT表,對於FAT表的初始化很簡單,只需要在FAT表的第1個表項內填寫介質標識即可。同時要注意,FAT12和FAT16的表項長度不同。
//定位到FAT表的起始點,這里的定位方式是利用了DBR只有一個扇區這個條件
firstFatSector = (PUCHAR)(bootSector + 1);
//填寫介質標識
firstFatSector[0] = (UCHAR)devExt->DiskGeometry.MediaType;
firstFatSector[1] = 0xFF;
firstFatSector[2] = 0xFF;
if (fatType == 16) {
firstFatSector[3] = 0xFF;
}
在FAT表之后,就是根目錄入口點了,在FAT12/16文件系統中,根目錄入口數據結構定義如下:
typedef struct _DIR_ENTRY
{
//文件名
UCHAR deName[8]; // File Name
//文件擴展名
UCHAR deExtension[3]; // File Extension
//文件屬性
UCHAR deAttributes; // File Attributes
//系統保留
UCHAR deReserved; // Reserved
//文件建立的時間
USHORT deTime; // File Time
//文件建立的日期
USHORT deDate; // File Date
//文件的第一個簇的編號
USHORT deStartCluster; // First Cluster of file
//文件大小
ULONG deFileSize; // File Length
} DIR_ENTRY, *PDIR_ENTRY;
在FAT12/16文件系統中,通常第一個根目錄入口點存儲了一個最終被作為卷標的目錄入口點,這里初始化它,在這之后,這個磁盤卷就算是被格式化完畢了,也就可以拿來使用了。
//由於緊跟着FAT表,所以根目錄入口點的表起始位置很容易定位
rootDir = (PDIR_ENTRY)(bootSector + 1 + fatSectorCnt);
//初始化卷標
rootDir->deName[0] = 'M';
rootDir->deName[1] = 'S';
rootDir->deName[2] = '-';
rootDir->deName[3] = 'R';
rootDir->deName[4] = 'A';
rootDir->deName[5] = 'M';
rootDir->deName[6] = 'D';
rootDir->deName[7] = 'R';
rootDir->deExtension[0] = 'I';
rootDir->deExtension[1] = 'V';
rootDir->deExtension[2] = 'E';
//將這個入口地點的屬性設置為卷標屬性
rootDir->deAttributes = DIR_ATTR_VOLUME;
驅動程序中的請求處理
1.請求的處理
在前面介紹中已經知道,WDF驅動框架會將所有發往之前建立的磁盤設備的請求都排對放入已經建立的隊列中,而放在隊列后絕大多數請求都得到了合適的處理,但是讀者關心的讀,寫和DeviceIOControl請求,由於注冊了回調函數,隊列會將這些請求交給注冊的回調函數去處理。
回調函數在收到了請求之后只能執行下面列舉的4鍾操作中的一種。
*重新排隊。
*完成請求
*撤銷請求
*轉發請求
在實際的Windows系統當中,設備之間是一種層疊的關系,在這個磁盤設備之上,還會有文件系統設備,一般應用程序的訪問都應該是訪問文件系統設備,而文件系統設備會負責做文件系統放面的一些工作,例如對FAT表的維護,對文件的讀/寫等,而這些操作最終都會轉化成對磁盤的讀/寫發往磁盤設備。
在這個Ramdisk驅動中,幾乎所有的發給回調函數的請求都被完成了,只是在完成之前需要做一些特殊處理。下面就針對讀/寫和DeviceIOControl這3類讀者所關注的請求加以分析。
2.讀/寫請求
讀/寫請求的回調函數原型如下,之所以把這兩個函數放在一起介紹,是因為讀者可以從他們的函數原型看出,他們的參數沒什么區別。他們的第一個參數是一個隊列對象,這個對象說明了請求的來源;第二個參數則是具體的請求;最后一個參數是讀/寫請求回調函數所特有的,用來說明需要讀或者寫多少字節的內容。
VOID
RamDiskEvtIoWrite(
IN WDFQUEUE Queue,
IN WDFREQUEST Request,
IN size_t Length
)
VOID
RamDiskEvtIoRead(
IN WDFQUEUE Queue,
IN WDFREQUEST Request,
IN size_t Length
)
在知道了函數原型之后就開始着手處理這個請求了。在之前建立隊列時曾經將磁盤設備的設備擴展和隊列的擴展聯系了起來,在這里就可以看出他的用處--讀者可以輕易地在這些回調函數里通過隊列對象獲取到磁盤的設備擴展,進而獲取到所有的相關參數。隊以一個磁盤來說,讀/寫請求就是讀/寫磁盤上的某一段區域的內容,這個區域由起點(offset)和長度(length)來划定,長度已經由回調函數的參數提供,而起始點就要通過WDF驅動框架提供的各種函數在第二個參數----請求參數中獲取了。讀/寫請求還有另外一個重要的參數就是緩沖區,它由系統提供,用來存放讀出來的數據或者需要寫入的數據,這個參數也需要從請求參數中獲取。
在獲取了所有必須的參數之后,作為以內存為介質的模擬磁盤設備來說,只需要簡單地將內存鏡像中適當地點,適當長度的數據拷貝到讀緩沖區中,或者將寫緩沖區的數據拷貝到內存鏡像中即可,這也就是作為一個內存盤來說,針對標准磁盤讀/寫請求的特殊處理。在真實應用中,在磁盤設備之上的文件系統設備會根據FAT表等數據結構,將對文件的訪問轉換成對磁盤設備的訪問,而磁盤對於上層來說,就是一個起始位置為0,總長度為磁盤卷總大小的扁平的尋址空間,任何由文件系統轉換過來的訪問都應該在這個空間之內。
下面首先看一下讀請求的具體處理過程。
//從隊列的擴展中獲取到相應的磁盤設備的設備擴展
PDEVICE_EXTENSION devExt = QueueGetExtension(Queue)->DeviceExtension;
//用於各種函數返回值的狀態變量
NTSTATUS Status = STATUS_INVALID_PARAMETER;
//用於獲取請求參數的變量
WDF_REQUEST_PARAMETERS Parameters;
//用於獲取請求起始地址的變量,這里要注意的是,這是一個位的數據
LARGE_INTEGER ByteOffset;
//這里是一個用於獲取讀緩沖區的內存句柄
WDFMEMORY hMemory;
__analysis_assume(Length > 0);
//初始化參數表變量,為之后從請求參數中獲取各種信息做准備
WDF_REQUEST_PARAMETERS_INIT(&Parameters);
//從請求參數中獲取信息
WdfRequestGetParameters(Request, &Parameters);
//將請求參數中讀的起始位置讀取出來
ByteOffset.QuadPart = Parameters.Parameters.Read.DeviceOffset;
//這里是自己實現的一個參數檢查函數,由於讀取的范圍不能超過磁盤鏡像的大小
//且必須是扇區對齊,所以這里需要有一個檢查函數,如果檢查失敗,則直接將這個
//請求以錯誤的參數(STATUS_INVALID_PARAMETER)為返回值結束
if (RamDiskCheckParameters(devExt, ByteOffset, Length)) {
Status = WdfRequestRetrieveOutputMemory(Request, &hMemory);
if(NT_SUCCESS(Status)){
Status = WdfMemoryCopyFromBuffer(hMemory, // Destination
0, // Offset into the destination
devExt->DiskImage + ByteOffset.LowPart, // source
Length);
}
}
WdfRequestCompleteWithInformation(Request, Status, (ULONG_PTR)Length);
3.DeviceIoControl請求
在上一節提到過,在正常情況下,文件系統會發給本驅動所建立的磁盤設備一些讀/寫請求,而實際上除了讀/寫請求外還會有一些控制方面的請求,這種請求統稱為DeviceIoControl請求。一個標准的磁盤卷設備,僅僅支持最小的能夠保證正常工作的DeviceIoControl請求就足夠了。在這里讀者可以簡單地把DeviceIoControl請求理解為系統發過來的一堆問題,例如這個磁盤有多大,它能寫什么數據之類的問題,處理只需要按照情況回答這些問題就行了。下面來看看Ramdisk驅動是如何處理DeviceIoControl請求的。
首先是DeviceIoContorl請求的處理函數原型。這個回調函數沒有返回值,其中第一個參數同樣是請求來自哪個隊列;第二個參數是請求參數;第三個參數和第四個參數是由於DeviceIoContorl回調函數所特有的參數,即輸出緩沖區長度,輸入緩沖區長度。由於DeviceIoControl請求通常是伴隨着一些請求的相關信息而傳入的,填滿了請求到的信息傳出,所以這里需要這個兩個緩沖區長度;最后一個參數是請求的功能號,即說明這是一個什么樣的DeviceIoControl請求。
VOID
RamDiskEvtIoDeviceControl(
IN WDFQUEUE Queue,
IN WDFREQUEST Request,
IN size_t OutputBufferLength,
IN size_t InputBufferLength,
IN ULONG IoControlCode
)
下面來看看這些請求是如何被處理的。首先讀者需要知道的是,DeviceIoControl請求有很多種,針對於每類不同的設備具有不同的含義其中有些是必須處理的,不處理這個設備就有可能不能啟動,或者不能正常工作;還有一些在最簡單的情況下是不需要處理的,不處理的后果最多會導致某些參數顯示不正確等小錯誤的發生。具體到Ramdisk驅動,只需要處理幾個DeviceIoControl請求即可。
//初始化返回狀態為非法的設備請求,這樣在其他無關緊要的,不需要處理的
//DeviceIoControl請求到來時,可以直接返回這個狀態
NTSTATUS Status = STATUS_INVALID_DEVICE_REQUEST;
//用來存放返回的DeviceIoContorl所需要處理的數據長度
ULONG_PTR information = 0;
//中間變量
size_t bufSize;
//和讀/寫回調函數相同,也可以通過隊列的擴展來獲取設備的擴展
PDEVICE_EXTENSION devExt = QueueGetExtension(Queue)->DeviceExtension;
//由於我們隊發過來的請求的長度很有信心,
//所以這里我們不需要輸入和輸出緩沖區的長度
UNREFERENCED_PARAMETER(OutputBufferLength);
UNREFERENCED_PARAMETER(InputBufferLength);
//判斷是哪個DeviceIoContorl請求
switch (IoControlCode) {
//這是一個獲取當前分區信息的DeviceIoControl請求,需要處理
case IOCTL_DISK_GET_PARTITION_INFO: {
PPARTITION_INFORMATION outputBuffer;
PBOOT_SECTOR bootSector = (PBOOT_SECTOR) devExt->DiskImage;
information = sizeof(PARTITION_INFORMATION);
Status = WdfRequestRetrieveOutputBuffer(Request, sizeof(PARTITION_INFORMATION), &outputBuffer, &bufSize);
if(NT_SUCCESS(Status) ) {
outputBuffer->PartitionType =
(bootSector->bsFileSystemType[4] == '6') ? PARTITION_FAT_16 : PARTITION_FAT_12;
outputBuffer->BootIndicator = FALSE;
outputBuffer->RecognizedPartition = TRUE;
outputBuffer->RewritePartition = FALSE;
outputBuffer->StartingOffset.QuadPart = 0;
outputBuffer->PartitionLength.QuadPart = devExt->DiskRegInfo.DiskSize;
outputBuffer->HiddenSectors = (ULONG) (1L);
outputBuffer->PartitionNumber = (ULONG) (-1L);
Status = STATUS_SUCCESS;
}
}
break;
case IOCTL_DISK_GET_DRIVE_GEOMETRY: {
PDISK_GEOMETRY outputBuffer;
//
// Return the drive geometry for the ram disk. Note that
// we return values which were made up to suit the disk size.
//
information = sizeof(DISK_GEOMETRY);
Status = WdfRequestRetrieveOutputBuffer(Request, sizeof(DISK_GEOMETRY), &outputBuffer, &bufSize);
if(NT_SUCCESS(Status) ) {
RtlCopyMemory(outputBuffer, &(devExt->DiskGeometry), sizeof(DISK_GEOMETRY));
Status = STATUS_SUCCESS;
}
}
break;
case IOCTL_DISK_CHECK_VERIFY:
case IOCTL_DISK_IS_WRITABLE:
//
// Return status success
//
Status = STATUS_SUCCESS;
break;
}
WdfRequestCompleteWithInformation(Request, Status, information);