这篇文章来介绍一下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);