文件系统驱动(IFS DDK)学习笔记 (转)

文件系统驱动是windows系统中最复杂的驱动种类之一。它的全称 IFS DDK 是指可安装文件系统 设备驱动程序开发工具。这方面的资料非常少,我从网上找到的资料大都是文档,都是一些在学习文件系统驱动的人自己写的,几乎没有这方面的专门书籍,更不用说中文资料了。而且ifs ddk中的帮助文档没多大意义,文件系统相关的ddk帮助极其简略,很多重要的部分仅仅轻描淡写的带过。

下图是我们的IFS DDK在堆栈中的位置

I/O管理器

文件驱动

中间驱动层

设备驱动

硬件抽象层

可以看出IFS DDK是在我们的硬件和操作系统之间工作的。

那么如何学好IFS DDK呢?

首先,要把 Windows 的底层基础学扎实。

其次,就是要把英文阅读能力练好。所有资料几乎全是英文的,DDK开发工具也全是英文,而且你别指望会有中文开发工具面市,至少短期内是不会有的,我费了老大劲才从Microsoft偷出了WDK工具包,现在的文件系统驱动是要收费的,就是博士说的4000$。这种东西一般是不会有人共享的。

再次,还有附加的一个条件就是要有足够的耐心,驱动程序设计将要花费的时间是你无法想象到的,没有耐性建议趁早转行。

最后,也是最重要的一点就是一定要亲自动手做!看再多的资料不动手,那只是停留在理论上,实践永远是空白。

看了上面是不是已经做好了心理准备了?准备好了我们就继续往下看。

开发文件系统驱动的目的

一是用于防病毒引擎。希望在系统读写文件的时候,捕获读写的数据内容,然后检测其中是否含有病毒代码。

二是用于加密文件系统,希望在文件写过程中对数据进行加密,在读的过程中进行解密。

三是设计透明的文件系统加速。读写磁盘的时候,合适的cache算法是可以大大提高磁盘的工作效率。windows本身的cache算法未必适合一些特殊的读写磁盘操作(如流媒体服务器上读流媒体文件)。

知道了开发目的,我们就来了解一下DDK里面的具体东西

例程Routine:简单说例程就是函数。

接口Api):编程开发接口,一个提供给你调用的函数。

Stream):实际上就是FileObject(文件对象),是一个文件中的物理数据流。

文件File):一个文件可能有多个FileObject,因为可能多次打开,多个FileObject可能对应一个文件。

Field):数据结构中的一个数据成员。数据库中称为字段。面向对象中称为数据成员。

回调(Callback)函数:一个由系统调用而不能自己调用的函数。

入口函数(DriverEntry)win32编程一样,驱动也有一个入口函数 (相当于main  WinMain),这里注意他们之间的运行方式是不同的,win32入口函数后是进入消息循环,驱动DriverEntry的主要功能是注册一些IRP的相当于回调处理方式的函数,也就是派遣函数。入口函数主要是创建设备对象及符号连接,以及其它初使化操作,如分配池内存等。

出口函数(DriverUnload):删除符号连接与设备对象,并释放已经分配的各种资源。

IRP(I/O请求包),驱动编程里十分重要的概念,说白了它就是一个数据结构,里面有对应操作需要的数据,比如我们在应用层调用了CreateFile函数,在驱动层就需要构建一个IRP(IRP_MJ_CREATE),通过这个IRP的处理完成对应操作,如果我们对用户打开,创建文件等操作有兴趣,就可以通过挂接的方式让系统将这个IRP发送到我们指定的函数(DriverEntry里面指定的)。

DriverObject->MajorFunction[IRP_MJ_CREATE]=FileDiskCreateClose

DriverObject->MajorFunction[IRP_MJ_CLOSE]=FileDiskCreateClose;响应建立关闭DriverObject->MajorFunction[IRP_MJ_READ]=FileDiskReadWrite; DriverObject->MajorFunction[IRP_MJ_WRITE]=FileDiskReadWrite;响应一般读写DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = FileDiskDeviceControl; 设置IRP控制函数

