夏日事多

这几天过得太匆忙,也许是把星期一给过丢了,以至一度认为今天是周二,直到朋友打电话来问明天要不要去攀岩时才注意到今天已是周三了。

这个平平常常的不二小三的日子里,我过得也是普普通通,象往常一样被各种小事、大事、或身边事挤得满满的:

1)从早上直到下午,一直在调程序,搞定一个“六亲乱认”的问题,是由于打开文件时对一个标志位的处理缺失导致的。

2)午饭时间下起了暴雨,阵雨来得凶,走得也快。待雨停,又去了趟面包店。

3)下午正和朋友讨论问题的当空,QQ上便传来严冬冬山难的消息。将自己托付与山,是职业登山者的宿命。只是生命如此年轻,正值年少轻狂,誓言征服全天下的壮志心胸!

4)“REPUBLIC OF CHINA”的通行证终于出来了,当个中国人真不容易,特别是再加个“THE PEOPLE’S” 前辍的情况下。

5)晚上跑步,天热,只跑了5k,一边跑一边筹划着马拉松的事情,事实是等我跑完,马拉着松早没影了。加量的问题真是个问题,也是个相当“科学”的问题,也是个决心  vs 惰性的问题。

7)就在跑步前,想给平板里拷几部电影,用了一个taobao上购物卖家赠送的读卡器,不太灵光,32G的TF卡读不出来了。跑完步,草草冲了一个澡,便开始尝试数据恢复。FAT32格式太不靠谱,动不动就会受伤。分析下来,损坏相当严重:

a) Boot Record被写坏了,好在这个可以重建 b) FAT Table 1和FAT Table 2的第一个扇区也全写坏了。Root Directory在第一簇,即2号簇,分析下来第一扇区后面的数据是连续的,同属一个文件(第二个扇区房始值是0x81,一簇大小是16K,即0x4000),所以第一扇区的重建比较轻松。 c) 电影目录的首簇号失效,文件名查找找到的位置竟然是0x701400200,不能被簇整除。将内容移到0x701400000,并修改电影目录的首簇号为0x001C0002。

经过以上修改,系统还是不能正常挂载,但好在winimage可以读取,最重要的数据(安装程序、ROM的备份)都可以备份出来了,电影文件部分可以正常读取,后来就没在花时间搞,只要将程序及ROM的备份搞定就完全没压力了。

8)等折腾完,睡觉时已是00:20,不平静的“小三”终于过去了。

<此日志为周四记录周三的事,主要是想再看一眼“小三”,渴望同“小三”一起多走一天>

离奇死亡事件薄之CoCreate篇

“周节棍的双杰伦,砌里侉叉 …”

一阵吵杂的手机铃声将正在发呆的瓶瓶吵醒,他呆滞的眼神中蓦地闪出亮光,心想:大买卖又来了?

果不其然,又是一桩离奇案件:CoCreate在密镇莫名死亡,临终前嘴里还含糊地说了句“The file xxx failed to load. could not unpack the file xxx. Either this file is invalid, you do not have sufficient memory, or no space is left. (Error 398)” ,然后便咽气了。

密镇是凶杀案频发的地方,因其本身治案环境不好,很多案件都成了无头案。这也是为什么作为私家侦探的瓶瓶会被委托查找真凶的原因。

瓶瓶深思了一会,便开始着手重构死亡现场。这里还要多说一句,随着科技的进步,新时代的侦探们也有了更高效的破案方式方法,虽然原始的从验尸(Postmortem,Crash dump analysis)开始的福氏方式依然盛行,但越来越多的人都开始采用这种重建虚拟现场的方式,毕竟新的方法可以让你一遍一遍地推演,并在推演中不断地发现新的线索,直到真相大白。

重构虚拟现场需要相关的工具和设备才行。每位侦探都有自己偏爱的工具组合,当然也逃不脱其时代的限制与烙印。比如两位同姓宋的侦探前辈:宋慈因处宋朝,手中也只有手术刀之类的工具,纵使锋利,但作用毕竟有限;再看19世纪英国的宋戴克,他的方形绿皮箱里可谓是包罗万象,从放大镜,显微镜,到酒精灯,试剂等一应俱全,俨然福尔摩斯的化学实验室了。

但对于21世纪像瓶瓶一样的侦探们来说,一切又化繁为简,一般一台电脑足矣,有时也需要多台并加上若干互联设备。瓶瓶的书房便是充斥着各色各样的电子设备,相互间连接着的五颜六色的电缆全绞缠在一起。

瓶瓶最得心应手的工具包还是windbg,外加一根1394线,便足以对付大多数棘手的凶案。从业之初,瓶瓶用得可是debug工具包,后来经过softice,最后才用上windbg,前几年还是windbg第五代,当时用得还是串口线,现在已升级至第六代了。

不到一个小时,瓶瓶便已连接好所有设备,并设定妥了虚拟现场环境。随着“滴”的一声响,模拟系统开始启动,windbg上开始显现一串串的字符。此时,瓶瓶圆睁的双眼透过厚厚的近似镜片直盯盯地注视着快速翻滚的屏幕,生怕错过每一个细节。同时又暗自庆幸,CoCreate并没有进行反模拟手术,否则,如果像3ds max那样完全依赖log来分析地话苦头可就大了。

忽然“铛”地一声响,屏幕跳出了CoCreate的临终遗言。此次模拟相当成功,但在细致分析windbg记录之前还没找到任何明显线索,在浏览了所有记录后,也未能找到什么重要的线索。不得不又架设好procmon工具,重新又模拟了一次。procmon工具可以协助记录CoCreate死亡前的所有活动记录,及所有与他接触的相关人员等等。在procmon记录中,发现最后一个与死者接触的人是unzip。

“CoCreate死前找unzip干什么呢?”,瓶瓶陷入深思,忽然间,他想到了死者的遗言中有说 "unpack”,“难道真和unzip有关”,瓶瓶一边思考着一边努力寻找着证据。

瓶瓶打开了死者临死前正翻阅的文件,题为xxx。这是一个加密的文件,密镇定了法律为了保护所有信息,必须对各种重要文件进行加密,否则不能随便交换或传阅。瓶瓶当然知道怎么将此加密文件解密成明文,毕竟如果没有点关系,还真干不成侦探这个行当。

刚打开明文文件,瓶瓶就发现文件头竟赫然是 “504B0304”, ”504B”正是zip压缩法发明者Phil Katz的名字缩写: PK。基本可以肯定xxx文件为zip文件格式,所以CoCreate死前才会去见unzip。若如此地话,那unzip倒底做了些什么呢?

瓶瓶大感意外的是:等找到unzip时,发现unzip也离奇死亡了。难道是…连环谋杀?看来此案件内情确实不简单,搞不好会惹火上身!想到此,瓶瓶不禁觉得背后发寒。

但事已至此,由不得自己了。瓶瓶先勘察了unzip的死亡现场,但另他失望的是,unzip临死留下的遗言更让人摸不着头脑:  ”End-of-central-directory signature not found. Either this file is not a zipfile, or it constitutes one disk of a multi-part archive…”。这倒底什么意思?进行了一番搜索后,瓶瓶发现如此死亡的案件竟有很多,只是,众多老案件至今还未结案。

多次探索均无功而返,瓶瓶决定先从模拟unzip死亡现场开始。好在unzip也是位名人,有关他的材料可谓是汗牛充栋,特别是有关他的生平传记(unzip60.zip),更是给现场模拟提供了最好的背景材料。

模拟中果然有新的发现,unzip在通过公开渠道获取的加密文件信息竟是错的。此公开渠道是_stat64调用:

int _stat64( const char path, struct __stat64 buffer );

_stat64会返回文件大小、时间戳等信息。关键的地方是:_stat64获取这些信息的方法不是通过GetFileInformationByHandle (IRP_MJ_QUERY_INFORMATION/FileStandardInformation),而是通过FindFirstFileEx (即IRP_MJ_DIRECTORY_CONTROL/IRP_MN_QUERY_DIRECTORY)得来的。这样操作如果在平时是没有问题的,但在密镇却会出错。

密镇会对所有文件进行过滤并加密,即便是授权的用户在用第二种办法查询文件时,所返回的文件大小与实际大小也是不相符的,因为密镇的所有文件都要附载加密信息而被增厚了。故unzip用错误的文件页码信息来校验文件时,必然会发生了意外。

这个问题当然是出在密镇身上,但令人可气的是,unzip在调用_stat64后又调用了GetFileTime (IRP_MJ_QUERY_INFORMATION/FileBasicInformation),既然都动用私人关系了,为什么不一次性到位,直接将文件大小也一并取了呢?!

案件侦测至此,案情已基本大白。只是善后工作该怎样去做,真是个让瓶瓶头痛的事情:

最彻底的办法,当然是改良密镇,虽然此方案涉及面太广。瓶瓶经过好一番努力,透过多个渠道才将此事办妥,本以为可以长舒一口气的当口,黑老大M$的代表Office Word发飙了,无论如何都反对。此事竟然黑老大也参与其中?这让瓶瓶不得其解,在没有搞清楚其中奥妙之前,也只能放弃此番所有努力而另辟奇径了,毕竟太岁头上的土动不得。

既然unzip已经死亡,那就不妨再推出个新个unzip吧。让新的unzip用新的私人渠道来获取加密文件的信息不就完了呗。

说到便做到,等将新的unzip装上设备,又进行新的一轮模拟,一切正常了。

