《Windows内核安全与驱动编程》-第九章-磁盘的虚拟-day-3.md

磁盘的虚拟

9.5 FAT12/16 磁盘卷初始化

9.5.2 Ramdisk 对磁盘的初始化

​ 在上一节中说到,在 Ramdisk 驱动中的 EvtDriverDeviceAdd 类函数里会调用 RamDiskFormatDisk 函数对所分配的用于做磁盘镜像的内存空间进行初始化。上一小节介绍了磁盘卷结构,接下来学习如何初始化这个空间。

​ 首先看一下这个函数的本地变量声名。

//一个指向磁盘启动扇区的指针
PBOOT_SECTOR bootSector = (PBOOT_SECTOR) devExt->DiskImage;
//指向FAT第一个FAT表的指针
PUCHAR		firstFatSector;
//用于记录有多少个根目录日寇点
ULONG rootDirEntries;
//用于记录每个簇由多少扇区组成
ULONG sectorsPerCluster;
//用于记录FAT文件系统的类型,是FAT12 还是16
USHORT fatType;
//用于记录在FAT表里一共有多少个表项
USHORT fatEntries;
//用于记录一个FAT表需要占用多少个扇区来存储
USHORT fatSectorCnt;
//用于指向第一个根目录入口点
PDIR_ENTRY rootDir;
//用于确定这个函数是可以存取分页内存的
PAGED_CODE();
//用于确定这个盘的引导扇区的大小确实是一个扇区大小
ASSERT(sizeof(BOOT_SECTOR)) == 512);
//用于确定我们操作的磁盘不是一个不可用的指针
ASSERT(devExt->DiskImage != NULL);
//清空磁盘镜像
RtlZeroMemory(devExt->DiskImage,devExt->DiskRegInfo.DiskSize);

​ 接下来格式化磁盘设备的设备扩展里的数据结构 DiskGeometry 。它保存着与磁盘物理结构相关的信息。这个数据结构是 WinDDK 所定义好的,几乎适用于所有磁盘。

typedef struce _DISK_GEOMETRY{
    //有多少个柱面
    LARGE_INTEGER Cylinders;
    //磁盘介质的类型
    MEDIA_TYPE MediaType;
    //每个柱面有多少磁道,也就是有多少个盘片
    ULONG TracksPerCylinder;
    //每个磁道有多少扇区
    ULONG SectorsPerTrack;
    //每个扇区有多少字节
    ULONG BytePerSector;
}DISK_GEOMETRY,*PDISK_GEOMETRY;

​ 该结构在今后的许多场合都会被作为磁盘的参数而访问。下面来给这些数据结构初始化

//每个扇区有512个字节
devExt->DiskGeometry.BytePerSector = 512;
//每个磁道有21个扇区
devExt->DiskGeometry.SectorsPerTrack = 32;
//每个柱面有两个磁道
...
//柱面数据由磁盘的总容量决定
devExt->DiskGeometry.Cylinders.QuadPart = devExt->DiskRegInfo.DiskSize/512/32/2;
//我们自定义的 RAMDISK_MEDIA_TYPE 磁盘类型
devExt->DiskGeometry.MediaType = RAMDISK_MEDIA_TYPE;

​ 继续初始化一些与文件系统相关的参数。

// 根据用户指定的值对用户的根目录项的数据进行初始化
rootDirEntries = devExt->DiskRegInfo.RootDirEntries;
// 每个簇有多少扇区的初始化
sectorsPecCluster = devExt->DiskRegInfo.SectorsPerCluster;

​ 在一开始,我们让 bootSector 指针指向了磁盘镜像的首地址,联系之前的磁盘结构。首地址应该为该分区的 DBR。所以接下来应该了解具体的 DBR 结构。

// 跳转指令,跳转到 DBR 中的引导程序
UCHAR bsJump[3];
// 该卷的 OEM 名称
CCHAR bsOemName[8];
//每个扇区有多少字节
USHORT bsBytesPerSec;
//每个簇有多少扇区
UCHAR bsSecPerClus;
//保留扇区数据,指的是第一个 FAT 表开始之前的扇区数,也包括 DBR 本身
USHORT bsResSector;
//这个卷有多少个 FAT 表
USHORT bsFATs;
//这个卷的根目录入口点有几个
USHORT bsRootEntrents;
//这个卷一共有多少个扇区,如果大于65535个扇区,则该字段为0
USHORT bsSectors;
//这个卷的介质类型
UCHAR bsMedia;
//每个 FAT 表占用多少个扇区
USHORT bsFATsecs;
//每个磁道有多少个扇区
USHORT bsSecPerTrack;
//有多少个磁头
USHORT bsHeads;
//有多少个隐藏扇区
ULONG bsHiddenSecs;
//一个卷超过65535个扇区时,会使用这个字段来说明总扇区数
ULONG bsHugeSectors;
//驱动器编号
UCHAR bsDriverNumber;
//保留字段
UCHAR bsReserved1;
//磁盘扩展引导区标签,Windows 要求这个标签为 0x28或者0x29
UCHAR bsBootSignature;
//磁盘卷ID
ULONG bsVolumeID;
//磁盘卷标
CCHAR bsLabel[11];
//磁盘上的文件系统类型
CCHAR bsFileSystemType[8];
//保留字段
CCHAR bsReserved2[448];
//DBR 结束签名
UCHAR bsSig2[2];