DriverObject->DriverUnload = FileDiskUnload; 设置卸载规则

CDO(control Device Object,控制设备对象,这个对象代表这个驱动,它是供我们应用层的程序使用的,在DriverEntry里面创建它,它是没有设备扩展的。我们一般见到说的DOdevice object)是用来绑定到文件系统驱动的FDO上的,和CDO不同。他们都使用IoCreateDevice函数创建。  

Device Extension(设备扩展),其实它就是一块保存有用数据的内存,这里一般就是一个结构体。它有一个好处就是它可以随DO一起传递,方便又节省资源。

访问输入输出参数:

Buffered方式:使用IoGetCurrentIrpStackLocation得到调用者堆栈区域指针IrpStack(PIO_STACK_LOCATION类型)

Irp->AssociatedIrp.SystemBuffer;输入输出缓冲

IrpStack->Parameters.DeviceIoControl.InputBufferLength;输入缓冲长度

IrpStack->Parameters.DeviceIoControl.OutputBufferLength;输出缓冲长度

IrpStack->Parameters.DeviceIoControl.IoControlCode;设备控制代码

如果需要输出参数,在填写完SystemBuffer后要设置IRPIoStatus成员的Information指示输出数据的长度.

Direct方式:MmGetSystemAddressForMdlSafe映射IRP->MdlAddress地址到内核空间

MmGetMdlByteCount,得到MDL大小,以字节为单位.通常应该在IRQL<=DISPATCH_LEVEL的情况下使用MDL,

Neither方式:这个方式比恐怖,除非确定驱动不是分层的并且运行在PASSIVE_LEVEL,一般不使用这种方式.比如写一个简单的Dump核心数据结构的驱动,该驱动只由我们的一个程序控制,那么可以直接把用户模式的地址传给驱动使用。

前面提到了IRP在驱动里是个非常重要的概念,那我们下面就着重看一下I/O请求包数据结构中的具体东东

 Flags(ULONG) 包含一些对驱动程序只读的标志。

 AssociatedIrp(union) 是一个三指针联合。其中,与WDK驱动程序相关的指针是AssociatedIrp.SystemBuffer SystemBuffer指针指向一个数据缓冲区,该缓冲区位于内核模式的非分页内存中。对于IRP_MJ_READIRP_MJ_WRITE操作,如果顶级设备指定DO_BUFFERED_IO标志,则I/O管理器就创建这个数据缓冲区。对于IRP_MJ_DEVICE_CONTROL操作,如果 I/O控制功能代码指出需要缓冲区,则I/O管理器就创建这个数据缓冲区。I/O管理器把用户模式程序发送给驱动程序的数据复制到这个缓冲区,并对数据进行管理,这也是创建IRP过程的一部分。这些数据可以是与WriteFile调用有关的数据,或者是DeviceIoControl调用中所谓的输入数据。对于读请求,设备驱动程序把读出的数据填到这个缓冲区,然后I/O管理器再把缓冲区的内容复制到用户模式缓冲区。对于指定了 METHOD_BUFFEREDI/O控制操作,驱动程序把所谓的输出数据放到这个缓冲区,然后I/O管理器再把数据复制到用户模式的输出缓冲区。

  IoStatus(IO_STATUS_BLOCK) 是一个仅包含两个域的结构,驱动程序在最终完成请求时设置这个结构。

  RequestorMode 等于一个枚举常量UserModeKernelMode,指定原始I/O请求的来源。驱动程序有时需要查看这个值来决定是否要信任某些参数。

  PendingReturned(BOOLEAN) 如果为TRUE,则表明处理该IRP的最低级派遣例程返回了STATUS_PENDING。完成例程通过参考该域来避免自己与派遣例程间的潜在竞争。

  Cancel(BOOLEAN) 如果为TRUE,则表明IoCancelIrp已被调用,该函数用于取消这个请求。如果为FALSE,则表明没有调用IoCancelIrp函数。

  CancelIrql(KIRQL) 是一个IRQL值,表明那个专用的取消自旋锁是在这个IRQL上获取的。当你在取消例程中释放自旋锁时应参考这个域。

  IRQLInterrupt ReQuest Level,中断请求级别。

  CancelRoutine(PDRIVER_CANCEL),是驱动程序取消例程的地址。可以使用IoSetCancelRoutine函数设置这个域但尽量不要直接修改该域。