连日的探查已让瓶瓶厚厚的镜片蒙上了一层灰尘,正好遮挡住了他更加疲惫的眼神。但无论如何,最后还是以最小的代价将善后工作完成了,瓶瓶终于能够安坐在弹簧椅上,惬意地随着电视哼起了 《游击队歌》。

密镇的电视共有200个频道,只是所有频道上都是相同的节目,而且都是红歌大比拼。红歌声中,瓶瓶淡定地等待着下一件凶杀案的发生。他知道,用不着等太久 ……

(本故事部分纯属虚构,请勿胡乱对号入座)

VPB的来龙去脉(二)

VPB全局锁:

为了保护对所有VPB成员的访问,I/O Manager用到了一个全局的spinlock:

0: kd> x nt!IopVpbSpinLock
fffff800`01af7640 nt!IopVpbSpinLock = <no type information>

这个全局spinlock并不能为系统驱动所直接引用(没有export出来),所以I/O Manager提供了两个支持函数(主要是为文件系统驱动所用):

VOID
IoAcquireVpbSpinLock(
    OUT PKIRQL Irql
    );

VOID 
IoReleaseVpbSpinLock(
    IN KIRQL  Irql
    );

所有对VPB成员的访问及修改都要在获取VPB锁的情况下进行,无论是I/O Manager还是文件系统驱动都要遵守。

但由于VPB spinlock是个全局锁,一旦被获取,后续所有的打开或创建文件操作(包括在其它文件系统卷上的)都将被阻止,所以持有此锁的时间不宜过长,必须尽可能快地释放。

关于IoAcquireVpbSpinLock()及IoReleaseVpbSpinLock()的内部实现,从XP以后在64位的系统上,I/O Manager用得是Queued SpinLock机制,有兴趣地话不妨自己动手去探索一下究竟。

VPB的引用及多头管理(Distributed Management):

VPB是靠ReferenceCount来管理其生存周期的,任何对设备卷的访问都会产生对此VPB的计数引用。

最主要的引用操作都在Naming Parsing阶段,即IopParseDevice()函数中。IopParseDevice()函数是I/O Manager向对象管理器(Object Manager)注册的“Device”一类对象的名称解析回调函数(Parse Procedure),这部分会在以后介绍“Name Parsing”时再做详述,在此先行略过。

IopParseDevice()在解析到卷设备的DeviceObject以后,会调用IopCheckVpbMounted()来检查此卷设备是不是已挂载文件系统。IopCheckVpbMounted()会做以下的事情:

  • 如果此卷还没有挂载文件系统,则调用IopMountVolume()轮询所有已注册的此类设备的文件系统驱动,尝试去挂载。一旦挂载成功,IopMountVolume()则会调用IopMountInitializeVpb()来设置Vpb内容,并标记VPB_MOUNTED标志。但是VPB_MOUNTED标志的清除却是由文件系统驱动在Dismount时做的,这也是一个典型的多头管理的例证。
  • 对已经挂载的卷设备,则直接增加VPB的引用计数并返回Vpb。

此时,如果返回的Vpb存在,则已经被引用过(ReferenceCount加1)。然后IopParseDevice()会创建FileObject对象,构造IRP_MJ_CREATE请求(文件打开或创建),分发给文件系统驱动。文件系统驱动会解析存储设备上目录及文件信息以定位此文件。

如果文件没有找到或创建失败,IopParseDevice()则要调用IopDereferenceVpbAndFree()以减小引用计数。对于一个则创建的"Clean”状态的Vpb,即便是它的ReferenceCount已归零,此Vpb并不会被IopDereferenceVpbAndFree()释放掉。原因是IopDereferenceVpbAndFree()只会释放非"Clean”状态(即Vcb->RealDevice->Vpb != Vpb)并且ReferenceCount为0的Vpb。正常情况下,从未被挂载过的卷设备的Vpb总是"Clean”的。(如果真得被释放掉,会有什么样的麻烦?)

一旦文件被成功打开或新创建,文件系统驱动会设置FileObject->Vpb项。注意,此操作是由文件系统驱动来做的,而不是I/O Manager,这又是另外一个多头管理的例证。当此文件被关闭时,它的文件对象/FileObject对Vpb的引用就会被清除(ReferenceCount减1)。关闭文件时,Object Manager会调用I/O Manager向其注册的"File” 对象的删除回调函数(Delete Procedure)IopDeleteFile(),然后IopDeleteFile()函数会减小Vpb的引用计数,并向文件系统驱动分发IRP_MJ_CLOSE请求以通知此FileObject的关闭操作。

VPB的释放:

正常情况下,VPB会随着DeviceObject的删除而被释放,具体操作在IopDeleteDevice()中执行。IopDeleteDevice()是I/O Manager所定义的"Device” 对象的删除回调函数(Delete Procedure),在设备移除时Object Manager会调用此函数来释放DeviceObject。

当然除正常情况下的寿终正寝外,总会有非正常死亡的情况存在。只是不同的文件系统驱动对VPB的处理不尽相同,如NTFS不支持Re-mount,可FastFat和CDFS却支持。在这里只以FastFat为例来展开介绍,毕竟WDK中有完整的FastFat的源码,研究起来也容易。

先介绍一下SwapVpb,它是FastFat VCB的一个成员:

0: kd> dt fastfat!_VCB
   +0x000 VolumeFileHeader : _FSRTL_ADVANCED_FCB_HEADER
   +0x058 VcbLinks         : _LIST_ENTRY
   +0x068 TargetDeviceObject : Ptr64 _DEVICE_OBJECT
   +0x070 Vpb              : Ptr64 _VPB
   +0x078 VcbState         : Uint4B
   +0x07c VcbCondition     : _VCB_CONDITION
      ……
   +0x408 ChangeCount      : Uint4B
   +0x410 SwapVpb          : Ptr64 _VPB
      ……
   +0x478 CloseContextCount : Uint4B

SwapVpb是在初始化Vcb时就创建一个备用的Vpb。之所以先准备好这个备用Vpb,是因为在真正需要新的Vpb记录时,有可能无法再从系统内存池中分配任何内存块,可以避免走到进退维谷的地步。

此过程会涉及Mount及Dismount操作,在后续的文章中我会对这两个过程加以详述,鉴于二者对理解VPB的释放过程并无太大妨碍,本文中将不作介绍。

非正常死亡的情况大体上分为两种:

Case 1: Force-dismount

Force-dismount一般发生于两种情形之下:

a) 用户执行FSCTL_DISMOUNT_VOLUME操作,即便FSCTL_LOCK_VOLUME操作失败

b) 用户强行将可插拔设备拔出(对于Changable/Removable Media设备,如CDROM/Floppy,将CD盘片及软盘退出并不属于此类)

这两种情形下,FastFat会调用FatCheckForDismount()并以Force=TRUE的方式强制解除卷设备对象Vpb->RealDevice和文件系统逻辑卷设备Vpb->DeviceObject之间的关联,具体操作可以参阅FatSwapVpb()代码。大体流程如下:

    /* Initialize Vcb->SwapVpb to a “Clean” state */
    Vcb->SwapVpb->Type = IO_TYPE_VPB;
    Vcb->SwapVpb->Size = sizeof( VPB );
    Vcb->SwapVpb->RealDevice = OldVpb->RealDevice;
    Vcb->SwapVpb->RealDevice->Vpb = Vcb->SwapVpb;
    Vcb->SwapVpb->Flags = FlagOn( OldVpb->Flags, VPB_REMOVE_PENDING );
    Vcb->SwapVpb = NULL;
    ……
    /* Update Vcb’s state to VcbBad */
    FatSetVcbCondition( Vcb, VcbBad);
    /* Let FastFat release Vcb->Vpb when destroying Vcb */
    SetFlag( Vcb->VcbState, VCB_STATE_FLAG_VPB_MUST_BE_FREED );

代码第一部分将Vcb->SwapVbp初始化成”Clean”状态,然后替换掉原来的Vpb,替换后RealDevice->Vbp指向了Vcb->SwapVpb,即RealDevice所表示的卷设备对象不再被任何文件系统所接管。但实际上,仍然会有文件正在被系统或应用程序打开,它们的FileObject->Vpb仍然指向老的Vpb,但所有的I/O操作将被文件系统拒绝,只有文件关闭请求仍会被处理,直到最好一个文件被关闭,FastFat的逻辑卷设备及Vcb才会被删除。

你可能会考虑到,在最后一个文件的FileObject被删除时,IopDeleteFile()会调用函数IoDereferenceVpbAndFree()减小Vpb的引用计数,此时的Vpb还是老的Vpb,那么老的Vpb会不会被释放呢?

答案是不会,因为老Vpb的ReferenceCount还没有归零,原因就在于文件系统并没有清除在卷挂载时由IopMountInitializeVpb()所增加的引用计数。FastFat的做法是在Vcb设置标志位:VCB_STATE_FLAG_VPB_MUST_BE_FREED,也就是说将此Vpb看作是Vcb的私有资源了。在Vcb被删除时,FatDeleteVcb()如果检测到此标志位将一并释放Vcb->Vpb。至于为什么要这么做,还有待进一步的研究。但是,如果将Vpb的释放交与I/O Manager(即IopDereferenceVpbAndFree())应该也是可行的。

下面来做个实验:

第一步:将插着FAT32格式SD卡的USB读卡器插入电脑,Windows会自动挂载并分配盘符,此时我们来观察一下其Vcb及Vpb的内容:

1: kd> ?? fastfat!FatData
struct _FAT_DATA
   +0x000 NodeTypeCode     : 0x500
   +0x002 NodeByteSize     : 304
   +0x008 LazyWriteThread  : 0xfffffa80`037151a0
   +0x010 VcbQueue         : _LIST_ENTRY [ 0xfffffa80`0384fa18 - 0xfffffa80`0384fa18 ]
    ……

