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

磁盘的虚拟

9.1 虚拟的磁盘

​ 虚拟磁盘,是我们可以创造的一个行为完全可控的磁盘驱动器。举一个例子: 制作一个虚拟的磁盘,对这个磁盘的所有写入操作都通过某种算法以口令加密,同时对这个磁盘的读取操作使用同样的算法和口令解密,那么在该程序运行时,操作系统和用户自己都可以看到正确的数据,而在这个程序不工作时,无论谁企图读这个磁盘都只能看到被加密过的数据。

9.2 一个具体的例子

WDK6001.18000 里自带的一个例子——Ramdisk,具体的路径为 $(WDK 安装目录) \src\kmdf\ramdisk 。这个例子实现了一个使用非分页内存 (nonpaged memory) 做的磁盘存储空间,并将其以一个独立的磁盘形式暴露给用户,用户可以将其格式化成一个 Windows 能够使用的卷,并且像操作一般的磁盘卷一样对它进行操作。由于使用了内存作为虚拟的存储介质,使这个磁盘具有的一个显著特点是性能的提高。由于这个优势,使它非常适合作为各种软件的缓冲盘。

​ 这个例子使用了微软的 WDF 驱动开发框架,所以这个驱动和普通的 WDM 驱动是不太一样的。下面讨论中所有的文件行号和文件内容均来自微软提供的 WDK 6001.18000 中安装目录下的 src\kmdf\ramdisk 目录。

入口函数

9.3.1 入口函数的定义

​ 任何一个驱动程序,都会有一个 DriverEntry 入口函数,就像应用程序里的main一样。这个函数的声音是这样的:

NTSTATUS DriverEntry(
	IN PDRIVER_OBJECT DrvierObject;
    IN PUNICODE_STRING RegisitryPath;
);

​ 这个函数的返回值是 NTSTATUS ,一般是用来确定程序错误时的原因。这些返回值的定义位于 WinDDK 安装目录下的 \inc\api\instatus.h 中。一般从英文意思就可以判断返回值含义。

​ 这个函数具有两个参数。其中第一个参数是一个 PDRIVER_OBJECT 类型的指针。它代表了 Windows 系统为这个驱动程序所分配的一个驱动对象。这个驱动对象是系统中对该驱动的唯一标识,里面包含了该驱动的各种信息、各个功能函数的入口地址等重要信息。

​ 第二个参数是一个 Unicode 字符串,它代表了驱动在注册表中的参数所存放的位置。它代表了驱动在注册表中的参数所存放的位置。由于每一个驱动都是一个类似服务的形式存在,熬字系统注册表的 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services 树下总有一个和驱动名字相同的子树用来描述这个驱动的一些基本信息,并体哦那个一个可使用的存储空间供驱动存放自己的特有信息。之所以会有这样的一个空间,是因为许多驱动加载的非常早,甚至仅仅晚于内核,这时候文件系统还没有建立,驱动程序如果需要在此时享有自己可操作的存储空间(例如用来记录一些参数),除了注册表外没有其他任何地方可以使用。Windows 内核在启动时加载了一个最小的文件系统,分析磁盘并将注册表的 HKEY_LOCAL_MACHINE\SYSTEM 树下的所有内容读入到内存里,这样就保证了这一部分的注册表内容在 Windows 内核刚加载之后就可以使用。而后 Windows 在启动的过程中将这棵子树的内容和磁盘上的注册表进行同步,之后的注册表操作就和一般的注册表操作一致了。在 Windows 内核加载之后,这个同步发生之前,任何加载的驱动程序都可以操作注册表并确保操作结果被最终留在了磁盘上,这就是 Windows 系统确保任何驱动都可以使用它提供的注册表路径来存储信息的技术所在,也是这个参数的意义。

9.3.2 Ramdisk 驱动入口函数

​ 在 Ramdisk 驱动代码的 DriverEntry 函数中只做了几件简单的事情,下面将这个函数的所有代码列出并加以说明。

WDF_DRIVER_CONFIG config;

WDF_DRIVER_CONFIG_INIT (&config,RamDiskEvtDeviceAdd);
return WdfDriverCreate(
	DriverObject,
	RegistryPath,
	WDF_NO_OBJECT_ATTRIBUTES,
	&config,
	WDF_NO_HANDLE
);

​ 首先声明变量 config ,然后进行初始化。WDF_DRIVER_CONFIG 结构通常用来说明这个驱动程序的一些可配置项,其中包括了这个驱动程序的 EvtDriverDeviceAddEvtDriverUnload 回调函数的入口地址和这个驱动在初始化时的一些标志和分配内存时所使用的 tag 值。 WDF_DRIVER_CONFIG_INIT 函数将用户提供的 EvtDriverDeviceAdd 函数填入其中,并初始化这个变量的其他部分。 EvtDriverDeviceAdd 回调函数是 WDF 驱动框架中的一个重要的回调函数,它可以用来在即插即拔管理器发现一个新设备的时候对这个新设备进行初始化操作。

