0. 作者,楚狂人自述
我长期网上为各位项目经理充当“技术实现者”的角色。我感觉Windows文件系统驱动的开发能找到的资料比较少。为了让技术经验不至于遗忘和引起大家交流的兴趣我以我的工作经验撰写本教程。
我的理解未必正确,有错误的地方望多多指教。有问题欢迎与我联系。我们也乐于接受各种驱动项目的开发。邮箱为[email protected],QQ为16191935。
对于这本教程,您可以免费获得并随意修改,向任何网站转贴。但是不得剽窃任何内容作为任何赢利出版物的全部或者部分。
1. 概述,钻研目的和准备
我经常在网上碰到同行请求开发文件系统驱动。windows的pc机上以过滤驱动居多。其目的不外乎有以下几种:
一是用于防病毒引擎。希望在系统读写文件的时候,捕获读写的数据内容,然后检测其中是否含有病毒代码。
二是用于加密文件系统,希望在文件写过程中对数据进行加密,在读的过程中进行解密。
三是设计透明的文件系统加速。读写磁盘的时候,合适的cache算法是可以大大提高磁盘的工作效率。windows本身的cache算法未必适合一些特殊的读写
磁盘操作(如流媒体服务器上读流媒体文件)。设计自己的cache算法的效果,我已在工作中有所感受。
如果你刚好有以上此类的要求,你可以阅读本教程。
文件系统驱动是windows系统中最复杂的驱动种类之一。不能对ifsddk中的帮助抱太多希望,以我的经验看来,文件系统相关的ddk帮助极其简略,很多重要的部分仅仅轻描淡写的带过。如果安装了ifsddk,应该阅读srcfilesysOSR_docs下的文档。而不仅仅是ddk帮助。
文件系统驱动开发方面的书籍很少。中文资料我仅仅见过侯捷翻译过的一本驱动开发的书上有两三章涉及,也仅仅是只能用于9x的vxd驱动。NT文件系统我见过一本英文书。我都不记得这两本书的书名了。
如果您打算开发9x或者nt文件系统驱动,建议你去网上下载上文提及的书。那两本书都有免费的电子版本下载。如果你打算开发Windows2000WindowsXPWindow2003的文件系统驱动,你可以阅读本教程。虽然本教程仅仅讲述文件系统过滤驱动。但是如果您要开发一个全新的文件系统驱动的话,本教程依然对你有很大的帮助。
学习文件系统驱动开发之前,应该在机器上安装ifsddk。ddk版本越高级,其中头文件中提供的系统调用也越多。经常有人询问如xpddk编译的驱动能不能在2000上运行等等的问题。我想可以这样解释:高级版本的ddk应该总是可以编译低级驱动的代码,而且得到的二进制版本也总是可以在低级系统上运行。但是反过来就未必可以了。如果在高级系统上编写用于低级系统上的驱动,要非常认真的注意仅仅调用低级系统上有的系统调用。
ifsddk可以在某些ftp上免费下载。
我的使用的是ifs ddk for xp,但是我实际用来开发的两台机器有一台是windows 2000,另一台是windows 2003.我尽量使我编译出来的驱动,可以在2000xp2003三种系统上都通过测试。
安装配置ddk和在vc中开发驱动的方法网上有很多的介绍。ifsddk安装之后,src目录下的filesys目录下有文件系统驱动的示例。阅读这些代码你就可以快速的学会文件系统驱动开发。
filter目录下的sfilter是一个文件系统过滤驱动的例子。另一个filespy完全是用这个例子的代码加工得更复杂而已。
如何用ddk编译这个例子请自己查看相关的资料。
文件系统过滤驱动编译出来后你得到的是一个扩展名为sys的文件。同时你需要写一个.inf文件来实现这个驱动的安装。我这里不讨论.inf文件的细节,你可以直接用sfilter目录下的inf文件修改。
对inf文件点鼠标右键弹出菜单选择“安装”,即可安装这个过滤驱动。但是必须重新启动系统才生效。
如果重启后蓝屏无法启动,可以用其他方式引导系统后到system32drivers目录下删除你的.sys文件再重启即可。我尝试这种情况下用安全模式结果还是蓝屏。所以我后来不得不在机器上装了两个2000系统。双系统情况下,一个系统崩溃了用另一个系统启动,删除原来的驱动即可。
如果要调试代码,请安装softice.
打开sfilter目录下的文件sources(这个文件没有扩展名),加入一行
BROWSER_INFO=1
然后打开Symbol Loader,File->Open选中你编译出来的xxx.sys,Modul->Load,Modul->Translate,然后就可以调试了。
打开softice,输入file *就可以看见代码。
如果准备好了,我们就可以开始琢磨windows文件系统过滤驱动的开发了。
1. 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.当系统用到你的驱动,会向你的DO发送IRP(这是windows所有驱动的共同工作方式)。你的任务是在dispatch function中处理这些请求。你可以让irp失败,也可以成功返回,也可以修改这些irp,甚至可以自己发出irp。
设备对象则是指DEVICE_OBJECT.下边简称DO.
但是实际上每个irp都是针对DO发出的。只有针对由该驱动所生成的DO的IRP,
才会发给该驱动来处理。
当一个应用程序打开文件并读写文件的时候,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.那么C:,D:则是Fat的两个Volume设备对象.
实际上"C:"是该设备的符号连接(Symbolic Link)名。而不是真正的设备名。可以打开Symbolic Links Viewer,能看到:
C: DeviceHarddiskVolume1
因此该设备的设备名为“DeviceHarddiskVolume 1” .
这里也看出来,文件系统驱动是针对每个Volume来生成一个DeviceObject,而不是针对每个文件的。实际上对文件的读写的irp,都发到Volume设备对象上去了。并不会生成一个“文件设备对象”。
掌握了这些概念的话,我们现在用简单的代码来生成我们的CDO,作为我们开发文件系统驱动的第一步牛刀小试。
我不喜欢用微软风格的代码。太长而且难看。我对大部分数据结构和函数进行了重定义。为此我写了一个名为wdf.h的头文件帮助我转换。有兴趣的读者可以发邮件向索取这个文件。没有也没有关系,我总是会写出wd_xxx系列的东西在DDK中的原形。
// -----------------wdf_filter.c中的内容-------------------------
#include "wdf.h"
wd_stat wdff_cdo_create(in wd_drv *driver,
in wd_size exten_len,
in wd_ustr *name,
out wd_dev **device)
{
return wd_dev_create(
driver,
exten_len,
name,
wd_dev_disk_fs,
wdf_dev_secure_open,
wd_false,
device);
}
wd_stat wd_main(in wd_drv* driver,
in wd_ustr* reg_path)
{
wd_ustr name;
wd_stat status = wd_stat_suc;
// 然后我生成控制设备,虽然现在我的控制设备什么都不干
wd_ustr_init(&name,L"/FileSystem/Filters/our_fs_filter");
status = wdff_cdo_create(driver,0,&name,&g_cdo);
if(!wd_suc(status))
{
if(status == wd_stat_path_not_found)
{
// 这种情况发生于FileSystemFilters路径不存在。这个路径是
// 在xp上才加上的。所以2000下会运行到这里
wd_ustr_init(&name,L"/FileSystem/our_fs_filter");
status = wdff_cdo_create(driver,0,&name,&g_cdo);
};
if(!wd_suc(status))
{
wd_printf0("error: create cdo failed.rn");
return status;
}
}
wd_printf0("success: create cdo ok.rn");
return status;
}
为了让代码看起来象上边的那样,我不得不做了很多转换。如
#define DriverEntry wd_main
一种爽的感觉,终于可以在写看起来更象是main()的函数中工作了。 wd_dev_create 这个函数内部调用的是IoCreateDevice.而wd_suc实际上是SUCCESS()这样的宏。
// ----------------------wdf.h中的内容------------------------------
#include "ntifs.h"
#define in IN
#define out OUT
#define optional OPTIONAL
#define wd_ustr UNICODE_STRING
#define wdp_ustr PUNICODE_STRING
#define wd_main DriverEntry
// 设备、驱动对象类型
typedef DRIVER_OBJECT wd_drv;
typedef DEVICE_OBJECT wd_dev;
typedef DRIVER_OBJECT wd_pdrv;
typedef PDEVICE_OBJECT wd_pdev;
enum {
wd_dev_disk_fs = FILE_DEVICE_DISK_FILE_SYSTEM,
wd_dev_cdrom_fs = FILE_DEVICE_CD_ROM_FILE_SYSTEM,
wd_dev_network_fs = FILE_DEVICE_NETWORK_FILE_SYSTEM
};
// 状态相关的类型和宏
typedef NTSTATUS wd_stat;
enum {
wd_stat_suc = STATUS_SUCCESS,
wd_stat_path_not_found = STATUS_OBJECT_PATH_NOT_FOUND,
wd_stat_insufficient_res = STATUS_INSUFFICIENT_RESOURCES,
wd_stat_invalid_dev_req = STATUS_INVALID_DEVICE_REQUEST,
wd_stat_no_such_dev = STATUS_NO_SUCH_DEVICE,
wd_stat_image_already_loaded = STATUS_IMAGE_ALREADY_LOADED,
wd_stat_more_processing = STATUS_MORE_PROCESSING_REQUIRED,
wd_stat_pending = STATUS_PENDING
};
_inline wd_bool wd_suc(wd_stat state)
{
return NT_SUCCESS(state);
}
#define wd_printf0 DbgPrint
_inline wd_void wd_ustr_init(in out wd_ustr* str,
in const wd_wchar* chars)
{
RtlInitUnicodeString(str,chars);
};
_inline wd_void wd_ustr_init_em(
in out wd_ustr*str,
in wd_wchar *chars,
in wd_size size)
{
RtlInitEmptyUnicodeString(str,chars,size);
};
wdf.h这个文件我仅仅节选了需要的部分。以上您已经拥有了一个简单的“驱动”的完整的代码。它甚至可以编译,安装(请修改sfilter.inf文件,其方法不过是将多处sfilter改为"our_fs_filter",希望这个过程中您不会出现问题)。然后把wdf.h和wdf_filter.c放在您新建立的目录下,这个目录下还应该有另两个文件。一个是Makefile,请从sfilter目录下拷贝。另一个是SOURCES,请输入如下内容:
TARGETNAME=our_fs_filter
TARGETPATH=obj
TARGETTYPE=DRIVER
DRIVERTYPE=FS
BROWSER_INFO=1
SOURCES=wdf_filter.c
使用ddk编译之后您将得到our_fs_filter.sys.把这个文件与前所描述的inf文件同一目录,按上节所叙述方法安装。
这个驱动不起任何作用,但是你已经成功的完成了"hello world".
2. 分发例程,fast io
上一节仅仅生成了控制设备对象。但是不要忘记,驱动开发的主要工作是撰写分发例程(dispatch functions.).接上一接,我们已经知道自己的DriverObject保存在上文代码的driver中。现在我写如下一个函数来指定一个默认的dispatch function给它。
//-----------------wdf.h中的代码----------------------
typedef PDRIVER_DISPATCH wd_disp_fuc;
_inline wd_void wd_drv_set_dispatch(in wd_drv* driver,
in wd_disp_fuc disp)
{
wd_size i;
for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
driver->MajorFunction = disp;
}
在前边的wd_main中,我只要加
wd_drv_set_dispatch(driver,my_dispatch_func);
就为这个驱动指定了一个默认的Dispatch Function.所有的irp请求,都会被发送到这个函数。但是,我可能不希望这个函数处理过于复杂,而希望把一些常见的请求独立出来,如Read,Write,Create,Close,那我又写了几个函数专门用来设置这几个Dispatch Functions.
//-----------------wdf.h中的代码----------------------
_inline wd_void wd_drv_set_read(
in wd_drv* driver,
in wd_disp_fuc read)
{
driver->MajorFunction[IRP_MJ_READ] = read;
}
_inline wd_void wd_drv_set_write(
in wd_drv* driver,
in wd_disp_fuc write)
{
driver->MajorFunction[IRP_MJ_WRITE] = write;
}
wd_void wd_drv_set_create(in wd_drv* driver,
in wd_disp_fuc create)
{
driver->MajorFunction[IRP_MJ_CREATE] = create;
driver->MajorFunction[IRP_MJ_CREATE_NAMED_PIPE] = create;
driver->MajorFunction[IRP_MJ_CREATE_MAILSLOT] = create;
}
wd_void wd_drv_set_file_sys_control(in wd_drv* driver,
in wd_disp_fuc control)
{
driver->MajorFunction[IRP_MJ_FILE_SYSTEM_CONTROL] = control;
}
wd_void wd_drv_set_clean_up(in wd_drv* driver,
in wd_disp_fuc clean_up)
{
driver->MajorFunction[IRP_MJ_CLEANUP] = clean_up;
}
wd_void wd_drv_set_close(in wd_drv* driver,
in wd_disp_fuc close)
{
driver->MajorFunction[IRP_MJ_CLOSE] = close;
}
别看我罗列n多代码,其实就是在设置driver->MajorFunction这个数组而已。因此在wd_main对dispatch functions的设置,就变成了下边这样的:
// 开始设置几个分发例程
wd_drv_set_dispatch(driver,my_disp_default);
wd_drv_set_create(driver,my_disp_create);
wd_drv_set_clean_up(driver,my_disp_clean_up);
wd_drv_set_file_sys_control(driver,my_disp_file_sys_ctl);
wd_drv_set_close(driver,my_disp_close);
wd_drv_set_read(driver,my_disp_read);
wd_drv_set_write(driver,my_disp_write);
下面的任务都在写my_xxx系列的这些函数了。但是对于这个DriverObject的设置,还并不是仅仅这么简单。
由于你的驱动将要绑定到文件系统驱动的上边,文件系统除了处理正常的IRP之外,还要处理所谓的FastIo.FastIo是Cache Manager调用所引发的一种没有irp的请求。换句话说,除了正常的Dispatch Functions之外,你还得为DriverObject撰写另一组Fast Io Functions.这组函数的指针在driver->FastIoDispatch.我不知道这个指针留空会不会导致系统崩溃。在这里本来是没有空间的,所以为了保存这一组指针,你必须自己分配空间。
下面是我常用的内存分配函数。
//-----------------wdf.h中的代码----------------------
// 最简单的分配内存的函数,可以指定分页非分页
_inline wd_pvoid wd_malloc(wd_bool paged,wd_size size)
{
if(paged)
return ExAllocatePool(PagedPool,size);
else
return ExAllocatePool(NonPagedPool,size);
}
// 释放内存
_inline wd_void wd_free(wd_pvoid point)
{
ExFreePool(point);
}
_inline wd_void wd_memzero(
wd_pvoid point,
wd_size size)
{
RtlZeroMemory(point,size);
}
有了上边的基础,我就可以自己写一个初始化FastIoDispatch指针的函数。
//-----------------wdf.h中的代码----------------------
wd_bool wd_fio_disp_init(wd_drv *driver,wd_ulong size)
{
wd_fio_disp *disp = wd_malloc(wd_false,size);
if(disp == wd_null)
return wd_false;
wd_memzero((wd_pvoid)disp,size);
driver->FastIoDispatch = disp;
driver->FastIoDispatch->SizeOfFastIoDispatch = size;
return wd_true;
}
这个函数为FastIoDispacth指针分配足够的空间并填写它的大小。下面是再写一系列的函数来设置这个函数指针数组。实际上,FastIo接口函数实在太多了,所以我仅仅写出这些设置函数的几个作为例子:
//-----------------wdf.h中的代码----------------------
_inline wd_void wd_fio_disp_set_query_standard(
wd_drv *driver,
wd_fio_query_standard_func func)
{
driver->FastIoDispatch->FastIoQueryStandardInfo = func;
}
_inline wd_void wd_fio_disp_set_io_lock(
wd_drv *driver,
wd_fio_io_lock_func func)
{
driver->FastIoDispatch->FastIoLock = func;
}
_inline wd_void wd_fio_disp_set_io_unlock_s(
wd_drv *driver,
wd_fio_unlock_single_func func)
{
driver->FastIoDispatch->FastIoUnlockSingle = func;
}
...
好,如果你坚持读到了这里,应该表示祝贺了。我们回顾一下,wd_main中,应该做哪些工作。
a.生成一个控制设备。当然此前你必须给控制设置指定名称。
b.设置Dispatch Functions.
c.设置Fast Io Functions.
// ----------------wd_main 的近况----------------------------
...
wd_dev *g_cdo = NULL;
wd_stat wd_main(in wd_drv* driver,
in wd_ustr* reg_path)
{
wd_ustr name;
wd_stat status = wd_stat_suc;
// 然后我生成控制设备,虽然现在我的控制设备什么都不干
wd_ustr_init(&name,L"/FileSystem/Filters/our_fs_filters");
status = wdff_cdo_create(driver,0,&name,&g_cdo);
if(!wd_suc(status))
{
if(status == wd_stat_path_not_found)
{
// 这种情况发生于FileSystemFilters路径不存在。这个路径是
// 在xp上才加上的。所以2000下可能会运行到这里
wd_ustr_init(&name,L"/FileSystem/our_fs_filters");
status = wdff_cdo_create(driver,0,&name,&g_cdo);
};
if(!wd_suc(status))
{
wd_printf0("error: create cdo failed.rn");
return status;
}
}
wd_printf0("success: create cdo ok.rn");
// 开始设置几个分发例程
wd_drv_set_dispatch(driver,my_disp_default);
wd_drv_set_create(driver,my_disp_create);
wd_drv_set_clean_up(driver,my_disp_clean_up);
wd_drv_set_file_sys_control(driver,my_disp_file_sys_ctl);
wd_drv_set_close(driver,my_disp_close);
wd_drv_set_read(driver,my_disp_read);
wd_drv_set_write(driver,my_disp_write);
// 指定fast io处理函数
if(!wd_fio_disp_init(driver,sizeof(wd_fio_disp)))
{
wd_dev_del(g_cdo);
wd_printf0("error: fast io disp init failed.rn");
return wd_stat_insufficient_res;
}
// 下面指定的这些函数都定义在wdf_filter_fio.h中,其实这些函数都统
// 一的返回了false
wd_fio_disp_set_check(
driver,
my_fio_check);
wd_fio_disp_set_read(
driver,
my_fio_read);
wd_fio_disp_set_write(
driver,
my_fio_write);
wd_fio_disp_set_query_basic(
driver,
my_fio_query_basic_info);
...
}
FastIo函数个数数量不明,我只觉得很多。因此不打算全部罗列,以"..."敷衍之。某些读者可能会认为这些代码无法调试安装。其实您可以参考sfilter中的示例自己完成这些代码。
使用Fast I/O
在这里我们将讲述Fast I/O的基本原理,简单描述各种各样的Fast I/O调用,以及得出如何使用此接口来提高程序性能的建议性结论。
Windows NT内核模式开发的标准做法是采用IRP作为基本的与驱动程序通信的手段,它的优点是IRP封装了上下文所需的详细操作并且允许从驱动程序的众多操作细节中分离出来。
这个方法在windos NT的分层设备体系中非常通用,有相当多的上层操作请求需要快速响应,在这种情况下,上层操作生成IRP决定了整个操作的成本并会导致系统性能的下降。鉴于此,NT系统引入的Fast I/O的概念。这种方法被用在文件系统驱动,如NTFS,HPFS,FAT和CDFS以及被WinSock使用的传输驱动AFD。
任何驱动都可以注册一系列Fast I/O接口,但使用起来还有很大的限制—在这些接口被调之前需要满足合适的条件。例如,读操作和写操作的Fast I/O接口只有当Windows NT cache管理器保留了文件的信息时才被调用。我们在接下的论述中将会讲述这些限制。
当然,Windows NT的Fast I/O最让人郁闷的是关于它的资料很少,即使文件系统开发包也没有讲述Fast I/O是如何工作和怎样来使用Fast I/O。
原理
提供了Fast I/O是非常方便的---许多I/O操作可以对相同的数据进行重复操作。例如和许多流行的操作系统一样,Windows NT用虚拟内存集成了文件系统的缓冲,这样的系统无论是在使用上还是在感觉上都很有效率。
这种集成的另一原因是Windows NT支持内存映射文件。支持读写和内存映射相同的数据要么需要代价很高的cache一致性策略,要么使用NT的策略---将所有数据存储在虚拟内存中。这样,即便是两个程序用不同的技术访问相同的数据,也确保了数据的一致性。
这种紧密的集成意味着无论是读还是写都经常是对cache中的数据来操作。在查找过程中,这种策略用来调用一个特殊的程序,此程序将虚拟机(VM)的cache中的数据移到用户内存中,反之亦然。这样就避免了生成IRP,并且不需要请求底层的驱动了。这就是Fast I/O操作的基本功能。
一旦在程序中定义了Fast I/O读写接口,那么同时还需要进行一步添加其它的通用Fast I/O操作到Fast I/O链中,Fast I/O链中有13个接口(在NT3.51中)。在我们接下来要讲的各接口过程中,你会明显地发现各接口是互相关联的。这些接口包含在FAST_IO_DISPATCH结构中,此结构在ntddk.h中有定义。这个结构的第一个元素表示结构的大小,为以后在结构添加新接口提供了一种向上兼容的机制。
I/O管理器和Fast I/O
I/O管理器在必要的时候负责调用Fast I/O接口。Fast I/O调用返回TRUE或FALSE表示Fast I/O操作是否完成。如果Fast I/O没有被完成或无效,则会产生一个IPR并发送到上层驱动,但是FAST_IO_DISPATCH结构中最近的三个接口却不是这样的,它们为I/O管理员提供了不同的服务,接下来我们将讨论它们。
FastIoCheckIfPossible
这是在FAST_IO_DISPATCH结构中第一个调用的,仅被用来作为一般的文件系统库操作(以FsRtl开头的函数)。原型如下:
typedef BOOLEAN (*PFAST_IO_CHECK_IF_POSSIBLE)(
IN Struct _FILE_OBJECT *FileObject,
IN PLARGE_INTEGER FileOffset,
IN ULONG Length,
IN BOOLEAN Wait,
IN ULONG LockKey,
IN BOOLEAN CheckForReadOperation,
OUT PIO_STATUS_BLOCK IoStatus,
IN struct _DEVICE_OBJECT *DeviceObject
);
这个函数被FsRtl库中提供的通用的Fast I/O函数调用,仅用在读写操作中,以获取使用了通用文件缓存管理系统中的读和写是否能在文件缓存中被响应(由参数CheckForReadOperation的值决定)。注意,除了这个函数没有分配任何数据空间外其它参数和读写的Fast I/O接口参数相似。
FastIoRead and FastIoWrite
当对一个已经分配了有效数据缓存的文件进行读请求时,这个函数被I/O管理器调用。原型如下:
typedef BOOLEAN (*PFAST_IO_READ)(
IN struct _FILE_OBJECT *FileObject,
IN PLARGE_INTEGER FileOffset,
IN ULONG Length,
IN BOOLEAN Wait,
IN ULONG LockKey,
OUT PVOID Buffer,
OUT PIO_STATUS_BLOCK IoStatus,
IN struct _DEVICE_OBJECT *DeviceObject
);
正如前面所讲的,基本的调用参数和FastIoCheckIfPossible相似,就是多了一个必要的数据缓存参数。要保证所有Fast I/O调用接口的参数有效,例如上面的Buffer指针,必需是有效的并且在读线程的上下文中能使用此指针。
Fast I/O函数可以完成以下两件事情之一:第一,当操作完成时设置IoStatus的返回值并给I/O管理器返回TRUE,这时I/O管理器会完成对应的I/O操作。第二,直接返回FALSE给I/O管理器使其构造一个IRP从而调用标准的分派例程。
要注意的是返回TRUE并不能保证数据被传输了。例如,一个从文件结束处开始的读请求会设置IoStatus.Results为STATUS_END_OF_FILE,并且没有数据被复制。但是当一个读操作读到了文件的结尾,这时会将IoStatus.Results设置为STATUS_END_OF_FILE,返回TRUE,并且将读到的数据复制到Buffer里。
同样,返回FALSE并不能说明所有的数据没有被处理。例如,当然很少有这种可能,数据已经被成功处理了,但抛出一个I/O错误,或者内存不可访问。
以上任何一种情况出现都会导致不良影响。例如,从缓存读数据时,可能要读的数据并不在缓存中,这时会导致一个页错误,从而会请求文件系统来处理这个页面错误。
Fast I/O的写函数与读函数不同之处仅仅在于Buffer参数一个是输入型而不是输出型的,其它的基本操作很相似。当然,一些错误可能不同---介质已经满,需要分配新页等等。
FastIoQueryBasicInfo and FastIoQueryStandardInfo
这两个操作为标准的NtQueryInformationFile API操作提供了支持,而FastIoQueryBasicInfo也经常被用来处理NtCreateFile的特定操作。文件的基本属性包括创建时间、访问时间和修改时间,以及隐藏、文件夹或其它属性等。文件的标准属性包括文件占用的空间、文件的大小、文件的硬连接号、被请求删除的标志,是否是文件夹的标识。
由于这些信息经常在缓存中,所以它是FAST I/O操作的最佳候选。其实许多程序用这种方法来获取文件基本的信息,因为这种方法提高了操作的效率和程序的性能,如文件管理器程序(winfile.exe)。
这两个Fast I/O例程有相同的接口:
typedef BOOLEAN (*PFAST_IO_QUERY_ABSIC_INFO)(
IN struct _FILE_OBJECT *FileObject,
IN BOOLEAN Wait,
OUT PFILE_BASIC_INFORMATION Buffer,
OUT PIO_STATUS_BLOCK IoStatus,
IN struct _DEVICE_OBJECT *DeviceObject);
Wait参数表示调用者是否阻塞以等待获取信息的返回,如果设置为FALSE,则调用要么立刻完成,要么返回FALSE,如果返回FALSE,则会生成一个包含整个操作必要的上下文件的IRP。有趣的是在NT3.51中当Wait设置为FALSE时这两个例程将不被调用,当然这在以后的版本中会修改。
这两个例程一但被调用,它们会查询文件对象第一次打开时保存的信息,这些信息也可能被实时变化,例如,文件的最近访问时间属性会被文件系统设置的当前的系统时间,当然设置这些属性取决于文件系统的实现。
FastIoLock,FastIoUnLockSingle,FastIoUnLockAll,and FastIoUnLockAllByKey
这些例程被用来控制特殊文件的加锁状态。对文件的加锁以字节为单位,所以可以对文件的多个字节进行加密。标准的NT文件系统使用文件系统运行时开发包(FsRtl函数)所提供的通用代码来验证锁权限并存储文件的加锁范围。锁状态通过调用NT API函数NtLockFile和NtUnLockFile来控制。
在Windows NT中有两种锁,一种是排它锁,是写锁,说明加锁的地放要进行修改;另一种是共享锁,是读锁,说明加锁的地放用来的读的。多个共享锁在重叠操作中可以被授权,并且一直保存到释放为止。将各种加锁的信息存储起来在访问这些信息的时候会提高速度。
FastIoLock的原型:
Typedef BOOLEAN (*PFAST_IO_LOCK)(
IN struct _FILE_OJBECT *FileObject,
IN PLARGE_INTEGER FileOffset,
IN PLARGE_INTEGER Length,
PEPROCESS ProcessId,
ULONG Key,
BOOLEAN FailImmediately,
BOOLEAN ExclusiveLock,
OUT PIO_STATUS_BLOCK IoStatus,
IN struct _DEVICE_OBJECT *DeviceObject
);
FileOffset 和Length参数对应加锁的范围,ProcessId标识加锁的进程,如果进行退出,锁将会清除。Key参数提供一个非透明的值,用来关联多个锁,例如调用FastIoUnLockAllByKey可以快速访问多个锁。FailImmediately用来标识当锁无效时是立刻返回失败还是阻塞直到锁可用。对于FsRtl函数,如果是无效的锁则忽略FailImmediately参数,函数返回FALSE。ExclusiveLock参数用来标识是排它锁还是共享锁。
FastUnlockSingle例程被用来释放对文件的加锁,原型如下:
Typedef BOOLEAN (*PFAST_IO_UNLOCK_SINGLE)(
IN struct _FILE_OBJECT *FileObject,
IN PLARGE_INTEGER FileOffset,
IN PLARGE_INTEGER Length,
PEPROCESS ProcessId,
ULONG Key,
OUT PIO_STATUS_BLOCK IoStatus,
IN struct _DEVICE_OBJECT *DeviceObject
);
对大多文件系统来说,如果文件没有加锁,此例程总是返回TRUE,即使朝无效的锁,操作也会完成,因为用IRP来操作也同样会产生相同的错误。
如果这个解锁操作成功,那么FileOffset,Length,ProcessId,和Key必须和相应的锁信息匹配,否则操作会返回错误STATUS_RANGE_NOT_LOCKED。FastIoUnlockAll例程用来释放特殊文件所有的锁,函数原型如下:
typedef BOOLEAN(*PFAST_IO_UNLOCK_ALL)(
IN struct _FILE_OBJECT *FileObject,
PEPROCESS ProcessId,
OUT PIO_STATUS_BLOCK IoStatus,
IN struct _DEVICE_OBJECT *DeviceObject
);
在这种情况下,Fast I/O例程查找进程ProcessId所操作的文件所有的锁,并删除,无论是排它锁还是共享锁。这个例程常用在当系统由于关掉程序或终止程序而调用NtCloseFile时。
FastIoUnlockAllByKey操作用来通过一些特殊的键值来删除一系列锁。原型如下:
Typedef BOOLEAN (*PFAST_IO_UNLOCK_ALL_BY_KEY)(
IN struct _FILE_OBJECT *FileObject,
PVOID ProcessId,
ULONG Key,
OUT PIO_STATUS_BLOCK_IoStatus,
IN struct _DEVICE_OBJECT *DeviceObject
);
提供这个例程是为了便于文件服务如SRV。在NT3.51的I/O管理器中没有出现这个调用。键值用来文件服务给远程客户机文件加锁时分配的。因为许多远程客户端,仅有ProcessId是远远不够的。同样,对于多文件服务器,仅使用键值也会在其它的文件服务器释放时导致错误。二者同时使用以确保正确操作并允许远程加锁。
4.设备栈,过滤,文件系统的感知
前边都在介绍文件系统驱动的结构,却还没讲到我们的过滤驱动如何能捕获所有发给文件系统驱动的irp,让我们自己来处理?前面已经解释过了设备对象。现在来解释一下设备栈。
任何设备对象都存在于某个设备栈中。设备栈自然是一组设备对象。这些设备对象是互相关联的,也就是说,如果得到一个DO指针,你就可以知道它所处的设备栈。
任何来自应用的请求,最终被windows io mgr翻译成irp的,总是发送给设备栈的顶端那个设备。
原始irp irp irp irp
--------------> ------> -------> ----->
DevTop Dev2 ... DevVolumne ... ???
<-------------- <------ <------- <-----
原始irp(返回) irp irp irp
上图向右的箭头表示irp请求的发送过程,向左则是返回。可见irp是从设备栈的顶端开始,逐步向下发送。DevVolumue表示我们实际要过滤的Volume设备,DevTop表示这个设备栈的顶端。我们只要在这个设备栈的顶端再绑定一个设备,那发送给Volume的请求,自然会先发给我们的设备来处理。
有一个系统调用可以把我们的设备绑定到某个设备的设备栈的顶端。这个调用是IoAttachDeviceToDeviceStack,这个调用2000以及以上系统都可以用(所以说到这点,是因为还有一个IoAttachDeviceToDeviceStackSafe,是2000所没有的。这常常导致你的filter在2000下不能用。)
我自己写了一个函数来帮我实现绑定功能:
//----------------------wdf.h中的内容----------------------------------
// 这个例程把源设备绑定到目标设备的设备栈中去,并返回源设备所直
// 接绑定的设备。注意源设备未必直接绑定在目标设备上。它应绑定在
// 目标设备的设备栈的顶端。
_inline wd_stat wd_dev_attach(in wd_dev *src,
in wd_dev *dst,
in out wd_dev **attached)
{
*attached = dst;
*attached = IoAttachDeviceToDeviceStack(src,dst);
if(*attached == NULL)
return wd_stat_no_such_dev;
return wd_stat_suc;
}
到这里,我们已经知道过滤对Volume的请求的办法。比如“C:”这个设备,我已经知道符号连接为“C:”,不难得到设备名。得到设备名后,又不难得到设备。这时候我们IoCreateDevice()生成一个Device Object,然后调用wd_dev_attach绑定,不是一切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()是一个非常有用的系统调用。这个调用注册一个回调函数。当系统中有任何文件系统被激活或者是被注销的时候,你注册过的回调函数就会被调用。
//----------------------wdf.h中的内容----------------------------------
wd_stat wdff_reg_notify(
in wd_drv *driver,
in wdff_notify_func func
)
{
return IoRegisterFsRegistrationChange(driver,func);
}
你有必要为此写一个回调函数。
//-------------------我的回调处理函数----------------------------------
wd_void my_fs_notify(
in wd_dev *dev,
in wd_bool active)
{
wd_wchar name_buf[wd_dev_name_max_len];
wd_ustr name;
wd_ustr_init_em(&name,name_buf,wd_dev_name_max_len);
// 如果注册了,就应该得到通知
wd_printf0("notify: a file sys have been acitved!!! rn");
// 得到文件系统对象的名字,然后打印出来
wd_obj_get_name(dev,&name);
wd_printf0("notify : file sys name = %wZrn",&name);
if(active)
{
wd_printf0("notify: try to attach.rn");
// ... 请在这里绑定文件系统的控制设备
}
else
{
wd_printf0("notify: unactive.rn");
// ...
}
}
应该如何绑定一个文件系统CDO?我们在下面的章节再详细描述。
现在我们应该再在wd_main函数中加上下边的内容:
if(wdff_reg_notify(driver,my_fs_notify) != wd_stat_suc)
{
wd_printf0("error: reg notify failed.rn");
wd_fio_disp_release(driver);
wd_dev_del(g_cdo);
g_cdo = wd_null;
return wd_stat_insufficient_res;
};
wd_printf0("success: reg notify ok.n");
我们再次回顾一下,wd_main中,应该做哪些工作。
a.生成一个控制设备。当然此前你必须给控制设置指定名称。
b.设置Dispatch Functions.
c.设置Fast Io Functions.
d.编写一个my_fs_notify回调函数,在其中绑定刚激活的FS CDO.
e.使用wdff_reg_notify调用注册这个回调函数。
5.绑定FS CDO,文件系统识别器,设备扩展
上一节讲到我们打算绑定一个刚刚被激活的FS CDO.前边说过简单的调用wd_dev_attach可以很容易的绑定这个设备。但是,并不是每次my_fs_notify调用发现有新的fs激活,我就直接绑定它。
首先判断是否我需要关心的文件系统类型。我用下面的函数来获取设备类型。
// ------------------wdf.h中的内容-------------------
_inline wd_dev_type wd_dev_get_type(in wd_dev *dev)
{
return dev->DeviceType;
}
文件系统的CDO的设备类型有下边的几种可能,你的过滤驱动可能只对其中某些感兴趣。
enum {
wd_dev_disk_fs = FILE_DEVICE_DISK_FILE_SYSTEM,
wd_dev_cdrom_fs = FILE_DEVICE_CD_ROM_FILE_SYSTEM,
wd_dev_network_fs = FILE_DEVICE_NETWORK_FILE_SYSTEM
};
你应该自己写一个函数来判断该fs是否你所关心的。
// -------------一个函数,判断是否我所关心的fs---------------
wd_bool my_care(wd_ulong type)
{
return (((type) == wd_dev_disk_fs) ||
((type) == wd_dev_cdrom_fs) ||
((type) == wd_dev_network_fs));
}
下一个问题是我打算跳过文件系统识别器。文件系统识别器是文件系统驱动的一个很小的替身。为了避免没有使用到的文件系统驱动占据内核内存,windows系统不加载这些大驱动,而代替以该文件系统驱动对应的文件系统识别器。当新的物理存储媒介进入系统,io管理器会依次的尝试各种文件系统对它进行“识别”。识别成功,立刻加载真正的文件系统驱动,对应的文件系统识别器则被卸载掉。对我们来说,文件系统识别器的控制设备看起来就像一个文件系统控制设备。但我们不打算绑定它。
分辨的方法是通过驱动的名字。凡是文件系统识别器的驱动对象的名字(注意是DriverObject而不是DeviceObject!)都为“FileSystem/Fs_Rec”.
//-------------------用这些代码来跳过文件系统识别器----------------------
wd_wchar name_buf[wd_dev_name_max_len];
wd_ustr name,tmp;
wd_ustr_init_em(&name,name_buf,wd_dev_name_max_len);
wd_ustr_init(&tmp,L"/FileSystem/Fs_Rec");
// 我不绑定识别器。所以如果是识别器,我直接返回成功。查看是否是识别
// 器的办法是看是否是FileSystemFs_Rec的设备。
wd_obj_get_name(wd_dev_drv(fs_dev),&name);
if(wd_ustr_cmp(&name,&tmp,wd_true) == 0)
{
wd_printf0("attach fs dev:is a recogonizer.rn");
return wd_stat_suc;
}
wd_printf0("attach fs dev: not a recogonizer.rn");
接下来我将要生成我的设备。这里要提到设备扩展的概念。设备对象是一个数据结构,为了表示不同的设备,里边将有一片自定义的空间,用来给你记录这个设备的特有信息。我们为我们所生成的设备确定设备扩展如下:
// 文件过滤系统驱动的设备扩展
typedef struct _my_dev_ext
{
// 我们绑定的文件系统驱动
wd_dev * attached_to;
// 上边这个设备的设备名。
wd_ustr dev_name;
// 这是上边的unicode字符串的缓冲区
wd_wchar name_buf[wd_dev_name_max_len];
} my_dev_ext;
之所以如此简单,是因为我们现在还没有多少东西要记录。只要记得自己绑定在哪个设备上就好了。如果以后需要更多的信息,再增加不迟。扩展空间的大小是在wdf_dev_create(也就是这个设备生成)的时候指定的。得到设备对象指针后,我用下面这个函数来获取设备扩展指针:
// --------------wdf.h中的内容------------------
_inline wd_void * wd_dev_ext(wd_dev *dev)
{
return (dev->DeviceExtension);
}
生成设备后,为了让系统看起来,你的设备和原来的设备没什么区别,你必须设置一些该设备的标志位与你所绑定的设备相同。
_inline wd_void wd_dev_copy_flag(wd_dev *new_dev,
wd_dev *old_dev)
{
if(old_dev->Flags & DO_BUFFERED_IO)
new_dev->Flags &= DO_BUFFERED_IO;
if(old_dev->Flags & DO_DIRECT_IO)
new_dev->Flags &= DO_DIRECT_IO;
if (old_dev->Characteristics & FILE_DEVICE_SECURE_OPEN)
new_dev->Characteristics &= FILE_DEVICE_SECURE_OPEN;
}
DO_BUFFERED_IO,DO_DIRECT_IO这两个标志的意义在于外部向这些设备发送读写请求的时候,所用的缓冲地址将有所不同。这点以后在过滤文件读写的时候再讨论。现在一切事情都搞完,你应该去掉你的新设备上的DO_DEVICE_INITIALIZING标志,以表明的的设备已经完全可以用了。
// --------------wdf.h中的内容------------------
_inline wd_void wd_dev_clr_init_flag(wd_dev *dev)
{
dev->Flags &= ~DO_DEVICE_INITIALIZING;
}
现在我写一个函数来完成以上的这个过程。你只要在上一节中提示的位置调用这个函数,就完成对文件系统控制设备的绑定了。
//-----------绑定一个文件系统驱动设备-------------------------
wd_stat my_attach_fs_dev(wd_dev *fs_dev)
{
wd_wchar name_buf[wd_dev_name_max_len];
wd_ustr name,tmp;
wd_dev *new_dev;
wd_stat status;
my_dev_ext *ext;
wd_ustr_init_em(&name,name_buf,wd_dev_name_max_len);
wd_ustr_init(&tmp,L"/FileSystem/Fs_Rec");
// 如果不是我关心的类型,我直接返回成功
if(!my_care(wd_dev_get_type(fs_dev)))
{
wd_printf0(("attach fs dev:not a cared type.rn"));
return wd_stat_suc;
}
wd_printf0("attach fs dev: is my cared type.rn");
// 我不绑定识别器。所以如果是识别器,我直接返回成功。查看是否是识别
// 器的办法是看是否是FileSystem/Fs_Rec的设备。
wd_obj_get_name(wd_dev_drv(fs_dev),&name);
if(wd_ustr_cmp(&name,&tmp,wd_true) == 0)
{
wd_printf0("attach fs dev:is a recogonizer.rn");
return wd_stat_suc;
}
wd_printf0("attach fs dev: not a recogonizer.rn");
// 现在来生成一个设备用来绑定
status = wd_dev_create(g_drv,sizeof(my_dev_ext),NULL,
wd_dev_get_type(fs_dev),
0,wd_false,&new_dev);
if(!wd_suc(status))
{
wd_printf0("attach fs dev: dev create failed.rn");
return status;
}
wd_printf0("attach fs dev: create dev success.rn");
// 接着设置设备的各种标志与之要绑定的标志一致
wd_dev_copy_flag(new_dev,fs_dev);
ext = (my_dev_ext *)wd_dev_ext(new_dev);
wd_printf0("begin to attach.rn");
status = wd_dev_attach(new_dev,fs_dev,&ext->attached_to);
wd_printf0("attach over.status = %8xrn",status);
if(!wd_suc(status))
{
wd_printf0("attach fs dev: dev attach failed.rn");
UNREFERENCED_PARAMETER(new_dev);
wd_dev_del(new_dev);
return status;
}
wd_printf0("attach fs dev: attach %wZ succeed.rn",&name);
wd_ustr_init_em(&ext->dev_name,ext->name_buf,wd_dev_name_max_len);
wd_ustr_copy(&ext->dev_name,&name);
wd_dev_clr_init_flag(new_dev);
return status;
}
6.IRP的传递,File System Control Dispatch
我们现在不得不开始写dispatch functions.因为你的设备已经绑定到文件系统控制设备上去了。windows发给文件系统的请求发给你的驱动。如果你不能做恰当的处理,你的系统的就会崩溃。
最简单的处理方式是把请求不加改变的传递到我们所绑定的设备上去。如何获得我们所绑定的设备?上一节已经把该设备记录在我们的设备扩展里。
//------------我用这个函数快速得到我所绑定的设备-----------
// 得到绑定的设备
_inline wd_dev *my_dev_attached(wd_dev *dev)
{
return ((wdff_dev_ext *)wd_dev_ext(dev))->attached_to;
}
如何传递请求?使用IoCallDriver,该调用的第一个参数是设备对象指针,第二个参数是IRP指针。
一个IRP拥有一组IO_STACK_LOCATION.前面说过IRP在一个设备栈中传递。IO_STACK_LOCATION是和这个设备栈对应的。用于保存IRP请求在当前设备栈位置中的部分参数。如果我要把请求往下个设备传递,那么我应该把当前IO_STATCK_LOCATION复制到下一个。
我写了一些函数来处理IO_STACK_LOCATION,另外wd_irp_call用来包装IoCallDriver的功能。
//---------------------wdf.h中的内容----------------------------
typdef wd_irpsp PIO_STACK_LOCAION;
_inline wd_irpsp *wd_cur_io_stack(wd_irp *irp)
{
return IoGetCurrentIrpStackLocation(irp);
}
_inline wd_void wd_skip_io_stack(wd_pirp irp)
{
IoSkipCurrentIrpStackLocation(irp);
}
_inline wd_void wd_copy_io_stack(wd_irp *irp)
{
IoCopyCurrentIrpStackLocationToNext(irp);
}
_inline wd_stat wd_irp_call(wd_dev *dev,wd_pirp irp)
{
return IoCallDriver(dev,irp);
}
有了上边这些,我现在可以写一个默认的Dispatch Functions.
// 默认的处理很简单,忽略当前调用栈,直接发送给绑定设备
wd_stat my_disp_default(in wd_dev *dev,in wd_pirp irp)
{
wd_dev *attached_dev;
if(!is_my_dev(dev))
return wd_irp_failed(irp,wd_stat_invalid_dev_req);
if(is_my_cdo(dev))
return wd_irp_failed(irp,wd_stat_invalid_dev_req);
attached_dev = my_dev_attached(dev);
if(!attached_dev)
return wd_irp_failed(irp,wd_stat_invalid_dev_req);
wd_skip_io_stack(irp);
return wd_irp_call(attached_dev,irp);
}
上边有一个函数is_my_dev来判断是否我的设备。这个判断过程很简单。通过dev可以得到DriverObject指针,判断一下是否我自己的驱动即可。is_my_cdo()来判断这个设备是否是我的控制设备,不要忘记在wd_main()中我们首先生成了一个本驱动的控制设备。实际这个控制设备还不做任何事情,所以对它发生的任何请求也是非法的。返回错误即可。wd_irp_failed这个函数立刻让一个irp失败。其内容如下:
// 这个函数可以立刻失败掉一个irp
_inline wd_stat wd_irp_failed(wd_pirp irp,wd_stat status_error)
{
irp->IoStatus.Status = status_error;
irp->IoStatus.Information = 0;
return wd_irp_over(irp);
}
如此一来,本不改发到我的驱动的irp,就立刻返回错误非法请求。但是实际上这种情况是很少发生的。
如果你现在想要你的驱动立刻运行,让所有的dispacth functions都调用my_disp_default.这个驱动已经可以绑定文件系统的控制设备,并输出一些调试信息。但是还没有绑定Volume.所以并不能直接监控文件读写。
对于一个绑定文件系统控制设备的设备来说,其他的请求直接调用上边的默认处理就可以了。重点需要注意的是上边曾经挂接IRP_MJ_FILE_SYSTEM_CONTROL的dispatch处理的函数my_disp_file_sys_ctl().
IRP_MJ_FILE_SYSTEM_CONTROL这个东西是IRP的主功能号。每个主功能号下一般都有次功能号。这两个东西标示一个IRP的功能。
主功能号和次功能号是IO_STACK_LOCATION的开头两字节。
//----------------我重新定义的次功能号-------------------
enum {
wd_irp_mn_mount = IRP_MN_MOUNT_VOLUME,
wd_irp_mn_load_filesys = IRP_MN_LOAD_FILE_SYSTEM,
wd_irp_mn_user_req = IRP_MN_USER_FS_REQUEST
};
enum {
wdf_fsctl_dismount = FSCTL_DISMOUNT_VOLUME
};
要得到功能号,要先得到当前的IO_STACK_LOCATION,这个上边已经有函数wd_cur_io_stack,相信这个不能难倒你。
当有Volumne被Mount或者dismount,你写的my_disp_file_sys_ctl()就被调用。具体的判断方法,就见如下的代码了:
// 可以看到分发函数中其他的函数处理都很简单,但是file_sys_ctl的
// 处理会比较复杂。我们已经在notify函数中绑定了文件系统驱动的控
// 制对象。当文件系统得到实际的介质的时候,会生成新的设备对象,
// 这种设备称为卷(Volume),而这种设备是在file_sys中的mount中生
// 成的,而且也是unmount中注销掉的。我们捕获这样的操作之后,就必
// 须生成我们的设备对象,绑定在这样的“卷”上,才能绑定对这个卷
// 上的文件的操作。
wd_stat my_disp_file_sys_ctl(in wd_dev *dev,in wd_pirp irp)
{
wd_dev *attached_dev;
wd_io_stack *stack = wd_cur_io_stack(irp);
if(!is_my_dev(dev))
return wd_irp_failed(irp,wd_stat_invalid_dev_req);
switch(wd_irpsp_minor(stack))
{
case wd_irp_mn_mount:
// 在这里,一个Volume正在Mount
return my_fsctl_mount(dev,irp);
case wd_irp_mn_load_filesys:
return my_fsctl_load_fs(dev,irp);
case wd_irp_mn_user_req:
{
switch(wd_irpsp_fs_ctl_code(stack))
{
case wdf_fsctl_dismount:
// 在这里,一个Volume正dismount
return my_fsctl_dismount(dev,irp);
}
}
}
wd_skip_io_stack(irp);
attached_dev = my_dev_attached(dev);
return wd_irp_call(attached_dev,irp);
}
你发现你又得开始写两个新的函数,my_fsctl_mount()和my_fsctl_dismount(),来处理卷的Mount和Dismount.显然,你应该在其中生成设备或者删除,绑定或者解除绑定。很快,你就能完全监控所有的卷了。
这样做是动态监控所有的卷的完美的解决方案。
如果是在xp以上,有一个调用可以获得一个文件系统上已经被Mount的卷。但是2000下不能使用。所以我们没有使用那个方法。何况仅仅得到已经Mount的卷也不是我想要的。
这里另外还有一个my_fsctl_load_fs函数。发生于IRP_MN_LOAD_FILESYS。这个功能码我只做一点点解释:当一个文件识别器(见上文)决定加载真正的文件系统的时候,会产生一个这样的irp。
你现在可以修改你的驱动,使插入拔出u盘的时候,在Volume加载卸载时候输出调试信息。回首一下我们的脉络:
a.生成一个控制设备。当然此前你必须给控制设置指定名称。
b.设置Dispatch Functions.
c.设置Fast Io Functions.
d.编写一个my_fs_notify回调函数,在其中绑定刚激活的FS CDO.
e.使用wdff_reg_notify调用注册这个回调函数。
f.编写默认的dispatch functions.
e.处理IRP_MJ_FILE_SYSTEM_CONTROL,在其中监控Volumne的Mount和Dismount.
f.下一步自然是绑定Volumne了,请听下回分解。
7.IRP完成函数,中断级,如何超越中断级别的限制
先讨论一下Volumne设备是如何得到的.首先看以下几个函数:
// ------------------wdf.h 中的内容 -------------------------
typedef VPB wd_vpb;
_inline wd_vpb * wd_dev_vbp(wd_dev *dev)
{
return dev->Vpb;
}
_inline wd_dev * wd_vbp_dev(wd_vpb *vpb)
{
return vpb->DeviceObject;
}
VPB是Volume parameter block.一个数据结构.它的主要作用是把实际存储媒介设备对象和文件系统上的卷设备对象联系起来.
wd_dev_vbp可以让你从一个Storage Device Object得到一个VPB,而wd_vbp_dev这个函数可以得到这个VPB所对应的Volmue设备.
现在首先要得到Storage Device Object.实际上这个东西保存在当前IO_STACK_LOCATION中.
// ------------------wdf.h 中的内容 -----------------------
_inline wd_dev *wd_irpsp_mount_storage(wd_io_stack *irpsp)
{
return irpsp->Parameters.MountVolume.Vpb->RealDevice;
};
那么,从irp出发,我最终可以通过以下的方式得到Volumue设备:
wd_irpsp *irpsp = wd_cur_io_stack(irp);
wd_dev *storage_dev = wd_irpsp_mount_storage(irp);
wd_vpb *vpb = wd_dev_vbp(storage_dev);
wd_dev *volume_dev = wd_vbp_dev(vpb);
不过实际情况并不这么简单.这里的IRP是一个MOUNT请求.而volume设备对象实际上是这个请求完成之后的返回结果.因此,在这个请求还没有完成之前,我们就试图去获得Volume设备对象,当然是竹篮打水一场空了.
这里,你可以直接拷贝当前IO_STACK_LOCATION,然后向下发送请求,但在此之前,要先给irp分配一个完成函数.irp一旦完成,你的完成函数将被调用.这样的话,你可以在完成函数中得到Volume设备,并实施你的绑定过程.
这里要讨论一下中断级别的问题.常常碰到人问某函数只能在Passive Level调用是什么意思.总之我们的任何代码执行的时候,总是处在某个当前的中断级之中.某些系统调用只能在低级别中断级中执行.请注意,如果一个调用可以在高处运行,那么它能在低处运行,反过来则不行.
我们需要知道的只是我们关心Passive Level和Dispatch Level.而且Dispatch Level的中断级较高.一般ddk上都会标明,如果注明irq level>=dispatch,那么你就不能在passive level的代码中调用它们了.
那么你如何判断当前的代码在哪个中断级别中呢?我一般是这么判断的:如果你的代码执行是由于应用程序(或者说上层)的调用而引发的,那么应该在Passive Level.如果你的代码执行是由于下层硬件而引发的,那么则可能在dispatch level.
希望不要机械的理解我的话!以上只是极为粗略的便于记忆的理解方法.实际的应用应该是这样的:所有的dispatch functions由于是上层发来的irp而导致的调用,所以应该都是Passive Level,在其中你可以调用绝大多数系统调用.而如网卡的OnReceive,硬盘读写完毕,返回而导致的完成函数,都有可能在Dispatch级.注意都是有可能,而不是绝对是.但是一旦有可能,我们就应该按就是考虑.
好,现在我们发现,我们已经注册了完成函数,并且这个函数执行中可能是dispatch level.
现在面临的问题是,我们已经决定在完成函数中调用 IoAttachDeviceToDeviceStack来绑定Volume.而DDK说明有:Callers of IoAttachDeviceToDeviceStack must be running at IRQL <= DISPATCH_LEVEL.
实际上前边说过有IoAttachDeviceToDeviceStackSafe,这个调用可以在Dispatch level进行.无奈这个调用仅仅出现在Xp以上的系统中.
超越中断级别的限制有几种方法.第一种是自己生成一个系统线程来完成此事.系统线程将保证在Passive Level中运行.另一种方法就是把自己的任务插入Windows工作者线程,这会使你的任务迟早得到执行.如果你的任务比较小,可以实行第二种方法.对系统来说比较省事,对程序员来说则反正都是麻烦.
我做了以下几个函数专门来插入任务到工作者线程.
//---------------wdf.h 中的内容 ------------------------
typedef WORK_QUEUE_ITEM wd_work_item;
typedef PWORKER_THREAD_ROUTINE wd_work_func;
// 任务的初始化
_inline wd_void wd_work_init(wd_work_item *item,
wd_work_func worker,
wd_void *context)
{
ExInitializeWorkItem(item,worker,context);
}
// 三种任务队列
typedef enum _wd_work_quque_type{
wd_work_crit = CriticalWorkQueue,
wd_work_delay = DelayedWorkQueue,
wd_work_hyper = HyperCriticalWorkQueue
} wd_work_queue_type;
_inline wd_void wd_work_queue(in wd_work_item *item,
in wd_work_queue_type type)
{
ExQueueWorkItem(item,(WORK_QUEUE_TYPE)type);
}
_inline wd_void wd_work_run(in wd_work_item *item)
{
(item->WorkerRoutine)(item->Parameter);
}
任务是一个数据结构,已经被我重定义为wd_work_item,wd_work_init能初始化它.初始化的时候你只需要填写一个你的任务的函数.同时一个context用来记录上下相关参数.(这是个空指针,你可以只想你任何想要的参数类型).
一般这个任务会自动执行,但是有时我们也想不插入队列,我们自己执行它.那么调用wd_work_run即可.
然后调用wd_work_queque插入工作者队列,之后会被执行.插入类型这里选择wd_work_delay.
希望你没有被这一串东西搞糊涂.现在我会写一个"设置完成函数"的函数.执行后,自动在Passive Level级执行你的完成函数.希望不会把你搞得晕头转向的:).
// 完成例程上下文。好几个fsctl需要注册完成例程。而例程中的工作可能
// 只能在passive level中运行,因此不得不加入一个work_item,把任务塞
// 入工作线程等待完成
typedef struct _my_fsctl_comp_con
{
wd_work_item work;
wd_dev *dev;
wd_irp *irp;
wd_dev *new_dev; // 这个元素仅仅用于mount的时候。因为我
// 们要生成一个新设备来绑定vdo.
} my_fsctl_comp_con;
wd_bool my_fsctl_set_comp(wd_dev *dev,
wd_irp *irp,
wd_dev *new_dev,
wd_irp_comp_func complete,
wd_work_func work_complete)
{
my_fsctl_comp_con *context;
context = (wdff_fsctl_comp_con *)wd_malloc(wd_false,
sizeof(wdff_fsctl_comp_con));
if(context == NULL)
{
wd_printf0("fsctl set comp: failed to malloc context.rn");
return wd_false;
}
// 初始化工作细节
wd_work_init(&context->work,
work_complete,
context);
context->dev = dev;
context->irp = irp;
context->new_dev = new_dev;
// 设置irp完成例程
wd_irp_comp(irp,complete,context);
return wd_true;
}
// 以下函数作为以上complete的参数被使用
wd_stat my_fsctl_comp(in wd_dev *dev,
in wd_irp *irp,
in wd_void *context)
{
wd_printf0("fsctl_comp: come in!!!rn");
UNREFERENCED_PARAMETER(dev);
UNREFERENCED_PARAMETER(irp);
// 判断当前中断级
if(wd_get_cur_irql() > wd_irql_passive)
{
wd_printf0("fsctl_comp:into quque!!!rn");
// 如果在passive更低的中断级别,必须插入延迟队列中运行
wd_work_queue((wd_work_item *)context,wd_work_delay);
}
else
{
// 否则可以直接执行
wd_printf0("fsctl_comp:run directly!!!rn");
wd_work_run((wd_work_item *)context);
}
return wd_stat_more_processing;
}
我想以上的过程应该已经可以理解了!注册了基本的完成历程complete函数(也就是我最后写的函数my_fsctl_comp后),irp执行完毕回调my_fsctl_comp,而我事先已经把已经做好的任务(wd_work_item)写在上下文指针中(context)中.一回调这个函数,我就wd_work_queque插入队列.结果wd_work_item中记录的work_complete函数显然会在Passive level中执行.我们的系统也将保持稳定.
work_complete函数将从context上下文指针中得到足够的参数,来完成对Volume的绑定.
希望你没有被弄昏头:),我们下回再分解.
8 终于绑定了Volume,读操作的捕获与分析
上文已经讲到绑定Volume之前的关键操作.我们一路逢山开路,逢水架桥,相信你从中也学到了驱动开发的基本方法.以后的工作,无非灵活运用这些方法而已.前边已经举出过绑定FS CDO的操作.那么现在绑定Volume,无非照猫画虎,而以后的教程中,我也不会逐一详尽的列举出细节的代码了.
但是绑定Volume的过程中,还是有些地方需要稍微注意:
1.获得Storage Device Object的问题.前边已经说过,为了得到Vbp,我要先得到Storage Device Object,方法如下:
wd_irpsp *irpsp = wd_cur_io_stack(irp);
wd_dev *storage_dev = wd_irpsp_mount_storage(irpsp);
这是在Irp完成之前,这样调用是对的.但是到完成函数中,情况就不同了.因为这个irpsp下保存的storage_dev可能被修改掉(这并非我自己调试的结果,而是阅读sfilter代码中的注释而得到的信息).既然有这个可能,我们只好先把storage_dev这个东西保存下来.实际上,可以在Device扩展增加一个项目storage_dev.在irp完成之前,生成我要绑定的设备(只是不绑定),并把这个设备指针传入完成函数上下文.
这样在完成函数中,我就能得到这个storage_dev,最终找到Volmue设备的指针,此时进行绑定就可以了.
2.绑定的过程,未必一次就能成功.因为Volmue设备的Initilize标记被清除之前,我的绑定是不能成功的.对这种情况,我抄袭了sfilter中的方法,就是延时500毫秒,然后再尝试.一共尝试8次.
我包装了一个函数用来延时:
_inline wd_void wd_delay_milli_se(wd_ulong milli_sec)
{
wd_llong delay = milli_sec*(-10)*1000;
wd_lgint interval;
interval.QuadPart = delay;
KeDelayExecutionThread(KernelMode,FALSE,&interval);
}
这个函数的参数是毫秒,但是我并不清楚有多小的精度.
其他的就不说了,祝愿你的运气足够的好.现在我们处理IRP_MJ_READ,如果你已经绑定了Volume,那么显然,发送给Volume的请求就会先发送给你.处理IRP_MJ_READ,能捕获文件的读操作.
进入你的my_disp_read()函数(假设你注册了这个函数来处理IRP_MJ_READ,请见前面关于分发函数的讲述),首先判断这个Dev是不是绑定Volume的设备.如果是,那么就是一个读文件的操作.
如何判断?记得我们先绑定Volume的时候,在我们的设备扩展中设置了storage_dev,如果不是(比如是FS CDO,我们没设置过),那么这么判断即可:
if(is_my_dev(dev))
{
my_dev_ext *ext = (my_dev_ext *)wd_dev_ext(dev);
if(ext->storage_dev)
{
// ... 到这里说明是对文件的读操作
}
}
其他的情况不需要捕获,请直接传递到下层.
读请求的IRP情况非常复杂,请有足够的心理准备.并不要过于依赖帮助,最好的办法就是自己打印IRP的各个细节,亲自查看文件读操作的完成过程.而我现在所说的东西,换另一我未尝试过的版本的windows是否还正确,我也无法下断言.
不过基本的东西是不会变的.
首先关心被读写的文件.IRP下有一个FileObject指针.这个东西指向一个文件对象.你可以得到文件对象的名字,这个名字是没有盘符的文件全路径.有人要问那么盘符如何获得?因为你已经知道了Volume,前边已经说过盘符不过是Volume的符号连接名,那么想要真正的全路径问题应该也不大了.
我现在偷一个懒,我现在只打印没有盘符的路径名.先写一个函数,从IRP得到FileObject.
_inline wd_file *wd_irp_file(wd_irpsp *irpsp)
{
return irpsp->FileObject;
}
然后写一个函数来获得文件名.这个函数参考的是FileMon的代码.
wd_void wd_file_get_name(in wd_file *file,
in out wd_ustr *name)
{
if( file->FileName.Buffer &&
!(file->Flags & FO_DIRECT_DEVICE_OPEN) )
RtlCopyUnicodeString(name,&file->FileName);
}
接下来有必要得到读文件的偏移量.和vxd的文件系统驱动不同,2000下文件系统得到的偏移量似乎都是从文件起始位置开始计算的.偏移量是一个LARGE_INTEGER.因为现在确实有些文件已经超过了长整型所能表示的大小.
以下函数用来得到偏移量.wd_lgint是经过重定义的LARGE_INTEGER.
_inline wd_lgint wd_irp_read_offset(wd_irpsp *irpsp)
{
return irpsp->Parameters.Read.ByteOffset;
}
注意以上的参数不是irp.是当前IO_STACK_LOCATION,也就是我的wd_irpsp.前面已经讲述过如何获取当前irpsp.
此外我还希望能得到我所读到的数据.这要注意,我们捕获这个请求的时候,这个请求还没有完成.既然没有完成,当然无数据可读.如果要获取,那就设置完成函数,在完成函数中完成请求.
完成Irp的时候忽略还是拷贝当前IO_STACK_LOCATION,返回什么STATUS,以及完成函数中如何结束Irp,是不那么容易搞清楚的一件事情.我想做个总结如下:
1.如果对irp完成之后的事情无兴趣,直接忽略当前IO_STACK_LOCATION,(对我的程序来说,调用wd_ship_cur_io_stack),然后向下传递请求,返回wd_irp_call()所返回的状态.
2.不但对irp完成之后的事情无兴趣,而且我不打算继续传递,打算立刻返回成功或失败.那么我不用忽略或者拷贝当前IO_STACK_LOCATION,填写参数后调用IoCompleteRequest,并返回我想返回的结果.
3.如果对irp完成之后的事情有兴趣,并打算在完成函数中处理,应该首先拷贝当前IO_STACK_LOCATION(wd_copy_cur_io_stack()),然后指定完成函数,并返回wd_irp_call()所返回的status.完成函数中,不需要调用IoCompleteRequest!直接返回Irp的当前状态即可.
4.同3的情况,有时候,会把任务塞入系统工作者线程或者希望在另外的线程中去完成Irp,那么完成函数中应该返回wd_stat_more_processing,此时完成Irp的时候应该调用IoCompleteRequest.另一种类似的情况是在dispatch函数中等待完成函数中设置事件,那么完成函数返回wd_stat_more_processing,dispatch函数在等待结束后调用IoCompleteRequest.
前边已经提到过设备的DO_BUFFERED_IO,DO_DIRECT_IO这两个标记.情况是3种:要么是两个标记中其中一个,要么是一个都没有.Volume设备出现DO_BUFFERED的情况几乎没有,我碰到的都是一个标记都没有.DO_DIRECT_IO表示数据应该返回到Irp->MdlAddress所指向的MDL所指向的内存.在无标记的情况下,表明数据读好,请返回到Irp->UseBuffer中即可.
UseBuffer是一个只在当前线程上下文才有效的地址.如果你打算按这个地址获得数据,你最好在当前线程上下文中.完成函数与my_disp_read并非同一个线程.所以在完成函数中按这个地址去获取数据是不对的.如何回到当前线程?我采用简单的办法.在my_disp_read中设置一个事件,调用wd_irp_call(即ddk中的IoCallDriver)之后开始等待这个事件.而在完成函数中设置这个事件.这样等待结束的时候,刚好Irp已经完成,我也回到了我的my_disp_read原来的线程.
wd_stat my_disp_read(in wd_dev *dev,in wd_pirp irp)
{
my_dev_ext *ext;
wd_dev *attached_dev;
wd_irpsp *irpsp = wd_cur_io_stack(irp);
wd_stat status;
wd_file *file = wd_irp_file(irpsp);
wd_lgint offset = wd_irp_read_offset(irpsp);
wd_size length = wd_irp_read_length(irpsp);
wd_wchar name_buf[512];
wd_ustr name;
wd_event event;
// 检查是否我的设备
if(!is_my_dev(dev))
return wd_irp_failed(irp,wd_stat_invalid_dev_req);
ext = (wdff_dev_ext *)wd_dev_ext(dev);
attached_dev = wdff_dev_attached(dev);
// 到这里判断得到这是对一个被绑定了的卷的读操作
if(ext->storage_dev == NULL)
{
wd_skip_io_stack(irp);
return wd_irp_call(attached_dev,irp);
}
// 到了这里,确认是对文件的读
wd_ustr_init_em(&name,name_buf,512);
if(file)
wd_file_get_name((wd_void *)file,&name);
else
{
wd_skip_io_stack(irp);
return wd_irp_call(attached_dev,irp);
}
wd_printf1("xxx irp flag = %xrn",wd_irp_flags(irp));
wd_printf1("xxx file read: %wZ rn",&name);
wd_printf1("xxx read offset = %ld ",offset);
wd_printf1("xxx read length = %ldrn",length);
// 以上我已经打印了读请求的相关参数,下面我希望得到读出的内容
wd_event_init(&event);
// 先拷贝当前io_stack,然后再指定完成例程
wd_copy_io_stack(irp);
wd_irp_set_comp(irp,my_disp_read_comp,&event);
// 对实际设备呼叫irp
status = wd_irp_call(attached_dev,irp);
if(status == wd_stat_pending)
wd_event_wait(&event);
wd_printf1("test read end:status = %x rn",status);
// 如果此时status = 0,那么内容应该就在Irp->UserBuffer中,请自己打印...
wd_printf1("test read end:read length = %ldrn",wd_irp_infor(irp));
return wd_irp_over(irp);
}
然后是my_disp_read_comp的内容,可以看见只是简单的设置事件,然后返回wd_stat_more_processing.
wd_stat my_disp_read_comp(in wd_dev *dev,
in wd_irp *irp,
in wd_void *context)
{
wd_event * event=
(wd_event *)context;
UNREFERENCED_PARAMETER(dev);
UNREFERENCED_PARAMETER(irp);
wd_event_set(event);
return wd_stat_more_processing;
}
尽管已经写了很多,尽管我们得到了读过程的所有参数和结果,我们依然不知道如果自己写一个文件系统,该如何完成读请求,或者过滤驱动中,如何修改读请求等等.我们下一节继续讨论读操作.
9 完成读操作
除非是一个完整的文件系统,完成读操作似乎是不必要的。过滤驱动一般只需要把请求交给下层的实际文件系统来完成。但是有时候比如加解密操作,我希望从下层读到数据,解密后,我自己来完成这一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).还有其他几种情况,资料上有解释,但是我没自己调试过,也就不胡说了。只拿自己调试过的几种情况来说说。
先写个函数,得到次功能号。
_inline wd_uchar wd_irpsp_minor(wd_io_stack *irpsp)
{
return irpsp->MinorFunction;
}
enum {
wd_mn_mdl = IRP_MN_MDL,
wd_mn_mdl_comp = IRP_MN_MDL_COMPLETE,
wd_mn_normal = IRP_MN_NORMAL
};
希望你喜欢我重新定义过的次功能号码。
wd_mn_normal的情况完全与上一节同(读者可能要骂了,上一节明明说就是这样的,结果还有其他的情况,那不是骗人么?但是微软的东西例外就是多,我也实在拿他没办法... ).
注意如上节所叙述,wd_mn_normal的情况,既有可能是在Irp->MdlAddress中返回数据,也可能是在Irp->UserBuffer中返回数据,这个取决于Device的标志.
但是如果次功能号为wd_mn_mdl则完全不是这个意思。这种irp一进来看数据,就赫然发现Irp->MdlAddress和Irp->UserBuffer都为空。那你得到数据后把数据往哪里拷贝呢?
wd_mn_mdl的意思是请自己分配一个mdl,然后把mdl指向你的数据所在的空间,然后返回给上层。自然mdl是要释放的,换句话说事业使用完毕要归还,所以又有wd_mn_mdl_comp,意思是一个mdl已经使用完毕,可以释放了。
mdl用于描述内存的位置。据说和NDIS_BUFFER用的是同一个结构。这里不深究,我写一些函数来分配和释放mdl,并把mdl指向内存位置或者得到mdl所指向的内存:
// 释放mdl
_inline wd_void wd_mdl_free(wd_mdl *mdl)
{
IoFreeMdl(mdl);
}
// 这个这个东西分配mdl,缓冲必须是非分页的。可以在dispatch level跑。
_inline wd_mdl *wd_mdl_alloc(wd_void* buf,
wd_ulong length)
{
wd_mdl * pmdl = IoAllocateMdl(buf,length,wd_false,wd_false,NULL);
if(pmdl == NULL)
return NULL;
MmBuildMdlForNonPagedPool(pmdl);
return pmdl;
}
// 这个函数分配一个mdl,并且带有一片内存
_inline wd_mdl *wd_mdl_malloc(wd_ulong length)
{
wd_mdl *mdl;
wd_void *point = wd_malloc(wd_false,length);
if(point == NULL)
return NULL;
mdl = wd_mdl_alloc(point,length);
if(mdl == NULL)
{
wd_free(point);
return NULL;
}
return mdl;
}
// 这个函数释放mdl并释放mdl所带的内存。
_inline wd_void wd_mdl_mfree(wd_mdl *mdl)
{
wd_void *point = wd_mdl_vaddr(mdl);
wd_mdl_free(mdl);
wd_free(point);
}
// 得到地址。如果想往一个MdlAddress里边拷贝数据 ...
_inline wd_void *wd_mdl_vaddr(wd_mdl *mdl)
{
return MmGetSystemAddressForMdlSafe(mdl,NormalPagePriority);
}
另外我通过这两个函数来设置和获取Irp上的mdl.
_inline wd_mdl *wd_irp_mdl(wd_irp *irp)
{
return irp->MdlAddress;
}
_inline wd_void wd_irp_mdl_set(wd_irp *irp,
wd_mdl *mdl)
{
irp->MdlAddress = mdl;
}
一个函数获得UserBuffer.
_inline wd_void * wd_irp_user_buf(wd_irp *irp)
{
return irp->UserBuffer;
}
要完成请求还有一个问题。就是irp->IoStatus.Information.在这里你必须填上实际读取得到的字节数字。不然上层不知道有多少数据返回。这个数字不一定与你的请求的长度等同(其实我认为几乎只要是成功,就应该都是等同的,唯一的例外是读取到文件结束的地方,长度不够了的情况)。我用下边两个函数来获取和设置这个数值:
_inline wd_void wd_irp_infor_set(wd_irp *irp,
wd_ulong infor)
{
irp->IoStatus.Information = infor;
}
_inline wd_ulong wd_irp_infor(wd_irp *irp)
{
return irp->IoStatus.Information;
}
也许你都烦了,但是还有事情要做。作为读文件的情况,如果你是自己完成请求,不能忘记移动一下文件指针。否则操作系统会不知道文件指针移动了而反复读同一个地方永远找不到文件尾,我碰到过这样的情况。
一般是这样的,如果文件读取失败,请保持原来的文件指针位置不要变。如果文件读取成功,请把文件指针指到“读请求偏移量+成功读取长度”的位置。
这个所谓的指针是指Irp->FileObject->CurrentByteOffset.
我跟踪过正常的windows文件系统的读行为,我认为并不一定是向我上边说的这样做。情况很复杂,有时动,有时不动(说复杂当然是因为我不理解),但是按我上边说的方法来完成,我还没有发现过错误。
我用以下函数来设置和获取这个。
_inline wd_void wd_file_offset_add(wd_file *file,wd_ulong len)
{
file->CurrentByteOffset.QuadPart += len;
}
_inline wd_void wd_file_offset_set(wd_file *file,wd_lgint offset)
{
file->CurrentByteOffset = offset;
}
_inline wd_lgint wd_file_offset(wd_file *file)
{
return file->CurrentByteOffset;
}
现在看看怎么完成这些请求,假设我已经有数据了。现在假设本设备缓冲标记为0,即正常情况采用Irp->UserBuffer返回数据. 这些当然都是在my_disp_read中或者是其他想完成这个irp的地方做的(希望你还记得我们是如何来到这里),假设其他必要的判断都已经做了:
wd_irpsp *irpsp = wd_irp_cur_io_stack(irp);
switch(wd_irpsp_minor(irpsp))
{
// 我先保留文件的偏移位置
case wd_mn_normal:
{
wd_void *point = wd_irp_user_buf(irp);
// 如果有数据,就往point ...里边拷贝 ...
wd_irp_infor_set(irp,length);
wd_irp_status_set(irp,wd_suc);
wd_file_offset_set(wd_irp_file(irp),offset+length);
return wd_irp_over(irp);
}
case wd_mn_mdl:
{
wd_void *mdl = wd_mdl_malloc(length); // 情况比上边的复杂,请先分配mdl
if(mdl == NULL)
{
// ... 返回资源不足 ...
}
wd_irp_mdl_set(irp,mdl);
wd_irp_infor_set(irp,length);
wd_irp_status_set(irp,wd_suc);
wd_file_offset_set(wd_irp_file(irp),offset+length);
return wd_irp_over(irp);
}
case wd_mn_mdl_comp:
{
// 没有其他任务,就是释放mdl
wd_mdl_mfree(wd_irp_mdl(irp));
wd_irp_mdl_set(irp,0);
wd_irp_infor_set(irp,0);
wd_irp_status_set(irp,wd_status_suc);
return wd_irp_over(irp);
}
default:
{
// 我认为其他的情况不过滤比较简单 ...
}
}
重要提醒:wd_mn_mdl的情况,需要分配一个mdl,并且这个mdl所带有的内存是有一定长度的,这个长度必须与后来的irp->IoStatus.Information相同!似乎上层并不以irp->IoStatus.Information返回的长度为准。比如明明只读了50个字节,但是你返回了一个mdl指向内存长度为60字节,则操作系统则认为已经读了60个字节!这非常糟糕。
最后提一下文件是如何结尾的。如果到某一处,返回成功,但是实际读取到的数据没有请求的数据长,这时还是返回wd_status_suc(STATUS_SUCCESS),但是此后操作系统会马上发irp来读最后一个位置,此时返回长度为0,返回状态STATUS_FILE_END即可。
10 自己发送Irp完成读请求
关于这个有一篇文档解释得很详细,不过我认为示例的代码有点太简略了,这篇文档在IFS所附带的OSR文档中,请自己寻找。
为何要自己发送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的代码:
_inline wd_irp *wd_irp_fsd_read_alloc(wd_dev *dev,
wd_void *buf,
wd_ulong length,
wd_lgint *offset,
wd_io_stat_block *io_stat)
{
return IoBuildAsynchronousFsdRequest(IRP_MJ_READ,dev,
buf,length,
offset,
io_stat);
}
io_stat我不知道是做何用,我一般填写NULL.类型是PIO_STATUS_BLOCK.buf则是缓冲。在Irp中被用做UserBuffer接收数据。offset是这次读的偏移量。
以上函数构造一个读irp.请注意,此时您还没有设置FileObject.实际上我是这样发出请求的:
irp = wd_irp_fsd_read_alloc(dev,buf,len,&start,NULL);
if(irp == NULL)
return;
irpsp = wd_irp_next_sp(irp);
wd_irpsp_file_set(irpsp,file);
wd_irp_comp(irp,my_req_comp,context);
请注意wd_irp_next_sp,我是得到了这个irp的下一个IO_STACK_LOCATION,然后我设置了FileObject.接下来应该设置了完成后,我就可以发送请求了!请求发送完毕后,一旦系统完成请求就会调用你的my_req_comp.
再看看my_req_comp如何收场:
wd_stat my_req_comp(in wd_dev *dev,
in wd_irp *irp,
in wd_void *context)
{
// 请注意,无论成功还是失败,我都得在这里彻底销毁irp
wd_irp_send_by_myself_free(irp);
// 返回这个,仅仅是因为irp我已经销毁,不想让系统再次销毁它而已。
return wd_stat_more_processing;
}
wd_stat_more_prcessing就是STATUS_MORE_PROCESSING_REQUIRED。之所以返回这个,是因为我已经把irp给删除了。我不想windows系统再对这个irp做什么。所以干脆说我还要继续处理,这样避免了io管理器再去动用这个irp的可能。
最后是那个irp删除函数的代码:
_inline wd_void wd_irp_send_by_myself_free(wd_irp *irp)
{
if (irp->MdlAddress)
{
MmUnmapLockedPages(MmGetSystemAddressForMdl(irp->MdlAddress),
irp->MdlAddress);
MmUnlockPages(irp->MdlAddress);
IoFreeMdl(irp->MdlAddress);
}
IoFreeIrp(irp);
};
因为我自己没有分配过MdlAddress下去过。所以如果下边返回了MDL,我得附带清理它。
好了,祝愿你心情愉快。如果你何我一样懒惰的话,不妨就用上边的简单方法自己发irp来读文件了。
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中的参数对应的,请自己研究。我先写一些函数包装来方便读取irpsp.
_inline wd_ulong wd_irpsp_file_opts(wd_irpsp *irpsp)
{
return irpsp->Parameters.Create.Options;
}
_inline wd_ushort wd_irpsp_file_attrs(wd_irpsp *irpsp)
{
return irpsp->Parameters.Create.FileAttributes;
}
enum {wd_file_opt_dir = FILE_DIRECTORY_FILE};
enum {wd_file_attr_dir = 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的完成函数。如果设置这里不再重复,请参考上边对文件读操作。
wd_stat my_create_comp(in wd_dev *dev,
in wd_irp *irp,
in wd_void *context)
{
wd_irpsp *irpsp = wd_irp_cur_sp(irp);
wd_file *file = wd_irpsp_file(irpsp);
UNREFERENCED_PARAMETER(dev);
if(wd_suc(wd_irp_status(irp))
{
// 如果成功了,把这个FileObject记录到集合里,这是一个
// 刚刚打开或者生成的目录
if(file &&
(wd_irpsp_file_opts(irpsp) & wd_file_opt_dir))
add_obj_to_set(file);
}
return wd_irp_status(irp);
}
这里顺便解释一下UNREFERENCED_PARAMETER宏。我曾经不理解这个宏的意思。其实就是因为本函数传入了三个参数,这些参数你未必会用到。如果你不用的话,大家知道c编译器会发出一条警告。一般认为驱动应该去掉所有的警告,所以用了这个宏来“使用”一下没有用到过的参数。你完全可以不用他们。
现在所有的目录都被你记录。那么得到一个FileObject的时候,判断一下这个FileObject在不在你的集合里,如果在,就说明是目录,反之是文件。
当这个FileObject被关闭的时候你应该把它从你的集合中删除。你可以捕获Close的IRP来做这个。记得本教程很早以前,我们已经安装过my_close函数的来处理IRP(请回忆或者翻阅第3节的内容),那么很简单了,在该函数中从你的集合中删除该FileObject即可。作为保险的做法,应该观察一下关闭是否成功。如果成功的话,再进行你的从集合中删除元素工作。
因为判断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的时候被删除。
以上的我都未实际调试,也不再提供示例的代码。有兴趣的读者请自己完成。