文件系統識別器是一個標准的NT內核模式驅動程序。它只實現一項功能:檢查物理介質設 備,如果它能夠識別存儲介質的格式便加載相應的文件系統驅動程序。你可能要問:為什麼 不把所有的文件系統一起加載呢?因為系統幾乎從不需要加載所有文件系統驅動程序,用一 個小驅動可以節約數百K系統內存。實際上,所有標准的NT物理介質文件系統都利用文件系統 識別器。舉個例子來說,如果CD-ROM沒有被訪問,那麼CDFS文件系統驅動程序將不會被加載 。
文件系統識別器是怎麼樣知道磁盤上存在什麼類型的文件系統呢?一般說來,檢查磁盤上 的標識符就可以了。標識符可能存儲在於分區表裡,從分區起始處加上一段偏移量就能定位 這個唯一值,這個值可以是序列號或者其他某種標識符。這些標識符必須完全不同,以免加 載了不正確的文件系統驅動程序。
以下是一些常用的文件系統標識:
文件系統名 文件系統標識 HFS 0x4244 NTFS ''NTFS'' FAT 0xe9或0xeb或0x49表1 常見的文件系統標識符
當一個文件系統程序被加載後,它必須分析磁盤以便確定介質上是否包含了它可以識別的 文件系統。如果介質上是可以識別的文件系統,該文件系統驅動程序將“裝配”這個文件系 統。文件系統識別器也分析介質來確認是否有可識別的文件系統。但是文件系統識別器不是 “裝配”到卷上,而是加載文件系統驅動程序。文件系統識別器完成任務便可以卸載了。
裝配過程
在NT系統中,當一個卷被訪問時才被裝配。一些卷在系統初始化被裝配,用磁盤管理程序 或可移動介質創建的卷會在晚些時候被裝配。因此,當你創建新的分區並且為它分配了盤符 ,直到有應用程序訪問這個卷時這個卷才會被裝配。所以,當你為軟驅更換了盤片,直到有 程序訪問軟盤時卷才被裝配。
一個WIN32應用程序通過盤符訪問卷。盤符只是對象管理器名字空間的一個符號連接。你 可以利用平台SDK裡的工具WINOBJ查看。盤符是物理磁盤卷的符號連接而不是文件文件系統驅 動程序創建的設備的符號連接。當IO管理器發現為物理存儲設備創建的設備對象有 FILE_DEVICE_DISK, FILE_DEVICE_TAPE, FILE_DEVICE_CD_ROM,或者 FILE_DEVICE_VIRTUAL_DISK標記時,這些設備對象就有卷參數塊(Volume Parameter Block )。VPB用於表示卷是否已經被裝配了。如果已經裝配了,VPB 指向屬於文件系統驅動程序的 設備對象。如果沒有被裝配,IO管理器將嘗試裝配這個卷。
IO管理器為當前物理介質類型( FILE_DEVICE_DISK_FILE_SYSTEM, FILE_DEVICE_TAPE_FILE_SYSTEM, FILE_DEVICE_CD_ROM _FILE _SYSTEM)的卷調用每一個注 冊的文件系統驅動程序。通過調用驅動的IRP_MJ_FILE_SYSTEM_ CONTROL派遣例程,傳遞給派 遣例程次功能碼是IRP_MN_MOUNT_VOLUME便可以實現裝配。驅動程序返回給IO管理器該卷是否 可以被裝配的信息。調用次序是後注冊先調用。所以被裝載最頻繁的文件系統驅動程序首先 得到裝配卷的機會。
實際上第一個注冊的是RAW文件系統,它注冊另外三種不同的文件系統。當RAW文件系統裝 配卷時,它便注冊這三種不同的文件系統。屬於RAW文件系統的卷只能被“全部訪問 ("whole volume" )”操作打開。磁盤管理器需要做這樣的操作。
文件系統識別器實際上就是一個只處理裝配請求的文件系統驅動程序。因此,它用相應的 文件系統類型創建設備對象,向IO管理器注冊為文件系統,然後等待被調用去裝配卷。如果 識別器確認了卷屬於它的文件系統,它返回錯誤碼STATUS_FS_DRIVER_REQUIRED,而不是接受 這個裝配請求。接著IO管理器調用識別器,讓它加載整個文件系統驅動程序。具體細節是發 送IRP IRP_MJ _FILE_SYSTEM_CONTROL,次功能碼為IRP_MN_LOAD_FILE_SYSTEM。
實現
實現一個文件系統識別器是非常直接的,我們提供一個例子程序,你可以利用它創建自己 的文件系統識別器。
#include <ntddk.h> // 定義可能隨著你的文件系統而改變 #define FSD_SERVICE_PATH L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\MyFsd" #define FSD_RECOGNIZER_NAME L"\\FileSystem\\MyFsdRecognizer" #define DEVICE_LOGICAL_BLOCKSIZE 512 // 每一個扇區的大小 // // IFS Kit中存檔的函數 // NTSYSAPI NTSTATUS NTAPI ZwLoadDriver(IN PUNICODE_STRING DriverServiceName); NTKERNELAPI VOID IoRegisterFileSystem(IN OUT PDEVICE_OBJECT DeviceObject); NTKERNELAPI VOID IoUnregisterFileSystem(IN OUT PDEVICE_OBJECT DeviceObject); // // 全局變量 // static PDRIVER_OBJECT RecognizerDriverObject; static PDEVICE_OBJECT RecognizerDeviceObject; static VOID Unload(PDRIVER_OBJECT); static NTSTATUS RecognizerFsControl(PDEVICE_OBJECT, PIRP); static NTSTATUS RecognizerDetectFileSystem(PIRP Irp); static NTSTATUS RecognizerIoControl( IN PDEVICE_OBJECT deviceObject, IN ULONG IoctlCode, IN PVOID InputBuffer, IN ULONG InputBufferSize, OUT PVOID OutputBuffer, OUT ULONG OutputBufferSize); static BOOLEAN RecognizerReadDiskSector( IN PDEVICE_OBJECT pDeviceObject, IN ULONG DiskSector, IN UCHAR* Buffer);
上面的代碼段定義了文件系統識別器需要的變量和外部函數的聲明。我們最關心的是 IoRegisterFileSystem(), IoUnregisterFileSystem(), 和 ZwLoadDriver().識別調用 IoRegisterFileSystem向IO管理器把自己注冊成為文件系統驅動程序。這意味著IO管理器將 在新卷被裝配時調用此識別器。一旦識別器加載了整個文件系統驅動程序,它可以調用 IoUnregisterFileSystem告訴IO管理器當新卷在裝配過程中,不要調用識別器。加載文件系 統驅動程序的函數是ZwLoadDriver.
// DriverEntry // // 驅動的入口點 // // 輸入參數: // DriverObject – 驅動的驅動程序對象 // RegistryPath – 驅動程序的服務鍵 // // 輸出參數: // None. // // 返回值: // success // // 注意: // 這只是一個實驗型的驅動. // NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { NTSTATUS code; UNICODE_STRING driverName; // 保存驅動程序對象的全局指針 RecognizerDriverObject = DriverObject; // 為IRP_MJ_FILE_SYSTEM_CONTROL設置派遣例程入口點 DriverObject->MajorFunction[IRP_MJ_FILE_SYSTEM_CONTROL] = RecognizerFsControl; // 這個驅動是可卸載的 DriverObject->DriverUnload = Unload; // 為識別器的設備名初始化一個unicode字符串. RtlInitUnicodeString(&driverName, FSD_RECOGNIZER_NAME); // 創建命名的設備對象. code = IoCreateDevice(RecognizerDriverObject, 0, &driverName, FILE_DEVICE_DISK_FILE_SYSTEM, 0, FALSE, &RecognizerDeviceObject); if (!NT_SUCCESS(code)) { DbgPrint("Recognizer failed to load, failure in IoCreateDevice call returned 0x%x\n", code); // 失敗. return (code); } // 把設備對象注冊為文件系統. IoRegisterFileSystem(RecognizerDeviceObject); // 完成 return STATUS_SUCCESS; }
上面的代碼描述了識別器的入口點DriverEntry。DriverEntry識別器初始化驅動程序。向IO 管理器注冊兩個回調例程。一個處理IRP_MJ_FILE_SYSTEM_ CONTROL,一個是卸載例程。我們 只需要識別文件系統的格式,所以不需要處理其他請求。當真正的驅動程序被加載後,它會 處理這些請求。
在DriverEntry中,創建了一個設備對象,並且指定了需要識別的文件系統的介質類型。調用 IoRegisterFileSystem把設備對象注冊為文件系統。整個過程非常簡單。需要注意的是,你 可以在一個驅動程序中識別幾種存儲介質類型的文件系統。這樣的一個驅動程序需要為每一 種存儲介質類型創建一個設備對象。
為什麼需要識別不同存儲介質呢?有兩種不同的可能性。 假如你的文件系統像UDF文件系統 一樣支持不同種類的存儲介質,便需要識別不同的存儲介質。因為Windows NT 通過匹配不同 的存儲介質類型來選擇文件系統,你的識別器可以為每一種類型的存儲介質創建一個設備對 象。在創建設備對象時指定介質類型就可以了,如用FILE_DEVICE_DISK_FILE_SYSTEM類型創 建對象,就像例子代碼中的那樣,用FILE_DEVICE_CD_ROM_FILE_SYSTEM創建第二個設備對象 。這樣,不管UDF文件系統存在與磁盤還是CD上都可以被你的識別器檢測到。另外一個可能的 原因是需要處理多種存儲介質類型。這種方法被微軟的文件系統驅動程序識別起采用,一個 驅動程序可以識別FAT,NTFD,CDFS文件系統。它用設備對象來決定加載哪個文件系統驅動程 序。它為每一個可以識別的文件系統創建一個設備對象。
// // 卸載例程 // // 輸入參數: // DeviceObject – 可能是我們設備對象指針 // // 輸出參數: // None. // // 返回值: // None. // static VOID Unload(PDRIVER_OBJECT DriverObject) { // // 如果存在設備對象,則刪除它. // if (RecognizerDeviceObject) { IoUnregisterFileSystem(RecognizerDeviceObject); IoDeleteDevice(RecognizerDeviceObject); RecognizerDeviceObject = 0; } // // 完成 // }
上面的代碼是識別器unload例程。當驅動被停止時(命令net stop) 或者對驅動程序對象沒 有引用時(文件系統驅動程序已經加載完畢),此例程被調用。如果停止驅動的命令有用戶 模式程序發出,RecognizerDeviceObject 不為NULL,所以卸載,刪除設備對象,返回。如果 請求有IO管理器發出,表示沒有對該驅動的引用了。
static NTSTATUS RecognizerFsControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp); UNICODE_STRING driverName; NTSTATUS code; // 是否是本驅動創建的設備對象 if (DeviceObject != RecognizerDeviceObject) { // 不是. Irp->IoStatus.Status = STATUS_NOT_IMPLEMENTED; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_NOT_IMPLEMENTED; } //檢查是否為 “裝配/加載”請求。如果不是,什麼也不做 if ((irpSp->MinorFunction != IRP_MN_MOUNT_VOLUME) && (irpSp- >MinorFunction != IRP_MN_LOAD_FILE_SYSTEM)) { // 什麼也不做 Irp->IoStatus.Status = STATUS_NOT_IMPLEMENTED; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_NOT_IMPLEMENTED; } // 處理 “加載”請求 if (irpSp->MinorFunction == IRP_MN_LOAD_FILE_SYSTEM) { // 加載文件系統 RtlInitUnicodeString(&driverName, FSD_SERVICE_PATH); code = ZwLoadDriver(&driverName); // 用加載的結果完成IRP Irp->IoStatus.Status = code; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); // 在成功加載了文件系統驅動程序的情況下,不在需要這個設備對象 if (NT_SUCCESS(code)) { IoUnregisterFileSystem(RecognizerDeviceObject); IoDeleteDevice(RecognizerDeviceObject); RecognizerDeviceObject = 0; // 文件系統已經被加載了。 } return (code); } //調用RecognizerDetectFileSystem決定該卷是否可以被本文件系統識別。 code = RecognizerDetectFileSystem(Irp); //檢查返回值,如果成功,說明這是可以識別的文件系統。 //接下來告訴調用者(IO管理器)應該調用加載文件系統驅動的例程(即發送 IRP_MN_LOAD_FILE_SYSTEM) if (NT_SUCCESS(code)) { // 卷可以被識別 Irp->IoStatus.Status = STATUS_FS_DRIVER_REQUIRED; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_FS_DRIVER_REQUIRED; } // 卷不可以被識別 Irp->IoStatus.Status = STATUS_UNRECOGNIZED_VOLUME; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_UNRECOGNIZED_VOLUME; }
上面給出了RecognizerFSControl的全部代碼,這個函數用於處理 IRP_MJ_FILE_SYSTEM_CONTROL 請求。這個函數總是在 PASSIVE_LEVEL 上被調用。這個函數 處理兩個次功能碼,分別是 IRP_MN_MOUNT_VOLUME 和IRP_MN_LOAD_FILE_SYSTEM。當有一個 卷被裝配時,IRP_MN_MOUNT_VOLUME請求被發送。IRP_MN_LOAD_FILE_SYSTEM 在文件系統驅動 程序必須被加載時調用。
在本文的前面部分,我們已經描述了在一個沒有被裝配的物理磁盤卷被訪問時IO管理器如何 調用所有注冊了的文件系統。每一個文件系統驅動程序都有機會去分析卷直到一個驅動程序 聲明可以識別此卷。所以文件系統識別器在這裡收到一個裝配請求,文件系統識別器調用 Recognizer _DetectFileSystem函數,這個函數將在稍候被描述。如果該函數返回 STATUS_SUCCESS,說明該卷該文件系統識別器的文件系統驅動識別。接著返回 STATUS_FS_DRIVER_REQUIRED給IO管理器。這個錯誤碼被IO管理器特殊對待,因為對文件系統 識別器的支持是建立在IO管理器的裝配過程。一旦收到此IRP,IO管理器便發送一個新的IO請 求給文件系統識別器。這一次發送的次功能碼IRP_MN_LOAD_FILE_SYSTEM。識別器用 ZwLoadDriver函數處理這個請求。該函數唯一的參數是需要加載的驅動的注冊表項。如果加 載驅動成功,ZwLoadDriver返回STATUS_SUCCESS,識別器可以反注冊和刪除識別器設備對象, 這將導致識別器驅動程序被卸載。從該文件系統被注冊開始,這是它第一次被調用。
當整個文件系統被加載後,IO管理器將和文件系統程序再進行一次裝配請求。因為整個文件 系統被加載了,識別器已經不再需要了。隨後的請求將被文件系統驅動程序處理。
// // RecognizerDetectFileSystem // // 用戶需要根據自己文件系統的不同改變這些代碼 // 輸入參數: // Irp – 請求裝配卷的IRP. // // 輸出參數: // None. // // Returns: // STATUS_SUCCESS – 這是我們的驅動可以識別的文件系統 // Other - I/O error // // Notes: // None. // static NTSTATUS RecognizerDetectFileSystem(PIRP Irp) { NTSTATUS code =STATUS_SUCCESS; DISK_GEOMETRY diskGeometry; PARTITION_INFORMATION partitionInfo; PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp); PVPB vpb = irpSp->Parameters.MountVolume.Vpb; PDEVICE_OBJECT mediaDeviceObject = irpSp- >Parameters.MountVolume.DeviceObject; unsigned char* pBuffer = NULL; //首先構造一個可以獲得卷的分區表等信息的IRP,用RecognizerIoControl傳遞到 下層驅動。 // IF error{ // return error //} // else // { //用RecognizerReadDiskSector 讀卷參數塊(VPB)} // If read error{ // return error //} // else // {檢查文件系統的標識符,如果不匹配 // return error //} // else // return code STATUS_SUCCESS; // endif // endif // endif // // 現在返回錯誤碼 code = STATUS_UNRECOGNIZED_VOLUME; return code; }
上面的代碼顯示RecognizerDetectFileSystem的全部內容。它負責實際的的卷的類型的檢測 。因為它的實現依賴於實際的物理卷。所以我們只提供一個通用的方法。你需要修改使得它 適用於實際的卷。
這個例程需要實現兩個關鍵的功能,一個是從存儲介質讀取數據,另外一個是從存儲介質中 獲得關於介質的關鍵信息。這些功能被兩個函數封裝了,第一個是 RecogizerReadDiskSector()從物理卷中讀取用於分析的數據。第二個函數 RecognizerIoControl(…)用於向下層設備發送請求,這些請求返回關於物理介質的信息,例 如每一個扇區多少個字節。IOCTL_DISK_GET_PARTITION_ INFORMATION用於返回已經被裝配了 的卷的信息。NT文件系統用分區信息用作匹配簽名算法的一部分。注意 RecognizerReadDiskSector()函數用的是相對於分區的扇區數而不是相對於磁盤的扇區數。 所以如果你傳入參數0,你將得到的是分區的第一個扇區,而不是磁盤的第一個分區。換句話 說,如果分區從第540扇區開始,用0做參數調用函數,你得到的是相對於分區為0,而相對於 磁盤為549的扇區數據。.
// // RecognizerIoControl // // 這個函數用於發送特定的IRP個下層驅動 // // 輸出參數: // MediaHandle – 特定設備的卷句柄 // Offset – 讀請求的邏輯偏移量 // Length – 讀的長度 // MDL – 數據將要被拷貝的MDL鏈t // // 輸出參數: // None. // // Returns: // STATUS_SUCCESS - I/O completed successfully // Other - I/O error // // Notes: // None. // static NTSTATUS RecognizerIoControl( IN PDEVICE_OBJECT deviceObject, IN ULONG IoctlCode, IN PVOID InputBuffer, IN ULONG InputBufferSize, OUT PVOID OutputBuffer, OUT ULONG OutputBufferSize) { PIRP irp; NTSTATUS code; KEVENT event; IO_STATUS_BLOCK iosb; // 初始化用於等待操作成功完成的事件對象 KeInitializeEvent(&event, SynchronizationEvent, FALSE); // 創建請求 // irp = IoBuildDeviceIoControlRequest(IoctlCode, deviceObject, InputBuffer, InputBufferSize, OutputBuffer, OutputBufferSize, FALSE, &event, &iosb); // 向下層驅動發送IRP code = IoCallDriver(deviceObject, irp); //如果可能的話,我們必須等待。注意,我們不接受異步過程調用。在IO操作完成 之前,我們不能返回。 if (code == STATUS_PENDING) { (void) KeWaitForSingleObject(&event, Executive, KernelMode, TRUE, 0); code = iosb.Status; } // // 設置最終的輸出緩沖區大小. // OutputBufferSize = iosb.Information; // // 完成 // return(code); } // // // RecognizerReadDiskSector // // 從一個特定的卷讀取一個扇區 // // 輸入參數: // pDeviceObject –磁盤設備對象的指針. // DiskSector – 需要讀取的扇區數目. // // Outputs: // Buffer – 用於接收讀取內容的緩沖區. // // Returns: // Returns TRUE if the disk sector was read. // // Notes: // None. // static BOOLEAN RecognizerReadDiskSector( IN PDEVICE_OBJECT pDeviceObject, IN ULONG DiskSector, IN UCHAR* Buffer // must be DEVICE_LOGICAL_BLOCKSIZE bytes long.) { LARGE_INTEGER sectorNumber; PIRP irp; IO_STATUS_BLOCK ioStatus; KEVENT event; NTSTATUS status; ULONG sectorSize; PULONG mbr; PAGED_CODE(); sectorNumber.QuadPart = (LONGLONG) DiskSector *DEVICE_LOGICAL_BLOCKSIZE; //創建一個用於檢查是否完成通知事件對象 KeInitializeEvent(&event, NotificationEvent, FALSE); // 獲得扇區大小 sectorSize = DEVICE_LOGICAL_BLOCKSIZE; // 分配內存. mbr = ExAllocatePool(NonPagedPoolCacheAligned, sectorSize); if (!mbr) { return FALSE; } // 創建讀MBR的IRP irp = IoBuildSynchronousFsdRequest(IRP_MJ_READ, pDeviceObject, mbr, sectorSize, §orNumber, &event, &ioStatus ); if (!irp) { ExFreePool(mbr); return FALSE; } // 把IRP傳送個端口驅動程序 status = IoCallDriver(pDeviceObject, irp); if (status == STATUS_PENDING) { KeWaitForSingleObject(&event, Suspended, KernelMode, FALSE, NULL); status = ioStatus.Status; } if (!NT_SUCCESS(status)) { ExFreePool(mbr); return FALSE; } // // 返回讀取的扇區信息 // RtlCopyMemory(Buffer,mbr,sectorSize); ExFreePool(mbr); return TRUE; }
上面的代碼包含了RecognizerIoControl()和RecognizerReadDiskSector(…)的實現。和所有 的代碼一樣,他們是自解釋的。因為該函數運行在PASSIVE_LEVEL 級別的IRQL上,所以可以 調用KeWaitForSingleObject()函數去等待相關操作完成。
運行這個驅動程序
現在我們已經文件系統識別的全部講述了一遍。現在是討論如何注冊文件系統識別器個文件 系統驅動的時候了。你需要像這樣配置識別器和驅動程序。
SYSTEM\CurrentControlSet\Services\MyFsdRecognizer Class Name: <NO CLASS> Last Write Time: 10/10/96 - 4:09 AM Value 0 Name: ErrorControl Type: REG_DWORD Data: 0x1 Value 1 Name: Group Type: REG_SZ Data: File system Value 2 Name: Start Type: REG_DWORD Data: 0x1 Value 3 Name: Type Type: REG_DWORD Data: 0x8 Key Name: SYSTEM\CurrentControlSet\Services\MyFsd Class Name: <NO CLASS> Value 0 Name: ErrorControl Type: REG_DWORD Data: 0x1 Value 1 Name: Group Type: REG_SZ Data: File system Value 2 Name: Start Type: REG_DWORD Data: 0x3 Value 3 Name: Type Type: REG_DWORD Data: 0x2
從上面的的配置信息你可以知道,識別器在系統啟動時啟動,它是“File System”組的一個 成員。文件系統驅動程序是手動啟動的,這一點是另人疑惑的。這樣做可以讓驅動程序被識 別器用ZwLoadDriver()加載,而不是被NT內核加載。
結論
一個文件系統識別器是一個簡單的驅動程序,但它卻是必須的。它可以使你的驅動“按需啟 動“,而且最小化內存需求。