EvtDriverDeviceAdd 回调函数,类似于 WDM 驱动程序中的 AddDevice 回调函数的翻版。这个回调函数在 PnP 类型的驱动中起着很重要的最用,任何一个支持 PnP 操作的驱动都应该有这么一个 EvtDriverDeviceAdd 函数。这里 PnP 是即插即拔的意思,在前面有学过。但是俺没有过开发经验,暂时还不太懂这个回调函数具体的意思。只理解为,刚刚发现新设备时的初始化函数。

​ 在初始化好 config 变量后,后续直接调用了 WdfDriverCreate 函数并返回。 WdfDriverCreate 函数是使用任何 WDF 框架提供的函数之前必须调用的一个函数,在原作者看来这仅仅是一次对原本驱动程序开发方式的一次包装。作用就是一些初始化工作。前两个参数我们都知道,第三个参数是用来说明这个 WDF 驱动的驱动对象不需要一些特殊属性。第四个参数是初始化后的 config 变量。最后一个参数作为该函数的输出结果——WDF 驱动的驱动对象。调用了这个函数之后,前面初始化过的 conigf 和 EvtDriverDeviceAdd 函数就和这个驱动挂钩起来。在今后的系统运行过程中,一旦发现了此类设备,RamDiskEvtDeviceAdd 就会被 Windows 的 PnP 管理器调用,这个驱动自己的处理流程也就要上演了。

​ WDM驱动模型和WDF驱动模型的最大的区别是:

  • wdf驱动框架对WDM进行了一次封装,WDF框架就好像C++中的基类一样,且这个基类中的model,IO model ,pnp和电源管理模型;且提供了一些与操作系统相关的处理函数,这些函数好像C++中的虚函数一样,WDF驱动中能够对这些函数进行override;特别是Pnp管理和电源管理!基本上都由WDF框架做了,而WDF的功能驱动几乎不要对它进行特殊的处理;

  • WDF驱动采用队列进行IO处理,而WDM中将所有的IO操作都用默认的队列进行处理,如果要进行IRp同步,必须使用StartIO;

  • WDF是面向对象的,而WDM是面向过程的,WDF提供对象的封装,如将IRP封装成WDFREQUEST,对象提供方法和Event。

9.4 EvtDriverDeviceAdd 函数

9.4.1 EvtDriverDetivceAdd 的定义

​ 在本驱动中, RamDiskEvtDeviceAdd 作为一个 EvtDriverDeviceAdd 函数在 DriverEntry 中被注册,在 DriverEntry 函数执行完毕之后,这个驱动就只依靠 RamDiskEvtDriverDevicAdd 函数和系统保持联系了。正如上一节所说的,系统在运行过程中一旦发现了这种类型的设备,就会调用RamDiskEvtDriverDevicAdd 函数。下面进行更仔细的分析:

NTSTATUS RamDiskEvtDeviceAdd(
	IN WDFDRIVER Driver,
	IN PWDFDEVICT_INIT DeviceInit
);

​ 这个函数的第一个参数在这个例子中并不会使用到;第二个参数是一个 WDFDEVICE_INIT 类型的指针,这个参数是 WDF 驱动模型中自动分配出来的一个数据结构,专门传递给 EvtDriverrDeviceAdd 类函数用来建立一个新设备。下面分段具体看这个驱动的 EvtDriverDeviceAdd 类函数是如何工作的。

9.4.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;
//声名一个 UNICODE_STRING 类型的变量,并将它初始化为 NT_DEVICE_NAME 宏所声名的字符串,这里实际上是 \\Device\\Ramdisk
DECLARE_CONST_UNICODE_STRING(ntDevictName,NT_DEVICE_NAME);
//保证这个函数可以操作paged内存
PAGRD_CODE();
//由于我们不适用 Driver 这个参数,为了避免警告,加入这句
UNREFFERENCED_PARAMETER(Driver);

9.4.3 磁盘设备的创建

EvtDriverDeviceAdd 类函数的一个重要任务是创建设备,而它的 WDFDEVICE_INIT 类型的参数就是用来做这件事的。首先,设备需要一个名字,这是因为这个设备将会通过这个名字暴露给应用层并且被应用层所使用,一个没有名字的设备是不可以用的。

​ 另外,需要将这个设备的类型设置为 FILE_DEVICE_DISK ,这是因为所有的磁盘设备都需要使用这个设备类型。将这个设备的 IO 类型设置为 Direct 方式,这样将在读写和 DeviceIoControlIRP 发送到这个设备时, IRP 所携带的缓冲区将可以直接被使用。将 Exclusive 设置为 FALSE ,这说明这个设备可以被多次打开。这里还需要将这个设备对象关联一个设备扩展的上下文,这是开发人员指定类型的一块内存区域,这块内存区域被这个设备的设备扩展指针所指,开发人员可以在这块内存区域里存储一些自定义的信息。

