前段时间琢磨文件过滤驱动的时候碰到一个棘手的问题,当时挂上wrk内核分析,发现了问题的关键点。分析的过程中明显感觉自己对IoMgr、ObjMgr、Fsd、Volume这些组件的结构关系掌握得很烂,于是最近这几天专门又挂上wrk内核,一点一点的追踪。有了一点收获,记录在这里,备忘。顺便也许会给一些朋友一点点帮助。

卷的创建

FtDisk.sys(或Disk.sys)调用FtpCreateNewDevice(或DiskCreateFdo)为卷创建FDO。
该函数会调用IoCreateDevice来创建DO,即卷的FDO。IoCreateDevice创建完毕DO后,会调用ObInsertObject将FDO挂入ObjMgr的空间,就是咱们用WinObj或WinObjEx看到的那个树形组织。
总结来说,调用关系如下:

FtpCreateNewDevice
    |- IoCreateDevice
        |- ObCreateObject
            |- ObInsertObject

这个过程完毕后,WinObj中 \Device\HarddiskVolumeX 的节点就“指向”了上面过程中创建的 FDO(后面称之为DiskVolumeFDO)。 注:Disk.sys是开源的,源码在wdk中。FtDisk没有源码,所以我调试的时候主要是参考的Disk.sys源码和IoCreateDevice的调用栈。

卷的挂载

系统初始化时,为了节省不必要的资源损耗,并不会将所有的文件系统(后简称fsd)都加载。而是另外提供一个功能模块Fs_rec.sys来根据具体的卷来加载必须的fsd。具体的过程如下:

Fs_rec.sys在初始化时,会声明几个MS支持的fsd类型的伪对象,比如fat、ntfs、cdfs、udfs。并通过调用IoRegisterFileSystem来把这几个伪对象挂入一个全局队列(请参考这个函数的源码),这个队列里保存着系统中所有的文件系统的CDO。而实际上在Fs_rec初始化的这个阶段,这个全局队列里实际里保存的都是Fs_rec创建的伪对象。这样做的目的是什么呢,咱们继续往下看。
当系统需要访问某个卷中的文件或目录等数据时,如果发现该卷没有挂载任何Fsd,就会调用IopMountVolume来挂载对应的Fsd(可参考wrk源码中的IopCheckVpbMounted函数)。这个函数会遍历之前提到的那个全局队列中的所有结点,挨个给链表中的结点包含的对象的发送IRP_MJ_FILE_SYSTEM_CONTROL.IRP_MN_MOUNT_VOLUME请求。
而前面咱们提到,这个时候队列里的全是伪对象,这些伪对象的Driver_object是Fs_rec.sys。于是Fs_rec这个时候会接收并处理这个IRP_MJ_FILE_SYSTEM_CONTROL请求,并针对具体的伪对象,比如fat的伪对象,来判断需要挂载的卷是否是fat卷。如果不是,则返回失败。如果是,那么Fs_rec就会把FastFat.sys加载到系统中,然后Fs_rec自身返回失败。而FastFat在初始化过程中,又会调用IoRegisterFileSystem把自己的CDO挂入那个全局链表(请参看fastfat源码中的fatinit.c中的DriverEntry函数)。这个时候全局链表就存在真正的fsd的CDO。而因为之前Fs_rec自身返回了失败,于是IopMountVolume会接着遍历全局链表,于是就会发现真正的fastfat的CDO,这个时候再发送IRP_MJ_FILE_SYSTEM_CONTROL.IRP_MN_MOUNT_VOLUME请求,接收到的返回值就是成功了。因为FastFat确实会真正的完成了整个卷的挂载过程。

总结一下:

最初,全局链表中只有Fs_rec创建的伪对象,这些伪对象负责把对应的fsd加载到系统,并返回错误,推动整个链表的遍历,直到真正的fsd完成卷的挂载,才会返回成功,结束遍历。 最初在梳理框架的时候参考的是《windows internals》,里面是这么描述的:

“After loading a file system driver, File System Recognizer forwards the mount IRP to the driver and lets the file system driver claim ownership of the volume.”[/quote]而实际上如我之前的描述,fs_rec只是负责把fsd加载到系统,并没把挂载请求发送给fsd。而是IopMountVolume发送这个请求。我们可以进一步看下调用栈,采用的是win2003的虚拟系统:

f78de190 808ed241 84a32340 84db4000 00000000 Fastfat!DriverEntry
f78de260 808ed355 800002a0 00000001 00000000 nt!IopLoadDriver+0x687
f78de288 80991a9b 800002a0 f78de310 f78de38c nt!IopLoadUnloadDriver+0x43
f78de304 8088284c f78de394 f78de39c 8082c605 nt!NtLoadDriver+0x143
f78de304 8082c605 f78de394 f78de39c 8082c605 nt!KiFastCallEntry+0xfc
f78de380 f779b15b f78de394 80a45be4 84685f08 nt!ZwLoadDriver+0x11
f78de39c f779b9b3 c000010e f779b906 80a45be4 Fs_Rec!FsRecLoadFileSystem+0x4f
f78de3c4 f779b0b0 84685f08 85836f68 84685f08 Fs_Rec!FatRecFsControl+0x35
f78de3d4 809ab1d2 84685f08 85836f68 84685f08 Fs_Rec!FsRecFsControl+0x6c
f78de404 8081f881 808ed918 f78de43c 808ed918 nt!IovCallDriver+0x110
f78de410 808ed918 84f1e8d0 8089f540 85046ea0 nt!IofCallDriver+0x11
f78de43c 808edc0c 84685f08 80a46150 85046ea0 nt!IopLoadFileSystemDriver+0x60
f78de490 80822ffc c000019c 84a5b800 00000000 nt!IopMountVolume+0x2ca

