0. 作者,楚狂人自述
我感觉Windows文件系统驱动的开发能找到的资料比较少。为了让技术经验不至于遗忘和引起大家交流的兴趣我以我的工作经验撰写本教程。
我的理解未必正确,有错误的地方望多多指教。有问题欢迎与我联系。我们也乐于接受各种驱动项目的开发。邮箱为[email protected],QQ为16191935。
作者是杭州楚狂人,首先在驱动开发网连载.此版本比驱网连载版本稍有修改.除了0外,已发1-8节.后继的章节将在不久后整理推出.您可以随时发邮件向我索取更新的版本.
最后更新日期是11月2日.
1. 概述,钻研目的和准备
我经常在网上碰到同行请求开发文件系统驱动。windows的pc机上以过滤驱动居多。其目的不外乎有以下几种:
一是用于防病毒引擎。希望在系统读写文件的时候,捕获读写的数据内容,然后检测其中是否含有病毒代码。
二是用于加密文件系统,希望在文件写过程中对数据进行加密,在读的过程中进行解密。
三是设计透明的文件系统加速。读写磁盘的时候,合适的cache算法是可以大大提高磁盘的工作效率。windows本身的cache算法未必适合一些特殊的读写磁盘操作(如流媒体服务器上读流媒体文件)。设计自己的cache算法的效果,我已在工作中有所感受。
如果你刚好有以上此类的要求,你可以阅读本教程。
文件系统驱动是windows系统中最复杂的驱动种类之一。不能对ifsddk中的帮助抱太多希望,以我的经验看来,文件系统相关的ddk帮助极其简略,很多重要的部分仅仅轻描淡写的带过。如果安装了ifsddk,应该阅读src\filesys\OSR_docs下的文档。而不仅仅是ddk帮助。
文件系统驱动开发方面的书籍很少。中文资料我仅仅见过侯捷翻译过的一本驱动开发的书上有两三章涉及,也仅仅是只能用于9x的vxd驱动。NT文件系统我见过一本英文书。我都不记得这两本书的书名了。
如果您打算开发9x或者nt文件系统驱动,建议你去网上下载上文提及的书。那两本书都有免费的电子版本下载。如果你打算开发Windows2000\WindowsXP\Window2003的文件系统驱动,你可以阅读本教程。虽然本教程仅仅讲述文件系统过滤驱动。但是如果您要开发一个全新的文件系统驱动的话,本教程依然对你有很大的帮助。
学习文件系统驱动开发之前,应该在机器上安装ifsddk。ddk版本越高级,其中头文件中提供的系统调用也越多。经常有人询问如xpddk编译的驱动能不能在2000上运行等等的问题。我想可以这样解释:高级版本的ddk应该总是可以编译低级驱动的代码,而且得到的二进制版本也总是可以在低级系统上运行。但是反过来就未必可以了。如果在高级系统上编写用于低级系统上的驱动,要非常认真的注意仅仅调用低级系统上有的系统调用。
ifsddk可以在某些ftp上免费下载。
我的使用的是ifs ddk for xp,但是我实际用来开发的两台机器有一台是windows 2000,另一台是windows 2003.我尽量使我编译出来的驱动,可以在2000\xp\2003三种系统上都通过测试。
安装配置ddk和在vc中开发驱动的方法网上有很多的介绍。ifsddk安装之后,src目录下的filesys目录下有文件系统驱动的示例。阅读这些代码你就可以快速的学会文件系统驱动开发。
filter目录下的sfilter是一个文件系统过滤驱动的例子。另一个filespy完全是用这个例子的代码加工得更复杂而已。
如何用ddk编译这个例子请自己查看相关的资料。
文件系统过滤驱动编译出来后你得到的是一个扩展名为sys的文件。同时你需要写一个.inf文件来实现这个驱动的安装。我这里不讨论.inf文件的细节,你可以直接用sfilter目录下的inf文件修改。
对inf文件点鼠标右键弹出菜单选择“安装”,即可安装这个过滤驱动。但是必须重新启动系统才生效。
如果重启后蓝屏无法启动,可以用其他方式引导系统后到system32\drivers目录下删除你的.sys文件再重启即可。我尝试这种情况下用安全模式结果还是蓝屏。所以我后来不得不在机器上装了两个2000系统。双系统情况下,一个系统崩溃了用另一个系统启动,删除原来的驱动即可。
如果要调试代码,请安装softice.
打开sfilter目录下的文件sources(这个文件没有扩展名),加入一行
BROWSER_INFO=1
然后打开Symbol Loader,File->Open选中你编译出来的xxx.sys,Modul->Load,Modul->Translate,然后就可以调试了。
打开softice,输入file *就可以看见代码。
如果准备好了,我们就可以开始琢磨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.当系统用到你的驱动,会向你的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:则是NTFS的两个Volume设备对象.
实际上"C:"是该设备的符号连接(Symbolic Link)名。而不是真正的设备名。可以打开Symbolic Links Viewer,能看到:
C: “\Device\HarddiskVolume1”
因此该设备的设备名为“\Device\HarddiskVolume1”.
这里也看出来,文件系统驱动是针对每个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)
{
// 这种情况发生于\FileSystem\Filters路径不存在。这个路径是
// 在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.\r\n");
return status;
}
}
wd_printf0("success: create cdo ok.\r\n");
return status;
}
为了让代码看起来象上边的那样,我不得不做了很多转换。如
#define wd_main DriverEntry
一种爽的感觉,终于可以在写看起来更象是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 PDRIVER_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".
3.分发例程,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[i] = 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)
{
// 这种情况发生于\FileSystem\Filters路径不存在。这个路径是
// 在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.\r\n");
return status;
}
}
wd_printf0("success: create cdo ok.\r\n");
// 开始设置几个分发例程
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.\r\n");
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中的示例自己完成这些代码。
现在我们的my_xxx系列的函数还没有开始写,因此驱动也不能编译通过。在后边的内容中再逐步介绍。
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!!! \r\n");
// 得到文件系统对象的名字,然后打印出来
wd_obj_get_name(dev,&name);
wd_printf0("notify : file sys name = %wZ\r\n",&name);
if(active)
{
wd_printf0("notify: try to attach.\r\n");
// ... 请在这里绑定文件系统的控制设备
}
else
{
wd_printf0("notify: unactive.\r\n");
// ...
}
}
应该如何绑定一个文件系统CDO?我们在下面的章节再详细描述。
现在我们应该再在wd_main函数中加上下边的内容:
if(wdff_reg_notify(driver,my_fs_notify) != wd_stat_suc)
{
wd_printf0("error: reg notify failed.\r\n");
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");
// 我不绑定识别器。所以如果是识别器,我直接返回成功。查看是否是识别
// 器的办法是看是否是\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.\r\n");
return wd_stat_suc;
}
wd_printf0("attach fs dev: not a recogonizer.\r\n");
接下来我将要生成我的设备。这里要提到设备扩展的概念。设备对象是一个数据结构,为了表示不同的设备,里边将有一片自定义的空间,用来给你记录这个设备的特有信息。我们为我们所生成的设备确定设备扩展如下:
// 文件过滤系统驱动的设备扩展
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.\r\n"));
return wd_stat_suc;
}
wd_printf0("attach fs dev: is my cared type.\r\n");
// 我不绑定识别器。所以如果是识别器,我直接返回成功。查看是否是识别
// 器的办法是看是否是\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.\r\n");
return wd_stat_suc;
}
wd_printf0("attach fs dev: not a recogonizer.\r\n");
// 现在来生成一个设备用来绑定
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.\r\n");
return status;
}
wd_printf0("attach fs dev: create dev success.\r\n");
// 接着设置设备的各种标志与之要绑定的标志一致
wd_dev_copy_flag(new_dev,fs_dev);
ext = (my_dev_ext *)wd_dev_ext(new_dev);
wd_printf0("begin to attach.\r\n");
status = wd_dev_attach(new_dev,fs_dev,&ext->attached_to);
wd_printf0("attach over.status = %8x\r\n",status);
if(!wd_suc(status))
{
wd_printf0("attach fs dev: dev attach failed.\r\n");
UNREFERENCED_PARAMETER(new_dev);
wd_dev_del(new_dev);
return status;
}
wd_printf0("attach fs dev: attach %wZ succeed.\r\n",&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(irpsp);
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.\r\n");
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!!!\r\n");
UNREFERENCED_PARAMETER(dev);
UNREFERENCED_PARAMETER(irp);
// 判断当前中断级
if(wd_get_cur_irql() > wd_irql_passive)
{
wd_printf0("fsctl_comp:into quque!!!\r\n");
// 如果在passive更低的中断级别,必须插入延迟队列中运行
wd_work_queue((wd_work_item *)context,wd_work_delay);
}
else
{
// 否则可以直接执行
wd_printf0("fsctl_comp:run directly!!!\r\n");
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的设备名,不太友好.
我现在偷一个懒,我现在只打印没有盘符的路径名.先写一个函数,从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 = %x\r\n",wd_irp_flags(irp));
wd_printf1("xxx file read: %wZ \r\n",&name);
wd_printf1("xxx read offset = %ld ",offset);
wd_printf1("xxx read length = %ld\r\n",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 \r\n",status);
// 如果此时status = 0,那么内容应该就在Irp->UserBuffer中,请自己打印...
wd_printf1("test read end:read length = %ld\r\n",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;
}
尽管已经写了很多,尽管我们得到了读过程的所有参数和结果,我们依然不知道如果自己写一个文件系统,该如何完成读请求,或者过滤驱动中,如何修改读请求等等.我们下一节继续讨论读操作.