​ 不得不说,DBR 的结构比之前接触到的结构都要复杂很多。所以接下来,又要以差不多的形式初始化 DBR

….初始化赋值

​ 值得一提的是,计算这个磁盘 FAT 表所占用的空间。FAT 表里存储的是一个将很多簇串联起来的链表,所以 FAT 表的表项的数量就是磁盘上实际用来存储的簇的数量。而这个簇的数量又是由磁盘总扇区数减去用来存储其他数据的扇区数之后除以每个簇的扇区数得到的。下面看一下如何实际计算得到:

//总扇区数减去保留的扇区数,再减去根目录入口点占据的扇区数,再除以每簇占据的扇区数得到簇的数量
//最后+2 因为FAT表的前两项是保留的

fatEntries = (bootSector->bsSector - bootSector->bsResSectors-boosSector->bsRootDirents/DIR_ENTRIES_PER_SECTOR)/boosSector->bsSecPerClus+2

….后续也是类似的对 FAT 结构进行初始化。相应的机械性操作我选择了略读。也是因为 Win10已经使用了NTFS结构,不再使用FAT12/16 文件结构。大致了解一下框架即可。

9.6 驱动中的请求处理

9.6.1 驱动中的请求处理

​ 在前面的介绍中知道,WDF 驱动框架会将所有发往之前建立的磁盘设备的请求都排队放入已经建立的队列中,而在放入队列后绝大多数都得到了合适的处理。但是我们这里需要自己注册读/写和 DeviceControl 请求的回调函数来处理它们。

​ 回调函数在收到请求之后,只能执行下面列举的4种操作的一种,但是不能忽略这个请求。这四种操作分别为:

  1. 重新排队。 回调函数可以把这个请求放到另一个队列里去等待其他的处理函数去处理。
  2. 完成请求。回调函数可以对这个请求做自己的一些处理,并且在处理完毕之后完成它。
  3. 撤销请求,回调函数可以要求撤销这个请求。
  4. 转发请求。回调函数可以把请求转发给其他的设备。

​ 在Windows系统当中,设备之间是一种层叠的关系,在这个磁盘设备之上还会有文件系统设备,一般应用程序的访问都应该是访问文件系统设备,而文件系统设备会负责做文件系统方面的维护。比如对 FAT 表的维护、对文件的读写等,而这些操作最终都会转换成对磁盘的读写发往磁盘设备。

9.6.2 读/写请求

​ 读与写,是差不多的一类请求。参数类型一样。

VOID RamDiskEvtIoRead(
	IN WDFQUEUE Queue,
	IN WDFREQUEST Request,
	IN size_t Length
);
VOID RamDiskEvtIoWrite(
	IN WDFQUEUE Queue,
	IN WDFREQUEST Request,
	IN size_t Length
);

​ 对于一个磁盘设备来说,读写请求就是要读写磁盘上的某一段区域的内容,这个区域由起点和长度来决定。长度已经由回调函数的参数提供,而起点就要通过 WDF 驱动框架提供的各种函数在第二个参数——请求参数中获取了。读写请求还有一个重要的参数就是缓冲区,它由系统提供,用来存放读出来或者写入的数据,这个参数也需要从请求参数中获取。

​ 在真实的应用中,在磁盘之上的文件系统设备会根据FAT表等数据结构,将对文件的访问转换成对磁盘设备的访问,而磁盘对于上层来说,就是一个起始位置为0,总长度为磁盘卷总大小的扁平寻址空间。任何文件系统转化过来的访问都应该在这个空间之内。

​ 下面看一下读请求的具体处理过程。写请求与读请求类似,不再重复。

