翻译:TimiXu
本文转载自TimiXu的Blog
文件系统识别器是一个标准的NT内核模式驱动程序。它只实现一项功能:检查物理介质设备,如果它能够识别存储介质的格式便加载相应的文件系统驱动程序。你可能要问:为什么不把所有的文件系统一起加载呢?因为系统几乎从不需要加载所有文件系统驱动程序,用一个小驱动可以节约数百K系统内存。实际上,所有标准的NT物理介质文件系统都利用文件系统识别器。举个例子来说,如果CD-ROM没有被访问,那么CDFS文件系统驱动程序将不会被加载。
文件系统识别器是怎么样知道磁盘上存在什么类型的文件系统呢?一般说来,检查磁盘上的标识符就可以了。标识符可能存储在于分区表里,从分区起始处加上一段偏移量就能定位这个唯一值,这个值可以是序列号或者其他某种标识符。这些标识符必须完全不同,以免加载了不正确的文件系统驱动程序。
以下是一些常用的文件系统标识:
文件系统名 | 文件系统标识 |
HFS | 0x4244 |
NTFS | ''NTFS'' |
FAT | 0xe9或0xeb或0x49 |
当一个文件系统程序被加载后,它必须分析磁盘以便确定介质上是否包含了它可以识别的文件系统。如果介质上是可以识别的文件系统,该文件系统驱动程序将“装配”这个文件系统。文件系统识别器也分析介质来确认是否有可识别的文件系统。但是文件系统识别器不是“装配”到卷上,而是加载文件系统驱动程序。文件系统识别器完成任务便可以卸载了。
装配过程
在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内核加载。
结论
一个文件系统识别器是一个简单的驱动程序,但它却是必须的。它可以使你的驱动“按需启动“,而且最小化内存需求。