任何内核模式程序在创建一个IRP时,同时还创建了一个与之关联的 IO_STACK_LOCATION结构数组:数组中的每个堆栈单元都对应一个将处理该IRP的驱动程序,另外还有一个堆栈单元供IRP的创建者使用。堆栈单元中包含该IRP的类型代码和参数信息以及完成函数的地址。

MajorFunction

MinorFunction

Flags

Control

Parameters

DeviceObject

FileObject

CompletionRoutine

Context

 

上图是I/O堆栈单元数据结构的示例图,下面对图中的函数做一下解释。

  MajorFunction(UCHAR)  IRP的主功能码。这个代码应该为类似 IRP_MJ_READ一样的值,并与驱动程序对象中MajorFunction表的某个派遣函数指针相对应。如果该代码存在于某个特殊驱动程序的I/O 堆栈单元中,它有可能一开始是,例如IRP_MJ_READ,而后被驱动程序转换成其它代码,并沿着驱动程序堆栈发送到低层驱动程序。我将在第十一章 (USB总线)中举一个这样的例子,USB驱动程序把标准的读或写请求转换成内部控制操作,以便向USB总线驱动程序提交请求。

  MinorFunction(UCHAR)  IRP的副功能码。它进一步指出该IRP属于哪个主功能类。例如,IRP_MJ_PNP请求就有约一打的副功能码,如IRP_MN_START_DEVICEIRP_MN_REMOVE_DEVICE,等等。

  Parameters(union)   是几个子结构的联合,每个请求类型都有自己专用的参数,而每个子结构就是一种参数。这些子结构包括Create(IRP_MJ_CREATE请求)Read(IRP_MJ_READ请求)StartDevice(IRP_MJ_PNPIRP_MN_START_DEVICE子类型),等等。

  DeviceObject(PDEVICE_OBJECT)   是与堆栈单元对应的设备对象的地址。该域由IoCallDriver函数负责填写。

  FileObject(PFILE_OBJECT)  是内核文件对象的地址,IRP的目标就是这个文件对象。驱动程序通常在处理清除请求(IRP_MJ_CLEANUP)时使用FileObject指针,以区分队列中与该文件对象无关的IRP

  CompletionRoutine(PIO_COMPLETION_ROUTINE)  是一个I/O完成例程的地址,该地址是由与这个堆栈单元对应的驱动程序的更上一层驱动程序设置的。你绝对不要直接设置这个域,应该调用IoSetCompletionRoutine函数,该函数知道如何参考下一层驱动程序的堆栈单元。设备堆栈的最低一级驱动程序并不需要完成例程,因为它们必须直接完成请求。然而,请求的发起者有时确实需要一个完成例程,但通常没有自己的堆栈单元。这就是为什么每一级驱动程序都使用下一级驱动程序的堆栈单元保存自己完成例程指针的原因。

  Context(PVOID)  是一个任意的与上下文相关的值,将作为参数传递给完成例程。你绝对不要直接设置该域;它由IoSetCompletionRoutine函数自动设置,其值来自该函数的某个参数。

 

 

过滤/挂钩IRP请求

    对于过滤/挂钩IRP请求我也不是十分理解,只是觉得比较有用,在这里拿出来与大家一起探讨,如果有知道的可以联系我。