1: kd> !list "-t nt!_LIST_ENTRY.Flink -e -x \"dd @$extret l4;  dt fastfat!_VCB @$extret-0x58\"  0xfffffa80`0384fa18"
dd @$extret l4;  dt fastfat!_VCB @$extret-0x58
fffffa80`0384fa18  070e34d0 fffff880 070e34d0 fffff880
   +0x000 VolumeFileHeader : _FSRTL_ADVANCED_FCB_HEADER
   +0x058 VcbLinks         : _LIST_ENTRY [ 0xfffff880`070e34d0 - 0xfffff880`070e34d0 ]
   +0x068 TargetDeviceObject : 0xfffffa80`053c7040 _DEVICE_OBJECT
   +0x070 Vpb              : 0xfffffa80`05b54a00 _VPB
   +0x078 VcbState         : 0x101002
   +0x07c VcbCondition     : 1 ( VcbGood )
     ……
   +0x410 SwapVpb          : 0xfffffa80`056f19e0 _VPB
   +0x418 AsyncCloseList   : _LIST_ENTRY [ 0xfffffa80`0384fdd8 - 0xfffffa80`0384fdd8 ]
   +0x428 DelayedCloseList : _LIST_ENTRY [ 0xfffff8a0`170d3880 - 0xfffff8a0`170d3880 ]
   +0x438 AdvancedFcbHeaderMutex : _FAST_MUTEX
   +0x470 CloseContext     : 0xfffff8a0`0af0eef0 CLOSE_CONTEXT
   +0x478 CloseContextCount : 2

1: kd> dt _VPB 0xfffffa80`05b54a00
nt!_VPB
   +0x000 Type             : 10
   +0x002 Size             : 96
   +0x004 Flags            : 1
   +0x006 VolumeLabelLength : 0x10
   +0x008 DeviceObject     : 0xfffffa80`0384f820 _DEVICE_OBJECT
   +0x010 RealDevice       : 0xfffffa80`05bab060 _DEVICE_OBJECT
   +0x018 SerialNumber     : 0x51e712c6
   +0x01c ReferenceCount   : 9
   +0x020 VolumeLabel      : [32]  "CANON_DC"

1: kd> dt _VPB 0xfffffa80`056f19e0
nt!_VPB
   +0x000 Type             : 0
   +0x002 Size             : 0
   +0x004 Flags            : 0
   +0x006 VolumeLabelLength : 0
   +0x008 DeviceObject     : (null)
   +0x010 RealDevice       : (null)
   +0x018 SerialNumber     : 0
   +0x01c ReferenceCount   : 0
   +0x020 VolumeLabel      : [32]  ""

1: kd> !devobj 0xfffffa80`05bab060
Device object (fffffa8005bab060) is for:
HarddiskVolume18 \Driver\volmgr DriverObject fffffa8004086e70
Current Irp 00000000 RefCount 9 Type 00000007 Flags 00003050
Vpb fffffa8005b54a00 Dacl fffff9a1171d06c0 DevExt fffffa8005bab1b0 DevObjExt fffffa8005bab318 Dope fffffa8004fa7fa0 DevNode fffffa80055d9d90
ExtensionFlags (0000000000) 
AttachedDevice (Upper) fffffa8005b4a040 \Driver\fvevol
Device queue is not busy.
1: kd> dt _DEVICE_OBJECT 0xfffffa80`05bab060
nt!_DEVICE_OBJECT
   +0x000 Type             : 3
   +0x002 Size             : 0x2b8
   +0x004 ReferenceCount   : 9
   +0x008 DriverObject     : 0xfffffa80`04086e70 _DRIVER_OBJECT
   +0x010 NextDevice       : 0xfffffa80`045ed440 _DEVICE_OBJECT
   +0x018 AttachedDevice   : 0xfffffa80`05b4a040 _DEVICE_OBJECT
   +0x020 CurrentIrp       : (null)
   +0x028 Timer            : (null)
   +0x030 Flags            : 0x3050
   +0x034 Characteristics  : 1
   +0x038 Vpb              : 0xfffffa80`05b54a00 _VPB
   +0x040 DeviceExtension  : 0xfffffa80`05bab1b0
   +0x048 DeviceType       : 7
   +0x04c StackSize        : 9 ''
   +0x050 Queue            : <unnamed-tag>
   +0x098 AlignmentRequirement : 0
   +0x0a0 DeviceQueue      : _KDEVICE_QUEUE
   +0x0c8 Dpc              : _KDPC
   +0x108 ActiveThreadCount : 0
   +0x110 SecurityDescriptor : 0xfffff8a0`170b0620
   +0x118 DeviceLock       : _KEVENT
   +0x130 SectorSize       : 0x200
   +0x132 Spare1           : 1
   +0x138 DeviceObjectExtension : 0xfffffa80`05bab318 _DEVOBJ_EXTENSION
   +0x140 Reserved         : (null)

可以看出Vcb(0xfffffa80`0384f9c0)的SwapVpb(0xfffffa80`056f19e)为空。其Vpb(0xfffffa80`05b54a00)与Vpb->RealDevice(0xfffffa80`05bab060)所指是一致的。

第二步:将USB读卡器拔出,然后再观察一下Vcb及Vpb的状态:

先看来看Vcb的内容:

0: kd> !list "-t nt!_LIST_ENTRY.Flink -e -x \"dd @$extret l4;  dt fastfat!_VCB @$extret-0x58\"  0xfffffa80`0384fa18"
dd @$extret l4;  dt fastfat!_VCB @$extret-0x58
fffffa80`0384fa18  070e34d0 fffff880 070e34d0 fffff880
   +0x000 VolumeFileHeader : _FSRTL_ADVANCED_FCB_HEADER
   +0x058 VcbLinks         : _LIST_ENTRY [ 0xfffff880`070e34d0 - 0xfffff880`070e34d0 ]
   +0x068 TargetDeviceObject : 0xfffffa80`053c7040 _DEVICE_OBJECT
   +0x070 Vpb              : 0xfffffa80`05b54a00 _VPB
   +0x078 VcbState         : 0x141102
   +0x07c VcbCondition     : 3 ( VcbBad )
    ……
   +0x410 SwapVpb          : (null)
    ……

可以看出,此时SwapVcb项已为空值,Vcb的状态已从VcbGood变成为了VcbBad,并且Vcb->VcbState已被设置上标志位VCB_STATE_FLAG_VPB_MUST_BE_FREED (0x00040000)。

接着再来看Vpb(0xfffffa80`05b54a00)的内容:

0: kd> dt _VPB 0xfffffa80`05b54a00
nt!_VPB
   +0x000 Type             : 10
   +0x002 Size             : 96
   +0x004 Flags            : 9
   +0x006 VolumeLabelLength : 0x10
   +0x008 DeviceObject     : 0xfffffa80`0384f820 _DEVICE_OBJECT
   +0x010 RealDevice       : 0xfffffa80`05bab060 _DEVICE_OBJECT
   +0x018 SerialNumber     : 0x51e712c6
   +0x01c ReferenceCount   : 6
   +0x020 VolumeLabel      : [32]  "CANON_DC"

Vpb->RealDevice指向0xfffffa80`05bab060,并没有变化。下面再来看RealDevice(0xfffffa80`05bab060)的内容:

0: kd> !devobj 0xfffffa80`05bab060
Device object (fffffa8005bab060) is for:
HarddiskVolume18 \Driver\volmgr DriverObject fffffa8004086e70
Current Irp 00000000 RefCount 6 Type 00000007 Flags 00003050
Vpb fffffa80056f19e0 Dacl fffff9a1171d06c0 DevExt fffffa8005bab1b0 DevObjExt fffffa8005bab318 Dope fffffa8004fa7fa0 DevNode fffffa80055d9d90
ExtensionFlags (0x00000004)  DOE_REMOVE_PENDING
AttachedDevice (Upper) fffffa8005b4a040 \Driver\fvevol
Device queue is not busy.
0: kd> dt _DEVICE_OBJECT 0xfffffa80`05bab060
nt!_DEVICE_OBJECT
   +0x000 Type             : 3
   +0x002 Size             : 0x2b8
   +0x004 ReferenceCount   : 6
   +0x008 DriverObject     : 0xfffffa80`04086e70 _DRIVER_OBJECT
   +0x010 NextDevice       : 0xfffffa80`045ed440 _DEVICE_OBJECT
   +0x018 AttachedDevice   : 0xfffffa80`05b4a040 _DEVICE_OBJECT
   +0x020 CurrentIrp       : (null)
   +0x028 Timer            : (null)
   +0x030 Flags            : 0x3050
   +0x034 Characteristics  : 1
   +0x038 Vpb              : 0xfffffa80`056f19e0 _VPB
   +0x040 DeviceExtension  : 0xfffffa80`05bab1b0
   +0x048 DeviceType       : 7
   +0x04c StackSize        : 9 ''
   +0x050 Queue            : <unnamed-tag>
   +0x098 AlignmentRequirement : 0
   +0x0a0 DeviceQueue      : _KDEVICE_QUEUE
   +0x0c8 Dpc              : _KDPC
   +0x108 ActiveThreadCount : 0
   +0x110 SecurityDescriptor : 0xfffff8a0`170b0620
   +0x118 DeviceLock       : _KEVENT
   +0x130 SectorSize       : 0x200
   +0x132 Spare1           : 1
   +0x138 DeviceObjectExtension : 0xfffffa80`05bab318 _DEVOBJ_EXTENSION
   +0x140 Reserved         : (null)

RealDevice(0xfffffa80`05bab060)的Vpb指向不再是0xfffffa80`05b54a00,而是0xfffffa80`056f19e0,0xfffffa80`056f19e0正是之前的Vcb->SwapVpb。下面现再显示一下Vpb(0xfffffa80`056f19e0)的内容,应该为”Clean”状态:

0: kd> dt _VPB 0xfffffa80`056f19e0
nt!_VPB
+0x000 Type : 10
+0x002 Size : 96
+0x004 Flags : 8
+0x006 VolumeLabelLength : 0
+0x008 DeviceObject : (null)
+0x010 RealDevice : 0xfffffa80`05bab060 _DEVICE_OBJECT
+0x018 SerialNumber : 0
+0x01c ReferenceCount : 0
+0x020 VolumeLabel : [32] ""

Vpb->ReferenceCount为0,Vpb->Flags的值为0x08,即VPB_REMOVE_PENDING标志。此标志由Pnp Manager设置,表示此设备已从系统中移除。

Case 2: Remount

Remount的前提是,用户已经将曾挂载好的Removable Media如光盘片、软盘或SD卡拔出了,并且文件系统经过了校验/Verify操作后已将此卷设定成VcbNotMounted状态。

当用户再次插入此Removable Media时,I/O Manager(IopCheckVpbMounted / IopMountVolume)会调用FatMountVolume()去尝试挂载此卷。FatMountVolume()会创建新的逻辑卷设备对象及Vcb结构,然后再于Vcb队列中(FatData.VcbQueue)中查找有无指向同一个设备同一个媒质的逻辑卷设备对象,如果Vcb队列中已存在,则进行Remount。Remount的实质就是重新启用先前生成的逻辑卷设备对象,并将当前新创建的逻辑卷设备删除。

这里只有一点值得强调:

IopMountVolume()通过Irp传递过来的Vpb (iosb->Parameters.MountVolume.Vpb)将会随着新创建的逻辑卷设备对象的删除而被释放掉,先前的挂载时的老Vpb将会重新启用。FastFat会更新老的Vpb至iosb->Parameters.MountVolume.Vpb,因为文件系统上层有可能有过滤/Filter驱动程序访问此iosb->Parameters.MountVolume.Vpb项,如果不更新就会导致内存访问错误发生。

VPB的问题综述:

VPB是缺乏对象化的一个结构体,结构虽不复杂,但其操作却相当琐碎且分散于I/O Manager及文件系统驱动的方方面面,与卷的Mount和Dismount过程息息相关。读者不妨考虑一个问题,能不能将VPB的操作都限制于I/O Manager之中,并做到对文件系统驱动完全透明吗?

VPB的来龙去脉(一)

VPB是Windows I/O Manager子系统的一个重要的数据结构,全称为 Volume Parameter Block。它的任务是绑定卷设备(如磁盘分区或虚拟磁盘)和接管此卷设备的文件系统(如FastFat,NTFS)。Windows系统上的挂载点(Mount Point,如盘符C:)只能定位至卷设备(如\Device\HarddiskVolume1),Windows Object Manager(对象管理器)在解析路径名时(即Name Parsing过程)还要通过卷设备的VPB信息进一步定位至接管此卷设备的文件系统驱动上。

Relationship of VPB, Storage Volume Device and File System Logical Volume Device

图一

VPB结构说明:

定义于WDK头文件inc\ddk\wdm.h中

typedef struct _VPB {
    CSHORT Type;
    CSHORT Size;
    USHORT Flags;
    USHORT VolumeLabelLength; // in bytes
    struct _DEVICE_OBJECT *DeviceObject;
    struct _DEVICE_OBJECT *RealDevice;
    ULONG SerialNumber;
    ULONG ReferenceCount;
    WCHAR VolumeLabel[MAXIMUM_VOLUME_LABEL_LENGTH / sizeof(WCHAR)];
} VPB, *PVPB

成员说明:

  • Type:  魔数标志 IO_TYPE_VPB
  • Size:   sizeof(VPB)
    Flags:  标志位,相关标志描述如下:
                VPB_MOUNTED (1<<0):       此卷已被文件系统识别并已挂载
                VPB_LOCKED (1<<1):           此卷已被文件系统锁定,锁定操作由请求FSCTL_LOCK_VOLUME完成
                VPB_PERSISTENT (1<<2):   将VPB一直保留在内存中(不释放),即使此VPB引用计数为0
                VPB_REMOVE_PENDING (1<<3): 表示此存储设备即将被卸载/删除。此标志由Pnp Manager(即插
                                                                  即用管理器)管理和使用。此标志在可在处理Pnp请求
                                                                   IRP_MN_CANCEL_REMOVE_DEVICE时清除
                 VPB_RAW_MOUNT (1<<4): 指定此卷仅由系统RAW文件系统接管
  • VolumeLabelLength:  卷标长度(in bytes)
  • DeviceObject:           未命名的文件系统逻辑卷对象(unnamed logical volume)
  • RealDevice:               物理卷设备(如\Device\HarddiskVolume1)
  • SerialNumber:           卷序列号
  • ReferenceCount:      VPB的引用计数,用以控制VPB的生命周期
  • VolumeLabel:             卷标,最长32个双字节
                                      #define MAXIMUM_VOLUME_LABEL_LENGTH (32 * sizeof(WCHAR))

    VPB的操作(包括初始化)是由I/O Manager及文件系统二者共同完成的,所以多头管理正是VPB的复杂性所在,好在正常情况下,VPB的申请和释放均由I/O Manager实施,此时VPB从创建到释放的生命周期中基本上是单线式发展的。但当遇到Force Dismount及Remount特殊情况时,文件系统会直接干预VPB的创建及释放,致使VPB的单线式的发展周期一分为二,致使VPB的管理变得更加复杂。

    我们先从正常情况开始介绍,先介绍几个VPB相关的内部函数:

    0: kd> x nt!*Vpb*
    fffff800`01d62940 nt!IovpBuildDriverObjectList = <no type information>
    fffff800`0189f150 nt!IopDereferenceVpbAndFree = <no type information>
    fffff800`01c54400 nt!IopCreateVpb = <no type information>
    fffff800`01968830 nt!IopReferenceVerifyVpb = <no type information>
    fffff800`01865d0c nt!IopMountInitializeVpb = <no type information>
    fffff800`01865db0 nt!IoAcquireVpbSpinLock = <no type information>
    fffff800`01af7640 nt!IopVpbSpinLock = <no type information>
    fffff800`01903ab0 nt!IopCheckVpbMounted = <no type information>
    fffff800`01865a48 nt!IopQueryVpbFlagsSafe = <no type information>
    fffff800`01c827b0 nt!HvpBuildMapAndCopy = <no type information>
    fffff800`01865de0 nt!IoReleaseVpbSpinLock = <no type information>
    fffff800`01c82ca0 nt!HvpBuildMap = <no type information>

    IoAcquireVpbSinLock()及IoReleaseVpbSpinLock()为系统支持函数(kernel support routine)。Windows I/O Manager在内核中维护一个全局的spinlock锁,以保护所有对Vpb结构体的访问。文件系统和I/O Manager本身在访问Vpb时都必须调用IoAcquireVpbSpinLock(),访问结束后再调用IoReleaseVpbSpinLock()释放全局自销锁。

    IopCreateVpb(): 从NonPagedPool中申请VPB,然后简单初始化VPB(type,size)并与RealDevice一起绑定

    IopCheckVpbMounted(): 检测DeviceObject是不是已经挂载,若没有则会调用IopMountVolume尝试去挂载

    IopMountInitializeVpb(): 设置新挂载卷设备的Vpb(设置VPB_MOUNTED标记,增加引用计数等),由
                                            IopMountVolume挂载成功后调用

    IopQueryVpbFlagsSafe(): 获取设备DeviceObject的VPB的标志位信息,即DeviceObject->Vpb->Flags。如
                                              果DeviceObject->Vpb为NULL,则返回0值。(较新Windows版本才引入的函数)
    IopReferenceVerifyVpb(): 如果卷设备已被挂载,则返回其VPB及对应的文件系统逻辑卷设备对象,此函数
                                              增加VPB的引用计数

    IopDereferenceVpbAndFree():顾名即知其义,此函数会减少VPB的引用计数,如果引用计数归0则会释放
                                                     此VPB至NonPagedPool。(特例VPB_PERSISTENT除外)

    VPB的创建及初始化:

    VPB真正是由IoCreateDevice()在创建新设备对象时分配的,当然IoCreateDevice()是调用的 IopCreateVpb()来做的VPB的分配及最初的初始化。

    IoCreateDevice()不是对所有的设备对象都分配VPB,而是只针对4种设备:

    • FILE_DEVICE_DISK (0x07)
    • FILE_DEVICE_TAPE (0x1f)
    • FILE_DEVICE_CD_ROM (0x02)
    • FILE_DEVICE_VIRTUAL_DISK (0x24)

    VPB的初始化并不复杂,因为VPB的结构体并不庞大,但是,其初始化操作相当分散,由下面的3个过程共同完成:

    • IoCreateDevice()/IopCreateVpb()
    • IopParseDevice()/IopCheckVpbMounted()/IopMountVolume()/File system IPR_MN_MOUNT_VOLUME
      handler(以FastFat为例:FatMountVolume)
    • IopParseDevice()/IopCheckVpbMounted()/IopMountVolume()/IopMountInitializeVpb()

    具体每一部分都做了什么并不难发掘,读者不妨用windbg作个简单实验。

    下面以FastFat (Windows 7 X64)为例介绍一个已加载卷的VPB内容,实验时我们插入一个FAT32格式的SD卡,系统会自动挂载。下面来枚举FastFat文件系统队列中所有的逻辑卷:

    0: kd> ?? fastfat!FatData
    struct _FAT_DATA
       +0x000 NodeTypeCode     : 0x500
       +0x002 NodeByteSize     : 304
       +0x008 LazyWriteThread  : (null)
       +0x010 VcbQueue         : _LIST_ENTRY [ 0xfffffa80`047a8b68 - 0xfffffa80`047a8b68 ]
       +0x020 DriverObject     : 0xfffffa80`05809e70 _DRIVER_OBJECT
       +0x028 DiskFileSystemDeviceObject : 0xfffffa80`04842880
       +0x030 CdromFileSystemDeviceObject : 0xfffffa80`05bf6890
       +0x038 Resource         : _ERESOURCE
       +0x0a0 OurProcess       : 0xfffffa80`036edb30 _KPROCESS
       +0x0a8 NumberProcessors : 2
       +0x0ac ChicagoMode      : 0y1
       +0x0ac FujitsuFMR       : 0y0
       +0x0ac AsyncCloseActive : 0y0
       +0x0ac ShutdownStarted  : 0y0
       +0x0ac CodePageInvariant : 0y1
       +0x0ac HighAsync        : 0y0
       +0x0ac HighDelayed      : 0y0
       +0x0b0 AsyncCloseCount  : 0
       +0x0b8 AsyncCloseList   : _LIST_ENTRY [ 0xfffff880`070e3578 - 0xfffff880`070e3578 ]
       +0x0c8 DelayedCloseCount : 1
       +0x0d0 DelayedCloseList : _LIST_ENTRY [ 0xfffff8a0`0b4315b0 - 0xfffff8a0`0b4315b0 ]
       +0x0e0 FatCloseItem     : 0xfffffa80`057e8480 _IO_WORKITEM
       +0x0e8 GeneralSpinLock  : 0
       +0x0f0 CacheManagerCallbacks : _CACHE_MANAGER_CALLBACKS
       +0x110 CacheManagerNoOpCallbacks : _CACHE_MANAGER_CALLBACKS

    0: kd> !list "-t nt!_LIST_ENTRY.Flink -e -x \"dd @$extret l4;  dt fastfat!_VCB @$extret-0x58\" 0xfffffa80`047a8b68"
    dd @$extret l4;  dt fastfat!_VCB @$extret-0x58
    fffffa80`047a8b68  070e34d0 fffff880 070e34d0 fffff880
       +0x000 VolumeFileHeader : _FSRTL_ADVANCED_FCB_HEADER
       +0x058 VcbLinks         : _LIST_ENTRY [ 0xfffff880`070e34d0 - 0xfffff880`070e34d0 ]
       +0x068 TargetDeviceObject : 0xfffffa80`053c7040 _DEVICE_OBJECT
       +0x070 Vpb              : 0xfffffa80`047e8830 _VPB
       +0x078 VcbState         : 0x101002
       +0x07c VcbCondition     : 1 ( VcbGood )
       +0x080 RootDcb          : 0xfffffa80`0480b940 _FCB
       +0x088 NumberOfWindows  : 2
       +0x090 Windows          : 0xfffff8a0`0b4b8530 _FAT_WINDOW
       +0x098 CurrentWindow    : 0xfffff8a0`0b4b853c _FAT_WINDOW
       +0x0a0 DirectAccessOpenCount : 0
       +0x0a4 ShareAccess      : _SHARE_ACCESS
       +0x0c0 OpenFileCount    : 1
       +0x0c4 ReadOnlyCount    : 1
       +0x0c8 InternalOpenCount : 2
       +0x0cc ResidualOpenCount : 2
       +0x0d0 Bpb              : BIOS_PARAMETER_BLOCK
       +0x100 First0x24BytesOfBootSector : (null)
       +0x108 AllocationSupport : <unnamed-tag>
       +0x128 DirtyFatMcb      : _LARGE_MCB
       +0x148 BadBlockMcb      : _LARGE_MCB
       +0x168 FreeClusterBitMap : _RTL_BITMAP
       +0x178 FreeClusterBitMapMutex : _FAST_MUTEX
       +0x1b0 Resource         : _ERESOURCE
       +0x218 ChangeBitMapResource : _ERESOURCE
       +0x280 VirtualVolumeFile : 0xfffffa80`0480aa10 _FILE_OBJECT
       +0x288 SectionObjectPointers : _SECTION_OBJECT_POINTERS
       +0x2a0 ClusterHint      : 2
       +0x2a8 CurrentDevice    : 0xfffffa80`047e8cd0 _DEVICE_OBJECT
       +0x2b0 VirtualEaFile    : (null)
       +0x2b8 EaFcb            : (null)
       +0x2c0 FileObjectWithVcbLocked : (null)
       +0x2c8 DirNotifyList    : _LIST_ENTRY [ 0xfffffa80`047a8dd8 - 0xfffffa80`047a8dd8 ]
       +0x2d8 NotifySync       : 0xfffffa80`056fec30
       +0x2e0 DirectoryFileCreationMutex : _FAST_MUTEX
       +0x318 VerifyThread     : (null)
       +0x320 CleanVolumeDpc   : _KDPC
       +0x360 CleanVolumeTimer : _KTIMER
       +0x3a0 LastFatMarkVolumeDirtyCall : _LARGE_INTEGER 0x0
       +0x3a8 Statistics       : 0xfffffa80`05b5ec80 _FILE_SYSTEM_STATISTICS
       +0x3b0 Tunnel           : TUNNEL
       +0x408 ChangeCount      : 0
       +0x410 SwapVpb          : 0xfffffa80`05b5ec10 _VPB
       +0x418 AsyncCloseList   : _LIST_ENTRY [ 0xfffffa80`047a8f28 - 0xfffffa80`047a8f28 ]
       +0x428 DelayedCloseList : _LIST_ENTRY [ 0xfffff8a0`0b4315c0 - 0xfffff8a0`0b4315c0 ]
       +0x438 AdvancedFcbHeaderMutex : _FAST_MUTEX
       +0x470 CloseContext     : 0xfffff8a0`0b553a90 CLOSE_CONTEXT
       +0x478 CloseContextCount : 1

    当前系统中只有一个FAT32卷,即我们刚插入的SD卡。下面打印出VPB的内容:

    0: kd> dt _VPB 0xfffffa80`047e8830
    nt!_VPB
       +0x000 Type             : 10
       +0x002 Size             : 96
       +0x004 Flags            : 1
       +0x006 VolumeLabelLength : 0x10
       +0x008 DeviceObject     : 0xfffffa80`047a8970 _DEVICE_OBJECT
       +0x010 RealDevice       : 0xfffffa80`047e8cd0 _DEVICE_OBJECT
       +0x018 SerialNumber     : 0x51e712c6
       +0x01c ReferenceCount   : 2
       +0x020 VolumeLabel      : [32]  "CANON_DC"

    Vpb->Flags显示卷是已挂载的,即标志位VBP_MOUNTED已被设置。从Vpb内容可知,此SD卡的卷标为"CANON_DC",实际上它就是CANON相机的存储卡。DeviceObject,即文件系统逻辑卷设备为0xfffffa80`047a8970;RealDevice,即存储栈上最底层的设备为0xfffffa80`047e8cd0

    0: kd> !devobj 0xfffffa80`047e8cd0
    Device object (fffffa80047e8cd0) is for:
    HarddiskVolume17 \Driver\volmgr DriverObject fffffa8004086e70
    Current Irp 00000000 RefCount 2 Type 00000007 Flags 00003050
    Vpb fffffa80047e8830 Dacl fffff9a109ae12b0 DevExt fffffa80047e8e20 DevObjExt fffffa80047e8f88 Dope fffffa800480c840 DevNode fffffa80047e8a00
    ExtensionFlags (0000000000) 
    AttachedDevice (Upper) fffffa80047e5ac0 \Driver\fvevol
    Device queue is not busy.

    0: kd> !devobj 0xfffffa80`047a8970
    Device object (fffffa80047a8970) is for:
      \FileSystem\fastfat DriverObject fffffa8005809e70
    Current Irp 00000000 RefCount 0 Type 00000008 Flags 00000000
    DevExt fffffa80047a8ac0 DevObjExt fffffa80047a8f90
    ExtensionFlags (0x00000800) 
                                 Unknown flags 0x00000800

    0: kd> dt _DEVICE_OBJECT 0xfffffa80`047e8cd0
    nt!_DEVICE_OBJECT
       +0x000 Type             : 3
       +0x002 Size             : 0x2b8
       +0x004 ReferenceCount   : 2
       +0x008 DriverObject     : 0xfffffa80`04086e70 _DRIVER_OBJECT
       +0x010 NextDevice       : 0xfffffa80`045ed440 _DEVICE_OBJECT
       +0x018 AttachedDevice   : 0xfffffa80`047e5ac0 _DEVICE_OBJECT
       +0x020 CurrentIrp       : (null)
       +0x028 Timer            : (null)
       +0x030 Flags            : 0x3050
       +0x034 Characteristics  : 1
       +0x038 Vpb              : 0xfffffa80`047e8830 _VPB
       +0x040 DeviceExtension  : 0xfffffa80`047e8e20
       +0x048 DeviceType       : 7
       +0x04c StackSize        : 9 ''
       +0x050 Queue            : <unnamed-tag>
       +0x098 AlignmentRequirement : 0
       +0x0a0 DeviceQueue      : _KDEVICE_QUEUE
       +0x0c8 Dpc              : _KDPC
       +0x108 ActiveThreadCount : 0
       +0x110 SecurityDescriptor : 0xfffff8a0`099c1210
       +0x118 DeviceLock       : _KEVENT
       +0x130 SectorSize       : 0x200
       +0x132 Spare1           : 1
       +0x138 DeviceObjectExtension : 0xfffffa80`047e8f88 _DEVOBJ_EXTENSION
       +0x140 Reserved         : (null)

    0: kd> dt _DEVICE_OBJECT 0xfffffa80`047a8970
    nt!_DEVICE_OBJECT
       +0x000 Type             : 3
       +0x002 Size             : 0x620
       +0x004 ReferenceCount   : 0
       +0x008 DriverObject     : 0xfffffa80`05809e70 _DRIVER_OBJECT
       +0x010 NextDevice       : 0xfffffa80`05bf6890 _DEVICE_OBJECT
       +0x018 AttachedDevice   : 0xfffffa80`0480ab50 _DEVICE_OBJECT
       +0x020 CurrentIrp       : (null)
       +0x028 Timer            : (null)
       +0x030 Flags            : 0
       +0x034 Characteristics  : 0
       +0x038 Vpb              : (null)
       +0x040 DeviceExtension  : 0xfffffa80`047a8ac0
       +0x048 DeviceType       : 8
       +0x04c StackSize        : 12 ''
       +0x050 Queue            : <unnamed-tag>
       +0x098 AlignmentRequirement : 0
       +0x0a0 DeviceQueue      : _KDEVICE_QUEUE
       +0x0c8 Dpc              : _KDPC
       +0x108 ActiveThreadCount : 0
       +0x110 SecurityDescriptor : (null)
       +0x118 DeviceLock       : _KEVENT
       +0x130 SectorSize       : 0x200
       +0x132 Spare1           : 1
       +0x138 DeviceObjectExtension : 0xfffffa80`047a8f90 _DEVOBJ_EXTENSION
       +0x140 Reserved         : (null)

    Vcb->Vpb 与Vcb->Vbp->RealDevice->Vpb是一致的。下面再来研究一下存储设备栈的最上层的DeviceObject,即Vcb->TargetDeviceObject (0xfffffa80`053c7040):

    1: kd> !devobj 0xfffffa80`053c7040
    Device object (fffffa80053c7040) is for:
      \Driver\volsnap DriverObject fffffa80045c79b0
    Current Irp 00000000 RefCount 0 Type 00000007 Flags 00002010
    DevExt fffffa80053c7190 DevObjExt fffffa80053c7d38
    ExtensionFlags (0x00000800) 
                                 Unknown flags 0x00000800
    AttachedTo (Lower) fffffa80047e5ac0 \Driver\fvevol
    Device queue is not busy.
    1: kd> dt _DEVICE_OBJECT 0xfffffa80`053c7040
    nt!_DEVICE_OBJECT
       +0x000 Type             : 3
       +0x002 Size             : 0xcf8
       +0x004 ReferenceCount   : 0
       +0x008 DriverObject     : 0xfffffa80`045c79b0 _DRIVER_OBJECT
       +0x010 NextDevice       : 0xfffffa80`04649060 _DEVICE_OBJECT
       +0x018 AttachedDevice   : (null)
       +0x020 CurrentIrp       : (null)
       +0x028 Timer            : (null)
       +0x030 Flags            : 0x2010
       +0x034 Characteristics  : 0x101
       +0x038 Vpb              : (null)
       +0x040 DeviceExtension  : 0xfffffa80`053c7190
       +0x048 DeviceType       : 7
       +0x04c StackSize        : 11 ''
       +0x050 Queue            : <unnamed-tag>
       +0x098 AlignmentRequirement : 0
       +0x0a0 DeviceQueue      : _KDEVICE_QUEUE
       +0x0c8 Dpc              : _KDPC
       +0x108 ActiveThreadCount : 0
       +0x110 SecurityDescriptor : (null)
       +0x118 DeviceLock       : _KEVENT
       +0x130 SectorSize       : 0x200
       +0x132 Spare1           : 0
       +0x138 DeviceObjectExtension : 0xfffffa80`053c7d38 _DEVOBJ_EXTENSION
       +0x140 Reserved         : (null)

    有没有觉察出有什么异常状况?TargetDeviceObject的DeviceType为FILE_DEVICE_DISK (0x07),但它的Vpb项却是空的。根据上面对IoCreateDevice的分析,对所有4种设备类型,在创建设备时都会分配VPB,难道是被释放了?

    经过研究才发现其中另有蹊跷。TargetDeviceObject是由VolSnap创建的,VolSnap是Windows Volume Shadow Copy Service的核心组件,VolSnap为每个卷设备都创建Fdo设备并将其加入设备栈中。但蹊跷的地方是,在创建设备对象时,VolSnap使用的DeviceType却是FILE_DEVICE_UNKNOWN (0x22),并在将设备加入存储设备栈后,再将TargetDeviceObject的DeviceType从FILE_DEVICE_UNKOWN改为FILE_DEVICE_DISK的,所以TargetDeviceObject的Vpb项为空。

    有兴趣地话可以反汇编一下volsnap!VolSnapAddDevice的代码。

    (to be continued)

  • 文件系统开发之注册篇

    文件系统是一类特殊的内核驱动,主要负责数据流(即文件)的管理。用户的视图所显示出的是各色各样的文件和目录,但最底层的存储设备则是以扇区为单位的连续的存储实体(如硬盘,光盘等),文件系统便处于二者中间,负责管理文件/目录在设备存储中的解析与定位,因此也可以将文件系统看作是一个转换机。

    不同类型的驱动都有不同的功能实现,并且在驱动加载时都要向操作系统注册,报告它是个什么类型的驱动及要管理什么样的设备等等。就如一个电饭煲驱动,要实现的功能大致有开关电源,温控及定时等功能,加载时可以向系统注册为烹调类设备的驱动。文件系统驱动也大致如此,只是功能更复杂,与内核的关系也更紧密,特别是和Virtual Memory, Cache Management及I/O Subsystem的关系和交互。

    不同的操作系统对文件系统的功能实现有不同的规范和要求,但从最基本的需求出发,各种文件系统的功能大致一样,只是系统的接口不同而已。这里只介绍Windows和Linux操作系统上文件系统的开发。Windows平台上称之为IFS (Installable File System),Linux系统则为VFS(Virtual Filesystem,也可解释为Virtual Filesystem Switch)。

    此篇文章只着重于文件系统的注册过程,至于卷的挂载(Mount)操作将在以后的文章中再作讲述。

     

    Windows平台上的文件系统注册过程:

    Windows内核(I/O Manager)提供了一组内核支持程序(kernel support routine)来完成文件系统的注册和移除。

    VOID
      IoRegisterFileSystem(
        IN OUT PDEVICE_OBJECT  DeviceObject   /* Deviect object representitive of file system*/
        );

    VOID
      IoUnregisterFileSystem(
        IN OUT PDEVICE_OBJECT  DeviceObject  /* Deviect object representitive of file system*/
        );

    注册调用很简单,以Ext2Fsd为例,Ext2Fsd针对光盘介质和磁盘介质的设备分别创建CdromdevObject及Diskdevobject,然后分别注册。

    NTSTATUS
    DriverEntry (
        IN PDRIVER_OBJECT   DriverObject,
        IN PUNICODE_STRING  RegistryPath
    )
    {
     

        PDEVICE_OBJECT              DiskdevObject = NULL;
        PDEVICE_OBJECT              CdromdevObject = NULL;

        ......

        /* create Ext2Fsd cdrom fs deivce */
        RtlInitUnicodeString(&DeviceName, CDROM_NAME); 
        Status = IoCreateDevice(  DriverObject,  0,  DeviceName,
                                                       
    FILE_DEVICE_CD_ROM_FILE_SYSTEM,
                                                        0,  FALSE,  &CdromdevObject );

        if (!NT_SUCCESS(Status)) {
            DEBUG(DL_ERR, ( "IoCreateDevice cdrom device object error.\n"));
            goto errorout;
        }

        /* create Ext2Fsd disk fs deivce */
        RtlInitUnicodeString(&DeviceName, DEVICE_NAME);
        Status = IoCreateDevice(  DriverObject,  0, &DeviceName, 
                                                       FILE_DEVICE_DISK_FILE_SYSTEM
                                                       0,  FALSE,  &DiskdevObject );

        if (!NT_SUCCESS(Status)) {
            DEBUG(DL_ERR, ( "IoCreateDevice disk deviceobject error.\n"));
            goto errorout;
        }

        ......

        /* register file system devices for disk and cdrom */
       
    IoRegisterFileSystem(DiskdevObject); 
        ObReferenceObject(DiskdevObject);

        IoRegisterFileSystem(CdromdevObject);
        ObReferenceObject(CdromdevObject);

        ......

        return status;
    }

    你或许会迷惑为什么Ext2Fsd会注册两次(针对cdrom设备和disk设备)?下面我们就来探讨IoRegisterFileSystem的内部实现。

    Windows I/O Manager 管理着4个队列:每个队列都管理不同类别的所有文件系统:

    以Windows 7 X64为例:

    0: kd> x nt!iop*FileSystemQueueHead
    fffff800`04076200 nt!IopCdRomFileSystemQueueHead = <no type information>
    fffff800`04076210 nt!IopDiskFileSystemQueueHead = <no type information>
    fffff800`040761e0 nt!IopTapeFileSystemQueueHead = <no type information>
    fffff800`040761f0 nt!IopNetworkFileSystemQueueHead = <no type information>

    通过名字就能看出每个队列所管理的文件系统的类型,所以当I/O Manager新发现某一类型存储设备时,就只从相应的队列中依次调用队列上的文件系统驱动来识别存储设备上的文件卷,而不是调用注册于系统中的所有文件系统。

    Ext2Fsd除了支持磁盘介质设备外,还可以支持光盘设备,所以要针对不同的储存设备类型分别注册。

    在Windbg中可以很方便地罗列出所有挂载的文件系统,以IopDiskFileSystemQueueHead为例:

    0: kd> !list "-t nt!_LIST_ENTRY.Flink -e -x \"dd @$extret l4;  !devobj @$extret-0x50\" poi(nt!IopDiskFileSystemQueueHead)"
    dd @$extret l4;  !devobj @$extret-0x50
    fffffa80`04839450  044e3d50 fffffa80 04076210 fffff800
    Device object (fffffa8004839400) is for:
     Ext2Fsd \FileSystem\Ext2Fsd DriverObject fffffa800482d210
    Current Irp 00000000 RefCount 1 Type 00000008 Flags 00000040
    Dacl fffff9a100324650 DevExt 00000000 DevObjExt fffffa8004839550
    ExtensionFlags (0x00000800) 
                                 Unknown flags 0x00000800
    AttachedDevice (Upper) fffffa8004843040 \FileSystem\FltMgr
    Device queue is not busy.

    dd @$extret l4;  !devobj @$extret-0x50
    fffffa80`044e3d50  044e22b0 fffffa80 04839450 fffffa80
    Device object (fffffa80044e3d00) is for:
    ExFatRecognizer \FileSystem\Fs_Rec DriverObject fffffa80044e2060
    Current Irp 00000000 RefCount 1 Type 00000008 Flags 00000040
    Dacl fffff9a100324650 DevExt fffffa80044e3e50 DevObjExt fffffa80044e3e60
    ExtensionFlags (0x00000800) 
                                 Unknown flags 0x00000800
    Device queue is not busy.

    dd @$extret l4;  !devobj @$extret-0x50
    fffffa80`044e22b0  044e24e0 fffffa80 044e3d50 fffffa80
    Device object (fffffa80044e2260) is for:
    FatDiskRecognizer \FileSystem\Fs_Rec DriverObject fffffa80044e2060
    Current Irp 00000000 RefCount 1 Type 00000008 Flags 00000040
    Dacl fffff9a100324650 DevExt fffffa80044e23b0 DevObjExt fffffa80044e23c0
    ExtensionFlags (0x00000800) 
                                 Unknown flags 0x00000800
    Device queue is not busy.

    dd @$extret l4;  !devobj @$extret-0x50
    fffffa80`044e24e0  0448d7f0 fffffa80 044e22b0 fffffa80
    Device object (fffffa80044e2490) is for:
    UdfsDiskRecognizer \FileSystem\Fs_Rec DriverObject fffffa80044e2060
    Current Irp 00000000 RefCount 1 Type 00000008 Flags 00000040
    Dacl fffff9a100324650 DevExt fffffa80044e25e0 DevObjExt fffffa80044e25f0
    ExtensionFlags (0x00000800) 
                                 Unknown flags 0x00000800
    Device queue is not busy.

    dd @$extret l4;  !devobj @$extret-0x50
    fffffa80`0448d7f0  036660b0 fffffa80 044e24e0 fffffa80
    Device object (fffffa800448d7a0) is for:
    Ntfs \FileSystem\Ntfs DriverObject fffffa800448d9c0
    Current Irp 00000000 RefCount 1 Type 00000008 Flags 00000040
    Dacl fffff9a100324650 DevExt 00000000 DevObjExt fffffa800448d8f0
    ExtensionFlags (0x00000800) 
                                 Unknown flags 0x00000800
    AttachedDevice (Upper) fffffa80044d2040 \FileSystem\FltMgr
    Device queue is not busy.

    dd @$extret l4;  !devobj @$extret-0x50
    fffffa80`036660b0  04076210 fffff800 0448d7f0 fffffa80
    Device object (fffffa8003666060) is for:
    RawDisk \FileSystem\RAW DriverObject fffffa8003667770
    Current Irp 00000000 RefCount 1 Type 00000008 Flags 00000050
    Dacl fffff9a100324650 DevExt 00000000 DevObjExt fffffa80036661b0
    ExtensionFlags (0x00000800) 
                                 Unknown flags 0x00000800
    AttachedDevice (Upper) fffffa800371b860 \FileSystem\FltMgr
    Device queue is not busy.

    可以看出,最晚注册的文件系统Ext2Fsd则被放在了双向链表的链首位置,即第一个;最早注册的Ntfs则是在链尾。不过有种例外,如果设备被指定DO_LOW_PRIORITY_FILESYSTEM标志,IoRegisterFileSystem会将此文件系统挂载至链表尾部。

    在Ntfs及Ext2Fsd之间,还有Windows系统本身提供的3个File system recognizer,均在模块Fs_Rec(即 fs_rec.sys)中实现。Recognizer是文件系统驱动中比较特殊的一类,它的功能很简单:如果发现有它能识别的卷设备,它则会加载相应的文件系统驱动来挂载此卷并卸载自己。Recognizer之所以存在的唯一目的就是节省内存资源。Recognizer驱动相比其文件系统驱动,代码量要小得多,比如,集多个Recognizer与一身的fs_rec.sys模块只有16K,而exfat及fastfat驱动在100K-200K之间,Ntfs更有1.5M之大。如果直接加载文件系统驱动,而实际系统中又没有此文件系统所管理的储存设备的话,那驱动程序所占用的内存就完全是资源的浪废。

    下面做个实验,插入一个FAT32格式的SD卡后,再来观察一下IopDiskFileSystemQueueHead链的变化:

    0: kd> !list "-t nt!_LIST_ENTRY.Flink -e -x \"dd @$extret l4;  !devobj @$extret-0x50\" poi(nt!IopDiskFileSystemQueueHead)"
    dd @$extret l4;  !devobj @$extret-0x50
    fffffa80`048428d0  04839450 fffffa80 04076210 fffff800
    Device object (fffffa8004842880) is for:
    Fat \FileSystem\fastfat DriverObject fffffa8005809e70
    Current Irp 00000000 RefCount 1 Type 00000008 Flags 00000040
    Dacl fffff9a100324650 DevExt 00000000 DevObjExt fffffa80048429d0
    ExtensionFlags (0x00000800) 
                                 Unknown flags 0x00000800
    AttachedDevice (Upper) fffffa80047a6870 \FileSystem\FltMgr
    Device queue is not busy.

    dd @$extret l4;  !devobj @$extret-0x50
    fffffa80`04839450  044e3d50 fffffa80 048428d0 fffffa80
    Device object (fffffa8004839400) is for:
     Ext2Fsd \FileSystem\Ext2Fsd DriverObject fffffa800482d210
    Current Irp 00000000 RefCount 1 Type 00000008 Flags 00000040
    Dacl fffff9a100324650 DevExt 00000000 DevObjExt fffffa8004839550
    ExtensionFlags (0x00000800) 
                                 Unknown flags 0x00000800
    AttachedDevice (Upper) fffffa8004843040 \FileSystem\FltMgr
    Device queue is not busy.

    dd @$extret l4;  !devobj @$extret-0x50
    fffffa80`044e3d50  044e24e0 fffffa80 04839450 fffffa80
    Device object (fffffa80044e3d00) is for:
     ExFatRecognizer \FileSystem\Fs_Rec DriverObject fffffa80044e2060
    Current Irp 00000000 RefCount 1 Type 00000008 Flags 00000040
    Dacl fffff9a100324650 DevExt fffffa80044e3e50 DevObjExt fffffa80044e3e60
    ExtensionFlags (0x00000800) 
                                 Unknown flags 0x00000800
    Device queue is not busy.

    dd @$extret l4;  !devobj @$extret-0x50
    fffffa80`044e24e0  0448d7f0 fffffa80 044e3d50 fffffa80
    Device object (fffffa80044e2490) is for:
     UdfsDiskRecognizer \FileSystem\Fs_Rec DriverObject fffffa80044e2060
    Current Irp 00000000 RefCount 1 Type 00000008 Flags 00000040
    Dacl fffff9a100324650 DevExt fffffa80044e25e0 DevObjExt fffffa80044e25f0
    ExtensionFlags (0x00000800) 
                                 Unknown flags 0x00000800
    Device queue is not busy.

    dd @$extret l4;  !devobj @$extret-0x50
    fffffa80`0448d7f0  036660b0 fffffa80 044e24e0 fffffa80
    Device object (fffffa800448d7a0) is for:
     Ntfs \FileSystem\Ntfs DriverObject fffffa800448d9c0
    Current Irp 00000000 RefCount 1 Type 00000008 Flags 00000040
    Dacl fffff9a100324650 DevExt 00000000 DevObjExt fffffa800448d8f0
    ExtensionFlags (0x00000800) 
                                 Unknown flags 0x00000800
    AttachedDevice (Upper) fffffa80044d2040 \FileSystem\FltMgr
    Device queue is not busy.

    dd @$extret l4;  !devobj @$extret-0x50
    fffffa80`036660b0  04076210 fffff800 0448d7f0 fffffa80
    Device object (fffffa8003666060) is for:
     RawDisk \FileSystem\RAW DriverObject fffffa8003667770
    Current Irp 00000000 RefCount 1 Type 00000008 Flags 00000050
    Dacl fffff9a100324650 DevExt 00000000 DevObjExt fffffa80036661b0
    ExtensionFlags (0x00000800) 
                                 Unknown flags 0x00000800
    AttachedDevice (Upper) fffffa800371b860 \FileSystem\FltMgr
    Device queue is not busy.

    FatDiskRecognizer发现FAT32格式的文件卷后会加载fastfat驱动,然后将自己卸载:

    新增加的Fat设备:

            Fat \FileSystem\fastfat DriverObject fffffa8005809e70

    消失的FatDiskRecognizer:

            FatDiskRecognizer \FileSystem\Fs_Rec DriverObject fffffa80044e2060

    Windows I/O Manager还提供文件系统注册事件的通知,这部分就是在IoRegisterFileSystem函数中实现的。IoRegisterFileSystem在将文件系统的Device Object挂入相应的队列中以后,还会调用所有的NotificationRoutine以通知其注册者。(注册者,即对此事件感兴趣的驱动。一般来说,对文件系统注册感兴趣的只有file system filter driver。)Windows 内核提供了3个函数用于文件系统注册事件的通知:

    NTSTATUS
      IoRegisterFsRegistrationChange(
        IN PDRIVER_OBJECT  DriverObject,
        IN PDRIVER_FS_NOTIFICATION  DriverNotificationRoutine
        );

    NTSTATUS
      IoRegisterFsRegistrationChangeEx(
        IN PDRIVER_OBJECT  DriverObject,
        IN PDRIVER_FS_NOTIFICATION  DriverNotificationRoutine
        );  /* available from Win2k SP4 and its successor OS */

    VOID 
      IoUnregisterFsRegistrationChange(
        IN PDRIVER_OBJECT  DriverObject,
        IN PDRIVER_FS_NOTIFICATION  DriverNotificationRoutine
        );

    关于这三个函数的使用请参阅DDK文档,在此不做熬述。

     

    Linux平台上的文件系统注册过程:

    Linux内核(VFS:Virtual Filesystem)提供了两个支持例程分别进行文件系统的注册和移除:

    int register_filesystem(struct file_system_type * fs)
    int unregister_filesystem(struct file_system_type * fs)

    struct file_system_type结构就代表一个文件系统驱动,此结构由文件系统驱动程序各自定义,其结构体如下:

    struct file_system_type {
        const char *name;
        int fs_flags;
        int (*get_sb) (struct file_system_type *, int,
                   const char *, void *, struct vfsmount *);
        void (*kill_sb) (struct super_block *);
        struct module *owner;
        struct file_system_type * next;
        struct list_head fs_supers;

        struct lock_class_key s_lock_key;
        struct lock_class_key s_umount_key;

        struct lock_class_key i_lock_key;
        struct lock_class_key i_mutex_key;
        struct lock_class_key i_mutex_dir_key;
        struct lock_class_key i_alloc_sem_key;
    };

    结构成员说明:

    • name: 文件系统名称:如ext3,ext4,nfs,nfs4,lustre等
    • fs_flags: 文件系统相关标志位,目前有如下取值:
      #define FS_REQUIRES_DEV 1            /* 此文件系统适用于卷设备,如硬盘分区等
                                                                      不适用于网络文件系统 */
      #define FS_BINARY_MOUNTDATA 2  /* 此文件系统有单独的mount tool, 参数的传递有
                                                                      自己的格式,此标志目前用于coda, FUSE, nfs,
                                                                      smbfs, ncpfs */
      #define FS_HAS_SUBTYPE 4                /* 仅 FUSE使用,支持多种user mode文件系统 */
      #define FS_REVAL_DOT    16384         /* 仅NFS使用,"."及".."会过期或失效  */
      #define FS_RENAME_DOES_D_MOVE    32768    /* 仅NFS使用,文件系统在.rename操作
                                                                                        已处理 d_move() 情形 */
    • get_sb:  挂载新卷时的回调函数,由VFS调用,多数情况是用户进行了mount(2)操作
    • kill_sb:   卸载已挂载卷时的回调函数,一般由用户进行umount(2)操作而触发
    • owner:   文件系统所在模块(module)
    • next:      供VFS使用,组成文件系统驱动的单向列表
    • fs_supers: 供VFS使用,双向链表链首,用以管理此文件系统所识别的所有的文件卷的
                      超级块( super_block)
    • s_lock_key … i_alloc_sem_key: 用于lockdep检查

    看一下kernel中ext4的相关代码:

    static struct file_system_type ext4_fs_type = {
        .owner        = THIS_MODULE,
        .name        = "ext4",
        .get_sb        = ext4_get_sb,
        .kill_sb    = kill_block_super,
        .fs_flags    = FS_REQUIRES_DEV,
    };

    /* 如果ext3模块没被定义,ext4文件系统将默认接管ext3文件卷 */
    #if !defined(CONTIG_EXT3_FS) && !defined(CONFIG_EXT3_FS_MODULE) && defined(CONFIG_EXT4_USE_FOR_EXT23)

    static struct file_system_type ext3_fs_type = {
        .owner        = THIS_MODULE,
        .name        = "ext3",
        .get_sb        = ext4_get_sb,
        .kill_sb    = kill_block_super,
        .fs_flags    = FS_REQUIRES_DEV,
    };

    static inline void register_as_ext3(void)
    {
        int err = register_filesystem(&ext3_fs_type);
        if (err)
            printk(KERN_WARNING
                   "EXT4-fs: Unable to register as ext3 (%d)\n", err);
    }

    static inline void unregister_as_ext3(void)
    {
        unregister_filesystem(&ext3_fs_type);
    }
    MODULE_ALIAS("ext3");
    #endif

    ext4模块的初始化函数(init_ext4_fs)会调用register_filesystem来注册文件系统为ext4.

    static int __init init_ext4_fs(void)
    {
        int err;

    ......

        register_as_ext3();
        err = register_filesystem(&ext4_fs_type);
        if (err)
            goto out;
    ......

            return err;
    }

    然后再研究一下register_filesystem及unregister_filesystem的代码($kernel/vfs/filesystems.c):

    static struct file_system_type *file_systems;
    static DEFINE_RWLOCK(file_systems_lock);

    /**
    *    register_filesystem - register a new filesystem
    *    @fs: the file system structure
    *
    *    Adds the file system passed to the list of file systems the kernel
    *    is aware of for mount and other syscalls. Returns 0 on success,
    *    or a negative errno code on an error.
    *
    *    The &struct file_system_type that is passed is linked into the kernel
    *    structures and must not be freed until the file system has been
    *    unregistered.
    */
     
    int register_filesystem(struct file_system_type * fs)
    {
        int res = 0;
        struct file_system_type ** p;

        BUG_ON(strchr(fs->name, '.'));
        if (fs->next)
            return -EBUSY;
        INIT_LIST_HEAD(&fs->fs_supers);
        write_lock(&file_systems_lock);
        p = find_filesystem(fs->name, strlen(fs->name));
        if (*p)
            res = -EBUSY;
        else
            *p = fs;
        write_unlock(&file_systems_lock);
        return res;
    }

    EXPORT_SYMBOL(register_filesystem);

    static struct file_system_type **find_filesystem(const char *name, unsigned len)
    {
        struct file_system_type **p;
        for (p=&file_systems; *p; p=&(*p)->next)
            if (strlen((*p)->name) == len &&
                strncmp((*p)->name, name, len) == 0)
                break;
        return p;
    }

    /**
    *    unregister_filesystem - unregister a file system
    *    @fs: filesystem to unregister
    *
    *    Remove a file system that was previously successfully registered
    *    with the kernel. An error is returned if the file system is not found.
    *    Zero is returned on a success.
    *   
    *    Once this function has returned the &struct file_system_type structure
    *    may be freed or reused.
    */
     
    int unregister_filesystem(struct file_system_type * fs)
    {
        struct file_system_type ** tmp;

        write_lock(&file_systems_lock);
        tmp = &file_systems;
        while (*tmp) {
            if (fs == *tmp) {
                *tmp = fs->next;
                fs->next = NULL;
                write_unlock(&file_systems_lock);
                return 0;
            }
            tmp = &(*tmp)->next;
        }
        write_unlock(&file_systems_lock);
        return -EINVAL;
    }

    通过代码可以得知,与Windows对文件系统驱动的管理不同的是,Linux只维护了一个全局的单向列表来管理所有的文件系统驱动,而且新文件系统默认被链接至链表末尾。

    Linux不允许同名的文件系统再次被注册,register_filesystem()每次都会调用find_filesystem()以检查同名的文件系统是不是已存在。Windows平台上因为是以DeviceObject为主体进行注册的,并不存在此项的检测,所以Windows允许多个同一卷类型的文件系统驱动的加载,但只有最晚加载的驱动才有效(默认无DO_LOW_PRIORITY_FILESYSTEM标志)。

    Pages:  1 2