1. 改版序
大约两年以前我在驱动开发网上发表了一组描述如何开发Windows文件系统过滤驱动的文章。非常庆幸这些文章能给大家带来帮助。
原本的文章中我使用了自己编写的代码。我不打算在这里论述代码风格的优劣并发起一场辩论,无可怀疑的是,读者们大多喜欢看到类似微软范例的代码。为此我把文章中的代码换成微软标准的文件过滤驱动范例sfilter的代码。赠于喜欢此书的读者和驱动开发的后来者们。
网友们帮我整理的原版已经非常流行。为了区别起见,称为第二版。
0. 作者,楚狂人自述
我感觉Windows文件系统驱动的开发能找到的资料比较少。为了让技术经验不至于遗忘和引起大家交流的兴趣我以我的工作经验撰写本教程。
我的理解未必正确,有错误的地方望多多指教。我在一家软件公司从事安全软件相关的开发工作。我非常庆幸和许多优秀的同事一起工作,特别要提到wowocock,陆麟,jiurl和曾经的同事Cardmagic.非常乐意与您交流。有问题欢迎与我联系。邮箱为MFC_Tan_Wen@163.com。
对于这本教程,您可以免费获得并随意修改,向任何网站转贴。但是不得使用任何内容作为任何赢利出版物的全部或者部分。
1. 概述,钻研目的和准备
我经常在碰到同行需要开发文件系统驱动。windows的pc机上以过滤驱动居多。其目的不外乎有以下几种:
一是用于防病毒引擎。希望在系统读写文件的时候,捕获读写的数据内容,然后检测其中是否含有病毒代码。
二是用于文件系统的透明附加功能。比如希望在文件写过程中对数据进行加密,数据个性化等等过程,针对特殊的过程进行特殊处理,增加文件系统效率等。
三一些安全软件使用文件过滤进行数据读写的控制,作为防信息泄漏软件的基础。
四也有一些数据安全厂家用来进行数据备份与灾难恢复。
如果你刚好有以上此类的要求,你可以阅读本教程。
文件系统驱动是windows系统中最复杂的驱动种类之一。不能对ifsddk中的帮助抱太多希望,以我的学习经验看来,文件系统相关的ddk帮助极其简略,很多重要的部分仅仅轻描淡写的带过。如果安装了ifsddk,应该阅读src/filesys/OSR_docs下的文档。而不仅仅是ddk帮助。
文件系统驱动开发方面的书籍很少。中文资料我仅仅见过侯捷翻译过的一本驱动开发的书上有两三章涉及,也仅仅是只能用于9x的vxd驱动。但我们付出巨大努力所理解的vxd架构如今已经彻底作古。NT文件系统我见过一本英文书。我都不记得这两本书的书名了。
如果您打算开发9x或者nt文件系统驱动,建议你去网上下载上文提及的书。那两本书都有免费的电子版本下载。如果你打算开发Windows2000/WindowsXP/Window2003的文件系统驱动,你可以阅读本教程。虽然本教程仅仅讲述文件系统过滤驱动。但是如果您要开发一个全新的文件系统驱动的话,本教程依然对你有很大的帮助。至少便于你理解文件系统驱动的一些概念。
学习文件系统驱动开发之前,应该在机器上安装ifsddk。ddk版本越高级,其中头文件中提供的系统调用也越多。一般的说用高版本的ifsddk都可以编译在低版本操作系统上运行的驱动(使用对应的编译环境即可)。ifsddk可以在某些ftp上免费下载。请不要发邮件向我索取。
我的使用的是ifs ddk for 2003,具体版本号为3790,但是我实际用来开发的两台机器有一台是windows 2000,另一台是windows 2003.我尽量使我编译出来的驱动,可以在2000/xp/2003三种系统上都通过测试。
同时最新的测试表明,在Vista系统上,sfilter也可以正常的运行。
安装配置ddk和在vc中开发驱动的方法网上有很多的介绍。ifsddk安装之后,src目录下的filesys目录下有文件系统驱动的示例。阅读这些代码你就可以快速的学会文件系统驱动开发。 filter目录下的sfilter是一个文件系统过滤驱动的例子。另一个filespy完全是用这个例子的代码加工得更复杂而已。 本文为觉得代码难懂的读者提供一个可能的捷径。
如何用ddk编译这个例子请自己查看相关的资料。
文件系统过滤驱动编译出来后你得到的是一个扩展名为sys的文件。同时你需要写一个.inf文件来实现这个驱动的安装。我这里不讨论.inf文件的细节,你可以直接用sfilter目录下的inf文件修改。以后我们将提供一个通用的inf文件.
对inf文件点鼠标右键弹出菜单选择“安装”,即可安装这个过滤驱动。但是必须重新启动系统才生效。这是静态加载的情况。静态加载后如果重启后蓝屏无法启动,可以用其他方式引导系统后到system32/drivers目录下删除你的.sys文件再重启即可。安全模式无法使你避免蓝屏。所以我后来不得不在机器上装了两个系统。双系统情况下,一个系统崩溃了用另一个系统启动,删除原来的驱动即可。
同时xp和2003版本的驱动大多可以动态加载。调试更加快捷,也请阅读相关的资料。
如果要调试代码,请安装softice或者windbg,并阅读windows内核调试的相关文档。
2 . hello world, 驱动对象与设备对象
这里所说的驱动对象是一种数据结构,在DDK中名为DRIVER_OBJECT。任何驱动程序都对应一个DRIVER_OBJECT.如何获得本人所写的驱动对应的DRIVER_OBJECT呢?驱动程序的入口函数为DriverEntry,因此,当你写一个驱动的开始,你会写下如下的代码:
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath )
{
}
这个函数就相当与喜欢c语言的你所常用的main().IN是无意义的宏,仅仅表明后边的参数是一种输入,而对应的OUT则代表这个参数是一种返回。这里没有使用引用,因此如果想在参数中返回结果,一律传入指针。
DriverObject就是你所写的驱动对应的DRIVER_OBJECT,是系统在加载你的驱动时候所分配的。RegisteryPath是专用于你记录你的驱动相关参数的注册表路径。这两者都由系统分配并通过这两个参数传递给你。
DriverObject重要之处,在于它拥有一组函数指针,称为dispatch functions.
开发驱动的主要任务就是亲手撰写这些dispatch functions.当系统用到你的驱动,会向你的驱动发送IRP(这是windows所有驱动的共同工作方式)。你的任务是在dispatch function中处理这些请求。你可以让irp失败,也可以成功返回,也可以修改这些irp,甚至可以自己发出irp。
设备对象则是指DEVICE_OBJECT.下边简称DO.
但是实际上每个irp都是针对DO发出的。只有针对由该驱动所生成的DO的IRP, 才会发给该驱动来处理。具体的分发函数,决定于DO下的DriverObject域。
当一个应用程序打开文件并读写文件的时候,windows系统将这些请求变成irp发送给文件系统驱动。
文件系统过滤驱动将可以过滤这些irp.这样,你就拥有了捕获和改变文件系统操作的能力。
象Fat32,NTFS这样的文件系统(File System,简称FS),可能生成好几种设备。首先文件系统驱动本身往往生成一个控制设备(CDO).这个设备的主要任务是修改整个驱动的内部配置。因此一个Driver只对应一个CDO.
另一种设备是被这个文件系统Mount的Volume。一个FS可能有多个Volume,也可能一个都没有。解释一下,如果你有C:,D:,E:,F:四个分区。C:,D:为NTFS,E:,F:为Fat32.那么E:,F:则是Fat的两个Volume设备对象.
实际上"C:"是该设备的符号连接(Symbolic Link)名。而不是真正的设备名。可以打开Symbolic Links Viewer,能看到:
C: /Device/HarddiskVolume1
因此该设备的设备名为“/Device/HarddiskVolume1”.
这里也看出来,文件系统驱动是针对每个Volume来生成一个DeviceObject,而不是针对每个文件的。实际上对文件的读写的irp,都发到Volume设备对象上去了。并不会生成一个“文件设备对象”。
掌握了这些概念的话,我们现在用简单的代码来生成我们的CDO,作为我们开发文件系统驱动的第一步牛刀小试。
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
// 定义一个Unicode字符串。
UNICODE_STRING nameString;
RtlInitUnicodeString( &nameString, L"//FileSystem//Filters//SFilter" );
// 生成控制设备
status = IoCreateDevice( DriverObject,
0, //has no device extension
&nameString,
FILE_DEVICE_DISK_FILE_SYSTEM,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&gSFilterControlDeviceObject );
// 如果因为路径没找到而生成失败
if (status == STATUS_OBJECT_PATH_NOT_FOUND) {
// 这是因为一些低版本的操作系统没有/FileSystem/Filters/这个目录
// 如果没有,我们则改变位置,生成到/FileSystem/下.
RtlInitUnicodeString( &nameString, L"//FileSystem//SFilterCDO" );
status = IoCreateDevice( DriverObject,
0,&nameString,
FILE_DEVICE_DISK_FILE_SYSTEM,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&gSFilterControlDeviceObject );
// 成功后,用KdPrint打印一个log.
if (!NT_SUCCESS( status )) {
KdPrint(( "SFilter!DriverEntry: Error creating control device object /"%wZ/", status=%08x/n", &nameString, status ));
return status;
}
} else if (!NT_SUCCESS( status )) {
// 失败也打印一个。并直接返回错误
KdPrint(( "SFilter!DriverEntry: Error creating control device object /"%wZ/", status=%08x/n", &nameString, status ));
}
return status;
}
sfilter.sys.把这个文件与前所描述的inf文件同一目录,按上节所叙述方法安装。
这个驱动不起任何作用,但是你已经成功的完成了"hello world".
初次看这些代码可能有一些慌乱。但是只要注意了以下几点,你就会变得轻松了:
<!--[if !supportLists]-->1) <!--[endif]-->习惯使用UNICODE_STRING字符串。这些字符串用Rtl…系列的函数来操作。你应该阅读DDK帮助,然后熟悉这些字符串的用法。
<!--[if !supportLists]-->2) <!--[endif]-->用KdPrint(())来代替printf输出信息。这些信息可以在DbgView中看到。KdPrint(())自身是一个宏,为了完整传入参数所以使用了两重括弧。这个比DbgPrint调用要稍好。因为在free版不被编译。
<!--[if !supportLists]-->3) <!--[endif]-->查看DDK帮助了解生成设备对象IoCreateDevice的用法。
请注意CDO生成后,保存在gSFilterControlDeviceObject中。这样以后我们得到一个DEVICE_OBJECT时,就很容易判断是否是我们的控制设备。
3 .分发例程,fast io
上一节仅仅生成了控制设备对象。但是不要忘记,驱动开发的主要工作是撰写分发例程(dispatch functions.).接上一接,我们已经知道自己的DriverObject保存在上文代码的driver中。现在我来指定一个默认的dispatch function给它。
for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
{
DriverObject->MajorFunction[i] = SfPassThrough;
}
作为过滤,一些特殊的分发例程,我必须特殊的给予处理。为此,给它们单独的分发函数:
DriverObject->MajorFunction[IRP_MJ_CREATE] = SfCreate;
DriverObject->MajorFunction[IRP_MJ_CREATE_NAMED_PIPE] = SfCreate;
DriverObject->MajorFunction[IRP_MJ_CREATE_MAILSLOT] = SfCreate;
DriverObject->MajorFunction[IRP_MJ_FILE_SYSTEM_CONTROL] = SfFsControl;
DriverObject->MajorFunction[IRP_MJ_CLEANUP] = SfCleanupClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = SfCleanupClose;
至于过滤中最简单的处理,当然就是不做任何处理,直接下发了,这就是我们常说的passthru.这是一个常用的缩写词。
NTSTATUS
SfPassThrough (
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
// 对于我们认为“不能”的事情,我们采用ASSERT进行调试模式下的确认。
// 而不加多余的判断来消耗我们的效率。这些宏在调试模式下不被编译。
ASSERT(!IS_MY_CONTROL_DEVICE_OBJECT( DeviceObject ));
ASSERT(IS_MY_DEVICE_OBJECT( DeviceObject ));
IoSkipCurrentIrpStackLocation( Irp );
return IoCallDriver( ((PSFILTER_DEVICE_EXTENSION) DeviceObject->DeviceExtension)->AttachedToDeviceObject, Irp );
}
可以看到,发送IRP的方法为调用IoCallDriver,其中第一个参数为目标设备。这存在一个问题,就是我们如何得到被过滤的目标设备对象。这个问题在后面解决。
但是对于这个DriverObject的设置,还并不是仅仅这么简单。
由于你的驱动将要绑定到文件系统驱动的上边,文件系统除了处理正常的IRP之外,还要处理所谓的FastIo.FastIo是Cache Manager调用所引发的一种没有irp的请求。换句话说,除了正常的Dispatch Functions之外,你还得为DriverObject撰写另一组Fast Io Functions.这组函数的指针在driver->FastIoDispatch.我不知道这个指针留空会不会导致系统崩溃。在这里本来是没有空间的,所以为了保存这一组指针,你必须自己分配空间。
驱动开发中分配内存有很多学问。最常见的情况,使用ExAllocatePool分配即可。
PFAST_IO_DISPATCH fastIoDispatch;
fastIoDispatch = ExAllocatePoolWithTag( NonPagedPool, sizeof( FAST_IO_DISPATCH ), SFLT_POOL_TAG );
if (!fastIoDispatch) {
// 分配失败的情况,删除我们先生成的控制设备
IoDeleteDevice( gSFilterControlDeviceObject );
return STATUS_INSUFFICIENT_RESOURCES;
}
// 内存清零。
RtlZeroMemory( fastIoDispatch, sizeof( FAST_IO_DISPATCH ));
fastIoDispatch->SizeOfFastIoDispatch = sizeof( FAST_IO_DISPATCH );
//我们过滤以下所有的函数:
fastIoDispatch->FastIoCheckIfPossible = SfFastIoCheckIfPossible;
fastIoDispatch->FastIoRead = SfFastIoRead;
fastIoDispatch->FastIoWrite = SfFastIoWrite;
fastIoDispatch->FastIoQueryBasicInfo = SfFastIoQueryBasicInfo;
fastIoDispatch->FastIoQueryStandardInfo = SfFastIoQueryStandardInfo;
fastIoDispatch->FastIoLock = SfFastIoLock;
fastIoDispatch->FastIoUnlockSingle = SfFastIoUnlockSingle;
fastIoDispatch->FastIoUnlockAll = SfFastIoUnlockAll;
fastIoDispatch->FastIoUnlockAllByKey = SfFastIoUnlockAllByKey;
fastIoDispatch->FastIoDeviceControl = SfFastIoDeviceControl;
fastIoDispatch->FastIoDetachDevice = SfFastIoDetachDevice;
fastIoDispatch->FastIoQueryNetworkOpenInfo = SfFastIoQueryNetworkOpenInfo;
fastIoDispatch->MdlRead = SfFastIoMdlRead;
fastIoDispatch->MdlReadComplete = SfFastIoMdlReadComplete;
fastIoDispatch->PrepareMdlWrite = SfFastIoPrepareMdlWrite;
fastIoDispatch->MdlWriteComplete = SfFastIoMdlWriteComplete;
fastIoDispatch->FastIoReadCompressed = SfFastIoReadCompressed;
fastIoDispatch->FastIoWriteCompressed = SfFastIoWriteCompressed;
fastIoDispatch->MdlReadCompleteCompressed = SfFastIoMdlReadCompleteCompressed;
fastIoDispatch->MdlWriteCompleteCompressed = SfFastIoMdlWriteCompleteCompressed;
fastIoDispatch->FastIoQueryOpen = SfFastIoQueryOpen;
// 最后指定给DriverObject.
DriverObject->FastIoDispatch = fastIoDispatch;
一开始就介绍FastIo是一件令人烦恼的事情。首先需要了解的是:FastIo是独立于普通的处理IRP的分发函数之外的另一组接口。但是他们的作用是一样的,就是由驱动处理外部给予的请求。而且所处理的请求也基本相同。
其次,文件系统的普通分发例程和fastio例程都随时有可能被调用。做好的过滤驱动显然应该同时过滤这两套接口。然而,一般都只介绍IRP过滤的方法。Fastio接口非常复杂。但是与IRP过滤是基本一一对应的。只要了解了前者,后者很容易学会。本节后附上陆麟曾经发过的一小段介绍fastio接口的文字。有兴趣的读者可以阅读一下。
在开发的初期学习阶段,你可以简单的设置所有的fastio例程返回FALSE并不做任何事。这样这些请求都会通过IRP重新发送被你的普通分发函数捕获。有一定的效率损失,但是并不是很大。
你可能需要一个fastio过滤函数的passthru的例子,下面以上面的第一个函数为例:
BOOLEAN
SfFastIoCheckIfPossible (
IN PFILE_OBJECT FileObject,
IN PLARGE_INTEGER FileOffset,
IN ULONG Length,
IN BOOLEAN Wait,
IN ULONG LockKey,
IN BOOLEAN CheckForReadOperation,
OUT PIO_STATUS_BLOCK IoStatus,
IN PDEVICE_OBJECT DeviceObject
)
{
PDEVICE_OBJECT nextDeviceObject;
PFAST_IO_DISPATCH fastIoDispatch;
PAGED_CODE();
if (DeviceObject->DeviceExtension) {
ASSERT(IS_MY_DEVICE_OBJECT( DeviceObject ));
// 得到我们绑定的设备,方法和前面的代码一样
nextDeviceObject = ((PSFILTER_DEVICE_EXTENSION) DeviceObject->DeviceExtension)->AttachedToDeviceObject;
ASSERT(nextDeviceObject);
// 得到目标设备的fastio分发函数接口
fastIoDispatch = nextDeviceObject->DriverObject->FastIoDispatch;
// 判断有效性
if (VALID_FAST_IO_DISPATCH_HANDLER( fastIoDispatch, FastIoCheckIfPossible )) {
// 直接调用
return (fastIoDispatch->FastIoCheckIfPossible)(
FileObject,
FileOffset,
Length,
Wait,
LockKey,
CheckForReadOperation,
IoStatus,
nextDeviceObject );
}
}
return FALSE;
}
如前面所说,最简单的方法,你也可以直接返回FALSE.
3.5附:陆麟关于fastio的简述
NT下FASTIO是一套IO MANAGER与DEVICE DRIVER沟通的另外一套API. 在进行基于IRP为基础的接口调用前, IO MANAGER会尝试使用FAST IO接口来加速各种IO操作. FASTIO本身的文档并不多见, 本篇就是要介绍一下FASTIO接口.
FastIoCheckIfPossible, 此调用并不是IO MANAGER直接调用. 而是被FsRtlXXX系列函数调用. 用于确认读写操作是否可以用FASTIO接口进行.
FastIoRead/FastIoWrite, 很明显, 是读写处理的调用.
FastIoQueryBasicInfo/FastIoQueryStandardInfo, 用于获取各种文件信息. 例如创建,修改日期等.
FastIoLock/FastIoUnlockSingle/FastIoUnlockAll/FastIoUnlockAllByKey,用于对文件的锁定操作. 在NT中.有2中锁定需要存在.1.排他性锁. 2.共享锁. 排他性锁在写操作前获取,不准其他进程获得写操作权限, 而共享锁则代表需要读文件某区间. 禁止有写动作出现. 在同一地址上, 如果有多个共享锁请求, 那是被允许的.
FastIoDeviceControl用于提供NtDeviceIoControlFile的支持.
AcquireFileForNtCreateSection/ReleaseFileForNtCreateSection是NTFS在映射文件内容到内存页面前进行的操作.
FastIoDetachDevice, 当REMOVABLE介质被拿走后, FILE SYSTEM的DEVICE对象会在任意的时刻被销毁. 只有正确处理这个调用才能把上层DEVICE和将要销毁的DEVICE脱钩. 如果不解决这个函数, 系统会当.
FastIoQueryNetworkOpenInfo, 当CIFS也就是网上邻居,更准确的说是网络重定向驱动尝试获取文件信息, 会使用这个调用. 该调用是因为各种历史原因而产生. 当时设计CIFS时为避免多次在网上传输文件信息请求, 在NT4时传输协议增加了一个FileNetworkOpenInformation的网络文件请求. 而FSD则增加了这个接口. 用于在一次操作中获得所有的文件信息. 客户段发送FileNetworkOpenInformation, 服务器端的FSD用本接口完成信息填写.
FastIoAcquireForModWrite, Modified Page Writer会调用这个接口来获取文件锁. 如果实现这个接口. 则能使得文件锁定范围减小到调用指定的范围. 不实现此接口, 整个文件被锁.
FastIoPrepareMdlWrite, FSD提供MDL. 以后向此MDL写入数据就代表向文件写入数据. 调用参数中有FILE_BOJECT描述要写的目标文件.
FastIoMdlWriteComplete, 写操作完成. FSD回收MDL.
FastIoReadCompressed, 当此调用被调用时, 读到的数据是压缩后的.应该兼容于标准的NT提供的压缩库. 因为调用者负责解压缩.
FastIoWriteCompressed,当此调用被调用时, 可以将数据是压缩后存储.
FastIoMdlReadCompressed/FastIoMdlReadCompleteCompressed, MDL版本的压缩读. 当后一个接口被调用时,MDL必须被释放.
FastIoMdlWriteCompressed/FastIoMdlWriteCompleteCompressed, MDL版本的压缩写.当后一个接口被调用时,MDL必须被释放.
FastIoQueryOpen, 这不是打开文件的操作. 但是却提供了一个IRP_MJ_CREATE的IRP. 我在以前版本的SECUSTAR的软件中错误地实现了功能. 这个操作是打开文件/获取文件基本信息/关闭文件的一个操作.
FastIoReleaseForModWrite,释放FastIoAcquireForModWrite调用所占有的LOCK.
FastIoAcquireForCcFlush/FastIoReleaseForCcFlush FsRtl会调用此接口,在LAZY WRITE线程将要把修改后的文件数据写入前调用.获取文件锁.
4.设备栈,过滤,文件系统的感知
前边都在介绍文件系统驱动的结构,却还没讲到我们的过滤驱动如何能捕获所有发给文件系统驱动的irp,让我们自己来处理?前面已经解释过了设备对象。现在来解释一下设备栈。
任何设备对象都存在于某个设备栈中。设备栈自然是一组设备对象。这些设备对象是互相关联的,也就是说,如果得到一个DO指针,你就可以知道它所处的设备栈。
任何来自应用的请求,最终被windowsIO管理器翻译成irp的,总是发送给设备栈的顶端那个设备。
原始irp irp irp irp
--------------> ------> -------> ----->
DevTop Dev2 ... DevVolume ...
<-------------- <------ <------- <-----
原始irp(返回) irp irp irp
上图向右的箭头表示irp请求的发送过程,向左则是返回。可见irp是从设备栈的顶端开始,逐步向下发送。DevVolumue表示我们实际要过滤的Volume设备,DevTop表示这个设备栈的顶端。我们只要在这个设备栈的顶端再绑定一个设备,那发送给Volume的请求,自然会先发给我们的设备来处理。
有一个系统调用可以把我们的设备绑定到某个设备的设备栈的顶端。这个调用是IoAttachDeviceToDeviceStack,这个调用2000以及以上系统都可以用(所以说到这点,是因为还有一个IoAttachDeviceToDeviceStackSafe,是2000所没有的。这常常导致你的filter在2000下不能用。)
以下一个函数来帮我实现绑定功能:
NTSTATUS
SfAttachDeviceToDeviceStack (
IN PDEVICE_OBJECT SourceDevice,
IN PDEVICE_OBJECT TargetDevice,
IN OUT PDEVICE_OBJECT *AttachedToDeviceObject
)
{
// 测试代码,测试这个函数是否可以运行在可页交换段
PAGED_CODE();
// 不要误解为:当windows版本高于等于0x0501时,运行以下代码。应该理解为:当我编译
// 的目标操作系统版本高于0x0501时,编译以下代码。反之,不编译。
#if WINVER >= 0x0501
// 当目标操作系统版本高于0x0501时时,我们有一个新的调用AttachDeviceToDeviceStackSafe
// 可调。这个调用比IoAttachDeviceToDeviceStack更可靠。反之,我们不调用新调用。
if (IS_WINDOWSXP_OR_LATER()) {
ASSERT( NULL != gSfDynamicFunctions. AttachDeviceToDeviceStackSafe );
// 请注意,如果我们直接调用IoAttachDeviceToDeviceStackSafe这个调用,则在
// 没有AttachDeviceToDeviceStackSafe这个调用的机器上,这个驱动无法被加载。
// 所以这里采用了动态加载这个函数的方式。以保证同一个驱动既可以在高版本操作系
// 统下运行,也可以在低版本下运行。而目标操作系统为高版本操作系统。
return (gSfDynamicFunctions.AttachDeviceToDeviceStackSafe)( SourceDevice,
TargetDevice,
AttachedToDeviceObject );
} else {
ASSERT( NULL == gSfDynamicFunctions.AttachDeviceToDeviceStackSafe );
#endif
// 目标操作系统为低版本的情况,则不需要动态加载调用。直接使用旧调用。
*AttachedToDeviceObject = TargetDevice;
*AttachedToDeviceObject = IoAttachDeviceToDeviceStack( SourceDevice,
TargetDevice );
if (*AttachedToDeviceObject == NULL) {
return STATUS_NO_SUCH_DEVICE;
}
return STATUS_SUCCESS;
#if WINVER >= 0x0501
}
#endif
}
关于动态加载系统调用,请查阅MmGetSystemRoutineAddress的帮助。
到这里,我们已经知道过滤对Volume的请求的办法。比如“C:”这个设备,我已经知道符号连接为“C:”,不难得到设备名。得到设备名后,又不难得到设备。这时候我们IoCreateDevice()生成一个Device Object,然后调用IoAttachDeviceToDeviceStack绑定,不是一切ok吗?所有发给“C:”的irp,就必然先发送给我们的驱动,我们也可以捕获所有对文件的操作了!
这确实是很简单的处理方法。我得到的FileMon的代码就是这样处理的,如果不想处理动态的Volume,你完全可以这样做。但是我们这里有更高的要求。当你把一个U盘插入usb口,一个“J:”之类的Volume动态诞生的时候,我们依然要捕获这个事件,并生成一个Device来绑定它。
一个新的存储媒质被系统发现并在文件系统中生成一个Volume的过程称为Mounting.其过程开始的时候,FS的CDO将得到一个IRP,其Major Function Code为IRP_MJ_FILE_SYSTEM_CONTROL,Minor Function Code为IRP_MN_MOUNT。换句话说,如果我们已经生成了一个设备绑定文件系统的CDO,那么我们就可以得到这样的IRP,在其中知道一个新的Volume正在Mount.这时候我们可以执行上边所说的操作。
那么现在的问题是如何知道系统中有那些文件系统,还有就是我应该在什么时候绑定它们的控制设备。
IoRegisterFsRegistrationChange()是一个非常有用的系统调用。这个调用注册一个回调函数。当系统中有任何文件系统被激活或者是被注销的时候,你注册过的回调函数就会被调用。
需要反复强调的是,文件系统的加载,和你插入一个U盘增加了一个卷完全是两回事。我们都知道有NTFS,FAT32,CDFS这些文件系统。当你系统中有磁盘使用了FAT32文件系统的时候,你的FASTFAT就已经被激活了。那么你再插入多少个U盘又有什么关系呢。插入光盘也是同样的一件事。物理媒质的增加,卷的增加和文件系统的激活完全不同。
下面看看sfilter的DriverEntry中对这个函数的调用:
status = IoRegisterFsRegistrationChange( DriverObject, SfFsNotification );
if (!NT_SUCCESS( status )) {
KdPrint(( "SFilter!DriverEntry: Error registering FS change notification, status=%08x/n", status ));
DriverObject->FastIoDispatch = NULL;
ExFreePool( fastIoDispatch );
IoDeleteDevice( gSFilterControlDeviceObject );
return status;
}
你有必要为此写一个回调函数。
VOID
SfFsNotification (
IN PDEVICE_OBJECT DeviceObject,
IN BOOLEAN FsActive)
{
UNICODE_STRING name;
WCHAR nameBuffer[MAX_DEVNAME_LENGTH];
PAGED_CODE();
RtlInitEmptyUnicodeString( &name, nameBuffer, sizeof(nameBuffer) );
SfGetObjectName( DeviceObject, &name );
SF_LOG_PRINT( SFDEBUG_DISPLAY_ATTACHMENT_NAMES,
("SFilter!SfFsNotification: %s %p /"%wZ/" (%s)/n",
(FsActive) ? "Activating file system " : "Deactivating file system",
DeviceObject,
&name,
GET_DEVICE_TYPE_NAME(DeviceObject->DeviceType)) );
if (FsActive) {
SfAttachToFileSystemDevice( DeviceObject, &name );
} else {
SfDetachFromFileSystemDevice( DeviceObject );
}
}
这里牵涉到一些关于动态加载的的问题。IoRegisterFsRegistrationChange可以注册对激活文件系统的回调。但是对注册的时候,早就已经激活的文件系统,回调是否有反应呢?早期的windows版本如windows2000是没有反应的。而2000sp4和windowsxp似乎是有反应的。所有的已存在文件系统会重新枚举一次。
所以2000下进行动态加载有一定的困难。因为你必须自己枚举所有已经激活的文件系统。同时枚举设备在2000下又是另一个困难,你必须使用未公开的调用。这个你可以拷贝wowocock的代码。他总是自己编写2000下缺少的调用。
我们再次回顾一下,DriverEntry中,应该做哪些工作。
第一步.生成一个控制设备。当然此前你必须给控制设置指定名称。
第二步.设置Dispatch Functions.
第三步.设置Fast Io Functions.
第四步.编写一个FileSystemNotify回调函数,在其中绑定刚激活的FS CDO.
第五步.使用IoRegisterFsRegistrationChange调用注册这个回调函数。
应该如何绑定一个FS CDO?这不是一个简单的主题。我们在下面的章节再详细描述。
5.绑定FS CDO,文件系统识别器,设备扩展
上一节讲到我们打算绑定一个刚刚被激活的FS CDO.前边说过简单的调用sfAttachDeviceToStack可以很容易的绑定这个设备。但是,并不是每次Fs system notify调用发现有新的fs激活,我就直接绑定它。
首先判断是否我需要关心的文件系统类型。你的过滤驱动可能只对文件系统的CDO的设备类型中某些感兴趣。
#define IS_DESIRED_DEVICE_TYPE(_type) /
(((_type) == FILE_DEVICE_DISK_FILE_SYSTEM) || /
((_type) == FILE_DEVICE_CD_ROM_FILE_SYSTEM) || /
((_type) == FILE_DEVICE_NETWORK_FILE_SYSTEM))
下一个问题是我打算跳过文件系统识别器。文件系统识别器是文件系统驱动的一个很小的替身。为了避免没有使用到的文件系统驱动占据内核内存,windows系统不加载这些大驱动,而代替以该文件系统驱动对应的文件系统识别器。当新的物理存储媒介进入系统,io管理器会依次的尝试各种文件系统对它进行“识别”。识别成功,立刻加载真正的文件系统驱动,对应的文件系统识别器则被卸载掉。对我们来说,文件系统识别器的控制设备看起来就像一个文件系统控制设备。但我们不打算绑定它。
分辨的方法是通过驱动的名字。凡是微软的标准的文件系统识别器的驱动对象的名字(注意是DriverObject而不是DeviceObject!)都为“/FileSystem/Fs_Rec”.
RtlInitUnicodeString( &fsrecName, L"//FileSystem//Fs_Rec" );
SfGetObjectName( DeviceObject->DriverObject, &fsName );
if (RtlCompareUnicodeString( &fsName, &fsrecName, TRUE ) == 0)
{
return STATUS_SUCCESS;
}
但是要注意没有谁规定文件系统识别器一定生成在驱动“/FileSystem/Fs_Rec”下。所以这个方法只跳过了部分“微软的规矩的”文件系统识别器。对于不能跳过的,我们在File System Control的过滤中有对应的处理。
接下来我将要生成我的设备。这里要提到设备扩展的概念。设备对象是一个数据结构,为了表示不同的设备,里边将有一片自定义的空间,用来给你记录这个设备的特有信息。我们为我们所生成的设备确定设备扩展如下:
// 文件过滤系统驱动的设备扩展
typedef struct _SFILTER_DEVICE_EXTENSION {
// 我们所绑定的文件系统设备
PDEVICE_OBJECT AttachedToDeviceObject;
// 与我们的文件系统设备相关的真实设备(磁盘),这个用于绑定时使用。
PDEVICE_OBJECT StorageStackDeviceObject;
// 如果我们绑定了一个卷,这是物理磁盘卷名。否则这是我们绑定的控制设备名。
UNICODE_STRING DeviceName;
// 用来保存名字字符串的缓冲区
WCHAR DeviceNameBuffer[MAX_DEVNAME_LENGTH];
} SFILTER_DEVICE_EXTENSION, *PSFILTER_DEVICE_EXTENSION;
之所以如此简单,是因为我们现在还没有多少东西要记录。基本上记得自己绑定在哪个设备上就好了。如果以后需要更多的信息,再增加不迟。扩展空间的大小是在wdf_dev_create(也就是这个设备生成)的时候指定的。得到设备对象指针后,用下面这个函数来获取我们所绑定的原始设备:
nextDeviceObject = ((PSFILTER_DEVICE_EXTENSION)DeviceObject->DeviceExtension)
-> AttachedToDeviceObject;
生成设备后,为了让系统看起来,你的设备和原来的设备没什么区别,你必须设置一些该设备的标志位与你所绑定的设备相同。
if ( FlagOn( DeviceObject->Flags, DO_BUFFERED_IO )) {
SetFlag( newDeviceObject->Flags, DO_BUFFERED_IO );
}
if ( FlagOn( DeviceObject->Flags, DO_DIRECT_IO )) {
SetFlag( newDeviceObject->Flags, DO_DIRECT_IO );
}
if ( FlagOn( DeviceObject->Characteristics, FILE_DEVICE_SECURE_OPEN ) ) {
SetFlag( newDeviceObject->Characteristics, FILE_DEVICE_SECURE_OPEN );
}
DO_BUFFERED_IO,DO_DIRECT_IO这两个标志的意义在于外部向这些设备发送读写请求的时候,所用的缓冲地址将有所不同。这点以后在过滤文件读写的时候再讨论。现在一切事情都做完,你应该去掉你的新设备上的DO_DEVICE_INITIALIZING标志,以表明的的设备已经完全可以用了。
newDeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
下面的函数来完成以上的这个过程。你只要在上一节中提示的位置调用这个函数,就完成对文件系统控制设备的绑定了。
NTSTATUS
SfAttachToFileSystemDevice (
IN PDEVICE_OBJECT DeviceObject,
IN PUNICODE_STRING DeviceName
)
{
PDEVICE_OBJECT newDeviceObject;
PSFILTER_DEVICE_EXTENSION devExt;
UNICODE_STRING fsrecName;
NTSTATUS status;
UNICODE_STRING fsName;
WCHAR tempNameBuffer[MAX_DEVNAME_LENGTH];
PAGED_CODE();
// 检查设备类型
if (!IS_DESIRED_DEVICE_TYPE(DeviceObject->DeviceType)) {
return STATUS_SUCCESS;
}
RtlInitEmptyUnicodeString( &fsName,
tempNameBuffer,
sizeof(tempNameBuffer) );
// 根据我们是否要绑定识别器
if (!FlagOn(SfDebug,SFDEBUG_ATTACH_TO_FSRECOGNIZER)) {
// 否则跳过识别器的绑定
RtlInitUnicodeString( &fsrecName, L"//FileSystem//Fs_Rec" );
SfGetObjectName( DeviceObject->DriverObject, &fsName );
if (RtlCompareUnicodeString( &fsName, &fsrecName, TRUE ) == 0) {
return STATUS_SUCCESS;
}
}
// 生成新的设备,准备绑定目标设备
status = IoCreateDevice( gSFilterDriverObject,
sizeof( SFILTER_DEVICE_EXTENSION ),
NULL,
DeviceObject->DeviceType,
0,
FALSE,
&newDeviceObject );
if (!NT_SUCCESS( status )) {
return status;
}
// 复制各种标志
if ( FlagOn( DeviceObject->Flags, DO_BUFFERED_IO )) {
SetFlag( newDeviceObject->Flags, DO_BUFFERED_IO );
}
if ( FlagOn( DeviceObject->Flags, DO_DIRECT_IO )) {
SetFlag( newDeviceObject->Flags, DO_DIRECT_IO );
}
if ( FlagOn( DeviceObject->Characteristics, FILE_DEVICE_SECURE_OPEN ) ) {
SetFlag( newDeviceObject->Characteristics, FILE_DEVICE_SECURE_OPEN );
}
devExt = newDeviceObject->DeviceExtension;
// 使用我们上一节提供的函数进行绑定
status = SfAttachDeviceToDeviceStack( newDeviceObject,
DeviceObject,
&devExt->AttachedToDeviceObject );
if (!NT_SUCCESS( status )) {
goto ErrorCleanupDevice;
}
// 记录设备名字
RtlInitEmptyUnicodeString( &devExt->DeviceName,
devExt->DeviceNameBuffer,
sizeof(devExt->DeviceNameBuffer) );
RtlCopyUnicodeString( &devExt->DeviceName, DeviceName );
ClearFlag( newDeviceObject->Flags, DO_DEVICE_INITIALIZING );
ErrorCleanupDevice:
IoDeleteDevice( newDeviceObject );
return status;
}
6.IRP的传递,File System Control Dispatch
我们现在不得不开始写dispatch functions.因为你的设备已经绑定到文件系统控制设备上去了。windows发给文件系统的请求发给你的驱动。如果你不能做恰当的处理,你的系统的就会崩溃。
最简单的处理方式是把请求不加改变的传递到我们所绑定的设备上去。如何获得我们所绑定的设备?上一节已经把该设备记录在我们的设备扩展里。
nextDeviceObject = ((PSFILTER_DEVICE_EXTENSION)DeviceObject->DeviceExtension)
-> AttachedToDeviceObject;
如何传递请求?使用IoCallDriver,该调用的第一个参数是设备对象指针,第二个参数是IRP指针。
一个IRP拥有一组IO_STACK_LOCATION.前面说过IRP在一个设备栈中传递。IO_STACK_LOCATION是和这个设备栈对应的。用于保存IRP请求在当前设备栈位置中的部分参数。如果我要把请求往下个设备传递,那么我应该把当前IO_STATCK_LOCATION复制到下一个。 但是当我不打算加以任何处理的时候,我简单忽略当前调用栈。
现在可以写一个默认的Dispatch Functions. 简单的passthru.
NTSTATUS
SfPassThrough (
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
// 对于我们认为“不能”的事情,我们采用ASSERT进行调试模式下的确认。
// 而不加多余的判断来消耗我们的效率。这些宏在调试模式下不被编译。
ASSERT(!IS_MY_CONTROL_DEVICE_OBJECT( DeviceObject ));
ASSERT(IS_MY_DEVICE_OBJECT( DeviceObject ));
IoSkipCurrentIrpStackLocation( Irp );
return IoCallDriver( ((PSFILTER_DEVICE_EXTENSION) DeviceObject->DeviceExtension) ->AttachedToDeviceObject, Irp );
}
这个函数在以前就出现过。现在可以进一步理解了。
上边有一个函数IS_MY_DEVICE_OBJECT来判断是否我的设备。这个判断过程很简单。通过DeviceObject可以得到DriverObject指针,判断一下是否我自己的驱动即可。IS_MY_CONTROL_DEVICE_OBJECT ()来判断这个设备是否是我的控制设备,不要忘记在DriverEntry()中我们首先生成了一个本驱动的控制设备。实际这个控制设备还不做任何事情,所以对它发生的任何请求也是非法的。ASSERT即可。同时我们使用IoSkipCurrentIrpStackLocation( Irp );忽略了当前调用栈空间。
假设我要立刻让一个irp失败。我可以这样:
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = error_code;
IoCompleteRequest( Irp, IO_NO_INCREMENT );
如此一来,本不该发到我的驱动的irp,就立刻返回错误非法请求。但是实际上这种情况是很少发生的。
如果你现在想要你的驱动立刻运行,让所有的dispacth functions都调用SfPassThrough.这个驱动已经可以绑定文件系统的控制设备,并输出一些调试信息。但是还没有绑定Volume.所以并不能直接监控文件读写。
对于一个绑定文件系统控制设备的设备来说,其他的请求直接调用上边的默认处理就可以了。重点需要注意的是上边曾经挂接IRP_MJ_FILE_SYSTEM_CONTROL的dispatch处理的函数SfFsControl().
IRP_MJ_FILE_SYSTEM_CONTROL这个东西是IRP的主功能号。每个主功能号下一般都有次功能号。这两个东西标示一个IRP的功能。
主功能号和次功能号是IO_STACK_LOCATION的开头两字节。
当有卷被Mount或者dismount,你写的SfFsControl ()就被调用。具体的判断方法,就见如下的代码了:
NTSTATUS
SfFsControl (
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation( Irp );
PAGED_CODE();
ASSERT(!IS_MY_CONTROL_DEVICE_OBJECT( DeviceObject ));
ASSERT(IS_MY_DEVICE_OBJECT( DeviceObject ));
switch (irpSp->MinorFunction) {
case IRP_MN_MOUNT_VOLUME:
return SfFsControlMountVolume( DeviceObject, Irp );
case IRP_MN_LOAD_FILE_SYSTEM:
return SfFsControlLoadFileSystem( DeviceObject, Irp );
case IRP_MN_USER_FS_REQUEST:
{
switch (irpSp->Parameters.FileSystemControl.FsControlCode) {
case FSCTL_DISMOUNT_VOLUME:
{
PSFILTER_DEVICE_EXTENSION devExt = DeviceObject->DeviceExtension;
SF_LOG_PRINT( SFDEBUG_DISPLAY_ATTACHMENT_NAMES,
("SFilter!SfFsControl: Dismounting volume %p /"%wZ/"/n",
devExt->AttachedToDeviceObject,
&devExt->DeviceName) );
break;
}
}
break;
}
}
IoSkipCurrentIrpStackLocation( Irp );
return IoCallDriver( ((PSFILTER_DEVICE_EXTENSION)DeviceObject->DeviceExtension)->AttachedToDeviceObject, Irp );
你得开始写新的函数,SfFsControlMountVolume ()很快,你就能完全监控所有的卷了。
这样做是sfilter动态监控所有的卷的完美的解决方案。
但是你会发现这个FSCTL_DISMOUNT_VOLUME并没有做解除绑定和销毁设备的处理。实际上这个请求的出现是理论性的。测试表明,至少拔出U盘这样的操作要捕获也是非常不容易的。这个请求似乎根本不会出现。而其他一些请求会出现,但是往往不是一一对应的关系。所以要真正准确的捕获Dismount操作是很困难的。
sfilter采用了通融的办法。并不解除绑定也不销毁设备。可能是因为,设备拔除后,多余的设备并没有影响。此外,拔出与插入这样的情况并不会太频繁,所以内存泄漏也不明显。这只是我个人的猜测,我并没有确认过是否有其他的机制能销毁设备。
如果是在xp以上,有一个调用可以获得一个文件系统上已经被Mount的卷。但是2000下不能使用。所以我们没有使用那个方法。何况仅仅得到已经Mount的卷也不是我想要的。
这里另外还有一个SfFsControlLoadFileSystem函数。发生于IRP_MN_LOAD_FILESYS。这个功能码的意义是当一个文件识别器(见上文)决定加载真正的文件系统的时候,会产生一个这样的irp。那么,如果我们已经绑定了文件系统识别器,现在就应该解除绑定并销毁设备。同时生成新的设备去绑定真的文件系统。绑定文件系统控制设备我们已经在上一章详细讲过,这里就不再重复追踪这个过程。
你现在可以修改你的驱动,感知卷被绑定的过程。
再回首一下我们的脉络:
第一步.生成一个控制设备。当然此前你必须给控制设置指定名称。
第二步.设置Dispatch Functions. 设置Fast Io Functions.
第三步.编写一个File System Notify回调函数,在其中绑定刚激活的FS CDO. 并注册这个回调函数。
第四步.编写默认的dispatch functions.
第五步.处理IRP_MJ_FILE_SYSTEM_CONTROL,在其中监控卷设备的Mount和Dismount.
第六步.下一步自然是绑定卷设备了,请听下回分解。
7.准备绑定卷,IRP完成函数,中断级
先讨论一下卷设备是如何得到的.首先在SfFsControlMountVolume中:
storageStackDeviceObject = irpSp->Parameters.MountVolume.Vpb->RealDevice;
VPB是Volume parameter block.一个数据结构.它在这里的主要作用是把实际存储媒介设备对象和文件系统上的卷设备对象联系起来.
你可以从一个Storage Device Object得到一个VPB, 此外可以从VPB中再得到对应的卷设备。
这里的IRP是一个MOUNT请求.而volume设备对象实际上是这个请求完成之后的返回结果.因此,在这个请求还没有完成之前,我们就试图去获得Volume设备对象,当然是竹篮打水一场空了.
既然如此,那么我们应该在SfFsControlMountVolume请求完成之后,再去获取VPB,得到卷设备。为何要在这里获得VPB呢?
这是因为在这个过程完成之后,下层的文件系统驱动可能已经修改了VPB的值。因此,我们这里把它预先保存下来。
这里,你可以直接拷贝当前IO_STACK_LOCATION,然后向下发送请求,但在此之前,要先给irp分配一个完成函数.irp一旦完成,你的完成函数将被调用.这样的话,你可以在完成函数中得到Volume设备,并实施你的绑定过程.
这里要讨论一下中断级别的问题.常常碰到人问某函数只能在Passive Level调用是什么意思.总之我们的任何代码执行的时候,总是处在某个当前的中断级之中.某些系统调用只能在低级别中断级中执行.请注意,如果一个调用可以在高处运行,那么它能在低处运行,反过来则不行.
我们需要知道的只是我们关心Passive Level和Dispatch Level.而且Dispatch Level的中断级较高.一般ddk上都会标明,如果注明irq level=passive,那么你就不能在dispatch level的代码中调用它们了.
那么你如何判断当前的代码在哪个中断级别中呢?我一般是这么判断的:如果你的代码执行是由于应用程序(或者说上层)的调用而引发的,那么应该在Passive Level.如果你的代码执行是由于下层硬件而引发的,那么则可能在dispatch level.
希望不要机械的理解我的话。以上只是极为粗略的便于记忆的理解方法.实际的应用应该是这样的:所有的dispatch functions由于是上层发来的irp而导致的调用,所以应该都是Passive Level,在其中你可以调用绝大多数系统调用.而如网卡的OnReceive,硬盘读写完毕,返回而导致的完成函数,都有可能在Dispatch级.注意都是有可能,而不是绝对是.但是一旦有可能,我们就应该按就是考虑。
下面是SfFsControlMountVolume的执行过程:
NTSTATUS
SfFsControlMountVolume (
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
PSFILTER_DEVICE_EXTENSION devExt = DeviceObject->DeviceExtension;
PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation( Irp );
PDEVICE_OBJECT newDeviceObject;
PDEVICE_OBJECT storageStackDeviceObject;
PSFILTER_DEVICE_EXTENSION newDevExt;
NTSTATUS status;
BOOLEAN isShadowCopyVolume;
PFSCTRL_COMPLETION_CONTEXT completionContext;
PAGED_CODE();
ASSERT(IS_MY_DEVICE_OBJECT( DeviceObject ));
ASSERT(IS_DESIRED_DEVICE_TYPE(DeviceObject->DeviceType));
storageStackDeviceObject = irpSp->Parameters.MountVolume.Vpb->RealDevice;
// 判断是否卷影,这个我们后面再提
status = SfIsShadowCopyVolume ( storageStackDeviceObject,
&isShadowCopyVolume );
// 如果不打算绑定卷影就跳过去
if (NT_SUCCESS(status) &&
isShadowCopyVolume &&
!FlagOn(SfDebug,SFDEBUG_ATTACH_TO_SHADOW_COPIES)) {
IoSkipCurrentIrpStackLocation( Irp );
return IoCallDriver( devExt->AttachedToDeviceObject, Irp );
}
// 我预先就生成设备,虽然现在还没有到绑定的时候
status = IoCreateDevice( gSFilterDriverObject,
sizeof( SFILTER_DEVICE_EXTENSION ),
NULL,
DeviceObject->DeviceType,
0,
FALSE,
&newDeviceObject );
if (!NT_SUCCESS( status )) {
KdPrint(( "SFilter!SfFsControlMountVolume: Error creating volume device object, status=%08x/n", status ));
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = status;
IoCompleteRequest( Irp, IO_NO_INCREMENT );
return status;
}
// 填写设备扩展
newDevExt = newDeviceObject->DeviceExtension;
newDevExt->StorageStackDeviceObject = storageStackDeviceObject;
RtlInitEmptyUnicodeString( &newDevExt->DeviceName,
newDevExt->DeviceNameBuffer,
sizeof(newDevExt->DeviceNameBuffer) );
SfGetObjectName( storageStackDeviceObject,
&newDevExt->DeviceName );
// 后面暂时省略
… …
}
卷影似乎是一种用于磁盘数据恢复的特殊设备。你可以过滤它们也可以不过滤。如何判断这里略去。你可以自己查看sfilter的相关代码。后面的代码我们省略了。因为出现了新的问题。接下来我们应该做什么呢,显然我们应该获得卷设备并绑定。但是现在卷设备还没有生成,我们必须等待这个请求结束。
当完成函数被调用的时候,请求就结束了。我们可以往完成函数中传递一个上下文指针来保存我们的信息。以便我们知道哪一次调用对应哪一次完成。这有一种经典的同步方法:我们初始化一个事件KEVENT,并通过上下文传递到完成函数中。在完成函数中设置这个事件。而我们的本函数则等待这个事件。那么等待结束时,这个请求就完成了。那么前面省略的地方,基本的代码如下:
KEVENT waitEvent;
// 初始化事件
KeInitializeEvent( &waitEvent,
NotificationEvent,
FALSE );
// 因为我们要等待完成,所以必须拷贝当前调用栈
IoCopyCurrentIrpStackLocationToNext ( Irp );
// 设置完成函数,并把事件的指针当上下文传入。
IoSetCompletionRoutine( Irp,
SfFsControlCompletion,
&waitEvent, //上下文指针
TRUE,
TRUE,
TRUE );
// 发送IRP并等待事件完成
status = IoCallDriver( devExt->AttachedToDeviceObject, Irp );
if (STATUS_PENDING == status) {
status = KeWaitForSingleObject( &waitEvent,
Executive,
KernelMode,
FALSE,
NULL );
ASSERT( STATUS_SUCCESS == status );
}
……
请注意IoSetCompletionRoutine的第三个参数,就是完成函数中的Context指针。这是我们传递信息到完成函数的接口。那么在完成函数中,我们这样写:
NTSTATUS
SfFsControlCompletion (
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp,
IN PVOID Context
)
{
UNREFERENCED_PARAMETER( DeviceObject );
UNREFERENCED_PARAMETER( Irp );
ASSERT(IS_MY_DEVICE_OBJECT( DeviceObject ));
ASSERT(Context != NULL);
KeSetEvent((PKEVENT)Context, IO_NO_INCREMENT, FALSE);
return STATUS_MORE_PROCESSING_REQUIRED;
}
UNREFERENCED_PARAMETER的意义在于,去掉C编译器对于没有使用的这个参数所产生的一条警告。
这样一来,只要执行过了KeWaitForSingleObject,则这个请求已经完成。那么在之后执行绑定卷的操作即可。
8.绑定卷的完成
这很容易让人联想,我为何不在完成函数中直接绑定了设备。而非要等完成函数设置了我的事件之后,再回来做这件事情。这是因为完成函数的中断级别过高。虽然dispatch中断级别应该可以执行IoAttachDeviceToDeviceStack。但是在绑定卷的过程中,Sfilter使用ExAcquireFastMutex这些不适宜在Dispatch级别使用的东西,应该是其原因。
用事件等待完成函数的发生是一个通用的办法。但是在Windows2000上,在绑定卷设备时使用却有其固有的缺陷而有导致死锁的可能。这是Windows早期的固有缺陷,与一种特殊设备相关。CardMagic曾经深入研究过这个问题,他说到我头晕为止。那么Windows2000上运行时,我们如何做呢?
解决方法是在完成函数中做这件事情。这又回到上面的问题,为何在完成函数中不能做?最终不得不再次采用折衷的方法:我们在完成函数中生成一个系统线程。系统线程执行的中断级为Passive level,足够我们很好的完成绑定的过程。
Windows本身有一个系统线程负责处理一些日常工作。我们也可以把自己的工作任务插入其中,免除我们需要自己生成线程的开销:
SfFsControlMountVolume后面的代码基本如下,充满了对目标操作系统的编译时和运行时的判断:
… …
#if WINVER >= 0x0501
if (IS_WINDOWSXP_OR_LATER()) {
KEVENT waitEvent;
KeInitializeEvent( &waitEvent,
NotificationEvent,
FALSE );
IoCopyCurrentIrpStackLocationToNext ( Irp );
IoSetCompletionRoutine( Irp,
SfFsControlCompletion,
&waitEvent,
TRUE,
TRUE,
TRUE );
status = IoCallDriver( devExt->AttachedToDeviceObject, Irp );
if (STATUS_PENDING == status) {
status = KeWaitForSingleObject( &waitEvent,
Executive,
KernelMode,
FALSE,
NULL );
ASSERT( STATUS_SUCCESS == status );
}
// 到这里请求完成,调用我们的函数绑定卷
status = SfFsControlMountVolumeComplete( DeviceObject,
Irp,
newDeviceObject );
} else {
#endif
completionContext = ExAllocatePoolWithTag( NonPagedPool,
sizeof( FSCTRL_COMPLETION_CONTEXT ),
SFLT_POOL_TAG );
if (completionContext == NULL) {
IoSkipCurrentIrpStackLocation( Irp );
status = IoCallDriver( devExt->AttachedToDeviceObject, Irp );
} else {
// 初始化一个工作任务,具体内容写在函数SfFsControlMountVolumeCompleteWorker中
ExInitializeWorkItem( &completionContext->WorkItem,
SfFsControlMountVolumeCompleteWorker,
completionContext );
// 写入上下文,以便把我的多个指针传递过去
completionContext->DeviceObject = DeviceObject;
completionContext->Irp = Irp;
completionContext->NewDeviceObject = newDeviceObject;
// 拷贝调用栈
IoCopyCurrentIrpStackLocationToNext( Irp );
// 请注意这里传入的上下文变成了我的工作任务,而不是事件,和高级版本有别
IoSetCompletionRoutine( Irp,
SfFsControlCompletion,
&completionContext->WorkItem, //context parameter
TRUE,
TRUE,
TRUE );
// 发送irp
status = IoCallDriver( devExt->AttachedToDeviceObject, Irp );
}
#if WINVER >= 0x0501
}
#endif
return status;
}
那么完成函数也必须相对应的修改一下,以满足需求:
NTSTATUS
SfFsControlCompletion (
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp,
IN PVOID Context)
{
UNREFERENCED_PARAMETER( DeviceObject );
UNREFERENCED_PARAMETER( Irp );
ASSERT(IS_MY_DEVICE_OBJECT( DeviceObject ));
ASSERT(Context != NULL);
#if WINVER >= 0x0501
if (IS_WINDOWSXP_OR_LATER()) {
KeSetEvent((PKEVENT)Context, IO_NO_INCREMENT, FALSE);
} else {
#endif
// 中断级别过高的时候,工作任务放到DelayedWorkQueue队列中执行
if (KeGetCurrentIrql() > PASSIVE_LEVEL) {
ExQueueWorkItem( (PWORK_QUEUE_ITEM) Context,
DelayedWorkQueue );
} else {
// 否则直接执行
PWORK_QUEUE_ITEM workItem = Context;
(workItem->WorkerRoutine)(workItem->Parameter);
}
#if WINVER >= 0x0501
}
#endif
return STATUS_MORE_PROCESSING_REQUIRED;
}
SfFsControlMountVolumeCompleteWorker做的事情很简单,就是调用SfFsControlMountVolumeComplete。我们最终所有的处理目标都是调用SfFsControlMountVolumeComplete。因为在这里,我们最终绑定卷设备。
NTSTATUS
SfFsControlMountVolumeComplete (
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp,
IN PDEVICE_OBJECT NewDeviceObject
)
{
PVPB vpb;
PSFILTER_DEVICE_EXTENSION newDevExt;
PIO_STACK_LOCATION irpSp;
PDEVICE_OBJECT attachedDeviceObject;
NTSTATUS status;
PAGED_CODE();
newDevExt = NewDeviceObject->DeviceExtension;
irpSp = IoGetCurrentIrpStackLocation( Irp );
// 我们前面保存过的vpb,获得
vpb = newDevExt->StorageStackDeviceObject->Vpb;
if (vpb != irpSp->Parameters.MountVolume.Vpb) {
if (NT_SUCCESS( Irp->IoStatus.Status )) {
// 获得一个互斥体,以便我们可以原子的判断我们是否绑定过一个卷设备.这可以防止
// 我们对一个卷绑定两次。
ExAcquireFastMutex( &gSfilterAttachLock );
// 判断是否绑定过了
if (!SfIsAttachedToDevice( vpb->DeviceObject, &attachedDeviceObject )) {
// 调用SfAttachToMountedDevice来完成真正的绑定.
status = SfAttachToMountedDevice( vpb->DeviceObject,
NewDeviceObject );
if (!NT_SUCCESS( status )) {
SfCleanupMountedDevice( NewDeviceObject );
IoDeleteDevice( NewDeviceObject );
}
ASSERT( NULL == attachedDeviceObject );
} else {
((PSFILTER_DEVICE_EXTENSION)attachedDeviceObject-> DeviceExtension)-> AttachedToDeviceObject,
&newDevExt->DeviceName) );
SfCleanupMountedDevice( NewDeviceObject );
IoDeleteDevice( NewDeviceObject );
ObDereferenceObject( attachedDeviceObject );
}
ExReleaseFastMutex( &gSfilterAttachLock );
} else {
SfCleanupMountedDevice( NewDeviceObject );
IoDeleteDevice( NewDeviceObject );
}
// 把请求完成掉
status = Irp->IoStatus.Status;
IoCompleteRequest( Irp, IO_NO_INCREMENT );
return status;
}
这个过程确实比较复杂,但是既然最后调用的是SfAttachToMountedDevice,那么我们最后的真相也不远了:
NTSTATUS
SfAttachToMountedDevice (
IN PDEVICE_OBJECT DeviceObject,
IN PDEVICE_OBJECT SFilterDeviceObject
)
{
PSFILTER_DEVICE_EXTENSION newDevExt = SFilterDeviceObject->DeviceExtension;
NTSTATUS status;
ULONG i;
PAGED_CODE();
ASSERT(IS_MY_DEVICE_OBJECT( SFilterDeviceObject ));
#if WINVER >= 0x0501
ASSERT(!SfIsAttachedToDevice ( DeviceObject, NULL ));
#endif
// 设备标记的复制
if (FlagOn( DeviceObject->Flags, DO_BUFFERED_IO )) {
SetFlag( SFilterDeviceObject->Flags, DO_BUFFERED_IO );
}
if (FlagOn( DeviceObject->Flags, DO_DIRECT_IO )) {
SetFlag( SFilterDeviceObject->Flags, DO_DIRECT_IO );
}
// 循环尝试绑定.绑定有可能失败。这可能和其他用户恰好试图对这个磁盘做特殊的操作比如
// mount或者dismount有关.反复进行8次尝试以避开这些巧合.
for (i=0; i < 8; i++) {
LARGE_INTEGER interval;
status = SfAttachDeviceToDeviceStack( SFilterDeviceObject,
DeviceObject,
&newDevExt->AttachedToDeviceObject );
if (NT_SUCCESS(status)) {
ClearFlag( SFilterDeviceObject->Flags, DO_DEVICE_INITIALIZING );
return STATUS_SUCCESS;
}
// 把这个线程延迟500毫秒后再继续.
interval.QuadPart = (500 * DELAY_ONE_MILLISECOND); KeDelayExecutionThread ( KernelMode, FALSE, &interval );
}
return status;
}
大结局是我们完成了绑定,过滤可以开始了。
9读写操作的捕获与分析
上文已经讲到绑定Volume之前的关键操作.我们一路逢山开路,逢水架桥,相信你从中也学到了驱动开发的基本方法.后的工作,无非灵活运用这些方法而已.而以后的教程中,我也不会逐一详尽的列举出细节的代码了.
现在我们处理IRP_MJ_READ和IRP_MJ_WRITE,如果你已经绑定了Volume,那么显然,发送给Volume的请求就会先发送给你.处理IRP_MJ_READ和IRP_MJ_WRITE,能捕获文件的读写操作.
进入你的SfRead ()/SfWrite函数(假设你注册了这两个函数来处理读写,请见前面关于分发函数的讲述),首先判断这个Dev是不是绑定Volume的设备.如果是,那么就是一个读写文件的操作.
如何判断?记得我们先绑定Volume的时候,在我们的设备扩展中设置了,如果不是(比如是FS CDO,我们没设置过),那么这么判断即可:
PSFILTER_DEVICE_EXTENSION devExt = DeviceObject->DeviceExtension;
if(devExt->StorageDev != NULL)
{ … }
其他的情况不需要捕获,请直接传递到下层.
读写请求的IRP情况非常复杂,请有足够的心理准备.并不要过于依赖帮助,最好的办法就是自己打印IRP的各个细节,亲自查看文件读操作的完成过程.
首先我们回忆一下前面分发函数的设置:
DriverObject->MajorFunction[IRP_MJ_CREATE] = SfCreate;
DriverObject->MajorFunction[IRP_MJ_CREATE_NAMED_PIPE] = SfCreate;
DriverObject->MajorFunction[IRP_MJ_CREATE_MAILSLOT] = SfCreate;
DriverObject->MajorFunction[IRP_MJ_FILE_SYSTEM_CONTROL] = SfFsControl;
DriverObject->MajorFunction[IRP_MJ_CLEANUP] = SfCleanupClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = SfCleanupClose;
这里没有Read的设置,为此后面加上一条:
DriverObject->MajorFunction[IRP_MJ_READ] = SfRead;
然后我们自己实现一个函数:
NTSTATUS
SfRead (
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(Irp);
PFILE_OBJECT file_object = irpsp->FileObject;
PSFILTER_DEVICE_EXTENSION devExt = DeviceObject->DeviceExtension;
// 对控制设备的操作,我直接失败
if (IS_MY_CONTROL_DEVICE_OBJECT(DeviceObject)) {
Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
Irp->IoStatus.Information = 0;
IoCompleteRequest( Irp, IO_NO_INCREMENT );
return STATUS_INVALID_DEVICE_REQUEST;
}
// 对文件系统其他设备的操作,passthru.
if(devExt->StorageDev != NULL)
{
return SfPassThrough(DeviceObject,Irp);
}
// 到这里说明是对卷的文件操作
… …
}
然后关心被读写的文件.IRP下有一个FileObject指针.这个东西指向一个文件对象.你可以得到文件对象的名字,但是实际上在读操作的过程中解析路径是不合理的。我们在后面“全路径过滤”中,再详细讲解文件名和路径的获取。
接下来有得到读文件的偏移量. 2000下文件系统得到的偏移量都是从文件起始位置开始计算的.偏移量是一个LARGE_INTEGER.这是一个在Windows驱动开发中常用的64位整数数据的共用体。
以下代码用来得到偏移量(从irpsp中):
LARGE_INTEGER offset;
Offset.QuadPart = irpsp->Parameters.Read.ByteOffset.QuadPart;
而读取文件的长度则是:
ULONG length;
length = irpsp->Parameters.Read.Length;
写的偏移量和长度则为:
Offset.QuadPart = irpsp->Parameters.Write.ByteOffset.QuadPart;
length = irpsp->Parameters.Write.Length;
此外我还希望能得到我所读到的数据.这要注意,我们捕获这个请求的时候,这个请求还没有完成.既然没有完成,当然无数据可读.如果要获取,那就设置完成函数,在完成函数中完成请求.
完成Irp的时候忽略还是拷贝当前IO_STACK_LOCATION,返回什么STATUS,以及完成函数中如何结束Irp,是不那么容易搞清楚的一件事情.我想做个总结如下:
1.如果对irp完成之后的事情无兴趣,直接忽略当前IO_STACK_LOCATION,(对我们的程序来说,调用IoSkipCurrentIrpStackLocation),然后向下传递请求,返回IoCallDriver所返回的状态.
2.不但对irp完成之后的事情无兴趣,而且我不打算继续传递,打算立刻返回成功或失败.那么我不用忽略或者拷贝当前IO_STACK_LOCATION,填写参数后调用IoCompleteRequest,并返回我想返回的结果.
3.如果对irp完成之后的事情有兴趣,并打算在完成函数中处理,应该首先拷贝当前IO_STACK_LOCATION(IoCopyCurrentIrpStackLocationToNext),然后指定完成函数,并返回IoCallDriver()所返回的status.完成函数中,不需要调用IoCompleteRequest!直接返回Irp的当前状态即可.
4.同3的情况,有时候,会把任务塞入系统工作者线程或者希望在另外的线程中去完成Irp,那么完成函数中应该返回STATUS_MORE_PROCESSING_REQUIRED,此时完成Irp的时候应该调用IoCompleteRequest.另一种类似的情况是在dispatch函数中等待完成函数中设置事件,那么完成函数返回STATUS_MORE_PROCESSING_REQUIRED,dispatch函数在等待结束后调用IoCompleteRequest.
前边已经提到过设备的DO_BUFFERED_IO,DO_DIRECT_IO这两个标记.情况是3种:要么是两个标记中其中一个,要么是一个都没有.Volume设备出现DO_BUFFERED的情况几乎没有,我碰到的都是一个标记都没有.DO_DIRECT_IO表示数据应该返回到Irp->MdlAddress所指向的MDL所指向的内存.在无标记的情况下,表明数据读好,请返回到
Irp->UseBuffer中即可.
不过在实际中,我都用更简单的方法判别.简单的说,Irp->MdlAddress如果不为NULL,则使用Irp->MdlAddress.缓冲区位置为MmGetSystemAddressForMdl(Irp->MdlAddress);否则直接使用Irp->UserBuffer.
UseBuffer是一个只在当前线程上下文才有效的地址.如果你打算按这个地址获得数据,除非你打算自己分配MDL锁定内存地址,否则你必须在当前线程上下文中.完成函数与SfRead并非同一个线程.所以在完成函数中按这个地址去获取数据是不对的.如何回到当前线程?我采用简单的办法.在SfRead中设置一个事件,调用IoCallDriver之后开始等待这个事件.而在完成函数中设置这个事件.这样等待结束的时候,刚好Irp已经完成,我也回到了我的SfRead原来的线程.
那么,获得读取内容的主要方法如下:
KEVENT waitEvent;
KeInitializeEvent( &waitEvent,
NotificationEvent,
FALSE );
IoCopyCurrentIrpStackLocationToNext ( Irp );
IoSetCompletionRoutine( Irp,
SfReadCompletion,
&waitEvent,
TRUE,
TRUE,
TRUE );
status = IoCallDriver( devExt->AttachedToDeviceObject, Irp );
if (STATUS_PENDING == status) {
status = KeWaitForSingleObject( &waitEvent,
Executive,
KernelMode,
FALSE,
NULL );
ASSERT( STATUS_SUCCESS == status );
}
到这里请求已经完成,可以去获得读取的内容了,在irp->UserBuffer或者irp->MdlAddress中。
这一段就是前面SfFsControlMountVolume中对应段落的拷贝,要求是一样的,就是把请求完成掉. SfReadCompletion的内容也是一样的,就是设置一下事件.
至于写操作的内容,则不用完成,可以直接从irp->UserBuffer或irp->MdlAddress中得到.
10.读请求的完成
尽管我们得到了读过程的所有参数和结果,我们依然不知道如果自己写一个文件系统,该如何完成读请求,或者过滤驱动中,如何修改读请求.
除非是一个完整的文件系统,完成读操作似乎是不必要的。过滤驱动一般只需要把请求交给下层的实际文件系统来完成。但是有时候比如加解密操作,我希望从下层读到数据,解密后,我自己来完成这一IRP请求。
这里要谈到IRP的minor function code.以前已经讨论到如果major function code 是IRP_MJ_READ则是Read请求。实际上有些主功能号下面有一些子功能号,如果是IRP_MJ_READ,检查其MINOR,应该有几种情况:IRP_MN_NORMAL,IRP_MN_MDL,IRP_MN_MDL|IRP_COMPLETE(这个其实就是IRP_MN_MDL_COMPLETE).还有其他几种情况,资料上有解释,但是我没自己调试过,也就不说了。只拿自己调试过的几种情况来说说。
IRP_MN_NORMAL的情况完全与上一节同
注意如上节所叙述,IRP_MN_NORMAL的情况,既有可能是在Irp->MdlAddress中返回数据,也可能是在Irp->UserBuffer中返回数据,这个取决于Device的标志.
但是如果次功能号为IRP_MN_MDL则完全不是这个意思。这种irp一进来看数据,就赫然发现Irp->MdlAddress和Irp->UserBuffer都为空。那你得到数据后把数据往哪里拷贝呢?
IRP_MN_MDL的要求是请自己分配一个MDL,然后把MDL指向你的数据所在的空间,然后返回给上层。自然MDL是要释放的,换句话说事业使用完毕要归还,所以又有IRP_MN_MDL_COMPLETE,意思是一个MDL已经使用完毕,可以释放了。
MDL用于描述内存的位置。据说和NDIS_BUFFER用的是同一个结构。这里不深究,我写一些函数来分配和释放mdl,并把mdl指向内存位置或者得到mdl所指向的内存:
// 这个函数分配mdl,缓冲必须是非分页的。可以在dispatch level运行。
_inline PMDL MyMdlAllocate(PVOID buf, ULONG length)
{
PMDL pmdl = IoAllocateMdl(buf,length,FALSE,FALSE,NULL);
if(pmdl == NULL)
return NULL;
MmBuildMdlForNonPagedPool(pmdl);
return pmdl;
}
// 这个函数分配一个mdl,并且带有一片内存
_inline PMDL MyMdlMemoryAllocate(ULONG length)
{
PMDL mdl;
void *buffer = ExAllocatePool (NonPagedPool,length);
if(buffer == NULL)
return NULL;
mdl = MyMdlAllocate (buffer,length);
if(mdl == NULL)
{
ExFreePool(buffer);
return NULL;
}
return mdl;
}
// 这个函数释放mdl并释放mdl所带的内存。
_inline void MyMdlMemoryFree(PMDL mdl)
{
void *buffer = MmGetSystemAddressForMdlSafe(mdl,NormalPagePriority);
IoFreeMdl(mdl);
ExFreePool(buffer);
}
要完成请求还有一个问题。就是irp->IoStatus.Information.在这里你必须填上实际读取得到的字节数字。不然上层不知道有多少数据返回。这个数字不一定与你的请求的长度等同(其实我认为几乎只要是成功,就应该都是等同的,唯一的例外是读取到文件结束的地方,长度不够了的情况)。必须设置这个数值:
irp->IoStatus.Information = infor;
也许你都烦了,但是还有事情要做。作为读文件的情况,如果你是自己完成请求,不能忘记移动一下文件指针。否则操作系统会不知道文件指针移动了而反复读同一个地方永远找不到文件尾,我碰到过这样的情况。 一般是这样的,如果文件读取失败,请保持原来的文件指针位置不要变。如果文件读取成功,请把文件指针指到“读请求偏移量+成功读取长度”的位置。
这个所谓的指针是指Irp->FileObject->CurrentByteOffset.
我跟踪过正常的windows文件系统的读行为,我认为并不一定是向我上边说的这样做。情况很复杂,有时动,有时不动(说复杂当然是因为我不理解),但是按我上边说的方法来完成,我还没有发现过错误。
现在看看怎么完成这些请求,假设我已经有数据了。这些当然都是在SfRead中或者是其他想完成这个irp的地方做的(希望你还记得我们是如何来到这里),假设其他必要的判断都已经做了:
switch(irpsp->MiniorFunction)
{
// 我先保留文件的偏移位置
case IRP_MN_NORMAL:
{
Void *buffer;
if(Irp->MdlAddress != NULL)
buffer = MmGetSystemAddressForMdlSafe(irp->MdlAddress,NormalPagePriority)
else
buffer = Irp->UserBuffer;
… … // 如果有数据,就往buffer中写入…
Irp->IoStatus.Information = length;
Irp-> IoStatus.Status = STATUS_SUCCESS;
Irp->FileObject->CurrentByteOffset.Quat = offset.Quat+length;
IoCompleteRequest( Irp, IO_NO_INCREMENT );
return STATUS_SUCCESS;
}
case IRP_MN_MDL:
{
PMDL mdl = MyMdlMemoryAllocate (length); // 情况比上边的复杂,请先分配mdl
if(mdl == NULL)
{
// ... 返回资源不足 ...
}
Irp->MdlAddress = mdl;;
… … // 如果有数据,就往MDL的buffer中写入…
Irp->IoStatus.Information = length;
Irp-> IoStatus.Status = STATUS_SUCCESS;
Irp->FileObject->CurrentByteOffset.Quat = offset.Quat+length;
IoCompleteRequest( Irp, IO_NO_INCREMENT );
return STATUS_SUCCESS;
}
case IRP_MN_MDL_COMPLETE:
{
// 没有其他任务,就是释放mdl
Irp->IoStatus.Information = length;
Irp-> IoStatus.Status = STATUS_SUCCESS;
Irp->FileObject->CurrentByteOffset.Quat = offset.Quat+length;
IoCompleteRequest( Irp, IO_NO_INCREMENT );
return STATUS_SUCCESS;
}
default:
{
// 我认为其他的情况不过滤比较简单 ...
}
}
重要提醒:IRP_MN_MDL的情况,需要分配一个mdl,并且这个mdl所带有的内存是有一定长度的,这个长度必须与后来的irp->IoStatus.Information相同!似乎上层并不以irp->IoStatus.Information返回的长度为准。比如明明只读了50个字节,但是你返回了一个mdl指向内存长度为60字节,则操作系统则认为已经读了60个字节!这非常糟糕。
最后提一下文件是如何结尾的。如果到某一处,返回成功,但是实际读取到的数据没有请求的数据长,这时还是返回STATUS_SUCCESS,但是此后操作系统会马上发irp来读最后一个位置,此时返回长度为0,返回状态STATUS_FILE_END即可。
已经解释了读请求。我不会再讲解写请求了。相信读者有能力自己搞清楚。
11.文件和目录的生成打开,关闭与删除
我们已经分析了读,写与读类似。文件系统还有其他的操作。比如文件或目录的打开(打开已经存在的或者创建新的),关闭。文件或目录的移动,删除。
实际上FILE_OBJECT并不仅仅指文件对象。在windows文件系统中,目录和文件都是用FileObject来抽象的。这里产生一个问题,对于一个已经有的FileObject,我如何判断这是一个目录还是一个文件呢?
对于一个已经存在的FileObject,我没有找到除了发送IRP来向卷设备询问这个FileObject的信息之外更好的办法。自己发送IRP很麻烦。不是我很乐意做的那种事情。但是FileObject都是在CreateFile的时候诞生的。在诞生的过程中,确实有机会得到这个即将诞生的FileObject,是一个文件还是一个目录。
Create的时候,获得当前IO_STACK_LOCATION,假设为irpsp,那么irpsp->Parameters.Create的结构为:
struct {
PIO_SECURITY_CONTEXT SecurityContext;
ULONG Options;
USHORT FileAttributes;
USHORT ShareAccess;
ULONG EaLength;
};
这个结构中的参数是与CreateFile这个api中的参数对应的,请自己研究。取得方法如下:
ULONG options = irpsp->Parameters.Create.Options;
file_attributes = irpsp->Parameters.Create.FileAttributes;
options中有一个FILE_DIRECTORY_FILE;
file_attribute中有一个FILE_ATTRIBUTE_DIRECTORY;
然后我们搞清上边Options和FileAttributes的意思。是不是Options里边有FILE_DIRECTORY_FILE标记就表示这是一个目录?实际上,CreateOpen是一种尝试性的动作。无论如何,我们只有当CreateOpen成功的时候,判断FileObject才有意义。否则是空谈。
成功有两种可能,一是已经打开了原有的文件或者目录,另一种是新建立了文件或者目录。Options里边带有FILE_DIRECTORY_FILE表示打开或者生成的对象是一个目录。那么,如果在Create的完成函数中,证实生成或者打开是成功的,那么返回得到的FILE_OBJECT,确实应该是一个目录。
当我经常要使用我过滤时得到的文件或者目录对象的时候,我一般在Create成功的的时候捕获他们,并把他们记录在一个“集合”中。这时你得写一个用来表示“集合”的数据结构。你可以用链表或者数组,只是注意保证多线程安全性。因为Create的时候已经得到了属性表示FileObject是否是目录,你就没有必要再发送IRP来询问FileObject的Attribute了。
还上边的FileAttributes,似乎这个东西并不可靠。因为在生成或者打开的时候,你只需要设置Options。我认为这个字段并无法说明你打开的文件对象是目录。
这你需要设置一下Create的完成函数。请参考上边对文件读操作。
NTSTATUS SfCreateComplete(
IN DEVICE_OBJECT *DeviceObject,
IN IRP *irp,
IN PVOID context)
{
PIO_STACK_LOCATION irpsp = IoGetCurretIrpStackLocation(irp);
PFILE_OBJECT file = irpsp->FileObject;
UNREFERENCED_PARAMETER(DeviceObject);
if(NT_SUCCESS(irp->IoStatus.Status)
{
// 如果成功了,把这个FileObject记录到集合里,这是一个
// 刚刚打开或者生成的目录
if(file && (irpsp->Parameters.Create.Options & FILE_DIRECTORY_FILE) != 0)
MyAddObjToSet (file); // 把FileObject保存到一个集合里。这个函数请自己实现.
return irp->IoStatus.Status;
}
}
这里顺便解释一下UNREFERENCED_PARAMETER宏。我曾经不理解这个宏的意思。其实就是因为本函数传入了三个参数,这些参数你未必会用到。如果你不用的话,大家知道c编译器会发出一条警告。一般认为驱动应该去掉所有的警告,所以用了这个宏来“使用”一下没有用到过的参数。你完全可以不用他们。
现在所有的目录都被你记录。那么得到一个FileObject的时候,判断一下这个FileObject在不在你的集合里,如果在,就说明是目录,反之是文件。
当这个FileObject被关闭的时候你应该把它从你的集合中删除。你可以捕获Cleanup的IRP来做这个。因为判断FileObject是文件还是目录的问题,我们已经见识了文件的打开和关闭工作。
现在看一下文件是如何被删除的。
删除的操作,第一步是打开文件,打开文件的时候必须设置为可以删除。如果打开失败,则直接导致无法删除文件。第二步设置文件属性为用于删除,第三步关闭文件即可。关闭的时候,文件被系统删除。
不过请注意这里的“删除”并非把文件删除到回收站。如果要测试,你必须按住shift彻底删除文件。文件删除到回收站只是一种改名操作。改名操作我们留到以后再讨论。
第一步是打开文件,我应该可以在文件被打开的时候,捕获到的irpsp的参数,记得前边的参数结构,中间有:
PIO_SECURITY_CONTEXT SecurityContext;
相关的结构如下:
typedef struct _IO_SECURITY_CONTEXT {
PSECURITY_QUALITY_OF_SERVICE SecurityQos;
PACCESS_STATE AccessState;
ACCESS_MASK DesiredAccess;
ULONG FullCreateOptions;
} IO_SECURITY_CONTEXT, *PIO_SECURITY_CONTEXT;
注意其中的DesiredAccess,其中必须有DELETE标记,才可以删除文件。
第二步是设置为”关闭时删除”。这是通过发送一个IRP(Set Information)来设置的。捕获主功能码为IRP_MJ_SET_INFORMATION的IRP后:
首先,IrpSp->Parameters.SetFile.FileInformationClass应该为FileDispositionInformation。
然后,Irp->AssociatedIrp.SystemBuffer指向一个如下的结构:
typedef struct _FILE_DISPOSITION_INFORMATION {
BOOLEAN DeleteFile;
} FILE_DISPOSITION_INFORMATION;
如果DeleteFile为TRUE,那么这是一个删除文件的操作。文件将在这个FileObject Close的时候被删除。
以上的我都未实际调试,也不再提供示例的代码。有兴趣的读者请自己完成。
12 自己发送Irp完成读请求
关于这个有一篇文档解释得很详细,不过我认为示例的代码有点太简略了,这篇文档在IFS所附带的OSR文档中,名字为”Rolling Your Own”,请自己寻找。
为何要自己发送Irp?在一个文件过滤驱动中,如果你打算读写文件,可以试用ZwReadFile.但是这有一些问题。Zw系列的Native API使 用句柄。一般句柄是有线程环境限制的。此外也有中断级别的限制。使用内核句柄要好一些。但是,Zw系列函数来读写文件,最终还是要发出Irp,又会被自己的过滤驱动捕获到。结果带来重入的问题。对资源也是浪费。那么最应该的办法是什么呢?当然是直接对卷设备发Irp了。
但是Irp是非常复杂的数据结构,而且又被微软所构造的很多未空开的部件所处理。所以自己发irp并不是一件简单的事情。 比较万能的方法是IoAllocateIrp,分配后自己逐个填写。问题是细节实在太多,很多无文档可寻。有兴趣的应该看看我上边所提及的
那篇文章“Rolling Your Own”。
有意思的是这篇文章后来提到了捷径,就是利用三个函数:
IoBuildAsynchronousFsdRequest(...)
IoBuildSynchronousFsdRequest(...)
IoBuildDeviceIoControlRequest(...)
于是我参考了他这方面的示例代码,发现运行良好,程序也很简单。建议怕深入研究的选手就可以使用我下边提供的方法了。
首先的建议是使用IoBuildAsynchronousFsdRequest(),而不要使用同步的那个。使用异步的Irp使irp和线程无关。而你的过滤驱动一 般很难把握当前线程(如果你开一个系统线程来专门读取文件那例外)。此时,你可以轻松的在Irp的完成函数中删除你分配过的Irp,避免去追究和线程相关的事情。
但是这个方法有局限性。文档指出,这个方法仅仅能用于IRP_MJ_READ,IRP_MJ_WRITE,IRP_MJ_FLUSH_BUFFERS,和IRP_MJ_SHUTDOWN.
刚好我这里仅仅要求完成文件读。
用Irp完成文件读需要一个FILE_OBJECT.FileObject是比Zw系列所用的句柄更好的东西。因为这个FileObject是和线程无关的。你可以放心的在未知的线程中使用他。
自己要获得一个FILE_OBJECT必须自己发送IRP_MJ_CREATE的IRP.这又不是一件轻松的事情。不过我跳过了这个问题。因为我是文件系 统过滤驱动,所以我从上面发来的IRP中得到FILE_OBJECT,然后再构造自己的IRP使用这个FILE_OBJECT,我发现运行很好。
在后面的”解决重入问题”一章中,我们给出自己打开文件,并跳过重入的简单方法.
但是又出现一个问题,如果IRP的irp->Flags中有IRP_PAGING(或者说是Cache管理器所发来的IRP)标记,则其FileObject我获得并使用后,老是返回错误。阅读网上的经验表明,带有IRP_PAGINGE的FileObject不可以使用.于是我避免使用这时的FileObject.我总是使用不带IRP_PAGING的Irp(认为是用户程序发来的读请求)的FileObject。
好,现在废话很多了,现在来看看构造irp的代码: // 构造IRP
if(read_or_write)
irp = IoBuildAsynchronousFsdRequest(
IRP_MJ_READ,dev,buffer,*length,offset,NULL);
else
irp = IoBuildAsynchronousFsdRequest(
IRP_MJ_WRITE,dev,buffer,*length,offset,NULL);
if(irp == NULL)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
irp->Flags = 0x43; // 这是我喜欢的一种Flags.建议你查DDK帮助然后填写标准的宏
KeInitializeEvent(&event,NotificationEvent,FALSE);
my_context.event = &event; // 为了可以等待完成,我设置一个事件传入
IoSetCompletionRoutine(irp,MyIrpComplete,&my_context,TRUE,TRUE,TRUE);
buffer是缓冲。在Irp中被用做UserBuffer接收数据。offset是 这次读的偏移量。以上代码构造一个读irp.请注意,此时您还没有设置FileObject.实际上我是这样发出请求的:
irpsp = IoGetNextIrpStackLocation(irp);
irpsp->FileObject = file;
status = IoCallDriver(dev,irp);
irp = NULL;
if(status == STATUS_PENDING)
KeWaitForSingleObject(&event,Executive,KernelMode,FALSE,NULL);
// 到这里请求就完成了,请做自己的处理…
再看看MyIrpComplete如何收场:
// 一个通用的irp完成函数
static NTSTATUS MyIrpComplete (
PDEVICE_OBJECT dev,
PIRP irp,
PVOID context)
{
// 设置事件
PMY_READ_CONTEXT my_context = (PMY_READ_CONTEXT)context;
KeSetEvent(my_context->event,IO_NO_INCREMENT,FALSE);
my_context->information = irp->IoStatus.Information;
my_context->status = irp->IoStatus.Status;
// 释放irp,过程非常复杂
if (irp->MdlAddress)
{
MmUnmapLockedPages(
MmGetSystemAddressForMdl(irp->MdlAddress),
irp->MdlAddress);
MmUnlockPages(irp->MdlAddress);
IoFreeMdl(irp->MdlAddress);
}
IoFreeIrp(irp);
// 返回处理未结束.
return STATUS_MORE_PROCESSING_REQUIRED;
}
13 如何实现路径过滤
文件过滤系统中很多过滤都是以操作的文件的路径作为过滤条件的.比如你想控制某个目录下的文件被加密.或者是被映射到其他地方.或者是其他的操作,你都必须用到路径过滤.但是即使是这么常见的一个操作,在IFSDDK的基础上做起来也绝对不是那么简单.
取得文件的路径有三种情况:
第一:在文件打开之前从打开文件的请求中提取路径.FileMon从FileObject->FileName中提取文件路径其实就是对这个的实现.后面我们讨论这种方法的困难.
第二:在文件CREATE IRP处理结束后获取路径.这是SFilter演示了的也是最容易解决的.
第三:在文件过滤其他IRP时,(如改名,查询,设置,读,写)的时候得到FileObject所对应的文件路径.
以上三种情况以第一种路径获取最为麻烦.第二种情况最为简单.我先介绍一下我在第二种情况下的做法.
基本做法与SFilter同.因为此时FileObject已经生成结束,对于这个对象,你可以使用ObQueryNameString.这个调用返回一个路径.对某一个文件对象,返回结果大致如此:
"/Device/HardDiskVolume1/MyDirectory/MyFile.Name"
这里再次强调一下,必须在Create完成之后再进行这个调用.如果直接在sfCreate()函数的早期调用这个,你只能得到:
"/Device/HardDiskVolume1"
这个也有一定的作用,可以设法获得盘符.但是得不到完整路径.
另一个需要注意的是,ObQueryNameString只能在CREATE IRP和CLEAN UP IRP的处理中使用,除非你能自己下发IRP,一般都会使系统进入死锁.
下面的问题是如何获得盘符.盘符的取得是要把"/Device/HardDiskVolume1"这样的名字转换为"C:"这样的符号连接名.也可以直接用卷设备的DEVICE_OBJECT去获取.这可以用到两个函数:
NTSTATUS
RtlVolumeDeviceToDosName(
IN PVOID VolumeDeviceObject,
OUT PUNICODE_STRING DosName);
NTSTATUS
IoVolumeDeviceToDosName(
IN PVOID VolumeDeviceObject,
OUT PUNICODE_STRING DosName);
其中第二个函数据称是更高级的版本.但是要XP以上系统才支持.2K只有RtlVolumeDeviceToDosName.这给你编写2K和XP兼容的驱动带来困难,因为你不得不动态导入IoVolumeDeviceToDosName,否则你的驱动在2K下可能无法加载.
但是这两个函数在2K下尤其是驱动静态加载的时候都似乎都有问题.在2K+SP4的情况下一般有IoVolumeDeviceToDosName函数的导出存在.我两个函数都试验了.每次都是动态加载没有问题,而静态加载过程中调用却会在IoVolumeDeviceToDosName或IoVolumeDeviceToDosName中死机.似乎有一个导致死锁的Device Io Control IRP被发到某一个设备导致系统死了.具体的原因我不清楚,而且也不知道是否是我编程的错误导致的.但是这种情况使我只好寻找更加可靠的办法.如果你有正确的方法,希望你能发一个邮件给我(mfc_tan_wen@163.com).
既然"C:","D:"这样的东西其实是符号连接,对应了"/Device/HardDiskVolume1","/Device/HardDiskVolume2"这样的设备的话,我可以用ZwQuerySymbolicLinkObject查询这个符号连接,找出它所对应的实际名,然后与"/Device/HardDiskVolume1"这样的字符串做比较,从而把"/Device/HardDiskVolume1"这样的设备名字转变为盘符.
我自己定义了一个能动态分配内存的wd_ustr_h字符串来代替UNICODE_STRING.有兴趣的读者可以自己实现它或者直接用UNICODE_STRING.
// 以下的代码得到一个符号连接的目标
wd_ustr_h wd_symbolic_target_ustr(PUNICODE_STRING symbolic)
{
... // 初始化对象特性表,代码被省略,请察看源代码...
InitializeObjectAttributes( ... ...
// 打开符号联接
status = ZwOpenSymbolicLinkObject(
&link_handle,
GENERIC_READ,
&attributes);
if(!NT_SUCCESS(status))
return NULL;
wd_ustr_init_em(&target, buf, 8*sizeof(WCHAR));
// 查询符号联接对象
status = ZwQuerySymbolicLinkObject(link_handle, &target, &length);
... // 判断返回值并分配空间等代码被省略...
if(NTSUCCESS(status))
{
// 保存一个目标字符串句柄指针,并返回
target_ret = wd_ustr_h_alloc_from_ustr(&target);
}
… …
ZwClose(link_handle);
return target_ret;
}
下面的方法是把一个类似"/Device/HardDiskVolume2"这样的字符串和"C:"-"Z:"所有的目标进行比较,以得到正确的盘符:
wd_ustr_h wd_vol_name_dos_name(wd_wchar *name)
{
// 符号连接的全称应该是L"//DosDevices//X:".X可以替换成C-Z任意一个字母.这里没有考虑A,B两个软驱.
wd_wchar vol_syb[] = { L"//DosDevices//X:" };
wd_ustr vol_name;
wd_wchar c;
if(name == NULL)
return NULL;
wd_ustr_init(&vol_name, name);
// 遍历,逐个得到目标并比对字符串
for(c = L'A'; c < (L'Z'+1); ++c)
{
wd_ustr_h my_target = NULL;
vol_syb[12] = c;
my_target = wd_symbolic_target(vol_syb);
if( my_target != NULL &&
wd_ustr_cmp(wd_ustr_h_ustr(my_target), &vol_name, wd_true) == 0 )
{
wd_ustr_h_free(my_target);
break;
}
if(my_target != NULL)
{
wd_printf0("FF:%wZ/r/n",wd_ustr_h_ustr(my_target));
wd_ustr_h_free(my_target);
}
}
// 判断返回结果
if(c == L'Z'+1)
return NULL;
else
return wd_ustr_h_alloc(&vol_syb[12]);
}
得到盘符后就可以组合得到完整的路径.这个方式我测试过,无论动态加载或者静态加载启动的时候调用,都不会死机.
然后是在文件过滤其他IRP时,(如改名,查询,设置,读,写)的时候得到FileObject所对应的文件路径.
在文件读写的时候往往没有好的办法可以得到文件路径.对FileObject进行ObQueryNameString很容易导致死机.这似乎是微软自己留下的问题.但是如果你直接向下层设备发IRP进行查询,就不会死机.发送QueryIRP的代码比较简单,可以在网上找到.但是IRP的构建依赖于非文档的方法,总是不那么可靠,未来难保在兼容性上不出现问题.我希望不用非文档的方法.
此时如果你读FileObject->FileName,会发现这个名字一般都依然存在.你可以用它作为文件路径.不过这依然有不少问题.首先FileObject->FileName只是为了生成这个文件而填写的请求路径.既然这个文件已经生成,那么这个路径就是可以丢弃的了(它存在并不表示不可能被丢弃).其次这个文件路径里面可能含有短名(例如mydire~1),这样的路径和你想要得全路径不同,有可能导致跳过你的安全过滤.而且其中不含有盘符.如果需要得到盘符,可能还需要Query,这又是一个常见的死机原因.
我用了一个可能不是很有效率的办法,既然我在Create IRP处理结束后已经得到了我要的长路径,那么我可以把它存在一个表中.当FileObject被CleanUp的时候,我清除这个表项以避免内存泄漏.然后再无论是Read,Write,Query,Set或者是Rename(Set的一种)的时候,我都可以通过这个表来查询我的路径.
应该用Map增加效率.Map是数据结构问题。请诸位读者自己实现了。方法简述如下:
1. SfCreate中,获得FileObject的文件路径(用前面的方法),并把FileObject指针和路径的对应关系,保存在一个Map中。
2. 在任何时候都可以在表中查询一个FileObject对应的路径.不必担心重入和中断级等等问题。
3. 在SfCleanUp中删去该FileObject对应的节点。
最后一个问题是如何在Create IRP处理之前得到路径名.
可能唯一的途径是通过FileObject->FileName.要注意FileObject->ReleatedObject不为空的情况.这个时候FileObject->FileName是RelatedObject的相对路径.首先要ObQueryNameString这个对象.得到路径之后再和FileObject->FileName组合.然后是其中可能含有的短名转换为长名的问题.我没有找到简易的方法.我的同事CardMagic提供了一个非常麻烦但是确实有效的办法.其思想是:
首先你假设你得到一个路径 /aaaaaa~1/bbbbbb~1/cccccc~1/dddddd~1.txt.然后你把它分解成:
/
aaaaaa~1
bbbbbb~1
cccccc~1
dddddd~1.txt
以上5个对象.首先打开用ZwCreateFile打开第一个目录.第一个目录总是"/",这不可能是短名.然后调用ZwQueryDirectoryFile枚举下面所有的文件和目录.如果你用FileIdBothDirectoryInformation进行查询.那么会得到一组FILE_ID_BOTH_DIR_INFORMATION,代表下面每个文件和目录:
typedef struct _FILE_ID_BOTH_DIR_INFORMATION {
ULONG NextEntryOffset;
ULONG FileIndex;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
ULONG EaSize;
CCHAR ShortNameLength;
WCHAR ShortName[12]; // 这里有短名
LARGE_INTEGER FileId;
WCHAR FileName[1]; // 这里有长名
} FILE_ID_BOTH_DIR_INFORMATION, *PFILE_ID_BOTH_DIR_INFORMATION;
长短名都到手了,那么我们当然可以找到"/"之下的第一个"aaaaaa~1"所对应的长名了.然后依次类推,逐个查询.这真是个麻烦的办法,但是确实有效.
有时我认为可以直接打开/aaaaaa~1/bbbbbb~1/cccccc~1/dddddd~1.txt 或者/aaaaaa~1/bbbbbb~1/cccccc~1/来QueryNameString,但是CardMagic说,网上有人说这样做依然不可靠,可能得到短名.
14 避免重入
在文件过滤驱动中进行文件操作,重入是最严重的麻烦根源之一.首先了解一下什么是重入.如果你调用一个函数,这次这个函数执行过程中,有必要再次调用这个函数,这就是重入.递归是一种常见的重入情况:
NTSTATUS SfCreate(…)
{
SfCreate(…);
}
以上就是一个死递归.合理的递归是必须有终点的.死的递归(包括过深的递归)会导致windows内核调用栈溢出,系统崩溃出现蓝屏.
不过一般都不会直接在驱动中写以上的代码.真实的情况是这样的:
NTSTATUS SfCreate(…)
{
…
ZwCreateFile(…) // <- 这里会导致发出Irp,并再次被我们过滤到,等于这里再次调用SfCreate(…)
…
}
但重入绝对不是问题本身.你可以自由的利用重入实现你的功能,但是你必须避免死递归.如果我能判断这个请求是我自己发出的,我则跳过,这时重入虽然发生,但是对我并没有影响:
NTSTATUS SfCreate(…)
{
…
if(这个IRP不由我的驱动自己发出)
{
ZwCreateFile(…) // <- 这里会导致发出Irp,并再次被我们过滤到,等于这里再次调用SfCreate(…)
}
…
}
理论上考虑,既然我们是文件过滤驱动,那么我们打开文件的时候,就没有理由再经过设备的顶层了,应该直接往我们的下层发送请求.这个功能是可以实现的,就是调用IoCreateFileSpecifyDeviceObjectHint来打开文件,而不要用ZwCreateFile.
IoCreateFileSpecifyDeviceObjectHint可以直接指定设备对象。你就直接指定下层设备就可以了.这样上面的设备根本收不到IRP,打开文件的重入现象也不会再发生。
但是IoCreateFileSpecifyDeviceObjectHint这个函数在普通版本的2000下没有.xp和2000+SP4和以上都有这个函数.如果要兼容所有的2000版本,你必须另想办法.
使用影设备(Shadow Device)是网络上广为流传的办法.是非常优秀的解决方案.我这里再介绍一个更方便使用的,我经常用的原理简单的”土办法”:
回到上面的问题,主要的困难是
if(这个IRP不由我的驱动自己发出)
这个if如何实现呢?
我可以在ZwCreateFile中传入特殊的参数.但是这些参数也可能为其他的过滤驱动或者根本就被普通用户所用.没有完全保险的办法,让我得到一个IRP的时候,得知这个操作是我的驱动自己发出的,自己不要再过滤它.
如果是一个应用程序,那么很简单,我只要判断一下当前进程,就知道这个请求是不是自己发出的(自己的应用程序当然知道自己的进程号).而驱动是没有自己进程的。这样就有办法了,驱动虽然没有进程,但是我们可以自己生成一个线程.当我们有什么操作要进行又不想死递归的时候,就把这个操作放到线程中去做.这个线程号我们是知道的。以后任何IRP来到的时候,我们检查一下当前线程,如果是我们自己的线程,就跳过去,这样就避免了死递归的可能。
// 这是一个函数类型,这中间你可以做任何事情
typedef void ( *PWIT_DO)(void *context);
// 这是一个数据结构,表示一个线程
typedef struct WIT_THTREAD_
{
LIST_ENTRY list; // Request list in our thread.
HANDLE tid; // Thread id.
KEVENT event; // An event to inform the thread to process a request.
KSPIN_LOCK lock; // A lock used by the list.
} WIT_THREAD,*PWIT_THREAD;
// 一个任务节点。我把我要做的一个任务,写入这个结构中.
typedef struct WIT_NODE_
{
LIST_ENTRY list;
void *context;
KEVENT event;
PWIT_DO do_somthing;
}WIT_NODE,*PWIT_NODE;
然后我自己来生成一个线程,代码如下:
PWIT_THREAD WITCreateThread(OUT NTSTATUS *status)
{
PWIT_THREAD my_thread;
my_thread = ExAllocatePoolWithTag(NonPagedPool,sizeof(WIT_NODE),WIT_TAG);
if(my_thread == NULL)
{
*status = STATUS_INSUFFICIENT_RESOURCES;
return NULL;
}
InitializeListHead(&my_thread->list);
KeInitializeSpinLock(&my_thread->lock);
KeInitializeEvent(&my_thread->event,SynchronizationEvent,FALSE);
*status = PsCreateSystemThread(
&my_thread->tid,
(ACCESS_MASK) 0L,
NULL,
NULL,
NULL,
WITThreadProc,
(PVOID)my_thread);
if(!NT_SUCCESS(*status))
{
ExFreePool(my_thread);
return NULL;
}
return my_thread;
}
这里的线程是一个死循环,寻找有没有要完成的任务.如果有,就完成它.
void WITThreadProc(IN PVOID context)
{
PWIT_THREAD mythread = (PWIT_THREAD)context;
PWIT_NODE node = NULL;
for (;;)
{
// 等待有任务发生
KeWaitForSingleObject(
&mythread->event,
Executive,
KernelMode,
FALSE,NULL);
// 有就完成
while ( node = (PWIT_NODE)ExInterlockedRemoveHeadList(
&mythread->list,
&mythread->lock))
{
node->do_somthing(node->context);
KeSetEvent(&node->event,IO_NO_INCREMENT,FALSE);
}
}
}
然后是我们如何插入任务的问题,代码如下:
NTSTATUS WITDoItInThread(IN PWIT_THREAD thread,IN OUT PVOID context,IN PWIT_DO do_somthing)
{
PWIT_NODE node;
node = ExAllocatePoolWithTag(NonPagedPool,sizeof(WIT_NODE),WIT_TAG);
if(node == NULL)
return STATUS_INSUFFICIENT_RESOURCES;
node->context = context;
node->do_somthing = do_somthing;
KeInitializeEvent(&node->event,SynchronizationEvent,FALSE);
ExInterlockedInsertTailList(
&thread->list,
&node->list,
&thread->lock);
KeSetEvent(
&thread->event,
(KPRIORITY) 0,
FALSE);
KeWaitForSingleObject(
&node->event,Executive, KernelMode,FALSE, NULL);
ExFreePool(node);
return STATUS_SUCCESS;
}
WITDoItInThread的调用非常容易。只要把需要完成的操作放在do_somthing里,参数与返回值放在context中,直接调用这个函数,这个函数就会把任务插入我们的工作线程队列,并等待完成后再返回.
现在你可以在这个线程里做任何事情,包括生成文件,读写和其他操作等.得到IRP的时候,可以通过线程id来判断是否我们自己的线程,来跳过重入问题:
BOOLEAN WITIsMyThread(IN PWIT_THREAD thread)
{
return (PsGetCurrentThreadId() == thread->tid);
}
15 结语与展望
这并不是一本关于文件过滤的技术大全.因此,我忽略了很多重要的主题.比如各种目录控制和文件属性查询与设置,Cache管理器,网络文件系统,文件系统和存储设备的关系等等。但是相信读者已经有能力自己去研究他们了。
非常感谢您阅读此书.此外,有以下的额外事项需要声明:
我们学习了微软的DDK使用的技术.我们将利用它,但不是终身为它服务.
您将要为Windows开发驱动程序.我们的目标是:让更多的用户使用我们的软件.享受到我们的努力成果.而不是为了扩充Windows的功能,为微软打零工.
不要觉得你的代码只需要为Windows能使用就足够了。如果你的代码中到处都用到DDK中定义的结构和调用,比如使用DEVICE_OBJECT并用直接用ExAllocatePool分配内存,你的项目可能进展比较快,但是后悔也来得快得多。
把DDK当作工具使用吧.但不要让他限制住你. 微软的技术是有效的,但并不是优秀的。
Sfilter对微软来说是比较古老的架构.之后发布了新的微端口文件过滤驱动的架构.把你的代码大量的插入Sfilter中,并融为一体是不智的.很快就会带来移植方面的困难.
新的微端口文件过滤驱动的应用还不多.微软总是喜欢发布更复杂的接口,以便掩盖内部.但是它的方向不一定是对的.新发布的架构被抛弃的情况也不罕见.后面附带一篇关于微端口文件过滤驱动的翻译文章,便于有兴趣的读者了解.