挂钩某个IRP处理函数

1 调用IoGetDeviceObjectPointer返回的设备对象(PDEVICE_OBJECT)

2 由设备对象得到驱动对象PDEVICE_OBJECT->DriverObject(PDRIVER_OBJECT),这里应该调用ObReferenceObjectByPointer函数增加驱动对象的引用计数,以防止该驱动程序在我们的驱动程序前被卸载。

3 再由驱动对象得到中断请求派遣函数表(PDRIVER_OBJECT->MajorFunction)

4 保存PDRIVER_OBJECT->MajorFunction[IRP_MJ_XXXXXXX](原中断请求派遣函数地址)

5 修改PDRIVER_OBJECT->MajorFunction[IRP_MJ_XXXXXXX](让它指向我们自定义的函数),使用锁总线前缀lockxchg指令进行赋值操作(让代码多线程和多处理器安全)

6 结束处理同System Service Hook

过滤某个设备的IRP请求:

1 初使化IRP请求派遣函数表MajorFunction,将它们全都指向一个派遣函数DispatchAny

2 再为MajorFunction指定几个我们感兴趣的IRP请求派遣函数

3 得到设备对象指针:IoGetDeviceObjectPointer

4 将设备加到设备堆栈上:IoAttachDeviceToDeviceStack,并保存下层设备对象,以供IoCallDriver时使用.

5 DispatchAny中将IRP传给下层驱动IoSkipCurrentIrpStackLocation,IoCallDriver.

6 在指定的几个请求派遣函数中对IRP进行处理.

说了这么多是不是有点迷糊了,下面我们来看一个驱动框架的创建过程

 

 

NTSTATUS DriverEntry(  

          IN   PDRIVER_OBJECT   DriverObject,  

          IN   PUNICODE_STRING   RegistryPath  )

{

  NTSTATUS status;

  DriverObject->DriverUnload   =   DriverUnload;    

  Device_object  *  ourCDO   =   NULL;   //控制设备对象,这里默认为空

  UNICODE_STRING           Name_2k

  RtlInitUnicodeString(&Name_2k,L"//FileSystem//OurFilter_Cdo");

  status = IoCreateDevice(DriverObject, 0,  &Name_orc);

  if(!NT_SUCCESS(status)) 

    return   status;  如果创建控制设备对象失败,返回status

  UNICODE_STRING    Link_orc;   创建一个Win32可见的符号连接

  RtlInitUnicodeString(&Link_orc,L"//DosDevices//OurFilter");  

  status = IoCreateSymbolicLink(&Link_orc   ,&Name_orc);  

  if(!NT_SUCCESS(status))  

    return   status;

}

void   DriverUnload(IN   PDRIVER_OBJECT   pDriverObject)  

{    

   UNICODE_STRING   uniNameString;  

   RtlInitUnicodeString(&Name_orc,   L"//FileSystem//OurFilter_Cdo");  

   IoDeleteSymbolicLink(&Name_orc);//删除win32可见的连接 

   IoDeleteDevice(pDriverObject->DeviceObject);//删除设备  

   return;  

}

 

  DriverObject拥有一组函数指针,称为dispatch  functions.开发驱动的主要任务就是亲手撰写这些dispatch  functions.当系统用到你的驱动时,会向你的DO发送IRP(这是windows所有驱动的共同工作方式)。你的任务是在dispatch   function中处理这些请求。你可以让irp失败,也可以成功返回,也可以修改这些irp,甚至可以自己发出irp

  这样一个最基本的驱动框架就出来了,再加入DbgPrint()调试信息,编译并调试后就可以看到输出的调试信息。大体框架是这样,里面有很多东西我也不理解,大概也有些错误。至于调试我现在还在学习中,DDK工具中的好多东西还没有弄明白。

你可能感兴趣的:(文件系统驱动(IFS DDK)学习笔记 (转))