//从队列的扩展中取到对应的磁盘设备的扩展
PDEVICE_EXTENSION devExt = QueueGetExtension(Queue)->DeviceExtension;
//用于保存各种函数返回值的状态变量
NTSTATUS Status = STATUS_INVALID_PARAMETER;
//用于获取请求参数的变量
WDF_REQUEST_PARAMETERS Parameters;
//用户获取读请求起始地址的变量,这里要注意的是,这是一个64位的数据
LARGE_INTEGER ByteOffset;
//这是一个用于获取读缓冲区的内存句柄
WDFMEMORY hMemory;
//初始化参数变量,为之后从请求参数中获取各种信息做准备
WDF_REQUEST_PARAMETERS_INIT(&Parameters);
//从请求参数中获取信息
WdfRequestGetParameters(Request,&Parameters);
//将请求参数中读的起始位置取出来
ByteOffset.QuadPart = Parameters.Parameters.Read.DeviceOffset;
//这里是自己实现的一个参数检查函数。由于读取的范围不能超过磁盘镜像的大小
//并且必须是扇区对齐,所以这里需要有一个参数检查。
if(RamDiskChrckParameters(devExt,ByteOffset,Length))
{
    //从请求参数中获取读缓冲区的内存句柄
    Status = WdfRequestRetrieveOutputMemory(Request,&hMemory);
    if(NT_SUCCESS(Status)){
        //根据之前获取的读参数进行内存拷贝,填写这个读请求的缓冲区
        Status - WdfMemoryCopyFromBuffer(
        	hMemory,
            0,//缓冲区的内存偏移量
            devExt->DiskImage+ByteOffset.LowPart,//source
            Length
        );
    }
}
//结束这个读请求,这里要注意的是,需要将读取的长度作为返回的信息一并返回
WdfRequestCompleteWithInformation(Request,Status,(ULONG_PTR)Length);

9.6.3 DeviceControl 请求

​ 一个标准的磁盘卷设备,需要支持数量庞大的 DeviceControl 请求,但是对于本例来说支持一个简单的请求就足以示范了。DeviceControl 请求是系统发过来的一堆问题,例如这个磁盘有多大,它能写什么数据之类的问题。处理函数只需要按需回答即可。函数原型为

VOID RamDiskEvtIoDeviceControl(
	IN WDFQUEUE Queue,
	IN WDFREQUEST Request,
	IN size_t OutputBufferLength,		//输出缓冲区长度
	IN size_t InputBufferLength,		//输入缓冲区长度
	IN ULONG IoControlCode				//是什么样的请求
)

​ 下面只处理一些,不处理这个设备就无法启动或者不能正常工作的请求。具体为什么是这几个请求,是开发人员长期的经验和相关文档来的….

//初始化返回状态为非法的设备请求,这样在其他无关紧要的或者不处理的
//请求到来时候,可以直接返回该状态
NTSTATUS Status = STATUS_INVALID_DEVICE_REQUEST;
//用来存放返回的 DeviceControl 所要求的数据长度
ULONG_PTR  information = 0 ;
//中间变量
size_z buffersize;
//与读写请求相同,通过队列的扩展来获取设备的拓展
PDEVICE_EXTENSION devExt  = QueueGetExtension(Queue)->DeviceExtension;
//由于是Windows发过来的标准请求,所以我们对请求的长度很信任。
//这里不需要处输入缓冲区和输出缓冲区的长度
UNREFERENCED_PARAMETER(OutputBufferLength);
UNREFERENCED_PARAMETER(InputBufferLength);

switch(IoControlCode){
//这里是一个获取当前分区信息的 DeviceIoControl请求,需要处理
case IOCTL_DISK_GET_PARTITION_INFO:{}
	//首先声名一个缓冲区指针
	PPARTITION_INFORMATION outputBuffer;
	//由于这个请求所需要的信息大部分是从 DBR 中获取的,所以需要一个
	//指向DBR的指针
	PBOOT_SECTOR bootSector = (PBOOT_SECTOR) devExt->DiskImage;
	//告诉上层这里返回信息的长度
	information = sizeof(PARTITION_INFORMATION);
	通过框架函数来获取这个请求所携带的输出缓冲区
	Status = WdfRequestRetrieveOutputBuffer(
		Request,
		sizeof(PARTITION_INFORMATION),
		&outputBuffer,		//缓冲区
		&bufferSize			//缓冲区大小
	);
	//在获取缓冲区成功的情况下,将DNR中的相关信息填入缓冲区
	if(NT_SUCCESS(Status)){
        ...
        
	}
}
break;
//获取当前磁盘硬件信息
case IOCTL_DISK_GER_DRIVER_GEOMETRY:{
    ...
}
   
}

​ 这里大概就是底层磁盘的处理结构了。通过丰富这些请求,可以返回到上层许多的信息。后面还有两个小章节讲述该 虚拟磁盘的编译和安装。但是目前感觉实用性不强,毕竟现在 XP还是很少了。当下目的是学到底层的一些处理流程,了解大致的处理框架即可。细节不再追究。

明日计划

继续学习驱动编程-磁盘过滤

搞毕业论文

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