​ 针对本驱动建立出的任何一个设备,这块内存区域的结构都是相同的。但是随设备对象的不同,可能具有的内容不同。这些内容将会在接下来的编程中作为针对设备对象的处理函数的参数。除此之外,还需要为设备的一些功能指定相应的处理函数,例如设备在销毁时调用哪个函数。WDF 驱动模型框架已经为开发人员做了许多事情,基本上实现了所有功能的标准处理过程,在大部分情况下这些标准处理过程就足够了,只需要我们根据自己的要求进行很少的功能处理即可。

// 首先需要为这个设备指定一个名词
// "\\Device\\Ramdisk"
status = WdfDeviceInitAssignName(DeviceInit,&neDeviceName);
if(!NT_SUCCESS(status))
{
    return status;
}

//接下来需要对这个设备进行一些属性的设置,包括设备类型和IO操作类型和设备的排他方式
WdfDeviceInitSetDeviceType(DeviceInit,FILE_DEVICE_DISK);
WdfDeviceInitSetIoType(DeviceInit,WdfDeviceIoDirect);
WdfDeviceInitSetExclusive(DeviceInit,FALSE);

//下面来指定这个设备的设备对象扩展
//DEVICE_EXTENSION 是一个在头文件中声名的结构体数据类型
//我们使用一个 WDF_OBJECT_ATTRIBUTES 类型的变量并用其设置好 DEVICE_EXTENSION
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(
	&deviceAttributes;
    DEVICE_EXTENSION
);
//下面用这个这个 WDF_OBJECT_ATTRIBUTES 类型的变量来指定这个设备的清除回调函数
//这个个 WDF_OBJECT_ATTRIBUTES 类型的变量将会在下面建立设备时作为一个参数传进去
deviceAttributes.EvtCleanupCallBack = RamDiskEvtDeviceContextCleanup;

//到这里,所有的准备工作做好。我们开始建立设备
//device 保存我们建立的设备
status = WdfDeviceCreate(&DeviceInit,&deviceAttributes,&device);
//这个 pDeviceExtension 是我们声名的一个局部指针变量,将其指向新建立的设备拓展区域
pDeviceExtension = DeviceGetExtension(device);

9.4.4 如何处理发往设备的请求

​ 在 WDM 开发中,常用的方式是设置这个设备哥哥请求的分发函数为自己实现的回调函数。比如,在这个例子里,可以将所有的读写请求都实现为去读写内存,这就是最简单的内存盘。处理方式实现起来需要一些技巧,一种常用的方式是建立一个或多个队列,将所有发送到这个设备的请求都插入到队列中,由另一个线程去处理队列。这就是一个典型的生产者——消费者模型。这样做的好处是有一个小小的缓冲,同时还不用担心由于缓冲带来的同步问题。因为所有的请求都被排成了队列。这里, WDF 驱动框架中,微软直接提供了这种处理队列,这样我们就不用自己再去实现了。

​ 为了实现为驱动制作一个处理队列这一目标,我们需要初始化一个队列配置变量 ioQueueConfig ,这个变量会说明队列的各种属性,一种简单的方式是将它配置为初始化默认状态,之后再对一些具有特殊属性的请求注册回调函数,例如为读请求注册回调函数。在初始化完成之后,再为指定的设备建立这个队列。 WDF 驱动框架会自动将所有发往这个设备的请求都放入队列中等待处理,同时发现符合我们感兴趣的属性(读写操作)时,调用之前注册过的处理函数去处理。对每个设备可以建立多个队列,在队列中也有类似设备的扩展。

WDF_IO_QUEUE_CONFIG_INIT_QUEUE(
	&ioQueueConfig,
	WdfIoQueueDispatchSequential
);
//	我们对读写和 DeviceControl 请求的处理设置为自己的函数,其余的使用默认值
ioQueueConfig.EvtIoDeviceControl = RamDiskEvtIoDeviceControl
ioQueueConfig.EvtIoRead = RamDiskEvtRead;
ioQueueConfig.EvtIoWrite = RamDiskEvtWrite;

//指定这个队列的队列对象扩展,这里的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);

//这里初始化队列扩展里的 DeviceExtension 项,将其
//设置为刚建立的设备拓展指针,这样以后在有队列的地方都
//可以轻松的获得这个队列对应的设备的设备扩展了
pQueueContext->DeviceExtension = pDeviceExtension;

​ 这里今天就先结束学习吧,体会到 WDF 就是一层对 WDM的封装。类似于现在的很多框架,为编程节省了很多精力。

明日计划:

翻译论文

继续学习驱动编程-磁盘的虚拟

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