目录热身阶段3问1:什么是过滤驱动(2009-4-15)3问2:什么是IRP(2009-4-15)4问3:驱动栈,设备栈(2009-4-15)7问4:文件系统过滤驱动(FSFD)为什么能过滤文件系统(FSD)(2009-4-16)9问5:怎么用好DDK(或WDK,现在起本书只说WDK)(2009-4-16)12Legacy驱动阶段13问6:DriverEntry和DriverUnload是干嘛的(2009-04-20)13问7:SfCreate(2009-04-20)13问8:SfDisplayCreateFileName(2009-04-23)15问9:fastio系列例程(2009-04-28)16问10:总结sfilter(2009-04-28)26问11:fspyKern.
h、fspydef.
h和filespy.
h(2009-04-28)27问12:fspyHash.
c(2009-04-28)28问13:上下文(2009-04-30)35问14:fspyCtx.
c(2009-04-28)40问15:FspyLib.
c(2009-04-28)50问16:总结filespy(2009-04-30)65问17:legacy驱动透明加解密设计开发示例(2009-04-30)65概要设计65定位机制设计67跟踪机制设计67加解密模块设计68其他设计69问18:总结legacy驱动(2009-05-04)70Mini驱动阶段71问19:passThrough(2009-05-04)71问20:ctx(2009-05-04)72问21:scanner(2009-05-05)76问22:swapBuffers(2009-05-05)78问23:总结mini驱动(2009-05-05)79问24:fastfat(2009-05-05)79后记80热身阶段问1:什么是过滤驱动(2009-4-15)经历过驱动开发之后,给我最大的一个认识就是:这里所谓的驱动和没有接触它之前所臆想的驱动有很大的不同.
我们最早入手的资料是楚狂人的《Windows文件系统过滤驱动开发教程(第二版)》和toolflat公开的SfilterRc4加解密源码.
由于legacy源码动辄几千行,而且没有理论基础显得艰涩难懂.
因此我们决定先研究开发教程(这一问中涉及的IRP和IRP_MJ_XXX的概念第2问会讲到).
这本书主要是依照DDK中sfilter的例子,逐步引入文件系统过滤驱动中的基本概念和基本方法,这可以以一种简单的方式尽快让你了解驱动.
不同环境下的文件名格式其他资料上都讲解得比较详细,这里不再讨论.
部分解析下《Windows文件系统过滤驱动开发教程(第二版)》第一章实用的东西不多,但对过滤驱动是做什么的、怎么做有一个简要的描述第二章主要是驱动入口DirverEntry例程及各种对象.
总的来说,DriverEntry例程代码的各个部分都是既定的,需要修改的只是部分变量和参数.
本章需要注意的问题点是驱动中的各种对象.
DirverObject代表这个驱动实例,它有一组函数指针即dispatch函数的函数指针.
这些函数类似于MFC中对应某个消息的处理函数,它们分别用于处理各自对应的IRP请求.
举例来说(以sfilter为例):SfCreate对应IRP_MJ_CREATE即驱动只要收到MajorFunctionCode为IRP_MJ_CREATE的IRP就交给SfCreate例程处理.
编写一个驱动的主要任务也就是确定要处理哪些IRP_MJ_XXX的IRP,然后编写相应的处理例程.
CDO(控制设备对象)和DO(设备对象)比较好区分:首先,每个驱动只对应一个CDO,而可以有多个DO;CDO无设备扩展,DO有设备扩展;CDO的主要任务是作为某个操作(一般都是用户自定义的IOCTL等)的目标,用于修改整个驱动的内部配置,而DO是驱动对应某个卷生成的,只有以它为目标的IRP才会被生成这个DO的驱动处理(这也与驱动栈的概念关联紧密,后面详述).
再来看本章给出的DriverEntry例程代码片段,主要目的是生成CDO.
这里主要有三个点:1、UNICODE_STRTING的使用,参考《驱动开发基础教程》;2、可用函数的调用.
需要说明的是由于驱动是由C语言编写的,因此大部分C运行时函数都可以用于驱动编程,但直接使用C运行时库有许多风险,推荐能使用DDK封装好的函数就尽量多用封装好的,除非你愿意自己做繁琐的版本移植和维护;3、对某个函数调用所产生的各种结果都要处理到位.
假如在用户模式下调用某函数不考虑其失败的情况,后面的程序要用到该函数返回的某个值(失败时该值无效)时就会发生错误,严重点也就程序运行时出错被迫终止并弹出一个相对友好的报错对话框.
在内核模式下的编程,假如有异常或错误没有及时处理,一般都会引起bugcheck,现象就是蓝屏.
本例只区分了成功或失败两种状态,在特殊场合,指定的函数调用的状态可能需要分很多种做相应处理.
第三章和第3.
5章主要讲了分发例程(dispatchroutine)和fastio这两种方式.
两种方式都不是小问题,后面会专门分别讲,这里只简述.
先说说分发例程.
在legacy驱动模型中,你必须对所有类型的请求都做处理,而不管你需不需要过滤它.
因而有些IRP_MJ_XXX类型的IRP你只需要把它传给驱动栈中在你之下的驱动.
既然处理方式一样,为了避免重复编码的无用功,就需要一个通用的"下传"例程,即例中的SfPassThrough.
它没什么可说的,就只是负责下传当前的IRP.
那些需要处理的IRP类型,就得按照例子中的示范给出对应的分发例程,如DriverObject->MajorFunction[IRP_MJ_CREATE]=SfCreate;这样只要驱动接收到IRP_MJ_CREATE类型的IRP就会把它交给SfCreate例程处理.
断言的用法与用户模式对应,不再赘述.
这里可以形象地认为邮局(驱动)收到邮件(IRP),邮局根据邮件上收信人的邮编和地址(MajorFunction和MinorFunction),把邮件交给收信人(SfCreate)做处理.
这里只是比喻,IRP最终的"收件人"并不一定是当前这个过滤驱动的分发例程.
Fastio是cache调用引发的,且没有IRP.
你可以认为fastio操作的数据一定是在缓存中.
也就是说,用户模式下各种IO到内核模式下只走两天路线:IRP和fastio.
假如你只关心磁盘里的数据,那fastio就没什么意义.
关于其中出现的内存申请方面的技巧后面会详述.
提到的两种锁在后来对某些数据结构的操作中会经常碰到.
初期只需要知道.
后面几章操作和编程方面,就示范目的而言讲解地比较详细,其中出现的很多概念和方法还需专门开问讨论,也不推荐过早考虑实际开发.
暂时要接受和理解这本书中提到的基本理论已经不容易了.
关于sfilter等源码需要另开题目讨论.
总结:一个驱动(以sfilter为例,这里要解释以sfilter为例的原因:它的数据结构和功能都相对简单,可参考的资料相对较多)主要组成部分有:数据结构、DriverEntry例程及其他各种例程.
它可以对自己所绑定的卷上的IO进行相应处理.
这就是实际编程中你看到的驱动.
NTFSI中过滤驱动的定义为:一个拦截到一些已有软件模块的请求的中间层驱动.
依靠在请求到达目标前截获请求,过滤驱动就有机会扩展或修改请求的原始接收者所提供的功能或服务.
比照上一段来理解,驱动这个概念就没有那么空了.
这里只是帮助你尽量块地融入到驱动的开发环境中.
不要只看某一份资料,偏执一处只会令你由无知变为"误知".
文件系统过滤驱动为什么能起到过滤文件系统的作用就需要有一定的知识基础才好理解了.
暂时略.
问2:什么是IRP(2009-4-15)问1和许多资料及网页都不断地提及IRP这个名词,对于刚接触驱动的人来说这玩意儿是陌生的,但在将来的学习和开发过程中它是一个关键性的基本概念之一.
MS在DDK中解释了IRP及其结构,但有所保留.
下面用到的驱动栈概念会在问3中详述.
NT用一个基于包的结构来描述I/O请求.
即任何一个I/O请求都能用一个单一的I/O请求包也就是IRP来描述.
当发出一个I/O系统服务时(比如创建文件或读文件的请求),I/O管理器就会通过构造一个描述此请求的IRP并把该IRP的一个指针传给设备驱动来开始对这个请求的处理.
假如OS想向I/O管理器和设备驱动完整地描述一个I/O请求,那么IRP中保存的信息就可以达到这个目的.
如下图,IRP可以认为是由两个部分组成的:一个"固定"部分和一个I/O栈.
固定部分包含这个请求的相关信息,有可能不同驱动中的IRP的固定部分相同,也可能在不同驱动间传递IRP时不需要关注它.
假如某个驱动要处理这个IRP,那么I/O栈就包含被指定给它的信息.
驱动栈中有几个要处理这个IRP的驱动,那么I/O栈中将至少有几个I/O栈位置.
为避免每一个IRP都要从NT的非分页池中分配,I/O管理器维护一对保存有预分配的IRP的旁观列表(lookasidelist,其相关知识会专门加以讨论).
NTV4.
0中,其中一个旁观列表保存带有一个单一I/O栈位置的IRP.
另外一个旁观列表保存带有三个I/O栈位置的IRP.
I/O管理器总是尽可能地使用这些旁观列表中的IRP.
因此,只有没有可用的IRP,或者要分配的IRP所需的I/O栈位置超过三个时,I/O管理器才会从非分页池中分配IRP(关于内存分配的问题会专门讨论).
否则,它会尽量使用旁观列表中的IRP.
IRP的固定部分中需要特别关注或有用的域如下:MdlAddres,UserBuffer和AssociatedIrp.
SystemBuffer–如果有一个关联I/O操作请求者的数据buffer,则这三个域被用来描述这个buffer.
后面会加以详述.
Flags–顾名思义,这个域包含描述I/O请求的标记.
例如,如果在此域中设置了IRP_PAGING_IO标记,则表示IRP所描述的读或写操作是一个分页请求.
以此类推,如果设置了IRP_NOCACHE则表示这个请求是在没有中间buffer的情况下被处理的.
IoStatus–当IRP被完成时,完成这个IRP的驱动把IoStatus.
Status域设置为I/O操作的完成状态,而把IoStatus.
Information域设置为一些要返回给调用者的额外信息.
一般,在一个传输请求(读或写等会引起数据传输的请求)中IoStatus.
Information将会包含实际上被读或被写的字节数.
RequestorMode–指示这个请求是从哪种模式(内核模式或用户模式)发起的.
Cancel,CancelIrql,andCancelRoutine–如果正在进行时需要取消这个IRP就会用到它们.
Cancel是一个BOOLEAN类型值,当被I/O管理器设置为TRUE时表示这个IRP正被取消.
CancelRoutine是一个指针,在把这个IRP保存到某个队列中时由一个驱动将它设置为指向一个函数,I/O管理器会调用这个函数来让该驱动取消此IRP.
因为CancelRoutine是在IRQLDISPATCH_LEVEL被调用,而CancelIRQL是驱动应该返回到的IRQL.
Tail.
Overlay.
Thread–指向请求线程的ETHREAD.
TailOverlay.
ListEntry–可能被驱动用来排队的位置,尽管它拥有这个IRP.
I/O栈位置的结构定义见NTDDK.
H中的IO_STACK_LOCATION.
要获得当前IRP中本驱动的I/O栈位置,驱动得调用函数IoGetCurrentIrpStackLocation(…).
当I/O管理器开始分配IRP并初始化IRP的固定部分时,它也初始化IRP中的首个I/O栈位置.
这个I/O栈位置中的信息与被传给将会处理这个请求的驱动栈中的首个驱动的信息是一致的.
I/O栈位置中有以下域:MajorFunction–关联这个请求的主I/O函数代码.
它从总体上指明了要执行的I/O操作的类型.
MinorFunction–关联这个请求的辅助I/O函数代码.
当被使用时,它可以进一步帮助程序员理解I/O请求的目的.
大多数设备驱动会忽略它们.
Flags–指定给正被执行的I/O函数的处理标记.
Control–一组指示I/O管理器如何处理这个IRP的标记,由I/O管理器设置并参阅.
例如,若设置了SL_PENDING位(因驱动调用了IoMarkIrpPending(…))就指示给I/O管理器此IRP还需其他处理.
类推之,SL_INVOKE_ON_CANCEL,SL_INVOKE_ON_ERROR和SL_INVOKE_ON_SUCCESS都指示了驱动的I/O完成例程什么时候应该被调用.
Parameters–这个域由几个子成员组成,每一个子成员都是基于MajorFunction被指定.
DeviceObject–这个I/O请求的目标设备对象.
FileObject–关联这个请求的文件对象.
在IRP的固定部分和首个I/O栈位置之后的部分都被适当地初始化,I/O管理器会在其内部对应此MajorFunction的分发入口点中调用驱动栈顶部的驱动.
因此,如果I/O管理器刚好已经构造了一个IRP来描述一个读请求,那么它会在它的读分发入口点中调用首个驱动.
也就是说,你的驱动写好了,绑定到目标卷上了,何时被调用是I/O管理器说了算的.
它可是驱动的"指挥官",IRP就是它的"电报".
总结:这里的IRP相关知识比较完整,理解它们离真正理解IRP还是有一段距离的.
IRP中所包含的数据结构都是驱动编程的重要依据.
要掌握它还需要花大量的时间在编程过程中体会它运用它,推荐多翻资料对比着看,以加强对它的理解.
这里你可以找段相对简单的分发例程源码,看看这段源码是怎么利用IRP的,利用了多少,为什么每个例程利用的部分都有所不同.
但不要急着自己摆弄,没有一定的基础前,很容易做大量的无用功且容易得出错误结论形成"成见",不利于后面的学习和开发.
或许有一天你会推翻你所翻阅的许多资料上提出的说法,也包括这里的某些见解.
正确的认识胜于一切.
问3:驱动栈,设备栈(2009-4-15)技术人员总是试图用形象的语言把一些结构或者知识描述给不懂的人听,类似于比拟手法,便于理解.
但作为开发人员,你需要的是本质,而不是别人给你的虚像(不总是假像),至少让自己确信某个说法是有根据的.
包括这里提到的见解也需要找机会自己验证和推敲.
最本质的东西就包括基本的数据结构.
关于设备栈很多资料上已经较为详细地描述了,这里只是概要.
一般,驱动编程里我们都说你在操作某个卷上的某个文件,而不单独说操作某个文件(这跟具体编程有关系,以后会专门讨论).
也就是说,在某个驱动想确知它是否能截获某个IRP之前,它必须首先确定I/O管理器会把这个IRP发往它所绑定的卷.
DO的部分数据结构如下:typedefstruct_DEVICE_OBJECT{PDRIVER_OBJECTDriverObject;//对应创建这个DO的驱动PDEVICE_OBJECTNextDevice;//对应当前DO后面的那个DOPDEVICE_OBJECTAttachedDevice;//对应当前DO绑定的真实设备……}DEVICE_OBJECT,*PDEVICE_OBJECT;把AttachedDevice域指向同一设备的DO连接起来:在不添加任何主观臆断的情况下,AttachedDevice域指向同一设备的DO是如图分布的(横排竖排都行,这里选择竖排是想突出"自上而下"的顺序便于理解).
只看DO的NextDevice域之间的联系,这些DO就形成了"天然的"逻辑上的链式结构(暂且不说是设备栈).
从绑定过程上看,越先绑定的DO越靠近真实设备,在链中的位置就越靠下.
从图中,我们还可以观察到关联DO的各个驱动也因为DO高低位置的不同而呈现出高低分层的样子,假如你有阅读过驱动加载顺序和绑定设备过程相关的资料,可能体会更多.
暂时不准备在这一问中介绍这些.
IRP的传递是以DO为目标的,当IRP进入这条驱动链时(暂且不说是驱动栈),从能截获到这个IRP的驱动角度看,其传递过程如下图所示(绿色的线为IRP未被处理前,向下传递的路线;红色的线为IRP被处理后,向上传递的路线):I/O管理器根据IRP来分别调用这条驱动链中各个驱动的顺序大致如此.
设备栈和驱动栈的概念不那么空洞了吧当然,你接触到更多的知识后,会发现这个调用过程随时都可能因为栈中某个驱动的特殊处理而发生变化.
比如驱动2强行令IRP表示I/O操作失败或成功并返回了,那么在处理这个IRP的过程中,栈中驱动2下面的驱动3等驱动就不会被I/O管理器调用.
后面会加以详述.
总结:这里介绍设备栈和驱动栈的目的,只是帮助你理解这两个比较抽象的名词.
它也简述了IRP的传递过程但还并不出完整和准确.
你可以比照问2中IRP结构中的I/O栈,逻辑上的驱动栈是可以和I/O栈对应起来的.
死记硬背的概念永远是模糊的.
问4:文件系统过滤驱动(FSFD)为什么能过滤文件系统(FSD)(2009-4-16)我在刚开始学习文件系统过滤驱动的时候始终都有一个疑问:看的资料一直讲怎么处理读写等操作的IRP啊,怎么找到数据啊……但我怎么知道我的驱动加载后就能过滤文件系统了接上一问,既然某个设备上的驱动是以栈的形式分布的,那么先观察下图:图中略去了其他细节,但不影响理解.
文件系统在用户请求处理过程中的作用是很明显的,即它收到I/O管理器传来的操作请求后负责在真实设备上执行该操作(调用硬件驱动等细节暂时略去),并将操作的结果返回给I/O管理器.
假如你想截获文件系统驱动准备处理或已经处理的操作,你就必须在图中两条直线指示的区域做处理.
既然文件系统驱动是驱动,自然也在关联硬件的驱动栈中.
在驱动栈中,你在文件系统上面的任一位置插入自己的过滤驱动,就是所谓的upper文件系统过滤驱动,在文件系统下面的任一位置就是lower文件系统过滤驱动.
两者的区别和能力从图上就可以分析出来:Upper过滤驱动可以在请求到达文件系统前就截获它并做自己的处理,而lower过滤驱动只能在文件系统做出处理后才能插手;Upper过滤驱动可以修改用户进程的意图,而lower过滤驱动则可以修改文件系统的意图;Upper过滤驱动修改经由文件系统处理后请求的完成状态只会对用户模式的进程产生影响,而lower过滤驱动修改完成状态后会对其上的包括文件系统驱动在内的所有驱动及用户进程都产生影响;Upper过滤驱动需要与I/O管理器及文件系统驱动交互,lower过滤驱动则需要与文件系统或硬件驱动交互(这里做了简化只是尽可能使之容易理解,可能会有其他中间驱动参与进来).
除了上述的区别外,还有许多实际编程过程中会遇到的众多不同点.
多查阅驱动加载顺序及驱动绑定设备过程相关的资料,你就会知道如何把你的过滤驱动放到栈中合适的位置了.
总结:回顾下问3中讲的驱动栈中的各个驱动的调用过程,发给驱动栈的I/O请求都要先经过upper过滤驱动,然后到达文件系统驱动,最后是lower过滤驱动(暂不考虑硬件驱动等).
结合上面对这两种过滤驱动区别和能力的简要分析,这个"过滤"就与IRP传递过程联系起来了,它就像是一个全自动流水线,在系统运行期间不停地运转.
现在你可能会想要证明自己的驱动确实是在工作着的.
不过最好先压下这个念头,等卷轴逐步展开至完毕后,全貌就在你眼前了.
问5:怎么用好DDK(或WDK,现在起本书只说WDK)(2009-4-16)这里用WDK泛指参考资料.
这一问主要也是说说参考资料的运用.
WDK是官方提供的驱动开发指南,它公布的东西可能会有所保留,但一定是MS要负责维护的,一旦有改动MS必须要提供修订通知.
它的使用方法可以参照使用MSDN的方法.
许多初学者很容易漏掉一样东西,那就是WDK附带的源码例子.
或许这些例子不会直接实现你想要实现的目标,但它们都有极大的参考价值.
这些例子用到的数据结构和一些技巧实际上就是WDK之外的"驱动开发指南".
NTFSI,这本书从诞生之日起就一直是NT文件系统驱动开发人员所能通过正常途径找到的最权威的文字资料.
它对NT执行体组件(I/O管理器、虚拟内存管理器、缓存管理器等)、基本数据结构(FileObject、FileControlBlock等)都有详细的介绍,文件系统驱动中几个主要的分发例程和NT系统服务等也都有讲解.
总之,如果你想长期从事Windows操作系统中的文件系统驱动(含过滤驱动)开发工作,那么花至少一年时间吃透它是值得的.
特别假如你有不错的英语基础,建议直接看英文版,网上有免费下载.
网络社区资源,国外的看www.
osr.
com,国内也有,比如驱动开发网.
网页问答的同时,它们也提供了有用资源的下载.
但问答中想要"大实话"还得去国外的.
形成良好的国内技术交流环境任重道远.
交流也得拿捏好分寸,别人花费数月甚至以年为单位的时间才得出了一些成果,这些成果很可能就是某个公司的核心技术,要免费提供给你很不现实.
最有效的提问就是你学习和开发过程中遇到了什么具体问题,并且你也查阅了WDK和手头的一些资料,那么把这个问题出现前后的情景和问题出现时的现象以及自己的分析、调用了什么程序出的这问题等完整地提供出来,就比较容易能引起别人的注意.
总之,交流的方式有诸多讲究,有些论坛里会介绍请教的技巧,需要提醒的是:你问的人没有义务回答你.
QQ群,只要有专门的技术交流群,那聊天中必定会有很多问题和解决问题的各种答案,这些宝贵的信息在网页上不一定能看到.
对某一问题,国内国外很可能都有人在某个地方给出了答案,但国外充分地利用了list这个工具所以会有公开的记录,国内可能只是私聊,一闪而过.
尤其一遇上几个技术牛人在群里"激烈地"讨论某个问题,那么有用信息就很可观了.
其实每个开发人员都会因为各种原因有交流愿望,就看你"够不够格".
总结:这一问,东西不多,主要是提个醒,拿到需求就埋头写代码不是个好习惯.
其结果往往是费时费力,到一定阶段又得推倒重来.
另外学习和开发过程中,要有目的性.
盲目地学习和盲目地开发都是很"致命的".
总是停留于知识介绍,估计强调再多也嫌烦.
只有在实际开发过程中碰到,你才会意识到它们的重要性,所以下问开始就直接拿源码开刀了.
需要强调的是,本书联合了现有的资料,所以每一问虽短,但要理解和掌握还需要多看多想多总结.
资料可以有所区分,但"坏"资料不一定就不包含关键或重要的信息.
Legacy驱动阶段Sfilter系列问6:DriverEntry和DriverUnload是干嘛的(2009-04-20)WDK把sfilter作为一个简单的legacy驱动例子,一看洋洋洒洒六千多行的源码,对于初学者来说可能有点无从下手的感觉.
这里先将代码分三段:定义、声明、函数定义.
我讲sfilter主要是沿着它的脉络把驱动编程中的一些东西展现给你,所以先不分析sfilter中的数据结构定义,声明没什么好讲的.
这样内容就"少"了很多.
本书开篇就提过DriverEntry例程,这个系列就从DriverEntry开始吧.
驱动的DriverEntry例程的作用和它的名字一样,为驱动入口.
驱动相关的初始化工作都要在这里完成.
比如初始化全局变量、创建CDO、初始化分发例程和fastio例程、绑定设备等等.
它在驱动程序的整个运行期间只被调用一次.
sfilter默认绑定所有的现有设备和将来动态挂载的设备,所以假如你的需求与这个默认不冲突,你需要修改的地方不多.
如果你想要手动指定驱动要绑定哪个设备的话你就需要参考filespy的例子了.
现在简单说下,就是要自定义一个IOCTL代码,然后在用户模式下的某个应用程序中将该IOCTL发给驱动的CDO;在驱动中定义处理该IOCTL的程序,实际的绑定设备和断开设备操作都由该程序完成.
SfFsNotification这个例程就是在设备挂载或断开的时候被文件系统自动调用来相应地绑定或断开该设备的.
DriverUnload例程自然是对应DriverEntry的,它主要完成驱动相关的卸载工作.
在开发过程中,这个例程很有用,但不推荐在产品中提供此例程.
总结:完了完了.
这里不是主战场.
问7:SfCreate(2009-04-20)驱动里面把create/open操作都集中用IRP_MJ_CREATE表示.
在文件系统驱动中,文件对象FileObject这个东西含义十分广泛,而不单单代表文件.
所以不要以为你截获的所有IRP_MJ_CREATE的目标都是文件.
先来看看IRP_MJ_CREATE吧.
WDK中说:当一个新文件或新目录正被创建时,或者当一个现有的文件、设备、目录或卷正被打开时,I/O管理器就会发出IRP_MJ_CREATE请求.
也就是说,假如你的SfCreate被调用,那代表以上这些操作正要发生,但还没发生.
拿透明加解密来说,这可能就是我所关注的文件正被创建或者正被打开.
是可能,不是一定.
在操作系统运行期间,create请求太多了,在你打开或创建文件的同时,其他进程都可能会针对不同目标发出多个create.
你可以拿filemon或者filespy(两个用于查看IRP的工具)看看,在你不进行任何操作的情况下,随时都可能有各种各样的create及其他信息涌现.
此时根据设计目标的不同,不同驱动基本上可能会有两种处理方法:1、在这里就判断出目标是否为要加密的文件;2、在完成例程中判断.
后者比较简单,就是自己发个IRP下去向文件系统驱动查询目标的名,再判断是否是要加密的文件.
听起来似乎很不可思议!
你确实可以自己发IRP来满足各种各样的需求.
但权利和义务是并存的,暂略.
理解过程可能漫长,但其实这些动作实现起来就几行代码的事.
前者相对较难,就是自己要像文件系统一样把这个名给"制造"出来.
Filemon和Filespy源码里都有这个制造过程,暂略.
好了,得出了名,你就知道它是什么东西了.
比如c:\1.
txt,c:\,c:\Documentsandsettings.
当然,内核模式下卷名不会是c:,这里方便理解.
拿到名了,也判断出是我要加密的文件了,那就想个办法把相关信息保存起来,以便我需要用到它时能找到.
你可能会问,既然我只加密,那我在SfWrite里面判断是要加密的文件,则进行加密不就行了不行.
这个方法不一定有效,而且很容易"跟丢"了你要跟踪的文件.
一些信息只被保证在create中是有效的.
因此create是个非常重要的dispatchroutine.
你需要在这里找出你要跟踪的文件;假如目标文件是你要跟踪的文件,则将必要信息保存起来,以启动跟踪;或许有的需求会让你做进程访问控制,那么你就需要在这里做处理,单单在read或write等例程中做控制就晚了.
来看看sfilter例子里面的create都做了哪些事情.
if(IS_MY_CONTROL_DEVICE_OBJECT(DeviceObject)){Irp->IoStatus.
Status=STATUS_INVALID_DEVICE_REQUEST;Irp->IoStatus.
Information=0;IoCompleteRequest(Irp,IO_NO_INCREMENT);returnSTATUS_INVALID_DEVICE_REQUEST;}这是一个比较典型的手法:直接完成IRP.
由于sfilter并没有提供手动配置sfilter驱动环境的功能,所以一般情况下不可能有以sfilter的CDO为目标的IRP.
这段代码的意思是,假如IRP的目标为sfilter的CDO,则返回的状态为STATUS_INVALID_DEVICE_REQUEST,附加信息为0.
IoSkipCurrentIrpStackLocation(Irp);returnIoCallDriver(((PSFILTER_DEVICE_EXTENSION)DeviceObject->DeviceExtension)->AttachedToDeviceObject,Irp);这句可以参考SfPassThrough例程,即不做处理,直接下发给驱动栈中的下一个驱动,且不关心这个IRP的完成状态.
这里要用到一个常用的小技巧:设置事件.
这里是直接把sfilter例子里面的代发粘贴出来,实际使用时需要参考WDK.
按照几条语句的调用顺序如下排列:KEVENTwaitEvent;//声明一个事件KeInitializeEvent(&waitEvent,NotificationEvent,FALSE);//将事件初始化为通知事件KeSetEvent(event,IO_NO_INCREMENT,FALSE);//给事件发信号KeWaitForSingleObject(&waitEvent,Executive,KernelMode,FALSE,NULL);//等待这个事件事件初始化的时候比较重要,你必须决定你要用这个事件做什么,例子中利用它作为通知事件以便在条件合适的时候(即在符合某一条件时调用KeSetEvent来给事件发信号)执行KeWaitForSingleObject之后的代码.
还有一种是同步事件.
作用同名,用于同步.
以下代码主要作用是设置完成例程.
新出现的两个调用可以参考WDK,非常详细.
IoCopyCurrentIrpStackLocationToNext(Irp);IoSetCompletionRoutine(Irp,SfCreateCompletion,&waitEvent,TRUE,TRUE,TRUE);status=IoCallDriver(((PSFILTER_DEVICE_EXTENSION)DeviceObject->DeviceExtension)->AttachedToDeviceObject,Irp);这里不要接着SfCreate往下看,应该转向SfCreateCompletion.
一旦SfCreateCompletion被调用,那就代表IRP_MJ_CREATE已经被文件系统驱动收到并处理过了.
例子中的SfCreateCompletion只是给事件发个信号,没有其他处理.
然后回到if(STATUS_PENDING==status){NTSTATUSlocalStatus=KeWaitForSingleObject(&waitEvent,Executive,KernelMode,FALSE,NULL);ASSERT(STATUS_SUCCESS==localStatus);}后面就比较好理解了,SfDisplayCreateFileName这里需要另外讲.
问8:SfDisplayCreateFileName(2009-04-23)SfDisplayCreateFileName单独开问并不是为了讲它,关键是它内部得到名的处理过程.
先看看它涉及的数据结构typedefstruct_GET_NAME_CONTROL{PCHARallocatedBuffer;//字符型指针CHARsmallBuffer[256];//字符型数组}GET_NAME_CONTROL,*PGET_NAME_CONTROL;typedefstruct_OBJECT_NAME_INFORMATION{UNICODE_STRINGName;}OBJECT_NAME_INFORMATION,*POBJECT_NAME_INFORMATION;首先声明和初始化OBJECT_NAME_INFORMATION类型的结构体作为容器,然后调用对象管理器例程ObQueryNameString试图得到对象名假如得不到名则得到对象所在的设备名.
若返回的状态为STATUS_BUFFER_OVERFLOW(编程过程中你会遇到很多状态值,这个意为"容器的容量不足"),则分配更大的内存重新获得名.
后面需要读者拿笔在纸上记录下每行代码执行后的结果,你必须要在这个过程中体会如何操作文件名.
不自己体会,看到更复杂的文件名本身的操作时,你就会更加困惑.
关于内存申请,也需多看资料多体会.
尤其是从分页内存池和非分页内存池中分配的内存的不同之处.
其他非fastio系列的例程要么非常简单易懂,要么其他资料上讲解得非常详细这里就不再赘述了.
总结:这一节内容非常少,但需要做的功课量不少,我有理由相信你只盯着那些代码看是看不出结果的,所以在目前还无法完成一个可调试驱动的情况下,你得拿纸笔演算.
要融入驱动开发这个环境,不太容易,但问题本就那么多,今天解决一个未来就少一个.
下一节开讲fastio系列的例程.
问9:fastio系列例程(2009-04-28)Fastio系列例程的资料不多,这里我翻译了一篇osr论坛上的技术文档.
NT中以I/ORequestPacket(IRP)作为与驱动通信的基础–IRP的优点是它封装了一个操作所必需的上下文并允许驱动从对驱动公开的详细资料中提取有用信息.
虽然这种方式非常全面且可扩展–允许清晰的分层的WindowsNT设备体系,但是要快速满足这个请求的一系列操作的消耗也十分明显.
在这种情形下,创建一个IRP的消耗甚至能支配整个操作的消耗,而这会在性能的临界区域使系统"慢下来".
因此,引入了fastI/O的概念.
这种I/O方式被某些文件系统驱动使用,比如NTFS,HPFS,FAT和CDFS也包括用于WinSock的AFD传输驱动.
任何驱动都能注册一组fastI/O入口点但是它们的使用一般都极其有限–只有满足合适的条件时一个fastI/O入口点才会被调用.
比如,读和写fastI/O入口点只在文件的信息由缓存管理器维护时才会被调用.
这些限制我们后面会加以描述.
当然fastI/O的最大问题就是其可利用的说明文档非常少–即使是filesystemdevelopmentkit也没有提供有关fastI/O如何工作或者如何使用它的介绍.
本文将提供对fastI/O基本原理的介绍,然后是对各种fastI/O调用的摘要性描述,最后是如何利用这个接口来改进你的驱动的性能.
基本原理FastI/O看起来像是一种快捷的方式–许多I/O操作重复地对同样的数据执行.
例如,与大多数现代的操作系统一样,WindowsNT将文件系统缓存与虚拟内存系统结合在一起–把系统内存当做文件系统信息的一个巨大的缓存来使用.
这样的文件系统极其有效且推进了系统的真实性能和感知性能.
这样结合的第二个原因是WindowsNT对内存映射的(memory-mapped)文件的支持.
支持读/写访问和内存映射的文件访问同一数据,需要高消耗(因而低性能),缓存器一致性方案或者NT的方案–所有的数据都存储在虚拟内存系统中.
这确保数据总是一致的–即使是两个不同的程序用不同的访问技术访问同一数据.
这个紧密的结合意味着读和写操作都能由被缓存的数据满足.
在性能的探究中,这个事实使得读和写操作可以通过简单地调用那些能将数据从VM缓存移动到用户的内存中或反向移动的例程来实现.
由于操作能被同步地满足且不需要调用lower驱动,这就避免了分配IRP的消耗.
这就是fastI/O操作实现的基本原理.
Fastio的入口点包含于ntddk.
h文件中的FAST_IO_DISPATCH结构中.
该结构的第一个元素描述这个结构的大小,这使得新元素能直接被兼容到这个结构中.
I/O管理器和FastI/O需要时I/O管理器负责调用fastI/O入口点.
它所使用的方式几乎都是直截了当的–fastI/O返回TRUE或者FALSE来指示fastI/O操作是否已被完成.
如果没有被完成或fastI/O不可用,则I/O管理器会创建一个IRP并发送给toplevel驱动.
FAST_IO_DISPATCH结构中的最后三个入口点不属于这个模型.
它们为I/O管理器提供其他服务.
后面会讨论它们.
FastIoCheckIfPossible仅能被文件系统当做普通的文件系统运行时库的一部分(FsRtl系列例程)来使用.
其原型为:typedefBOOLEAN(*PFAST_IO_CHECK_IF_POSSIBLE)(INstruct_FILE_OBJECT*FileObject,INPLARGE_INTEGERFileOffset,INULONGLength,INBOOLEANWait,INULONGLockKey,INBOOLEANCheckForReadOperation,OUTPIO_STATUS_BLOCKIoStatus,INstruct_DEVICE_OBJECT*DeviceObject);此例程仅用于读和写操作.
同样的,当使用文件系统缓存管理实现时,它被FsRtl库中提供的一般fastI/O例程调用来允许文件系统探测读或写是否(依赖于CheckForReadOperation参数的Boolean值)能由文件系统缓存来满足.
注意此调用的参数极其地接近读和写fastI/O入口点的相应参数–除了这个例程没有任何与之关联的数据buffer.
FastIoRead和FastIoWrite如果一个文件关联有有效的被缓存的数据,那么当对其发出一次读请求时,I/O管理器就会调用这个例程.
其原型为:typedefBOOLEAN(*PFAST_IO_READ)(INstruct_FILE_OBJECT*FileObject,INPLARGE_INTEGERFileOffset,INULONGLength,INBOOLEANWait,INULONGLockKey,OUTPVOIDBuffer,OUTPIO_STATUS_BLOCKIoStatus,INstruct_DEVICE_OBJECT*DeviceObject);像先前讲到的,此调用的基本参数与FastIoCheckIfPossible使用的参数极其相近.
所有这些入口点都被保证传入的参数值有效,例如,buffer对调用线程的上下文中的读有效且可用.
fastI/O例程能做以下两种事情:完成本次操作,设置IoStatus域来指示本次操作的结果并返回TRUE给I/O管理器.
如果是这种情形,I/O管理器将完成这个I/O操作.
例程也可以返回FALSE,此时I/O管理器将会简单地创建一个IRP并调用标准的分发入口点.
注意返回TRUE不总是担保数据已经被转移了.
例如,一次起始位置越过文件结尾的读会引起IoStatus.
Results被设置为STATUS_END_OF_FILE,数据不会被复制.
而一次起始位置和结束位置之间包含文件结尾的读将会令其返回TRUE,IoStatus.
Results同样会被设置为STATUS_END_OF_FILE,但这一次所有该文件中剩余的数据会被复制到buffer中,同时超出的部分不会被复制.
类似地,返回FALSE不总是担保一些数据没有被转移.
虽然可能性很小,一些数据可能已经被成功地复制了但此后会经历一个I/O错误,或者使buffer的内存变得不可访问.
以上任何一种情形都会产生一系列的二次影响.
比如,当从缓存中读时,一些被读的数据可能当前不在缓存中.
这会引起一个页错误,其结果是引起一个回调到文件系统中来满足这个页错误.
对于写的情形唯一的差别在Buffer参数是IN而不是OUT.
基本的处理模式非常类似.
当然,一些错误的条件是不同的–媒体可能满了,需要分配新的页,等等.
FastIoQueryBasicInfoandFastIoQueryStandardInfo这些操作提供对标准NtQueryInformationFileAPI操作的支持,同时针对某些操作FastIoQueryBasicInfo也结合NtCreateFile一起使用.
basic信息包括文件什么时候被创建,它什么时候被最后一次访问,什么时候被最后一次修改及一切这个文件的特殊的属性–比如隐藏文件,一个目录或其他适当的属性.
standard信息包括文件的分配大小,当前大小,到这个文件的连接的数量,一个指示该文件的删除是否已经被请求和一个指示器指示该文件是否是一个目录.
因为这些信息经常被缓存到内存中,fastI/O操作是完美的候选方案.
的确,许多标准的效用基于例程探测此信息,这就意味着提高这些操作的性能将充分地增强感知效用的性能.
比如文件管理器(winfile.
exe).
这两个fastI/O例程有相同的接口:typedefBOOLEAN(*PFAST_IO_QUERY_BASIC_INFO)(INstruct_FILE_OBJECT*FileObject,INBOOLEANWait,OUTPFILE_BASIC_INFORMATIONBuffer,OUTPIO_STATUS_BLOCKIoStatus,INstruct_DEVICE_OBJECT*DeviceObject);Wait参数指示调用者是否愿意阻塞来等待必需的信息.
如果它被设为FALSE则这个调用必须要么完成而不等待,要么返回FALSE–此情形下能创建一个正常的包含整个操作必需的上下文的IRP.
值得注意的是在NT3.
51中这两个入口点都没有在当Wait被设置为FALSE时调用.
一旦调用,如果FileObject是首次被打开,那么这些例程会看它们为这个文件存储的信息.
这个信息也能"onthefly"地重构–比如,最后一次访问时间被一些文件系统驱动设置为当前系统的时间.
当然,设置这些值由文件系统实现决定.
FastIoLock,FastIoUnlockSingle,FastIoUnlockAll,andFastIoUnlockAllByKey这些入口点用于控制关联一个特殊文件的锁状态.
被这些调用控制的锁定(locking)是针对单一文件的字节范围(byterange)锁定.
因此能锁住一个文件的多个字节范围.
虽然不是必需的,标准NT文件系统利用文件系统运行时包(FsRtl系列例程)的服务,提供一个共有的代码基础来检验能否准予一个被请求的锁并存储关于目前保存在文件上的锁范围的信息.
锁状态能通过NTAPI的调用NtLockFile和NtUnlockFile来控制.
WindowsNT中的锁有两种–独占的锁,即被锁住的字节范围是因为修改而锁的;共享的锁,即被锁住的字节范围是因为读而锁的.
针对重叠的字节范围的多重共享锁能被准予,然后其中的任何一个都会被存储直到后来它被释放.
关于任何一个锁的各种信息都会被存储以便其能为后来的快速访问所获得.
FastIoLock的接口为:typedefBOOLEAN(*PFAST_IO_LOCK)(INstruct_FILE_OBJECT*FileObject,INPLARGE_INTEGERFileOffset,INPLARGE_INTEGERLength,PEPROCESSProcessId,ULONGKey,BOOLEANFailImmediately,BOOLEANExclusiveLock,OUTPIO_STATUS_BLOCKIoStatus,INstruct_DEVICE_OBJECT*DeviceObject);对应FileOffest和Length参数的字节范围由调用者锁定.
ProcessId提供信息来识别能解除这个锁的进程–允许后来的清除,比如,当该进程退出时.
Key参数提供一个不透明的值,比如它能被用于为通过FastIoUnlockAllByKey调用的快速访问把多重锁结合在一起.
FailImmediately指示此调用应该被阻塞直到锁可用,或者应该直接返回失败.
对于FsRtl例程,FailImmediately被忽略–假如锁不可用,就会返回FALSE给调用者.
ExclusiveLock参数指示此次锁请求是独占(写)访问还是共享(读)访问.
FastUnlockSingle例程用于释放在文件的一部分上的字节范围锁定.
这个调用的原型为:typedefBOOLEAN(*PFAST_IO_UNLOCK_SINGLE)(INstruct_FILE_OBJECT*FileObject,INPLARGE_INTEGERFileOffset,INPLARGE_INTEGERLength,PEPROCESSProcessId,ULONGKey,OUTPIO_STATUS_BLOCKIoStatus,INstruct_DEVICE_OBJECT*DeviceObject);对于大多数文件系统,除非文件已经关联OpLocks,这个操作总是返回TRUE,因为即使字节锁范围不能被访问这个操作也已经完成了(虽然会是一个错误的状态).
既然同样的IRP调用会产生同样的结果,处理暂时被完成.
对于成功之后的解锁操作,FileOffset,Length,ProcessId和Key都必须匹配一个现有的字节范围锁.
否则,这个操作将被完成且将被返回给调用者的IoStatus中会设置错误STATUS_RANGE_NOT_LOCKED.
FastIoUnlockAll例程用于释放被进程控制的文件的所有关联的字节范围锁.
这个函数的原型为:typedefBOOLEAN(*PFAST_IO_UNLOCK_ALL)(INstruct_FILE_OBJECT*FileObject,PEPROCESSProcessId,OUTPIO_STATUS_BLOCKIoStatus,INstruct_DEVICE_OBJECT*DeviceObject);此时,fastI/O例程查询特定文件的可用锁列表并删除一切给定ProcessId关联的锁,不论独占的还是共享的.
当NtCloseFile被调用时系统会使用这个函数,要么是因为一个程序关闭了它要么是一个进程被删除.
FastIoUnlockAllByKey操作用于删除一组已经被调用者用一个特殊的key值逻辑地关联的字节范围锁.
这个例程的原型如下:typedefBOOLEAN(*PFAST_IO_UNLOCK_ALL_BY_KEY)(INstruct_FILE_OBJECT*FileObject,PVOIDProcessId,ULONGKey,OUTPIO_STATUS_BLOCKIoStatus,INstruct_DEVICE_OBJECT*DeviceObject);这个调用主要是为方便文件服务器比如SRV.
NT3.
51中的I/O管理器没有利用它.
key允许一个文件服务器把一个文件锁与一些远程客户端关联起来.
既然它可能表示像这样的远程客户端,单独的ProcessId就不够了.
同样地,既然存在多重文件服务器,单独的Key的使用可能引起其他文件服务器的文件锁的不正确释放.
结合两者能确保正确的操作和允许远程系统锁定.
FastIoDeviceControl此入口点用于支持NtDeviceIoControlFile调用,该调用本质上是用来实现对内核常驻的驱动的私有通信通道.
与其他FastI/O例程一样,如果此操作完成则返回TRUE,否则的话返回FALSE.
若为FALSE,I/O管理器创建一个IRP并调用驱动的分发入口点.
其原型如下:typedefBOOLEAN(*PFAST_IO_DEVICE_CONTROL)(INstruct_FILE_OBJECT*FileObject,INBOOLEANWait,INPVOIDInputBufferOPTIONAL,INULONGInputBufferLength,OUTPVOIDOutputBufferOPTIONAL,INULONGOutputBufferLength,INULONGIoControlCode,OUTPIO_STATUS_BLOCKIoStatus,INstruct_DEVICE_OBJECT*DeviceObject);Wait指示调用者是否愿意等待此操作完成,即使Wait被设置为TRUE,FastI/O例程也可能返回FALSE.
InputBuffer从"可选的"角度来说它指向一个可选的且由潜在的有效的调用者提供的buffer,尽管FastI/O例程实现能在InputBuffer缺失或其长度错误时指示一个错误(例如,STATUS_INVALID_PARAMETER)而返回TRUE.
OutputBuffer指向内存(可能有效),它依赖于IoControlCode访问类型值.
如果IoControlCode指示访问类型为3,则OutputBuffer无效且FastI/O例程负责确认它.
否则,此buffer在调用FastI/O例程之前有效.
这种方式与正常的IRP_MJ_DEVICE_CONTROL分发入口点一样.
FastI/O例程实现完全依赖于被传给FastIoDeviceControl例程的IoControlCode值.
尤其是这个入口点关联那些实现一个充足的私有通信接口的内核模式驱动.
因此,没有一个NT自身的文件系统真正地实现这个入口点,但是WinSock支持的驱动AFD频繁地使用这个接口来与底层的传输驱动通信.
因此,这些通信真正的模型完全依赖于FastI/O例程的驱动实现.
AcquireFileForNtCreateSectionandReleaseFileForNtCreateSection这两个例程与FastI/O分发表中的其他入口不相同.
它们用于解决某些围绕HPFS文件系统的锁定(作为NT自身的四个文件系统之一,只有HPFS真正地使用这两个例程).
此二者的原型一样,如下:typedefVOID(*PFAST_IO_ACQUIRE_FILE)(INstruct_FILE_OBJECT*FileObject);AcquireFileForNtCreateSection在从存储在文件系统中的一个文件映射到页之前被调用来确保一切驱动指定的锁定已经完成.
ReleaseFileForNtCreateSection被调用来释放之前为文件映射操作获得的锁.
如果没有提供这些入口,I/OManager会利用一个默认的机制来确保正确地同步文件映射操作.
FastIoDetachDevice最后一个例程是最古怪的.
ntddk.
h文件提供了这个调用的一个线索–即当一个设备对象将要被删除时会调用它.
我们发现这个调用在开发文件系统过滤驱动时极其有效,因为当底层的媒体被改变时移动式文件系统的设备对象都被破坏了.
有时这会立即发生,但它也能在媒体已经被移除之后的几乎任何时间发生,这依赖于系统的什么部分仍然在缓存着其信息.
此调用的原型为:typedefVOID(*PFAST_IO_DETACH_DEVICE)(INstruct_DEVICE_OBJECT*SourceDevice,INstruct_DEVICE_OBJECT*TargetDevice);根据我们的经验,移动式媒体文件系统的一个过滤驱动必须能处理这个调用否则系统将停止.
[注意由此开始的fastI/O调用在原始文章中并没有被描述.
]FastIoQueryNetworkOpenInfoLanManager(CIFS)文件服务器的一个普通的操作是打开一个文件并重获其标准属性和基本属性.
然后这个信息被组合并发送给远程客户端(LanManager/CIFS重定向器)以便它能为远程应用程序所用.
在NT4.
0之前,SRV需要多次传递一个IRP到底层的文件系统来提取相同的信息.
从NT4.
0开始一个新的信息类型FileNetworkOpenInformation被添加到文件系统接口中,以便一个像SRV这样的网络文件服务器能以一个单一的操作提取这个信息.
为促进这个处理的速度,这个fastI/O入口点也就被添加了进来.
其原型如下:typedefBOOLEAN(*PFAST_IO_QUERY_NETWORK_OPEN_INFO)(INstruct_FILE_OBJECT*FileObject,INBOOLEANWait,OUTstruct_FILE_NETWORK_OPEN_INFORMATION*Buffer,OUTstruct_IO_STATUS_BLOCK*IoStatus,INstruct_DEVICE_OBJECT*DeviceObject);与其他fastI/O例程一样,FileObject表示在其上调用者正起作用的文件.
Wait参数指示调用者是否愿意阻塞.
如果Wait为FALSE则这个例程不能被阻塞.
然后调用者能使用标准的IRP方式来从文件系统中获得此信息.
Buffer参数指向数据应该被复制的位置,IoStatus块指向标准的I/O状态信息.
最后,DeviceObject表示正被查询的文件系统实例.
既然是被SRV使用,这仅由物理文件系统实现.
FastIoAcquireForModWrite这个调用被添加进来是为了允许在内存管理器的ModifiedPageWriter真实地执行I/O之前的一个锁定文件的轮流机制.
这个函数完全可选–但如果你不实现它,文件系统运行时库将使用文件对象的公有头中的ERESOURCE指针来确保正确地同步(因此你的文件系统必须正在使用标准的NT锁定模型)typedefNTSTATUS(*PFAST_IO_ACQUIRE_FOR_MOD_WRITE)(INstruct_FILE_OBJECT*FileObject,INPLARGE_INTEGEREndingOffset,OUTstruct_ERESOURCE**ResourceToRelease,INstruct_DEVICE_OBJECT*DeviceObject);FileObject识别要被锁的指定文件;EndingOffset指示ModifiedPageWriter将要写的文件的最高字节.
你能在你的文件系统中使用此信息来同步任何试图截短文件的操作.
ResourceToRelease是一个你的FSD能返回的可选参数.
如果你返回任何非PERESOURCE的东西你必须实现这个入口点(FastIoReleaseForModWrite),后面会讲到.
DeviceObject表示文件系统实例.
FastIoMdlRead这个调用被添加进来是为了允许SRV(或其他内核常驻的服务)的最理想的性能.
以前版本的WindowsNT允许SRV直接重获MDLs到缓存中,但只能是在使用IRP方式时.
现在这个新的入口点为SRV提供一个直接的函数调用方法来取得同样的结果.
typedefBOOLEAN(*PFAST_IO_MDL_READ)(INstruct_FILE_OBJECT*FileObject,INPLARGE_INTEGERFileOffset,INULONGLength,INULONGLockKey,OUTPMDL*MdlChain,OUTPIO_STATUS_BLOCKIoStatus,INstruct_DEVICE_OBJECT*DeviceObject);FileObject唯一地识别正被读的文件,同时FileOffset和Length参数识别这个文件正被读的区域.
LockKey用于执行正确的强制的字节范围的锁定检查,当这个读为"应用级"的读时.
MdlChain是一个(或多个)描述缓存buffer的MDLs.
IoStatus块指示此读操作的完成状态(因为可能需要从磁盘中读数据).
DeviceObject表示文件系统实例.
文件系统使用来自FsRtl包的支持例程或直接调用CcMdlRead来实现它.
FastIoMdlReadCompleteSRV(或其他内核常驻的服务)调用这个入口点来释放一个通过调用FastIoMdlRead获得的MDL.
typedefBOOLEAN(*PFAST_IO_MDL_READ_COMPLETE)(INstruct_FILE_OBJECT*FileObject,INPMDLMdlChain,INstruct_DEVICE_OBJECT*DeviceObject);FileObject识别其MDL正被释放的文件.
MdlChain是先前调用FastIoMdlRead返回的MDL链.
DeviceObject表示文件系统实例.
文件系统使用来自FsRtl包的支持例程或直接调用CcMdlRead来实现它.
FSFD写者要注意了!
CcMdlReadComplete调用这个入口点且忽视返回值(到NT4.
0SP3时),这将会在过滤一切使用CcMdlReadComplete的文件系统(也包括像这样运行的FAT和NTFS)时造成内存丢失.
FastIoPrepareMdlWriteSRV(或其他内核常驻的服务)调用这个入口点来获得一个指向缓存中能被直接写的内存范围的指针.
这避免了在提供buffers方面固有的"内存复制"限制(就像应用程序做的那样).
typedefBOOLEAN(*PFAST_IO_PREPARE_MDL_WRITE)(INstruct_FILE_OBJECT*FileObject,INPLARGE_INTEGERFileOffset,INULONGLength,INULONGLockKey,OUTPMDL*MdlChain,OUTPIO_STATUS_BLOCKIoStatus,INstruct_DEVICE_OBJECT*DeviceObject);FileObject唯一地识别正被写的文件,FileOffset和Length识别该文件正被写的区域.
LockKey用于执行正确的强制的字节范围锁定检查,当这个写为"应用级"的写时.
MdlChain是一个(或多个)描述缓存buffer的MDLs.
IoStatus块指示此写操作的完成状态(因为可能需要从磁盘中读数据).
DeviceObject表示文件系统实例.
文件系统使用来自FsRtl包的支持例程或直接调用CcMdlWrite来实现它.
FastIoMdlWriteCompleteSRV(或其他内核常驻的服务)调用这个入口点来释放一个通过调用FastIoMdlRead获得的MDL.
typedefBOOLEAN(*PFAST_IO_MDL_WRITE_COMPLETE)(INstruct_FILE_OBJECT*FileObject,INPLARGE_INTEGERFileOffset,INPMDLMdlChain,INstruct_DEVICE_OBJECT*DeviceObject);FileObject识别其MDL正被释放的文件.
MdlChain是先前调用FastIoMdlRead返回的MDL链.
DeviceObject表示文件系统实例.
文件系统使用来自FsRtl包的支持例程或直接调用CcMdlReadComplete来实现它.
FSFD写者要注意了!
CcMdlWriteComplete调用这个入口点且忽视返回值(到NT4.
0SP3时),这将会在过滤一切使用CcMdlReadComplete的文件系统(也包括像这样运行的FAT和NTFS)时造成内存丢失.
FastIoReadCompressed这个调用被添加进来为了允许SRV(或其他内核常驻的服务)从底层的文件系统引进压缩格式的数据.
这仅能对使用标准的WindowsNT压缩库(比如"LZW1")压缩的文件使用.
这个例程能被用于复制数据到一个调用者提供的buffer或它能以MDL的形式返回数据.
typedefBOOLEAN(*PFAST_IO_READ_COMPRESSED)(INstruct_FILE_OBJECT*FileObject,INPLARGE_INTEGERFileOffset,INULONGLength,INULONGLockKey,OUTPVOIDBuffer,OUTPMDL*MdlChain,OUTPIO_STATUS_BLOCKIoStatus,OUTstruct_COMPRESSED_DATA_INFO*CompressedDataInfo,INULONGCompressedDataInfoLength,INstruct_DEVICE_OBJECT*DeviceObject);FileObject唯一地识别正被读的文件,同时FileOffset和Length参数识别这个文件正被读的区域.
LockKey用于执行正确的强制的字节范围锁定检查,当这个读为"应用级"的读时.
Buffer参数是被压缩的数据将被复制进去的一个(可选的)buffer.
MdlChain(也可选)是一个(或多个)描述缓存buffer的MDLs的指针.
IoStatus块指示此读操作的完成状态(因为可能需要从磁盘中读数据).
DeviceObject表示文件系统实例.
CompressedDataInfo和CompressedDataInfoLength识别提供的buffer,正被读的块的压缩信息将被复制到那.
DeviceObject表示文件系统实例.
FastIoWriteCompressed这个调用被添加进来为了允许SRV(或其他内核常驻的服务)存储压缩格式的数据到底层的文件系统中.
这仅能对使用标准的WindowsNT压缩库(比如"LZW1")压缩的文件使用.
typedefBOOLEAN(*PFAST_IO_WRITE_COMPRESSED)(INstruct_FILE_OBJECT*FileObject,INPLARGE_INTEGERFileOffset,INULONGLength,INULONGLockKey,INPVOIDBuffer,OUTPMDL*MdlChain,OUTPIO_STATUS_BLOCKIoStatus,INstruct_COMPRESSED_DATA_INFO*CompressedDataInfo,INULONGCompressedDataInfoLength,INstruct_DEVICE_OBJECT*DeviceObject);FileObject唯一地识别正被读的文件,同时FileOffset和Length参数识别这个文件正被读的区域.
LockKey用于执行正确的强制的byterange锁定检查,当这个写为"应用级"的写时.
MdlChain是一个(或多个)描述缓存buffer的MDLs的指针.
IoStatus块指示此读操作的完成状态(因为可能需要从磁盘中读数据).
CompressedDataInfo和CompressedDataInfoLength识别提供的buffer,在其内正被读的块的压缩信息将被复制.
DeviceObject表示文件系统实例.
DeviceObject表示文件系统实例.
FastIoMdlReadCompleteCompressed这个调用被SRV(或其他内核常驻的服务)用于释放通过调用FastIoMdlReadCompressed获得的一个MDL.
typedefBOOLEAN(*PFAST_IO_MDL_READ_COMPLETE_COMPRESSED)(INstruct_FILE_OBJECT*FileObject,INPMDLMdlChain,INstruct_DEVICE_OBJECT*DeviceObject);FileObject识别其MDL正被释放的文件.
MdlChain是先前调用FastIoMdlReadCompressed返回的MDL链.
DeviceObject表示文件系统实例.
FastIoMdlWriteCompleteCompressed这个调用被SRV(或其他内核常驻的服务)用于释放通过调用FastIoMdlWriteCompressed获得的一个MDL.
typedefBOOLEAN(*PFAST_IO_MDL_WRITE_COMPLETE_COMPRESSED)(INstruct_FILE_OBJECT*FileObject,INPLARGE_INTEGERFileOffset,INPMDLMdlChain,INstruct_DEVICE_OBJECT*DeviceObject);FileObject识别其MDL正被释放的文件.
MdlChain是先前调用FastIoMdlWriteCompressed返回的MDL链.
DeviceObject表示文件系统实例.
FastIoQueryOpen这个例程被SRV(或其他内核常驻的服务)用来打开一个文件,重获它的"网络信息"并关闭这个文件–全都在一个单一操作中.
被传递到此例程中的Irp是一个IRP_MJ_CREATE请求及底层文件系统要真实地重获文件信息所必须的信息.
typedefBOOLEAN(*PFAST_IO_QUERY_OPEN)(INstruct_IRP*Irp,OUTPFILE_NETWORK_OPEN_INFORMATIONNetworkInformation,INstruct_DEVICE_OBJECT*DeviceObject);Irp参数识别正被打开的文件(这是一个被构造的IRP_MJ_CREATE请求).
NetworkInformation参数指向一个buffer,文件系统应该存储必需的信息到其内.
DeviceObject表示文件系统实例.
FastIoReleaseForModWrite这个例程被FsRtl包用来释放一切通过调用FastIoAcquireForModWrite(之前已描述过的)获得的资源.
typedefNTSTATUS(*PFAST_IO_RELEASE_FOR_MOD_WRITE)(INstruct_FILE_OBJECT*FileObject,INstruct_ERESOURCE*ResourceToRelease,INstruct_DEVICE_OBJECT*DeviceObject);如果FastIoReleaseForModWrite返回一个PERESOURCE则一个文件系统没必要实现这个入口点.
然而,如果文件系统正在利用一个更加复杂的同步模型,或者期望由于其他原因被调用来释放文件,则这个例程能被实现.
FileObject识别正被释放的指定的文件.
ResourceToRelease对应之前调用FastIoAcquireForModWrite返回的值.
DeviceObject表示文件系统实例.
FastIoAcquireForCcFlush这个例程被FsRtl包用来在缓存管理器的LazyWriter把脏数据写回到文件系统之前获得一切必需的文件系统资源.
如果这个入口点没有被文件系统实现,则FsRtl例程使用公有的头(FileObject->FsContext)中的信息来锁它所指向的PERESOURCEs.
typedefNTSTATUS(*PFAST_IO_ACQUIRE_FOR_CCFLUSH)(INstruct_FILE_OBJECT*FileObject,INstruct_DEVICE_OBJECT*DeviceObject);FileObject表示正被flush的指定的文件.
DeviceObject表示文件系统实例.
FastIoReleaseForCcFlushFsRtl包使用这个例程来释放通过调用FastIoAcquireForCcFlush获得的一切文件系统资源.
如果文件系统没有实现这个入口点,则FsRtl例程使用公有的头(FileObject->FsContext)中的信息来解锁它所指向的PERESOURCEs.
typedefNTSTATUS(*PFAST_IO_RELEASE_FOR_CCFLUSH)(INstruct_FILE_OBJECT*FileObject,INstruct_DEVICE_OBJECT*DeviceObject);FileObject表示正被flush的指定的文件.
DeviceObject表示文件系统实例.
FastI/O的使用大多数WindowsNT驱动永远不需要使用这些fastI/O入口点.
I/O管理器不要求使用它们且会为了以上这些I/O操作中的大部分操作创建IRP(AcquireFileForNtCreateSection,ReleaseFileForNtCreateSection和FastIoDetachDevice例外).
不过,对文件系统驱动来说,这些调用在提高文件系统的性能方面起着十分重要的作用.
同样地,对于任何试图开发他们自己的TDI的人来说,他们能使用一个很像AFD的驱动来提供用户模式访问包和内核模式TDI接口之间的接口(通过IOCTL控制通道).
此外,虽然对于恰当的性能来说这不是必需的,但它似乎可以作为一个特色在提高系统性能时被添加进来.
一个第三种类的驱动将会是那些结合了一些文件系统的特色和网络通信的内核模式驱动.
比如,NPFS(命名管道文件系统),使用fastI/O来提供一个性能调谐的通信通道.
最后,FastI/O的第四种使用是在FSFD的开发中–在这里实际上它们是必需的.
如同它们表现出来的一样,如果底层的设备有一个特殊的FastI/O入口点,过滤驱动也必须有该FastI/O入口点–否则在某种情况下将会引起系统崩溃.
总结:可以看到这块需要理解的东西主要是fastio是做什么用的,我们应该怎么处理fastio.
Fastio既然是把数据从缓存里面移至用户的进程上下文中,那么所操作的目标数据都在缓存中.
不要简单地以为这里做加解密处理就可以控制各个进程中的明密文,因为进程之间还可以以其他方式共享数据且没有任何IRP和fastio产生.
假如需要关注的东西有主次之分的话,对于fastio,你应该心里有个明镜了吧问10:总结sfilter(2009-04-28)加上《Windows文件系统过滤驱动开发教程(第二版)》和《Windows驱动编程基础》这两本书,咱们基本上把FSFD中最简单的sfilter过了一遍(这里虽然说了这两本书但不见得就推崇它们,摘取你想要了解的部分阅读就行了).
总结下吧前一本书中详解的绑定设备的过程大部分驱动都不需要深究,你本来就要绑定所有现有卷和新挂载的卷,不过如果你需要做个用户模式下的应用界面来提供有选择的设备绑定功能,那你还需要看看filespy的例子.
除了这些的就没什么了,主要就截获IRP_MJ_CREATE,显示下文件名或者设备名.
为什么没有其他的sfilter是个比较古老的框架,实际上WDK里面已经在某些地方做了优化性质的修改,其他像处理IRP_MJ_READ、IRP_MJ_WRITE等的例程就得你自己根据你的需求添加了.
你也可以像sfilter示范的那样添加自己的类似SfDisplayCreateFileName这样的支持例程.
当然,维护它们也要参照sfilter的示范.
先声明,然后在#pragmaalloc_text的相应位置添加,然后是定义.
那这个框架有什么优缺点呢怎么说,简单干净是它最大的优点,在没有多余功能的情况下它的脉络是相当清晰的.
把示范用的SfCreate去掉,基本就是一个较为完整的框架了.
不过,功能极少也使得它的参考价值大大减低,而某些其他框架里面已经提供的你得自己修改后挪过来甚至自己编写.
不过legacy有个通病,就是不管这种IRP类型比如IRP_MJ_WRITE你需不需要处理,你至少都得提供个例程处理它,在这个例子中就是SfPassThrough.
所有的fastio基本上也是做重复的调用工作.
虽然这些是框架已经提供的,毕竟都堆到代码里面显得非常臃肿.
Filespy系列Filespy一直是我想研究但没有时间研究的源码,尤其是logrecord技术.
对写一个有用的测试用工具相当重要.
站在我的角度说,可能分析这些源码的时候有一些东西是一笔带过的,但恰恰可能是你需要一点一点地体会的.
驱动无论学习还是开发都是一项漫长的计划,操之过急往往会一事无成.
有经验的人说简单,那都是付出巨大努力之后的简单,你现在觉得简单,那是你还浮于表面的简单.
Filespy分内核模式下的驱动和用户模式下的界面两个部分.
界面那个不在驱动范围内.
这里主要是把filespy的驱动部分梳理一遍.
问11:fspyKern.
h、fspydef.
h和filespy.
h(2009-04-28)Filespy驱动部分的头文件,需要注意的是filespy有一部分声明和定义是和用户模式公用的.
#defineUSE_STREAM_CONTEXTS0#ifUSE_STREAM_CONTEXTS&&WINVER>8)&(HASH_SIZE-1))以下是几个重要的全局变量LIST_ENTRYgHashTable[HASH_SIZE];//全局链表的表头KSPIN_LOCKgHashLockTable[HASH_SIZE];//用于多线程同步的自旋锁ULONGgHashMaxCounters[HASH_SIZE];//哈希入口数量限制ULONGgHashCurrentCounters[HASH_SIZE];//哈希入口当前计数下面主要讲与用哈希表保存文件名相关的例程.
以帮助你理解这个技巧.
初始化例程,初始化全局哈希表和全局自旋锁数组.
VOIDSpyInitNamingEnvironment(VOID){inti;for(i=0;iFlink;//从表头开始遍历while(pList!
=ListHead){pHash=CONTAINING_RECORD(pList,HASH_ENTRY,List);if(FileObject==pHash->FileObject){//是否要查找的FileObjectreturnpHash;//是,则返回哈希表入口}pList=pList->Flink;//不是,则访问前一结点}returnNULL;//没有,则返回NULL}删除指定的哈希表入口.
VOIDSpyNameDelete(INPFILE_OBJECTFileObject){UINT_PTRhashIndex;KIRQLoldIrql;PHASH_ENTRYpHash;PLIST_ENTRYpList;PLIST_ENTRYlistHead;hashIndex=HASH_FUNC(FileObject);//原来用这个宏提取的值要用作索引值~KeAcquireSpinLock(&gHashLockTable[hashIndex],&oldIrql);//保证多线程同步listHead=&gHashTable[hashIndex];pList=listHead->Flink;while(pList!
=listHead){pHash=CONTAINING_RECORD(pList,HASH_ENTRY,List);if(FileObject==pHash->FileObject){//找到了则删除目标入口INC_STATS(TotalContextNonDeferredFrees);gHashCurrentCounters[hashIndex]--;RemoveEntryList(pList);SpyFreeBuffer(pHash,&gNamesAllocated);break;}pList=pList->Flink;//继续向前遍历}KeReleaseSpinLock(&gHashLockTable[hashIndex],oldIrql);}删除所有的哈希表入口VOIDSpyNameDeleteAllNames(VOID){KIRQLoldIrql;PHASH_ENTRYpHash;PLIST_ENTRYpList;ULONGi;INC_STATS(TotalContextDeleteAlls);for(i=0;iDeviceExtension;UINT_PTRhashIndex;KIRQLoldIrql;PHASH_ENTRYpHash;PHASH_ENTRYnewHash;PLIST_ENTRYlistHead;PUNICODE_STRINGnewName;PCHARbuffer;UNREFERENCED_PARAMETER(Context);if(FileObject==NULL){return;}hashIndex=HASH_FUNC(FileObject);INC_STATS(TotalContextSearches);listHead=&gHashTable[hashIndex];////假如是在create中,则产生名,而不查找现有的哈希表入口//if(!
FlagOn(LookupFlags,NLFL_IN_CREATE)){KeAcquireSpinLock(&gHashLockTable[hashIndex],&oldIrql);pHash=SpyHashBucketLookup(&gHashTable[hashIndex],FileObject);if(pHash!
=NULL){////把文件名复制给LogRecord,并确保文件名是NULL结束的,同时增加LogRecord的长度//SpyCopyFileNameToLogRecord(&RecordList->LogRecord,&pHash->Name);KeReleaseSpinLock(&gHashLockTable[hashIndex],oldIrql);INC_STATS(TotalContextFound);return;}KeReleaseSpinLock(&gHashLockTable[hashIndex],oldIrql);}////如果表中没有,则添加它.
如果是在DISPATCH_LEVEL则不查找//buffer=SpyAllocateBuffer(&gNamesAllocated,gMaxNamesToAllocate,NULL);if(buffer!
=NULL){newHash=(PHASH_ENTRY)buffer;newName=&newHash->Name;RtlInitEmptyUnicodeString(newName,(PWCHAR)(buffer+sizeof(HASH_ENTRY)),RECORD_SIZE-sizeof(HASH_ENTRY));if(SpyGetFullPathName(FileObject,newName,devExt,LookupFlags)){newHash->FileObject=FileObject;KeAcquireSpinLock(&gHashLockTable[hashIndex],&oldIrql);////再次查找,多线程环境下有可能在我们放弃自旋锁之后,某个线程会把它保存到表中//pHash=SpyHashBucketLookup(&gHashTable[hashIndex],FileObject);if(pHash!
=NULL){////确实有某个线程保存它了,那就把在表中找到的名给LogRecord.
//////把文件名给LogRecord,确保它是NULL结束的,并增加LogRecord的长度//SpyCopyFileNameToLogRecord(&RecordList->LogRecord,&pHash->Name);KeReleaseSpinLock(&gHashLockTable[hashIndex],oldIrql);SpyFreeBuffer(buffer,&gNamesAllocated);return;}////没有找到,则添加新入口//////把文件名给LogRecord,确保它是NULL结束的,并增加LogRecord的长度//SpyCopyFileNameToLogRecord(&RecordList->LogRecord,newName);InsertHeadList(listHead,&newHash->List);gHashCurrentCounters[hashIndex]++;if(gHashCurrentCounters[hashIndex]>gHashMaxCounters[hashIndex]){gHashMaxCounters[hashIndex]=gHashCurrentCounters[hashIndex];}KeReleaseSpinLock(&gHashLockTable[hashIndex],oldIrql);}else{////Wearenotsupposedtokeepthelogrecordentry,copy//whatevertheygaveusin//SpyCopyFileNameToLogRecord(&RecordList->LogRecord,newName);INC_STATS(TotalContextTemporary);SpyFreeBuffer(buffer,&gNamesAllocated);}}else{////Setadefaultstringevenifthereisnobuffer//SpyCopyFileNameToLogRecord(&RecordList->LogRecord,&OutOfBuffers);}return;}总结:哈希表保存FileObject的方法基本原理就是把符合条件的FileObject保存到一个全局链表中,并用从FileObject中提取的哈希值作为其索引值.
对这个表的访问都用自旋锁同步.
对这个表可以进行初始化、添加入口、查找入口、删除指定入口和删除所有入口的操作.
问13:上下文(2009-04-30)在讲下一个源文件之前,我需要引入一个概念:上下文.
你可能在其他资料上阅读过关于它的介绍.
这里再讲下.
原文来自osr.
NT标准内核模式驱动的一个重要方面就是特定的驱动函数在其中执行的"上下文".
文件系统开发者和所有NT内核模式驱动程序员能从对执行上下文的一个坚实的理解中获益.
如果谨慎地利用,理解了执行上下文能使设备驱动设计的产品更高效而低耗.
本文中,我们将探究执行上下文的概念.
作为介绍这个概念的一个示范,本文用对一个允许用户应用在内核模式中以所有权限和特权的方式来执行的描述收尾.
顺着这条路线,我们也将讨论在设备驱动中执行上下文的特殊应用.
什么是上下文当我们提到一个例程的上下文时我们就是在说它的线程和进程的执行环境.
在NT中,这个环境由当前线程环境块(TEB)和进程环境块(PEB)确定.
因此,上下文包括了虚拟内存设置(告诉我们哪一个物理内存页对应哪一个虚拟内存地址),句柄转化,分发器信息,栈以及寄存器设置的一般意图和浮动点.
当我们问一个特定的内核例程正运行在什么上下文中时,我们真正在问的是"(NT内核)分发器确定的当前线程是什么"由于每一个线程属于且仅属于一个进程,当前线程意味着一个指定的当前进程.
总而言之,当前线程和当前进程意味着上文提到的那些使线程和进程独一无二的东西(句柄,虚拟内存,调度程序状态,寄存器).
虚拟内存上下文或许是上下文中对内核模式驱动程序员最有用的一个方面.
回顾下NT映射用户进程到低2GB的虚拟地址空间,而映射操作系统代码本身到高2GB的虚拟地址空间.
当一个线程来自一个正在执行的用户进程时,它的虚拟地址范围将从0到2GB,而所有高于2GB的地址将被设置为"不可访问的"来阻止用户直接访问操作系统代码和结构.
当操作系统代码正在执行时,它的虚拟地址范围从2GB到4GB,当前用户进程(如果有一个的话)被映射进0到2GB的地址.
在NTV3.
51和V4.
0中,代码被映射进高2GB地址的规则从来没有改变过.
然而,代码被映射进低2GB的地址空间改变了,它基于当前的进程是哪个.
除了以上这些,在NT指定的虚拟内存安排中,进程P中的一个给定的有效用户虚拟地址X(X小于等于2GB)将与内核虚拟地址X对应同样的物理内存位置.
这是真的,当然,仅当进程P是当前进程且(因此)进程P的物理页被映射进操作系统的低2GB虚拟地址时.
这句话的另外一种解释方法是"仅当P是当前进程上下文环境时这是真的".
因此,给定了同一进程上下文,用户虚拟地址和直到2GB的内核虚拟地址提到的是相同的物理位置.
内核模式驱动程序员所关心的上下文的另一个方面就是线程调度(scheduling)上下文.
当一个线程等待(比如对一个没被发信号的对象用Win32函数WaitForSingleObject(.
.
.
)),该线程的调度上下文被用于存储信息(定义了线程在等待什么).
当发出一个没被满足的等待时,线程从就绪(ready)队列中被移除,仅当等待已经被满足时返回(通过指定分发器对象为被发信号的).
上下文也对句柄的使用产生影响.
因为句柄被指定给一个特定的进程,在一个进程的上下文中被创建的句柄放到另一个进程的上下文中将没有任何用处.
不同的上下文内核模式例程运行在以下三个不同类别的上下文中的一种里:系统进程上下文;一个指定的用户线程(和进程)上下文;任意用户线程(和进程)上下文.
在每个内核模式驱动执行期间,其各部分可能运行在以上三个类别的上下文中的任何一种.
例如,一个驱动的DriverEntry(.
.
.
)函数总是在系统进程的上下文中运行.
系统进程上下文与用户线程上下文无关(因此没有TEB),也没有用户进程被映射到内核虚拟地址空间的低2GB中.
另一方面,DPCs(比如一个驱动的ISR或定时器(timer)的DPC的终止函数)运行在一个任意用户线程的上下文中.
这意味着在一个DPC的执行期间,任何用户线程都可能是"当前"线程,因此任何用户进程都可能被映射到内核虚拟地址的低2GB中.
驱动的分发例程在其中运行的上下文特别有趣.
多数情况下,内核模式驱动的分发例程将会在调用用户线程的上下文中运行.
图1展示了为什么会是这样.
当一个用户线程发出一个I/O功能调用到一个设备时,例如通过调用Win32ReadFile(.
.
.
)函数,这会导致一个系统服务请求.
在Intel架构的处理器上,这样的请求通过要经过一个中断门的软件中断来实现.
中断门将处理器的当前特权级改为内核模式,这引起一个切换到内核栈,然后调用系统服务分发器.
系统服务分发器调用操作系统中的处理指定系统服务的函数.
对ReadFile(.
.
.
)来说就是I/O子系统中的NtReadFile(.
.
.
)函数.
NtReadFile(.
.
.
)函数构建一个IRP,并调用驱动的读分发例程(对应被ReadFile(.
.
.
)请求中的文件句柄所引用的文件对象).
所有这些都发生在IRQLPASSIVE_LEVEL.
图1在以上描述的整个进程中,始终都没有用户请求的调度或队列发生.
因此,在用户线程和进程上下文中没有发生改变.
在这个例子中,驱动的分发例程运行在发出ReadFile(.
.
.
)请求的用户线程的上下文中.
这意味着当驱动的读分发例程运行时,是用户线程在执行内核模式驱动代码.
驱动的分发例程总是在请求的用户线程的上下文中运行吗不是.
V4.
0KernelModeDriversDesignGuide的16.
4.
1.
1节告诉我们,"仅仅最高级NT驱动,比如文件系统驱动,能确定它们的分发例程将会在这样的一个用户模式线程中被调用.
"我们的例子将会证明这不是对的.
FSDs将在请求的用户线程的上下文中被调用是确信无疑的.
事实上任何驱动作为一个用户I/O请求的结果被直接调用,而不是首先通过另外一个驱动,它被保证在请求的用户线程的上下文中被调用.
这包括FSDs.
但它也意味着大多数用户写的标准内核模式驱动提供了直接使用应用的函数,比如那些处理控制设备的标准内核模式驱动,将令它们的分发函数在请求的用户线程中被调用.
事实上,一个驱动的分法例程不会在调用的用户的线程的上下文中被调用的唯一方法,是如果用户的请求首先被指给了一个更高级驱动,比如文件系统驱动.
如果更高级驱动把这个请求传给一个系统工作者(worker)线程,则在上下文中将会有一个其导致的改变.
当这个IRP最终被传递给较低级驱动时,无法保证当更高级驱动使这个IRP向前时更高级驱动运行的上下文是原始请求的用户线程.
然后较低级驱动将会运行在任意线程上下文中.
一般规则是当用户在没有其他驱动介入的情况下直接访问一个设备时,该设备的驱动分发例程将总是运行在请求的用户线程的上下文中.
我们可以利用这点来做一些有趣的事情.
影响一个分发函数运行在调用的用户线程的上下文中的结果是什么一些有用而也有一些让人恼火.
例如,让我们设想一个设备从一个分发函数中用ZwCreateFile(.
.
.
)来创建一个文件.
当同一个驱动试图用ZwReadFile(.
.
.
)从该文件中读时,读会失败除非在创建被发出的同一用户进程的上下文中发出读.
这是因为句柄和文件对象都被基于每进程的存储了.
继续这个例子,如果成功地发出了ZwReadFile(.
.
.
)请求,驱动能可选地通过等待一个关联读操作的事件来选择等待读的完成.
当等待被发出时发生了什么当前用户线程参考指出的事件对象而处于一个等待的状态.
这么多的异步I/O请求!
分发器寻找下一个最高优先级可运行的线程.
当事件对象因为ReadFile(.
.
.
)请求完成而被设置为被发信号的状态时,驱动将仅当用户的线程再次成为一个NCPU系统上N个最高优先级可运行的线程中的一个时才会运行.
在请求的用户线程的上下文中运行也有一些非常有用的结果.
例如,用值为-2的句柄(即"当前线程")调用ZwSetInformationThread(.
.
.
)会允许驱动改变当前线程的所有属性.
类似的,用值为NtCurrentProcess(.
.
.
)(ntddk.
h中定义为-1)的句柄调用ZwSetInformationProcess(.
.
.
)将允许驱动改变当前进程的特征.
注意因为这两个调用都是从内核模式发出的,所以没有做安全检查.
因此,用这种方法改变线程/进程的属性后有可能线程自己就不能访问了.
然而,这个能直接访问用户虚拟地址的能力或许是运行在请求的用户线程的上下文中的最有用的结果.
考虑下,例如,一个简单的共享内存类型的设备的驱动能直接被一个用户模式应用使用.
让我们假定这个设备上的一个写操作由直接从一个用户的缓冲中复制1K的数据到设备上一个共享内存驱动,而设备共享内存区域总是可被访问的.
设备驱动的传统设计大概都是用缓冲的I/O,因为要移动的数据都小于一个页的长度.
因此,在每次写请求上I/O管理器都分配非分页池中的一个与用户数据缓冲等大的缓冲,并从用户缓冲复制数据到这个非分页池中的缓冲中.
然后I/O管理器会调用驱动的写分发例程,并在IRP中提供一个指向这个非分页池缓冲的指针(Irp->AssociatedIrp.
SystemBuffer).
然后驱动从非分页池缓冲复制数据到设备的共享内存区域.
这个设计效率如何只说一点,数据通常被复制两次.
更不必说I/O管理器需要为非分页池缓冲做池分配工作.
我不会说它是消耗最低的设计.
假定我们想再次用传统方法来改进这个设计的性能.
我们可能让驱动改用直接I/O.
这时,包含用户数据的页会被做可访问探测并由I/O管理器锁到内存中.
然后I/O管理器会用一个MDL(在IRP的Irp->MdlAddress中提供给驱动的指针)来描述用户的数据.
现在,当驱动的写分发例程得到IRP,它需要用MDL来构造能作为它的复制操作来源的一个系统地址.
这个系统地址就是调用IoGetSystemAddressForMdl(.
.
.
)(它会调用MmMapLockedPages(.
.
.
)来映射MDL中的页表入口到内核虚拟地址空间)得到的.
然后驱动能用从该调用返回的内核虚拟地址来从用户缓冲里复制数据到设备的共享内存区域.
这个设计的效率如何比上一个好.
但是映射也不是一个低消耗的操作.
那么怎么在这两个常规的设计中做选择呢假设用户应用直接告知驱动,我们直到驱动的分发例程总会在请求的用户线程的上下文中被调用.
结果是我们能用"neitherI/O"绕过直接和缓冲的I/O设计的消耗.
驱动通过在设备对象的标记中不设置DO_DIRECT_IO位和DO_BUFFERED_IO位来指示它想要使用"neitherI/O".
当驱动的写分法例程被调用,用户数据缓冲的用户模式虚拟地址将会被定位在IRP的Irp->UserBuffer中.
因为用户空间位置的内核模式虚拟地址与其用户模式虚拟地址一样,驱动能直接使用Irp->UserBuffer中的地址,并复制用户数据缓冲中的数据到设备的共享内存区域.
当然,为了防止用户缓冲访问的问题,驱动会在一个try…except块中执行复制.
无映射;无重复制;无池分配.
仅仅一个直接向前的复制.
除了它没有什么我会说是低消耗.
如果用户传给驱动的缓冲指针有效而在用户进程中无效时,使用"neitherI/O"方式也会使系统慢下来.
try…except块不会捕获这个问题.
举个这样的指针的例子,指针引用了被用户进程只读映射的内存,但是从内核模式读/写.
这时,驱动的动作是简单地把数据放在用户应用看起来是只读的空间里!
这是个问题吗它依赖于驱动和应用.
只有你能决定为了设计的好处这个潜在的风险是否值得.
推波助澜最后一个例子将论证运行在请求的用户线程的上下文中的一个驱动的所有可能性.
这个例子将示范当驱动正在运行时,发生的所有事情只是驱动在内核模式里运行在调用用户进程的上下文中.
我们已经写了一个虚假设备的驱动SwitchStack.
因为它是一个虚假设备驱动它不关联任何硬件.
这个驱动支持创建,关闭和一个用METHOD_NEITHER的单一的IOCTL.
当一个用户应用发出IOCTL,它提供一个指针指向一个类型为void的变量作为IOCTL的输入缓冲和一个指针指向一个函数(取得一个void指针并返回void)作为IOCTL的输出缓冲.
当处理这个IOCTL时,驱动调用指示的用户函数,传递PVOID类型的上下文参数.
然后用户地址空间里的作为结果的函数在内核模式中执行.
给定NT的设计,很少有回调用户函数不能做的事.
它能发出Win32函数调用,弹出对话框,执行文件I/O.
唯一不同的是用户应用运行在内核模式中,在内核栈上.
当一个应用运行在内核模式中时,它不受特权限制,限额和保护检查.
因为在内核模式中执行的所有函数都有IOPL,用户应用甚至能发出IN和OUT指令(当然是在一个Intel架构的系统上).
你的想象(外加判断力)仅受你用这个驱动能做的事情的类型的限制.
//++//SwitchStackDispatchIoctl////这是一个分发例程,处理设备I/O控制函数并发送给设备////输入://DeviceObject指向一个设备对象//Irp指向一个IRP////返回://IRP的NSTATUS完成状态////--NTSTATUSSwitchStackDispatchIoctl(INPDEVICE_OBJECT,DeviceObject,INPIRPIrp){PIO_STACK_LOCATIONIos;NTSTATUSStatus;////获得当前I/O栈位置的指针//Ios=IoGetCurrentIrpStackLocation(Irp);////确保这对我们来说是一个有效的IOCTL.
.
.
//if(Ios->Parameters.
DeviceIoControl.
IoControlCode!
=IOCTL_SWITCH_STACKS){Status=STATUS_INVALID_PARAMETER;}else{////获得要调用的函数的指针//VOID(*UserFunctToCall)(PULONG)=Irp->UserBuffer;////和要被传递的参数//PVOIDUserArg;UserArg=Ios->Parameters.
DeviceIoControl.
Type3InputBuffer;////用参数调用用户的函数//(VOID)(*UserFunctToCall)((UserArg));Status=STATUS_SUCCESS;}Irp->IoStatus.
Status=Status;Irp->IoStatus.
Information=0;IoCompleteRequest(Irp,IO_NO_INCREMENT);return(Status);}图2–IOCTL分发函数图2包含驱动的DispatchIoCtl函数的代码.
使用标准Win32系统服务调用如下调用驱动:DeviceIoControl(hDriver,(DWORD)IOCTL_SWITCH_STACKS,&UserData,sizeof(PVOID),&OriginalWinMain,sizeof(PVOID),&cbReturned,设计这个例子不是为了鼓励你写程序来运行在内核模式.
不过,这个例子所做的事情示范了当你的驱动正在运行时,其实它仅仅是用它所有的变量,消息队列,窗口句柄等等运行在一个普通的Win32程序的上下文中.
唯一的不同是它运行在内核模式中,在内核栈上.
总结:上下文也是驱动中的一个重要概念,理解它很重要.
其实很多概念比如函数和例程,可能在你看来它们所表示的对象类型都是一样的.
不过不要因为有这种想法就抵触新概念,这对你没什么好处.
问14:fspyCtx.
c(2009-04-28)如同上一问,这个源文件也主要包含了一项技术:上下文操作.
它与哈希中相同的部分就不再赘述了.
在说上下文的支持例程之前,先看看其数据结构:typedefstruct_SPY_STREAM_CONTEXT{////用于跟踪每流上下文的OS结构.
详见WDK//FSRTL_PER_STREAM_CONTEXTContextCtrl;////用于跟踪每设备上下文的连接列表(在设备扩展中).
//LIST_ENTRYExtensionLink;////这是当前有多少使用这个上下文的线程计数.
其使用方式如下://-创建时它被设置为1//-每当它被返回给线程时增1//-每当线程完成了则减1//-当底层的流被free时减1//-当计数变为零时上下文会被删除//LONGUseCount;////保存文件的名//UNICODE_STRINGName;////上下文的标记.
用于指示其状态//CTX_FLAGSFlags;////绑定的流(fcb)//PFSRTL_ADVANCED_FCB_HEADERStream;}SPY_STREAM_CONTEXT,*PSPY_STREAM_CONTEXT;这个数据结构用于保持对某个流的跟踪.
以下是创建流上下文的例程NTSTATUSSpyCreateContext(INPDEVICE_OBJECTDeviceObject,INPFILE_OBJECTFileObject,INNAME_LOOKUP_FLAGSLookupFlags,OUTPSPY_STREAM_CONTEXT*pRetContext){PFILESPY_DEVICE_EXTENSIONdevExt=DeviceObject->DeviceExtension;PSPY_STREAM_CONTEXTctx;ULONGcontextSize;UNICODE_STRINGfileName;WCHARfileNameBuffer[MAX_PATH];BOOLEANgetNameResult;PAGED_CODE();ASSERT(IS_FILESPY_DEVICE_OBJECT(DeviceObject));////设置局部变量//*pRetContext=NULL;RtlInitEmptyUnicodeString(&fileName,fileNameBuffer,sizeof(fileNameBuffer));////获得文件全路径名//getNameResult=SpyGetFullPathName(FileObject,&fileName,devExt,LookupFlags);contextSize=sizeof(SPY_STREAM_CONTEXT)+fileName.
Length;ctx=ExAllocatePoolWithTag(NonPagedPool,contextSize,FILESPY_CONTEXT_TAG);if(!
ctx){returnSTATUS_INSUFFICIENT_RESOURCES;}////初始化上下文结构//RtlZeroMemory(ctx,sizeof(SPY_STREAM_CONTEXT));ctx->UseCount=1;////插入文件名//RtlInitEmptyUnicodeString(&ctx->Name,(PWCHAR)(ctx+1),contextSize-sizeof(SPY_STREAM_CONTEXT));RtlCopyUnicodeString(&ctx->Name,&fileName);////如果不想保存这个上下文,则把它标记为临时的//if(!
getNameResult){SetFlag(ctx->Flags,CTXFL_Temporary);INC_STATS(TotalContextTemporary);}////返回对象上下文//INC_STATS(TotalContextCreated);*pRetContext=ctx;////清除局部的nameControl结构//returnSTATUS_SUCCESS;}以下几个例程是其各种操作NTSTATUSSpyGetContext(INPDEVICE_OBJECTDeviceObject,INPFILE_OBJECTFileObject,INNAME_LOOKUP_FLAGSLookupFlags,OUTPSPY_STREAM_CONTEXT*pRetContext)/*++这个例程会检查给定上下文是否已经存在.
如果没有,它就会创建一个并返回.
创建失败则返回NULL--*/{PFILESPY_DEVICE_EXTENSIONdevExt=DeviceObject->DeviceExtension;PSPY_STREAM_CONTEXTpContext;PFSRTL_PER_STREAM_CONTEXTctxCtrl;NTSTATUSstatus;BOOLEANmakeTemporary=FALSE;ASSERT(IS_FILESPY_DEVICE_OBJECT(DeviceObject));INC_STATS(TotalContextSearches);////检查all-contexts-temporary状态有没有被设置.
如果没有则做常规的搜索//if(devExt->AllContextsTemporary!
=0){////准备将此上下文标记为临时的//makeTemporary=TRUE;}else{//NOT-TEMPORARY//设法定位这个上下文结构.
申请列表锁会保证线程同步//SpyAcquireContextLockShared(devExt);ctxCtrl=FsRtlLookupPerStreamContext(FsRtlGetPerStreamContextPointer(FileObject),devExt,NULL);if(NULL!
=ctxCtrl){////一个上下文已经被绑定到给定流上//pContext=CONTAINING_RECORD(ctxCtrl,SPY_STREAM_CONTEXT,ContextCtrl);ASSERT(pContext->Stream==FsRtlGetPerStreamContextPointer(FileObject));ASSERT(FlagOn(pContext->Flags,CTXFL_InExtensionList));ASSERT(!
FlagOn(pContext->Flags,CTXFL_Temporary));ASSERT(pContext->UseCount>0);////检查我们能不能用它(当文件被重命名时这个事情会发生//if(FlagOn(pContext->Flags,CTXFL_DoNotUse)){////我们不能使用这个上下文,解锁并设置下标记表明我们将会创建一个临时的上下文//SpyReleaseContextLock(devExt);makeTemporary=TRUE;}else{////Wewantthiscontextsobumptheusecountandrelease//thelock//InterlockedIncrement(&pContext->UseCount);SpyReleaseContextLock(devExt);INC_STATS(TotalContextFound);SPY_LOG_PRINT(SPYDEBUG_TRACE_CONTEXT_OPS,("FileSpy!
SpyGetContext:Found:p)Fl=%02xUse=%d\"%wZ\"\n",pContext,pContext->Flags,pContext->UseCount,&pContext->Name));////Returnthefoundcontext//*pRetContext=pContext;returnSTATUS_SUCCESS;}}else{////Wedidn'tfindacontext,releasethelock//SpyReleaseContextLock(devExt);}}////不论什么原因,这里我们没有找出一个上下文//检查此文件支不支持上下文.
注意NTFS目前不支持分页文件的上下文//if(!
FsRtlSupportsPerStreamContexts(FileObject)){INC_STATS(TotalContextsNotSupported);*pRetContext=NULL;returnSTATUS_NOT_SUPPORTED;}////如果程序走到这一步,就需要创建一个上下文了//status=SpyCreateContext(DeviceObject,FileObject,LookupFlags,&pContext);if(!
NT_SUCCESS(status)){*pRetContext=NULL;returnstatus;}////如果必要的话标记上下文为临时的//if(makeTemporary){SetFlag(pContext->Flags,CTXFL_Temporary);INC_STATS(TotalContextTemporary);SPY_LOG_PRINT(SPYDEBUG_TRACE_CONTEXT_OPS,("FileSpy!
SpyGetContext:RenAllTmp:(%p)Fl=%02xUse=%d\"%wZ\"\n",pContext,pContext->Flags,pContext->UseCount,&pContext->Name));}else{////把上下文插入到连接表上.
注意连接例程会检查这个入口是否已经被添加//到列表中(当我们正在构建它的时候可能会发生).
如果是这样它就会//release我们创建的那个,并使用它在表中找到的那个//连接例程完全能处理临时上下文//SpyLinkContext(DeviceObject,FileObject,&pContext);}SPY_LOG_PRINT(SPYDEBUG_TRACE_CONTEXT_OPS,("FileSpy!
SrGetContext:Created%s(%p)Fl=%02xUse=%d\"%wZ\"\n",(FlagOn(pContext->Flags,CTXFL_Temporary)"Tmp:pContext,pContext->Flags,pContext->UseCount,&pContext->Name));////Returnthecontext//ASSERT(pContext->UseCount>0);*pRetContext=pContext;returnSTATUS_SUCCESS;}PSPY_STREAM_CONTEXTSpyFindExistingContext(INPDEVICE_OBJECTDeviceObject,INPFILE_OBJECTFileObject)/*++检查给定流是否已经存在一个上下文.
如果是这样则增加引用计数并返回该上下文.
如果没有,则返回NULL.
--*/{PFILESPY_DEVICE_EXTENSIONdevExt=DeviceObject->DeviceExtension;PSPY_STREAM_CONTEXTpContext;PFSRTL_PER_STREAM_CONTEXTctxCtrl;PAGED_CODE();ASSERT(IS_FILESPY_DEVICE_OBJECT(DeviceObject));INC_STATS(TotalContextSearches);SpyAcquireContextLockShared(devExt);ctxCtrl=FsRtlLookupPerStreamContext(FsRtlGetPerStreamContextPointer(FileObject),devExt,NULL);if(NULL!
=ctxCtrl){////找到了,则增加使用计数.
//pContext=CONTAINING_RECORD(ctxCtrl,SPY_STREAM_CONTEXT,ContextCtrl);ASSERT(pContext->Stream==FsRtlGetPerStreamContextPointer(FileObject));ASSERT(pContext->UseCount>0);InterlockedIncrement(&pContext->UseCount);////Release列表锁//SpyReleaseContextLock(devExt);INC_STATS(TotalContextFound);SPY_LOG_PRINT(SPYDEBUG_TRACE_CONTEXT_OPS,("FileSpy!
SpyFindExistingContext:Found:p)Fl=%02xUse=%d\"%wZ\"\n",pContext,pContext->Flags,pContext->UseCount,&pContext->Name));}else{////当我们创建新的上下文时Release列表锁//SpyReleaseContextLock(devExt);pContext=NULL;}returnpContext;}VOIDSpyReleaseContext(INPSPY_STREAM_CONTEXTpContext)/*++减少给定上下文的使用计数.
如果它变为零,则free它.
--*/{PAGED_CODE();SPY_LOG_PRINT(SPYDEBUG_TRACE_DETAILED_CONTEXT_OPS,("FileSpy!
SpyReleaseContext:Release(%p)Fl=%02xUse=%d\"%wZ\"\n",pContext,pContext->Flags,pContext->UseCount,&pContext->Name));////减少使用计数,如果为零则free上下文//ASSERT(pContext->UseCount>0);if(InterlockedDecrement(&pContext->UseCount)Flags,CTXFL_InExtensionList));////Freethememory//SPY_LOG_PRINT(SPYDEBUG_TRACE_CONTEXT_OPS,("FileSpy!
SpyReleaseContext:Freeing(%p)Fl=%02xUse=%d\"%wZ\"\n",pContext,pContext->Flags,pContext->UseCount,&pContext->Name));INC_STATS(TotalContextDeferredFrees);SpyFreeContext(pContext);}}剩下的例程目的相对单一,比较易懂,就不再罗列了.
总结:新手在看这些例程的时候可能有些迷惑,为什么我要费劲地用哈希或者上下文来保存一些数据,我直接来一个操作就判断一次不就行了不行.
你可以那样尝试,但必定会跟丢很多你应该跟踪的操作.
还是那句话,只有create中许多重要的信息才被操作系统保证是有效的.
流上下文技术看起来和哈希区别不大,主要操作基本上也是创建、初始化、插入、查找、删除等.
所以比照哈希来看更容易理解.
在legacy模型下,基本的跟踪技巧就是这么多了.
Create中遇到某个目标时,你可以判断是不是已经在你的列表里了,没有那是不是你关注的目标是就插入到列表中开始跟踪,不是就放过去.
假如已经有了的话,那就说明你已经在跟踪这个目标了.
然后读写等操作的时候你就可以通过在表中查找来判断是不是你的跟踪对象了.
当然,保存的信息可多可少,保存哪些才不会浪费内存,都需要根据你的实际情况对数据结构进行设计.
问15:FspyLib.
c(2009-04-28)这个源文件中的例程有很多重复了sfilter中的,可能稍有不同,但已经较容易理解了,这部分就略过.
这个源文件中的许多例程都是"自定义"的,用于支持程序中的某系操作的,类似于库函数.
先找个简单的说吧.
BOOLEANSpyFindSubString(INPUNICODE_STRINGString,INPUNICODE_STRINGSubString)/*++这个例程检查SubString是不是String的一个子串--*/{ULONGindex;////首先,检查这俩字符串是否相等//if(RtlEqualUnicodeString(String,SubString,TRUE)){returnTRUE;}////如果不等,则检查SubString在不在String中//for(index=0;index+SubString->LengthLength;index++){if(_wcsnicmp(&String->Buffer[index],SubString->Buffer,SubString->Length)==0){////是子串,则返回TRUE//returnTRUE;}}returnFALSE;}相当简单吧你可以参照它提供自己的支持例程,甚至形成自己的支持例程库.
必要时,这些支持例程可以直接移植到可以兼容它们的驱动项目中.
本章主要讲找出文件名的例程,因为找出文件名会成为很多新手们的"一道坎",在掌握它的过程中,你也会掌握很多驱动编程的技巧和领会一些限制,比如IRQL的限制等等.
BOOLEANSpyGetFullPathName(INPFILE_OBJECTFileObject,INOUTPUNICODE_STRINGFileName,INPFILESPY_DEVICE_EXTENSIONdevExt,INNAME_LOOKUP_FLAGSLookupFlags)/*++这个例程获取FileObject的全路径名.
注意包含路径名各部分的buffer可能会保存在分页池中,因此在DISPATCH_LEVEL时我们不能查找名.
查找名的方法基于LookupFlags可以有以下几种:1.
FlagOn(FileObject->Flags,FO_VOLUME_OPEN)或(FileObject->FileName.
Length==0).
这是一次卷打开,就用devExt中的DeviceName就可以了2.
NAMELOOKUPFL_IN_CREATE和NAMELOOKUPFL_OPEN_BY_ID被设置.
这是通过文件id打开的,如果有足够的空间则以FileName的格式排列文件id.
3.
NAMELOOKUPFL_IN_CREATE被设置且FileObject->RelatedFileObject!
=NULL.
这是一次相对打开,因此全路径名的构建必须结合FileObject->RelatedFileObject和FileObject->FileName.
4.
NAMELOOKUPFL_IN_CREATE被设置且FileObject->RelatedFileObject==NULL.
这是一次绝对打开,因此全路径名就在FileObject->FileName中5.
没有设置任何LookupFlags这是在CREATE之后的查找.
FileObject->FileName不一定有效,所以得用ObQueryNameString来获得全路径名--*/{NTSTATUSstatus;ULONGi;BOOLEANretValue=TRUE;UCHARbuffer[sizeof(FILE_NAME_INFORMATION)+MAX_NAME_SPACE];////复制用户给这个设备取的名.
这些名对用户可能很重要.
注意网络文件系统//在内部显示的是连接名.
如果是直接打开网络文件系统设备,我们会把设备//名返回给用户//if(FILE_DEVICE_NETWORK_FILE_SYSTEM!
=devExt->ThisDeviceObject->DeviceType){RtlCopyUnicodeString(FileName,&devExt->UserNames);}elseif(FlagOn(FileObject->Flags,FO_DIRECT_DEVICE_OPEN)){ASSERT(devExt->ThisDeviceObject->DeviceType==FILE_DEVICE_NETWORK_FILE_SYSTEM);RtlCopyUnicodeString(FileName,&devExt->DeviceName);////到此为止,在这种情况下,没有更多的处理了//returnTRUE;}////检查我们是否能请求名//if(FlagOn(LookupFlags,NLFL_ONLY_CHECK_CACHE)){RtlAppendUnicodeToString(FileName,L"[-=NotInCache=-]");returnFALSE;}////在DPC级我们无法获得名//if(KeGetCurrentIrql()>APC_LEVEL){RtlAppendUnicodeToString(FileName,L"[-=AtDPCLevel=-]");returnFALSE;}////假如这是一个ToplevelIrp则代表当前操作为嵌套的,且可能会有一些其他的锁仍在起作用//这时获得名无法排除死锁的可能//if(IoGetTopLevelIrp()!
=NULL){RtlAppendUnicodeToString(FileName,L"[-=NestedOperation=-]");returnFALSE;}////CASE1:FileObject表示一次卷打开.
标记会被设置或者还没有指定文件名//if(FlagOn(FileObject->Flags,FO_VOLUME_OPEN)||(FlagOn(LookupFlags,NLFL_IN_CREATE)&&(FileObject->FileName.
Length==0)&&(FileObject->RelatedFileObject==NULL))){////我们已经复制了卷名,因此返回就行了//}////CASE2:用ID打开文件//elseif(FlagOn(LookupFlags,NLFL_IN_CREATE)&&FlagOn(LookupFlags,NLFL_OPEN_BY_ID)){#defineOBJECT_ID_KEY_LENGTH16UNICODE_STRINGfileIdName;RtlInitEmptyUnicodeString(&fileIdName,(PWSTR)buffer,sizeof(buffer));if(FileObject->FileName.
Length==sizeof(LONGLONG)){////用FILEID打开,产生一个名//swprintf(fileIdName.
Buffer,L"",*((PLONGLONG)FileObject->FileName.
Buffer));}elseif((FileObject->FileName.
Length==OBJECT_ID_KEY_LENGTH)||(FileObject->FileName.
Length==OBJECT_ID_KEY_LENGTH+sizeof(WCHAR))){PUCHARidBuffer;////用ObjectID打开的,产生一个名//idBuffer=(PUCHAR)&FileObject->FileName.
Buffer[0];if(FileObject->FileName.
Length!
=OBJECT_ID_KEY_LENGTH){////跳过buffer起始位置的win32反斜线//idBuffer=(PUCHAR)&FileObject->FileName.
Buffer[1];}swprintf(fileIdName.
Buffer,L"",*(PULONG)&idBuffer[0],*(PUSHORT)&idBuffer[0+4],*(PUSHORT)&idBuffer[0+4+2],*(PUSHORT)&idBuffer[0+4+2+2],*(PUSHORT)&idBuffer[0+4+2+2+2],*(PULONG)&idBuffer[0+4+2+2+2+2]);}else{////未知的ID格式//swprintf(fileIdName.
Buffer,L"[-=UnknownID(Len=%u)=-]",FileObject->FileName.
Length);}fileIdName.
Length=wcslen(fileIdName.
Buffer)*sizeof(WCHAR);////把fileIdName附加到FileName后//RtlAppendUnicodeStringToString(FileName,&fileIdName);////不缓存ID名//retValue=FALSE;}////CASE3:正在打开有RelatedFileObject的文件.
//elseif(FlagOn(LookupFlags,NLFL_IN_CREATE)&&(NULL!
=FileObject->RelatedFileObject)){////一定是相对打开.
用ObQueryNameString来获得相对FileObject的名//然后附加这个fileObject的名////Note://FileObject和FileObject->RelatedFileObject中的名是可访问的.
但更进一步的相对//fileobject链(即FileObject->RelatedFileObject->RelatedFileObject)可能不可访问//这就是我们用ObQueryNameString来获得RelatedFileObject的名的原因.
//PFILE_NAME_INFORMATIONrelativeNameInfo=(PFILE_NAME_INFORMATION)buffer;(//SpyQueryFileSystemForFileName例程稍后介绍status=SpyQueryFileSystemForFileName(FileObject->RelatedFileObject,devExt->AttachedToDeviceObject,sizeof(buffer),relativeNameInfo,&returnLength);if(NT_SUCCESS(status)&&((FileName->Length+relativeNameInfo->FileNameLength+FileObject->FileName.
Length+sizeof(L'\\'))MaximumLength)){////我们能获得relativefileobject的名且在FileNamebuffer中有足够的空间//那么就以下面的格式构建文件名:volumeName]\[relativeFileObjectName]\[FileObjectName]//VolumeName已经在FileName中了//RtlCopyMemory(&FileName->Buffer[FileName->Length/sizeof(WCHAR)],relativeNameInfo->FileName,relativeNameInfo->FileNameLength);FileName->Length+=(USHORT)relativeNameInfo->FileNameLength;}elseif((FileName->Length+FileObject->FileName.
Length+sizeof(L"FileName->MaximumLength){////不管是查询relativefileObject名失败还是没有足够的空间用于//relativeFileObject名,我们都有足够的空间用于FileName中的fileObjectName]"//status=RtlAppendUnicodeToString(FileName,L"ASSERT(status==STATUS_SUCCESS);}////如果relatedfileobject字符串后没有反斜线或者fileobject字符串前面没有//反斜线,那就添加一个//if(((FileName->LengthBuffer[(FileName->Length/sizeof(WCHAR))-1]!
=L'((FileObject->FileName.
LengthFileName.
Buffer[0]!
=L'\\'))){RtlAppendUnicodeToString(FileName,L"\\");}////此时,把FileObject->FileName复制给FileNameunicodestring.
//RtlAppendUnicodeStringToString(FileName,&FileObject->FileName);}////CASE4:用绝对路径打开某个文件//elseif(FlagOn(LookupFlags,NLFL_IN_CREATE)&&(FileObject->RelatedFileObject==NULL)){////这里是绝对路径,直接复制给FileName.
//RtlAppendUnicodeStringToString(FileName,&FileObject->FileName);}////CASE5:我们是在CREATE操作之后获取filename//elseif(!
FlagOn(LookupFlags,NLFL_IN_CREATE)){PFILE_NAME_INFORMATIONnameInfo=(PFILE_NAME_INFORMATION)buffer;ULONGreturnLength;status=SpyQueryFileSystemForFileName(FileObject,devExt->AttachedToDeviceObject,sizeof(buffer),nameInfo,&returnLength);if(NT_SUCCESS(status)){if((FileName->Length+nameInfo->FileNameLength)MaximumLength){////有足够的空间用于filename,把它复制给FileName.
//RtlCopyMemory(&FileName->Buffer[FileName->Length/sizeof(WCHAR)],nameInfo->FileName,nameInfo->FileNameLength);FileName->Length+=(USHORT)nameInfo->FileNameLength;}else{////没有足够的空间用于filename,那就把EXCEED_NAME_BUFFER错误消息复制过去//RtlAppendUnicodeToString(FileName,L"[-=NameToLarge=-]");}}else{////从底层的文件系统获取文件名时发生错误,把错误消息复制给FileName.
//swprintf((PWCHAR)buffer,L"[-=Error0x%xGettingName=-]",status);RtlAppendUnicodeToString(FileName,(PWCHAR)buffer);////不缓存错误的名//retValue=FALSE;}}////程序走到这里,表示我们已经得到了一个有效名//有时当我们查询到一个名时它结尾处会有一个反斜线,而有时又没有//为确保上下文是正确的,如果某个反斜线前面是":"则移除它//if((FileName->Length>=(2*sizeof(WCHAR)))&&(FileName->Buffer[(FileName->Length/sizeof(WCHAR))-1]==L'\\')&&(FileName->Buffer[(FileName->Length/sizeof(WCHAR))-2]!
=L':')){FileName->Length-=sizeof(WCHAR);}////检查我们是不是正打开着目标目录.
如果是那就移除尾部的名和反斜线//注意我们不会移除开始的反斜线(就在首字母之后).
//if(FlagOn(LookupFlags,NLFL_OPEN_TARGET_DIR)&&(FileName->Length>0)){i=(FileName->Length/sizeof(WCHAR))-1;////检查路径结尾是否有反斜线,如果有则跳过//(因为文件系统跳过了).
//if((i>0)&&(FileName->Buffer[i]==L'\\')&&(FileName->Buffer[i-1]!
=L':')){i--;}////向后扫面最后一部分//for(;i>0;i--){if(FileName->Buffer[i]==L'\\'){if((i>0)&&(FileName->Buffer[i-1]==L':')){i++;}FileName->Length=(USHORT)(i*sizeof(WCHAR));break;}}}returnretValue;}上述例程步骤清晰,且注释相当详细.
你可以参考filemon的源码,两者的获取全路径名的例程有许多相似之处.
不过要完全理解还是不容易,假如你会使用windbg和Vmware了的话,建议多调试几次这个例程,单步执行下去,可能更容易理解.
不会赶紧抽时间学吧,网上关于这块有现成的教程,不过windbg的妙用只可意会无法言传.
我最初看到下面这两个例程的时候喜悦的心情难以言表.
当我阅读了许多资料之后,一个重要的技巧——创建自己的IRP并下发,始终没有真实而完整的例子.
虽然"Rollingyourown"(网上有译文)讲解地比较详细,但依然无法令读者迅速而直观地体验到用法(驱动开发公开的东西大多都是这样,不公开的那就更让人郁闷).
当然,你必须了解分发例程和完成例程的调用过程,否则重入等问题会很困扰你.
先看完成IRP的完成例程.
NTSTATUSSpyQueryCompletion(INPDEVICE_OBJECTDeviceObject,INPIRPIrp,INPKEVENTSynchronizingEvent)/*++只要查询请求被文件系统完成了,这个例程就会被调用来处理必要的清除工作.
--*/{UNREFERENCED_PARAMETER(DeviceObject);////确保IRP的状态被复制给用户的IO_STATUS_BLOCK了,即保证这个IRP的发起者会知道这个操作//的最终状态//ASSERT(NULL!
=Irp->UserIosb);*Irp->UserIosb=Irp->IoStatus;////给SynchronizingEvent发信号,这样IRP的发起者就知道这个操作已经完成了//KeSetEvent(SynchronizingEvent,IO_NO_INCREMENT,FALSE);////到此,我们已经搞定了.
现在开始清除我们分配的IRP//IoFreeIrp(Irp);////在这里,如果我们返回STATUS_SUCCESS,I/O管理器会执行必要的清除工作//这包括://*如果这是一个bufferedI/O操作就把数据从系统buffer复制到用户的buffer中.
//*freeIRP中的所有MDL.
//*复制Irp->IoStatus给Irp->UserIosb以便irp的发起者可以看到操作的最终状态//*如果这是一个异步请求或这是一个被pend的同步请求,I/O管理器会发信号给Irp->UserEvent,//否则它就会发信号给FileObject->Event.
//(这意味着如果IRP发起者没有Irp->UserEvent且没有等待FileObject->Event情况就会很糟糕//你不能牵强地认为在系统中有谁在等待FileObject->Event或者I/O管理器给这个事件//后谁知道谁应该被唤醒////因为以上操作中有些操作需要发起线程的上下文(例如,I/O管理器在复制时需要UserBuffer地址//是有效的),I/O管理器会把这些工作入列到IRP的发起线程的某个APC中////因为这个IRP是FileSpy分配并初始化的,所以我们知道需要做哪些清除工作//在这种特殊情况下,我们也可以比I/O管理器更加高效地完成清除工作//因此,在这里最好由我们来执行清除工作,然后free这个IRP而不是将控制权交给I/O管理器////返回STATUS_MORE_PROCESS_REQUIRED,I/O管理器就会停止对这个IRP的处理,直到它被告知//要重新启动对这个IRP的处理(调用IoCompleteRequest)//因为I/O管理器已经执行了所有我们让它做的事情,所以由我们做清除工作,然后返回//STATUS_MORE_PROCESSING_REQUIRED,要I/O管理器恢复对这个IRP的处理得调IoCompleteRequest.
//returnSTATUS_MORE_PROCESSING_REQUIRED;}完成例程没多少代码,但里面包含的思想很多,包括为什么由我们完成清除工作,怎么让I/O管理器替我们"打工"等.
不要小看了这些思考,驱动主要就凭着这些思考进行开发,或许今天你写了100行代码还没有实现自己要做的事情,但想清楚这个情景之后可能不到20行代码就搞定了.
NTSTATUSSpyQueryFileSystemForFileName(INPFILE_OBJECTFileObject,INPDEVICE_OBJECTNextDeviceObject,INULONGFileNameInfoLength,OUTPFILE_NAME_INFORMATIONFileNameInfo,OUTPULONGReturnedLength)/*++这个例程弄了个IRP向底层的文件系统查询FileObject参数的名Note:在这里ObQueryNameString不能用.
因为它会引起递归的查询操作--*/{PIRPirp;PIO_STACK_LOCATIONirpSp;KEVENTevent;IO_STATUS_BLOCKioStatus;NTSTATUSstatus;PAGED_CODE();irp=IoAllocateIrp(NextDeviceObject->StackSize,FALSE);//分配IRP的例程有多个,还没有看过Rollingyourown先看看吧.
if(irp==NULL){returnSTATUS_INSUFFICIENT_RESOURCES;}////把当前线程设置为这个IRP的线程,这样如果I/O管理器需要回到发起这个IRP的线程上下文时//它就知道应该回到哪个线程了.
看到这,你也知道IRP中的这个域是干嘛的了.
//irp->Tail.
Overlay.
Thread=PsGetCurrentThread();////设置这个IRP是从内核发起的.
这样I/O管理器就知道它里面的buffer不需要探测了.
为什么buffer//还得探测用户模式下的buffer地址不一定有效,探测是为了保证地址访问安全.
//irp->RequestorMode=KernelMode;////初始化UserIosb和UserEvent//ioStatus.
Status=STATUS_SUCCESS;ioStatus.
Information=0;irp->UserIosb=&ioStatus;irp->UserEvent=NULL;已经置零////设置IRP_SYNCHRONOUS_API标记来表示这是一个同步I/O请求//irp->Flags=IRP_SYNCHRONOUS_API;irpSp=IoGetNextIrpStackLocation(irp);irpSp->MajorFunction=IRP_MJ_QUERY_INFORMATION;irpSp->FileObject=FileObject;////设置IRP_MJ_QUERY_INFORMATION的参数//用来保存文件名信息的buffer应该放到systemBuffer域//irp->AssociatedIrp.
SystemBuffer=FileNameInfo;irpSp->Parameters.
QueryFile.
Length=FileNameInfoLength;irpSp->Parameters.
QueryFile.
FileInformationClass=FileNameInformation;////设置完成例程,这样当我们发出的IRP请求完成时我们能知道,从而知道何时//free这个IRP//KeInitializeEvent(&event,NotificationEvent,FALSE);IoSetCompletionRoutine(irp,SpyQueryCompletion,&event,TRUE,TRUE,TRUE);status=IoCallDriver(NextDeviceObject,irp);SPY_LOG_PRINT(SPYDEBUG_TRACE_NAME_REQUESTS,("FileSpy!
SpyQueryFileSystemForFileName:Issuednamerequest--IoCallDriverstatus:0x%08x\n",status));if(STATUS_PENDING==status){(VOID)KeWaitForSingleObject(&event,Executive,KernelMode,FALSE,NULL);}ASSERT(KeReadStateEvent(&event)||!
NT_SUCCESS(ioStatus.
Status));SPY_LOG_PRINT(SPYDEBUG_TRACE_NAME_REQUESTS,("FileSpy!
SpyQueryFileSystemForFileName:Finishedwaitingfornamerequesttocomplete.
.
.
\n"));*ReturnedLength=(ULONG)ioStatus.
Information;returnioStatus.
Status;}看完以上两个例程你有什么感觉没错,你可以自己这么"Rolling"IRP来执行各种操作,不过我要提醒你,这个"伟大的举动"背后还有一些限制,比如IRQL,线程的上下文环境等等.
你需要自己去执行尝试、验证、总结的循环.
总结:以上几问包括这一问,内容需要反复咀嚼.
看了不等于知道,知道不等于理解,理解不等于会用.
看别人的结论,十分容易,如同走马观花,一扫而过,但要领会光看还差得远.
你可以把你的读后感和一些突然冒出来的想法甚至是一些想要验证的东西都写下来,把你对这段源码这个例程的流程的理解画成图……好记忆不如烂纸笔.
问16:总结filespy(2009-04-30)filespy.
c不讲是因为除去logrecord和与应用程序交互这两块其他的与sfilter中重复太多没有必要再讲.
Filespy主要实现的是在驱动里截获所有的符合条件的IRP然后通过用户界面的形式展现给用户.
这个工具在测试和分析过程中的作用和filemon一样,但由于其设计意图是针对所有人,因此假如你想打造自己的观测和分析用工具,你就得认真学习和理解filespy源码,并根据自己的实际需求修改它.
与sfilter不同,Filespy实现了更多功能.
通过它,你可以学习自己产生文件名、跟踪某个fcb或者FileObject、自己发IRP并完成这个IRP、与应用程序交互等等技巧.
想彻底弄明白并吸收它,你必须至少花费一个月的时间.
当然,在你学习的过程中,积攒下来的流程图等记录会越来越具体、越来越清晰,哪怕每修改一个注释你都会怀着无比兴奋和骄傲的心情,这就是你学习、提高、成功的一步步见证和在现.
Filespy里没讲的地方不是不重要,相反,对于很多需求来说它们是必须要掌握的.
既然已经展现给你了我的分析和学习源码的方法,就没必要再重复已经做过的事情.
当然我并没有提到数据结构的设计和分析,那是基本技能.
最后说说filespy的优缺点.
主要是其移植性得到了开发群里大家的一致好评.
Sfilter在移植性方面暴露的问题在filespy这基本看不到.
许多选择legacy模型的驱动开发人员也都将sfilter框架上完成的东西移植到filespy上来了.
主要缺点就是对初学者来说这份源码是个不小的挑战,而且介绍文字很少.
问17:legacy驱动透明加解密设计开发示例(2009-04-30)声明:以下设计和开发思路、经验来自于我个人,不是标准或规范.
仅供参考和学习.
概要设计既然是加解密,首先我们的驱动应该有加解密模块.
要对谁加解密,就是定位或者说是判断模块.
需要加密则加密,不需要加密则不加干涉.
一个基本的思路就成型了.
先来看看基本思路示意图.
在底层的文件系统过滤驱动看来,文件内容的数据传输行为基本都归于读写.
这里没有应用编程里的复制、剪切等概念.
首先我要向你"灌输"一些知识,这些都是你应该通过学习理论知识掌握的,而且这只是当前我认为正确的东西.
毕竟微软没有明确给出标准或者规范,这里不能做假设,我无法让你先把某个错误当正确答案,等一段时间后当你对它深信不疑的时候我再告诉你这是错误的.
任何一个操作所对应的IRP都不是单一的.
的确,比如读写就分别对应IRP_MJ_READ和IRP_MJ_WRITE或者相应的fastio,但在你的读写IO发生之前,必然会有IRP_MJ_CREATE,即你得先打开或者创建某个文件你才能对它进行操作.
而每个IRP_MJ_CREATE又分别对应一个IRP_MJ_CLEANUP和一个IRP_MJ_CLOSE.
打开之后你得关闭,系统也需要解除对它的引用.
这里需要特别说明一下,比如你打开或创建文件对应的是IRP_MJ_CREATE,这个好理解,但你关闭文件却不是对应IRP_MJ_CLOSE,它的语义是说对文件的引用解除了.
关闭对应的是IRP_MJ_CLEANUP,即你在桌面上关闭某个文件时I/O管理器会给文件系统驱动发IRP_MJ_CLEANUP.
FileObject代表一次打开,不同线程打开或创建某个文件都会有一个新的FileObject产生,或许你在调试时发现相同地址的FileObject会出现多次,但两个相同地址的FileObject从不会同时出现.
Fcb是文件系统维护的一种数据结构.
在内存中,它是唯一标识某个文件的.
综上,你打开或创建某个文件时,会有多个FileObject,但只会出现一个Fcb(双fcb理论是驱动开发的一个新方向,有完整的测试版本,但目前尚未经过产品验证,有兴趣可以自己探索).
先假设打开或创建某个文件,进行读写等操作,然后关闭它,这个过程中只有一个IRP_MJ_CREATE、一个IRP_MJ_CLEANUP和一个IRP_MJ_CLOSE.
依据上图,我们需要在文件打开也就是截获IRP_MJ_CREATE时开始对这个文件的处理,直到收到这个文件的IRP_MJ_CLEANUP和IRP_MJ_CLOSE时结束处理.
在这中间有对文件的读写就判断是否需要加解密.
而在实际应用中,因为操作系统是多线程环境,再加上异步同步操作的交叉,你所监测到的这些IRP会有许多个.
漏掉其中任何一个都可能会导致你的加解密失败.
如:已加密的区域被后来某个漏掉的写操作覆盖了;文件中部分被加解密了而部分没被加解密等等.
定位机制设计那你如何确定哪些IRP是以你的文件为目标的呢这是定位模块的职责.
有人在fcb和FileObject结构里都找到了Type这个域,但它的区分度太低,假如你要对特定文件类型如文本文件(.
txt)进行加解密,通过它是无法区分出来的.
相信通过一段时间的学习,你会发现许多现有产品都是通过获得文件名,然后判断其文件名后缀的方式来区分的.
前面也讲过怎么获取文件名,这里不再赘述.
不要想在每个IRP分发例程中都做这样的判断,因为许多与之相关的信息都只被保证在create中有效.
所以你得跟踪FileObject.
即在create中把有用信息与FileObject关联起来,在读写或其他操作中检查当前FileObject是否你跟踪的那个就行了.
但只跟踪FileObject是远远不够的(相信你已经在其他资料和网页中读到不同流可能对应同一文件,不同流会有不同的名,只判断FileObject的文件名很可能会漏掉许多本来应该被跟踪的FileObject),因此我们还需要跟踪流对象,即fcb.
跟踪机制设计跟踪机制可以归于定位模块.
整个跟踪机制的示意图如下:由上图看,跟踪IRP时以FileObject为单位,跟踪FileObject时以Fcb为单位,是不是所有对这个文件的IRP都尽在掌握呢跟踪机制是过滤驱动的一个根本功能,据我所知许多拥有3年以上开发经验的"老程序员"还在研究如何改进它们跟踪机制.
目前你需要关注的基本就这些,将来你可能因为某些需求而必须得关注和处理这个文件的父目录的IRP.
这些IRP的处理也比较麻烦.
结合本书前面讲到的哈希和上下文,要构建上述的跟踪机制你的驱动可能需要建立两种链表:Fcb跟踪结构链表.
也是驱动的一个全局链表,其上有几个fcb跟踪结构入口就表示你在跟踪几个fcb即文件.
这些Fcb跟踪结构入口的添加和删除以及fcb跟踪结构的分配和释放都须依照对应的fcb的创建和释放.
链表结点结构如何设计就得参考你的需求了.
FileObject跟踪结构链表.
这个链表应作为对应fcb跟踪结构的一个子成员.
FileObject跟踪结构与FileObject的关系同fcb跟踪结构和fcb的关系.
由于系统运行期间,对文件的操作十分频繁,因此在跟踪过程中会有非常频繁的内存分配和释放行为,推荐使用lookAsideList来进行内存的分配和释放,否则很容易出现"内存碎片"过多而导致系统bugcheck的问题,它的介绍也相当详细,这里不再多讲.
同时用于保存信息的内存类型你也需要仔细研究.
非分页和分页内存的区别还是相当大的.
非分页内存常驻物理内存中,但总大小有限且系统其他部分都在使用,有可能会申请失败.
分页内存随时都有可能被交换出物理内存,这意味着你必须自己采取各种方法来保证你申请的分页内存在被访问时其引用地址总是有效的,且IRQL必须符合要求,其总大小相对来说非常宽裕.
它们的使用同样得自己体会,没有统一的标准.
关于跟踪的设计思路就说到这里,例子参见我开发初期基于sfilter的源码.
不过那些源码中跟踪机制并没有现在这么完整,哈希技巧也被我做了简化修改,仅供参考.
加解密模块设计加解密模块的功能主要分以下两个:判断哪些IRP中的数据应该加解密;怎么加解密.
先说怎么加解密.
也就是用什么算法,数据分不分组等,这些没什么好说的.
但是处理分页文件时要万分小心,分页文件不是常驻内存的,对它的操作有可能引起bugcheck.
这个功能实现的另外一个重点是怎么找到数据.
有直接用buffer存的,用MDL的等等,与数据的三种传输方式(buffered,direct,neither,详细介绍参考WDK和OSR的技术文章等)紧密相关,一般文件I/O都是bufferedI/O和direct这两种方式,即数据要么在或要被放到UserBuffer里,要么由MDL描述.
neitherI/O常见于设备I/O.
网上流传的toolflat的rc4源码关于怎么加解密这块相当经典,很值得学习,而且经验证是比较稳定的.
再说说怎么判断.
这个功能的实现完全取决于你的需求.
Fastio操作的数据都来自缓存,FastIoRead就是从缓存中读数据到用户进程的上下文中,FastIoWrite就是从用户进程的上下文中写数据到缓存.
区分IRP代表哪种类型的操作需要它的flags域.
假如读写带NOCACHE_IO(这里简写了)标记,那操作的目标必定是磁盘上的数据,即读代表从磁盘上的文件中读数据,写代表把数据写到磁盘上的文件中.
分页I/O即读写带PAGING_IO和SYN_PAGING_IO跟磁盘中的交换区或者说是虚拟内存有关,它通常都伴随有NOCACHE_IO标记,带有NOCACHE_IO标记时代表是磁盘和交换区之间的数据交互.
带DEFER_IO_COMPLETION标记的则属于延迟IO,其目标数据在哪未知,这种IRP表示文件系统驱动在执行真实的I/O操作时才会决定是读写磁盘,还是读写缓存或者是交换区.
这里只是说了IRP的flags域,在实际开发中,根据不同的设计目标,用到的域可能或多或少.
另外你监控.
txt文件的时候,在不用其他软件对它进行操作时,除了notepad进程外,Windows自己的system进程和explorer进程也会参与进来,前者主要是执行部分写操作,而后者什么IRP都可能会参与.
用户平时用鼠标进行的复制剪切等操作的主要执行者就是explorer进程.
这对明密文控制目标的实现可能会产生很多麻烦.
其他设计实际开发过程中,加解密就不是字面上加和解这么简单的事情了.
文件系统驱动复杂和需求多样性导致了你的驱动的复杂.
这里举几个例子.
不允许某些进程访问指定的文件.
你可以直接令对应的IRP失败并返回状态值STATUS_ACCESS_DENIED,这样系统就会弹出对话框告知用户该访问被拒绝.
不过需要注意的是仅仅在读写或完成例程中拒绝是不行的,因为数据依然会经系统的ReadAhead机制流出去,所以必须在分发例程中做处理.
加密标记的设计.
这是一个相当重要的功能.
因为如果你不在文件本身上"做个标记",你的驱动根本无法断定当前操作的文件是否经过加密.
假如加密标记放到数据流的中间某个位置,对单个文件来说理论上可行,不过可以想见的是问题会相当复杂.
网上的讨论相信你也"有所耳闻"了,没错,主要分header和tail两种.
两者的优缺点网上都有详细介绍,这里略过.
Header.
既然是header,那就必须添加到文件数据流的最前端.
因此必须在首次写操作之前把它添加到文件中,这里首选是创建文件成功后的create完成例程.
添加之后有个问题,那就是你必须令进程的读写操作的起始位置偏移到header后面相应的位置.
即StartByteOffset(实际操作的是这个结构的子成员)+headersize,且返回值必须与没有header的情况下一致.
所有与FileSize相关的IRP如QueryFileInfo、SetFileInfo等都得做相应处理,令进程相信文件并没有header.
Tail.
如名,必须保证被添加到文件的末端.
这个添加方式比较多,比如你可以保证在最后一次IRP_MJ_CLEANUP中添加,当然这可能不太好判断.
还有一种方式就是IRP_MJ_WRITE写完之后,随即写个tail到后面,当然,过滤驱动需要自己始终维护文件的真实大小,在QueryInfo和SetInfo等请求中做隐藏的时候还要维护ValidDataLength.
假如有读操作的startByteOffset是从FileSize后开始的,那么过滤驱动就得直接令操作返回STATUS_END_OF_FILE,假如读的startByteOffset在FileSize内,但startByteOffset+readLength大于FileSize,则需要"切割"readLength以隐藏tail.
写也需要模拟从FileSize后开始和越过FileSize的情况下文件系统驱动所做的处理.
如下图,关于读取时如何去掉header和tail的处理方式比较统一,即逻辑去而非物理去.
也就是说我只是在过滤驱动中通过维护offset及FileSize等信息达到向访问文件的应用程序"隐藏"header和tail的目的,令应用程序感觉不到文件中还存在这样的数据.
当然设计思路没必要都一样,你可以创建个临时文件,先把header写进去,然后把从实际文件中读到的数据写到这个临时文件中,或者先把数据写进去,再添加个tail.
很多人都提到了清缓.
既然当前没有哪家公司可以做到禁止不同进程在内存中共享同一块数据,那就只能想办法将内存中的这块数据清除.
强行清缓即直接调用CC例程中的CcPurgeCacheSection例程清缓,很容易出现死锁或损坏数据的情况.
你可以通过FILE_NO_INTERMEDIATE_BUFFERING标记的IRP_MJ_CREATE打开文件一下让文件系统驱动"帮"你完成数据回写和清缓.
总结:一个具有基本的稳定加解密能力的驱动应具有稳定的跟踪机制、加解密机制和加密标记.
不过加解密机制和加密标记本身就是在破坏原有数据,稍有不慎,就会导致非常严重的后果.
实际上驱动加解密都要面临这样的风险,即使是投放市场多年的产品也不敢保证自己不会导致数据损坏或丢失.
所以,有些驱动可能会增加如实时备份这样的功能.
驱动开发中,写代码方面几乎没有难度.
主要是与业务的实际结合.
这要求你必须在拿到需求后,对相关问题做大量的分析和验证,否则很难取得突破.
问18:总结legacy驱动(2009-05-04)Legacy驱动模型比较老,但威力也相当强.
它需要重复做许多基础性的工作,比如查找文件名,而这在mini驱动中仅仅是一个简单的函数调用.
学习和使用legacy驱动,必须要熟悉并掌握sfilter和filespy等例子,刚起步时,你对系统内部各种机制完全不了解或不完全了解,这时写的代码可以通过调试达到不出错的水平,但能不能起实际作用很难保证.
这也是很多人停滞不前的主要原因,NTFSI这本书的其中一个重要价值就在于它详细地介绍了系统各个组件及一些过程和数据结构.
在熟读这本书的基础上,你会少走许多弯路.
另外,欲速则不达.
想现在动手写个简单的驱动很容易,把sfilter例子里面的SfCreate里面的代码都删掉,用DbgPrint或其他打印函数打印些消息,然后用DbgView工具看看.
慢慢地根据自己的需要把相关的分发例程添加上并写入自己的实现,驱动就会慢慢成形.
不过,一般来说,加解密这样的功能不全部完成,你很难看到效果.
另外,legacy驱动比较接近真实的文件系统驱动,通过它你能对文件系统和系统其他各个组件间的交互等有更加深刻和清楚的认识,有一定的基础之后阅读fastfat源码或者其他文件系统驱动甚至操作系统源码相对来说会更加容易一些.
Mini驱动阶段首先关于mini驱动中基础性问题,请参考WDKchs目录下的我翻译的《文件系统minifilter驱动》一书.
大部分内容可以参考,关于mini驱动中嵌入legacy驱动的内容因水平有限不能保证其正确性.
Mini驱动是微软大力推广的一种驱动模型,它在某种程度上封装了legacy驱动,并提出了很多很好的概念模型.
以下分例子先带你进入mini驱动的世界.
然后结合legacy中的一些相关问题进行深入讨论.
问19:passThrough(2009-05-04)在讲legacy驱动的时候我们先讲了最简单的SfPassThrough例程,mini驱动里面passThrough不是最简单的,但很有代表性.
我们从这个例子入手来了解下mini驱动.
同样,声明和部分全局定义不讲.
先从DriverEntry例程入手.
它的作用同legacy驱动中的DriverEntry例程,完成初始化工作.
通过《文件系统minifilter驱动》你大致了解了mini驱动的一些知识.
status=FltRegisterFilter(DriverObject,&FilterRegistration,&gFilterHandle);这个例程从字面意思理解是注册过滤器,看看WDK的介绍,和参数FilterRegistration的定义,基本就是集成了legacy中设置分发例程等那些操作,只不过这里你只需注册你要过滤的IRP类型,而不需要为每种IRP和fastio等都设置相应的分发例程和fastio例程.
这使得这个例程看起来相当简洁.
PtUnload例程简单易懂.
如你所知,mini驱动中的pre和post回调例程分别对应legacy驱动中的分发例程和完成例程.
不过有一点还要强调一下,pre和post收到的入参是一样的,即使你在pre中修改了入参的值,post收到的入参值也会和修改之前pre收到的入参值一样.
不过,你可以通过CompletionContext域在pre和post中传递信息.
这个例子本身没什么值得注意的地方,主要就是检查是不是我们要找的操作,是则打印相关信息.
总结:这个驱动代码不足千行,非常简单易懂.
有了legacy驱动的基础学习它没有任何难度,自己对照着sfilter再扫一遍代码吧.
其pre和post入参值比较规范,IRP和I/O栈位置信息都集成到了Data中,而对象的信息都集成到了FltObjects中.
通过它基本了解了mini驱动的架构,是不是比legacy驱动的架构要清楚和简洁了许多呢下面开始是需要详细讲解的第一个例子ctx.
问20:ctx(2009-05-04)通过这段时间的学习,你应该认识到光避开困难或难理解的部分是不明智的,最终你还是得回来一个一个弄清楚.
这个例子可以说相当不简单.
在filespy中讲到的上下文技术跟这个例子介绍的主要技术有相关之处.
首先来看ctxstruc.
h和ctxproc.
h两个头文件.
结构体_CTX_GLOBAL_DATA和legacy里面的设备扩展比较类似.
第一种上下文是实例上下文.
当然,以下这些上下文只是个示范,你可以在你的驱动中重新定义这些结构.
typedefstruct_CTX_INSTANCE_CONTEXT{PFLT_INSTANCEInstance;//这个上下文对应的实例PFLT_VOLUMEVolume;//关联这个实例的卷UNICODE_STRINGVolumeName;//关联这个实例的卷的名}CTX_INSTANCE_CONTEXT,*PCTX_INSTANCE_CONTEXT;下面是文件上下文,不过仅vista和vista以后的操作系统支持.
typedefstruct_CTX_FILE_CONTEXT{UNICODE_STRINGFileName;//关联这个上下文的文件名////因为这个上下文的FileName永远不会被修改,所以不需要同步保护//文件名在这个上下文被创建时填入,清除时被清除.
//}CTX_FILE_CONTEXT,*PCTX_FILE_CONTEXT;下面是流上下文,对应fcb.
typedefstruct_CTX_STREAM_CONTEXT{UNICODE_STRINGFileName;//关联这个上下文的名ULONGCreateCount;//你在这个流上看到的create的次数ULONGCleanupCount;//你在这个流上看到的cleanup的次数ULONGCloseCount;//你在这个流上看到的close的次数PERESOURCEResource;//用于同步保护}CTX_STREAM_CONTEXT,*PCTX_STREAM_CONTEXT;最后是流句柄上下文,对应FileObjecttypedefstruct_CTX_STREAMHANDLE_CONTEXT{UNICODE_STRINGFileName;//关联这个上下文的名PERESOURCEResource;//用于同步保护}CTX_STREAMHANDLE_CONTEXT,*PCTX_STREAMHANDLE_CONTEXT;由上看来,各个上下文分别对应不同的数据结构.
与legacy中跟踪结点数据结构意义相同.
例程声明里,ctx给出了几个自定义的支持例程,暂时不必深究,用时你自然会知道它们是做什么用的.
Ctxinit.
c里面的例程不是本问的重点.
CtxContextCleanup重要,不过要留到后面讲.
重点一:support.
c通过这段时间的学习,相信对UNICODE_STRING已经不再陌生了.
不过如下这样提供自己的支持例程既能提高代码复用率,又能提高回调例程的可阅读性.
NTSTATUSCtxAllocateUnicodeString(__inoutPUNICODE_STRINGString)/*++分配一个UNICODE_STRING--*/VOIDCtxFreeUnicodeString(__inoutPUNICODE_STRINGString)/*++释放一个UNICODE_STRING--*/重点二:context.
c以下这些例程都是与上下文有关的,可以比照legacy里面的相关内容来看.
Legacy中上下文的维护几乎完全是程序员自己维护,这里fltmgr做了许多工作.
CtxFindOrCreateFileContext此例程查找目标文件的流上下文.
可选的,如果上下文不存在,则创建一个新的并绑定新上下文到文件上.
CtxCreateFileContext此例程创建目标文件的文件上下文.
CtxFindOrCreateStreamContext此例程查找目标流的流上下文.
可选的,如果上下文不存在,则创建一个新的并绑定新上下文到流上.
CtxCreateStreamContext此例程创建目标流的流上下文.
CtxUpdateNameInStreamContext此例程更新流上下文中的文件名.
NTSTATUSCtxUpdateNameInStreamContext(__inPUNICODE_STRINGDirectoryName,__inoutPCTX_STREAM_CONTEXTStreamContext){NTSTATUSstatus;PAGED_CODE();////释放现有名//if(StreamContext->FileName.
Buffer!
=NULL){CtxFreeUnicodeString(&StreamContext->FileName);}////分配并复制目录名(一定要注意在更新名时,设置好长度分配足够的空间)//StreamContext->FileName.
MaximumLength=DirectoryName->Length;status=CtxAllocateUnicodeString(&StreamContext->FileName);if(NT_SUCCESS(status)){RtlCopyUnicodeString(&StreamContext->FileName,DirectoryName);}returnstatus;}CtxCreateOrReplaceStreamHandleContext此例程查找目标流句柄的流句柄上下文.
可选的,如果上下文已存在,则创建一个新的并绑定新上下文到流句柄上来取代旧的.
CtxCreateStreamHandleContext此例程创建目标流句柄的流句柄上下文.
CtxUpdateNameInStreamHandleContext此例程更新流句柄上下文中的文件名.
以上这些例程都有许多相似的地方,对上下文的操作都由flt系列的例程完成,它们的说明在WDK中很详细,这里我就不再多说.
再来说这个清除例程.
VOIDCtxContextCleanup(__inPFLT_CONTEXTContext,__inFLT_CONTEXT_TYPEContextType){PCTX_INSTANCE_CONTEXTinstanceContext;PCTX_FILE_CONTEXTfileContext;PCTX_STREAM_CONTEXTstreamContext;PCTX_STREAMHANDLE_CONTEXTstreamHandleContext;PAGED_CODE();switch(ContextType){caseFLT_INSTANCE_CONTEXT:instanceContext=(PCTX_INSTANCE_CONTEXT)Context;DebugTrace(DEBUG_TRACE_INSTANCE_CONTEXT_OPERATIONS,("[Ctx]:Cleaningupinstancecontextforvolume%wZ(Context=%p)\n",&instanceContext->VolumeName,Context));////在这里,过滤驱动应该释放被分配给实例上下文内存或同步对象//实例上下文自己不能被释放.
它的释放工作由fltmgr完成//CtxFreeUnicodeString(&instanceContext->VolumeName);DebugTrace(DEBUG_TRACE_INSTANCE_CONTEXT_OPERATIONS,("[Ctx]:Instancecontextcleanupcomplete.
\n"));break;……}你没必要自己做清除工作,在mini驱动中,你要做的只是注册某个上下文类型,创建、设置、查找、修改它,你所调用的例程名义上有释放但实际上只是减少它的引用计数.
即,当你使用它时,比如查找和创建都会令它的引用计数增加1,当你"释放"它时,只是它的引用计数会减少1,当它的引用计数减为零时,fltmgr会删除相应的上下文.
当然,上下文不仅仅只有以上这些,除了fltmgr支持的上下文类型之外,驱动还可以像legacy中一样自己维护自定义的跟踪结构.
总结:这个例子的主要任务就是向用户示范怎么定义和使用各种上下文.
由于上下文支持方面许多"麻烦"的重复性工作已经由fltmgr完成了,所以本例中对fltmgr提供的上下文支持例程再做一层封装,就使得上下文操作十分简洁易懂,可移植性大大提高.
这个例子是不是和filespy里面的用于跟踪的哈希和上下文技术十分相似呢或许根本就是相同的实现不同的表象而已.
问21:scanner(2009-05-05)这个例子也相当经典,我第一次看到它的时候马上就联想到反病毒.
到现在,大家在学习和实际开发过程中,一定有用filespy或filemon偶尔或经常观察到一些杀毒软件的行为.
我的开发机用的是卡巴斯基,以它为例(它的详细分析超出本书的范围).
每当你操作某个文件时,它就会对目标文件发出延迟读请求,实际上它就是在扫描文件中有无特征码.
本例虽没有杀毒软件产品那么"有内涵",但已经是个较为完整的雏形了.
不过因为之前的工作偏重加解密,所以这个例子的精华部分有待读者自行研究.
我只提供开胃菜.
先来看看scanner.
h.
因为此例要与用户进程通信或者说是交互,所以它的全局数据结构里多出了与用户进程通信的部分.
typedefstruct_SCANNER_DATA{PDRIVER_OBJECTDriverObject;//表示这个驱动的对象PFLT_FILTERFilter;//过滤器句柄,调用FltRegisterFilter的结果PFLT_PORTServerPort;//监听引入的连接PEPROCESSUserProcess;//连接到端口的用户进程PFLT_PORTClientPort;//用于连接到用户模式的客户端端口}SCANNER_DATA,*PSCANNER_DATA;在legacy中我们讲了,能跟踪,还得判断需要跟踪哪些文件.
现在来看看scanner.
c.
以下数据结构和例程用于判断后缀名.
constUNICODE_STRINGScannerExtensionsToScan[]={RTL_CONSTANT_STRING(L"doc"),RTL_CONSTANT_STRING(L"txt"),RTL_CONSTANT_STRING(L"bat"),RTL_CONSTANT_STRING(L"cmd"),RTL_CONSTANT_STRING(L"inf"),/*RTL_CONSTANT_STRING(L"ini"),Removed,tomuchusage*/{0,0,NULL}};BOOLEANScannerpCheckExtension(__inPUNICODE_STRINGExtension){constUNICODE_STRING*ext;if(Extension->Length==0){returnFALSE;}ext=ScannerExtensionsToScan;while(ext->Buffer!
=NULL){if(RtlCompareUnicodeString(Extension,ext,TRUE)==0){////匹配.
这个文件需要跟踪//returnTRUE;}ext++;}returnFALSE;}在我的mini驱动中,我把这个例程做了修改,即不仅令它判断出当前文件是否需要跟踪,还要它返回当前文件的后缀名是什么.
WDK中的这些个示范例程都具有相当好的扩展性,在根据需求改进它们的同时,你也会很快掌握自己设计例程的诀窍.
在post-create中的这段代码就是用于判断了.
你也可以在pre-create的时候判断.
文件名的构建过程是不是比legacy里面要简单和直观了许多status=FltGetFileNameInformation(Data,FLT_FILE_NAME_NORMALIZED|FLT_FILE_NAME_QUERY_DEFAULT,&nameInfo);if(!
NT_SUCCESS(status)){returnFLT_POSTOP_FINISHED_PROCESSING;}FltParseFileNameInformation(nameInfo);////检查扩展//scanFile=ScannerpCheckExtension(&nameInfo->Extension);////Releasefilenameinfo,we'redonewithit//FltReleaseFileNameInformation(nameInfo);if(!
scanFile){//此扩展名我们不关注returnFLT_POSTOP_FINISHED_PROCESSING;}总结:正如前所说,我只提供开胃菜.
这方面有很多例子,比如codeproject上的一个防火墙例子,它使用事件建立用户进程和驱动间的通信.
有做NDIS的问我这样的事件之间用不用做同步,这些事件仅仅用于通信,激活驱动中的某个选项或者处理.
假如你的驱动中有某几个处理必须有序进行,那么最好做同步处理,一般而言,没必要做同步.
问22:swapBuffers(2009-05-05)对于加解密应用而言,这个例子很基本也很重要.
它可以直接编译来调试,不过功能也仅仅是用于交换buffer.
它的交换处理没有明确的限制,而且各种情形都有相应处理,所以可以直接移植到你的代码中.
typedefstruct_PRE_2_POST_CONTEXT{PVOLUME_CONTEXTVolCtx;//指向卷上下文PVOIDSwappedBuffer;//指向用于交换的buffer}PRE_2_POST_CONTEXT,*PPRE_2_POST_CONTEXT;NPAGED_LOOKASIDE_LISTPre2PostContextList;//用于pre和post回调例程之间传递信息它的读处理思路:申请用于交换的buffer用它代替用户buffer,以便读到的数据会被存到它里面把它里面的数据复制到用户buffer中写的处理思路:申请用于交换的buffer把用户buffer里面的数据复制到它里面用它代替用户buffer,实际写的数据是它里面的综上,加解密的时机应在例程调用RtlCopyMemory附近,理论上前后都行,不过你得经过严格的测试才能决定这里需要如何处理,尤其是分页I/O.
OSR上还有人提出这样的观点(可能他们就是这么实现的,这里只是简述并不完整),我的驱动接收到I/O管理器发来的IRP后我自己再创建IRP去实现其中某些特定的操作,然后将完成结果复制给原来的IRP.
这也用了一个中间交换的思路.
反过来,这个中间buffer交换的思路也可以用到legacy上.
要注意FltSetCallbackDataDirty(Data)这个调用,因为mini驱动中,pre和post回调例程收到的入参都只是原始数据的一份副本.
你在pre中修改了参数你必须调用这个函数才能令改变生效,但post中的入参不会因此而改变,所以还需要通过PRE_2_POST_CONTEXT结构传递已改变的信息.
总结:本例意图十分明确,如果你想加上自己的一些想法测试的话可能还需要费点力气对它进行改造.
毕竟它并没有区分目标,随意地改动正在交换的数据系统很快就会bugcheck.
问23:总结mini驱动(2009-05-05)Mini驱动篇幅不长,但并不代表需要理解的东西不多,主要一些东西与legacy有交叉.
相对legacy来说,mini的几个例子都很有代表性.
比如以上介绍的这几个例子:ctx侧重上下文的使用;scanner侧重判断和扫描;swapBuffer侧重buffer交换,处理数据.
对应的,mini例子中的MiniSpy可以比照着filespy来看.
两者设计目的一样,思路也基本一致,不过基于mini的MiniSpy更加简洁易懂.
强烈建议有时间的话一定要把以上3个例子和MiniSpy仔仔细细地品味一遍,把它们掌握了,基本上你对mini驱动也掌握得差不多了.
在legacy中一般推荐你选择sfilter、filespy等这样现有的例子,根据自己的需求把它们改造成有用的框架,再在这样的框架上添加代码以实现自己的功能.
而mini驱动相对简单,通过这几个例子你也可以看出.
我的mini驱动框架就是糅合了ctx、scanner和swapBuffer的主要功能,然后把legacy中我认为将来可能有用的技巧性代码添加了进来.
如跟踪机制的数据结构主要为ctx,判断文件类型主要采用scanner中的判断文件名后缀,加解密主要通过swapBuffer的方法处理数据.
其实legacy和mini驱动面临的问题还是一样,只不过mini简化了部分东西.
因此,mini的设计思路和legacy是基本相同的.
当然,两者之间的选择看个人,如无特殊需求建议使用mini驱动模型,毕竟说明文档要比legacy的多,简单易用,而且受到微软官方的大力支持.
收尾问24:fastfat(2009-05-05)我手上有四份我认为很有参考价值的源码.
分别是fastfat、wrk、NT4.
0和NT5.
0.
都能从网上下载到.
Fastfat主要是文件系统源码,无论开发文件系统驱动还是过滤驱动都有极大的参考价值,其他三份源码还包括许多其他方面,适于慢慢研究.
Fastfat源码的各个部分区分的很开,比如处理read、write等的程序都集中在如read.
c、write.
c等专门的独立源文件中.
即便如此,没有相当的基础,看了也看不懂.
正如znsoft说NTFSI一样,看第一遍,什么都模糊,什么都不懂;看第二遍,有点感觉了,大概知道怎么回事;第三遍开始,就渐渐清晰了,这时候的感觉也开始靠谱了(不是原话).
Fastfat源码的重要性毋庸置疑,同样地,因其内容深、广,不花费相当的时间是无法学到东西的.
遗憾的是我并没有多少时间去仔细地研究NTFSI和fastfat.
这里只简单地拿读来"切割"下fastfat源码.
首先它有一个主要的处理例程,即FatCommonRead,其他各个操作都有相应的通用处理例程.
同文件内的其他例程都用于处理某些特殊情况.
在FatCommonRead中,不同条件的判断将各种类型的读操作逐渐剥离开,并被分别处理.
因此,不要惧怕那动辄上千上万的代码,一点一点分析本质终究会出来的.
如果大脑"缓存"不够用,就用纸笔这样的"外设",最好就是能构建成流程图或关系图,直观,一目了然.
总结:终于迎来了最后一问,不过文件系统驱动及过滤驱动绝非这里所展现的那么少.
我有一些"懒惰"的习惯,比如我不会花很多时间去记忆一些不常用甚至下一次使用不知道会是什么时候的东西,而是整理成资料,方便查阅.
在整个学习开发过程中,在群里、论坛上认识的一些技术牛人对我的影响十分深刻.
他们几乎都有一个共同点:不轻易相信其他人的结论,执着而始终冷静地追求问题的本质,从不自满于现有成就而不断寻求更完美的解决方案.
他们或许会有这样那样的缺点,但在技术上,他们都是值得尊敬的.
wordpress高级全行业大气外贸主题,wordpress通用全行业高级外贸企业在线询单自适应主题建站程序,完善的外贸企业建站功能模块 + 高效通用的后台自定义设置,更实用的移动设备特色功能模块 + 更适于欧美国外用户操作体验 大气简洁的网站风格设计 + 高效优化的网站程序结构,更利于Goolge等SEO搜索优化和站点收录排名。点击进入:wordpress高级全行业大气外贸主题主题价格:¥398...
ZJI又上新了!商家是原Wordpress圈知名主机商:维翔主机,成立于2011年,2018年9月启用新域名ZJI,提供中国香港、台湾、日本、美国独立服务器(自营/数据中心直营)租用及VDS、虚拟主机空间、域名注册等业务。本次商家新上韩国BGP+CN2线路服务器,国内三网访问速度优秀,适用8折优惠码,优惠后韩国服务器最低每月440元起。韩国一型CPU:Intel 2×E5-2620 十二核二十四线...
greencloudvps怎么样?greencloudvps是一家国外主机商,VPS数据中心多,之前已经介绍过多次了。现在有几款10Gbps带宽的特价KVM VPS,Ryzen 3950x处理器,NVMe硬盘,性价比高。支持Paypal、支付宝、微信付款。GreenCloudVPS:新加坡/美国/荷兰vps,1核@Ryzen 3950x/1GB内存/30GB NVMe空间/1TB流量/10Gbps...