上面是fsd的加载过程,可见,是由Fs_rec来完成的。再看看卷挂载的调用栈:

f78de254 b94eec7f 844d45d0 84b4c948 84b9af00 Fastfat!FatMountVolume
f78de274 b94eed28 844d45d0 858aae70 4ec07aef Fastfat!FatCommonFileSystemControl+0x3f
f78de2c0 809ab1d2 84772e20 858aae70 858aafb0 Fastfat!FatFsdFileSystemControl+0x82
f78de2f0 8081f881 f732d23e f78de370 f732d23e nt!IovCallDriver+0x110
f78de2fc f732d23e 84df2bd0 858aae70 00000000 nt!IofCallDriver+0x11
f78de370 f732d616 84df2bd0 858aae70 80a45be4 fltMgr!FltpFsControlMountVolume+0x186
f78de3a0 809ab1d2 84df2bd0 858aae70 858aafd4 fltMgr!FltpFsControl+0x5a
f78de3d0 8081f881 f74ef27c f78de404 f74ef27c nt!IovCallDriver+0x110
f78de3dc f74ef27c 80a45be4 84917210 00000000 nt!IofCallDriver+0x11
f78de404 809ab1d2 84917210 858aae70 84f1e8d0 Dfs!DfsFilterFsControl+0x82
f78de434 8081f881 808edaf4 f78de490 808edaf4 nt!IovCallDriver+0x110
f78de440 808edaf4 80a46150 85046ea0 80a461d0 nt!IofCallDriver+0x11
f78de490 80822ffc 84917210 84a5b800 00000000 nt!IopMountVolume+0x1b2

OK,整个框架说完了,咱们来细说fsd的挂载过程, Fsd会在自己的IRP_MJ_FILE_SYSTEM_CONTROL.IRP_MN_MOUNT_VOLUME响应函数里会为这个待挂载的卷创建一个FsdVolumeFDO(可参考fastfat源码中fsctrl.c中的FatMountVolume函数)。并对DiskVolumeFDO->vpb进行设置,使

DiskVolumeFDO->vpb->DeviceObject = FsdVolumeFDO,
DiskVolumeFDO->vpb->RealDevice = DiskVolumeFDO。

到这里,卷和Fsd就通过vpb建立了连接,而Fsd也通过FsdVolumeFDO向系统声明了自己负责这个卷进行解析、管理。

这样,挂载之后,所有这个卷上的数据请求,比如文件、目录的打开、删除、读写,都会被转交给FsdVolumeFDO,最后由Fsd进行处理。具体的转交过程在后面描述。

卷中文件、目录的路径解析

现在咱们来说说系统访问具体某个文件时,系统的解析过程。以访问\??\c:\windows\boxcounter.dll这个文件为例。
当ObjMgr接收到这个访问请求时,会在它的名空间里对这个路径进行解析,在解析的过程中它发现\??\c:实际指向\Device\HarddiskVolume0,这是一个Device,指向DiskVolumeFDO,它提供了路径解析函数IopParseDevice(保存在object_header->Type->TypeInfo->ParseProcedure中)。
ObjMgr使用IopParseDevice继续对剩余路径\windows\boxcounter.dll进行解析,最后导致ObjMgr发送一个打开请求给Fsd(没有上层文件过滤驱动的情况下),发送的方式可能有两种:

  • 使用fastio函数
  • 分配并初始化一个IRP,然后通过IoCallDriver,将它发送到fsd。

但是咱们在回顾下,对于整个路径解析过程,ObjMgr只在自己的名空间里找到了DiskVolumeFDO,并没有找到具体的fsd。那么它是如何把请求交给fsd的呢。答案就是2中提到的vpb。ObjMgr通过DiskVolumeFDO->vpb->DeviceObject找到了FsdVolumeFDO,然后把请求发到这个FDO上,最后Fsd接收到这个请求,完成了剩余的解析过程。

在分析过程中,我很奇怪为什么MS没有把FSD和Disk驱动做成一个设备栈,想来想去没有想出什么很出众的解释。于是请教sinister师傅,得到这样的解释,附在这里:

嘿黑 11:06:17
贾佳师傅,有个问题请教:
MS为什么不把FSD和磁盘驱动堆叠成一个设备栈,而要用vpb来链接两者。有什么原因吗?或者说有什么历史原因?

sinister 9:26:48
我觉得是设计的考虑,因为FSD层单独出来,也是一个区域的划分,比如网络层的TDI驱动,其实就是个FSD的应用。这种设计符合MS的CLIENT/SERVER 的设计模型,因为封装且又不开源的原因,只要上层要数据下层返回就行。这样不必关心下面的细节。又能留出接口。像LINUX这种开源的设计,这种实现就基本上 FS与BLOCK(磁盘设备)层之间没有什么明显的划分了。都是通过一个结构制过去就行了。连设备栈这种东西都没有。 [/quote]

参考资料:

  1. wrk内核
  2. 《windows internals》(第四版、第五版)
  3. 《Windows内核情景分析-采用开源代码ReactOS》
  4. FastFat源码
  5. “关于文件系统和磁盘驱动的一点学习心得”- weolar
  6. “Inside WINDOWS NT Object Manager” - gloomy
  7. disk.sys源码