啧,大佬飘过……文字较多,可以直接看最后面的
最近在捣腾发送IRP_MJ_CREATE打开文件,同时要能解 reparse point。几顿操作猛如虎之后,终于可以正常解析 reparse point了,然后就像检验一下,这整个流程和IoCreateFile的区别大不大。就在打开的时候,加了个 FILE_DELETE_ON_CLOSE标记,看看FileObject销毁后文件是否被删除。 FileObject被销毁后,文件确实被删掉了。然后就突发奇想,试试直接发送IRP_MJ_READ,和IRP_MJ_WRITE,之后销毁 FileObject,文件是否依然可以被删掉。
结果当然是文件没有被删掉。看了一下 FileObject的引用计数。发现引用计数多了好几个。是代码有问题吗?应该不会犯这种错误。保险起见,还是检查了一下代码,再三确认代码没问题之后,唯一可疑的就是缓存管理器缓存了 FileObject。然后,在发送 IRP_MJ_READ的时候,关掉缓存,果然,这个时候, FileObject的引用计数正常,调用 ObDereferenceObject后文件马上就被删掉了。
但是微软提供的接口,IoCreateFile,NtReadFile,NtWriteFile就没有这个问题。在句柄被关掉之后,文件就被删掉了。为此,我还特意验证了一下。 IoCreateFile用 FILE_DELETE_ON_CLOSE打开文件后,再调用 NtReadFile, 或者NtWriteFile,都会引起 FileObject的引用计数递增。但是调用NtClose关闭句柄之后,文件马上就被删掉了。根据论坛之前的那篇《IRP操作文件填坑日记》来看,没有句柄,应该就是直接调用ObDereferenceObject来销毁 FileObject就行了的。然后我又用IrpCreateFile创建的 FileObject来生成一个句柄,再关掉句柄,而不是用 ObDereferenceObject 销毁 FileObject。这次,文件马上就被删掉了。也就是说, NtClose除了销毁句柄之外,还做了些其他的事情,之后才是调用 ObDereferenceObject销毁对象。
仔细翻了翻WRK,发现 NtClose会调用Object的OkayToCloseProcedure,然后就是一些其他的清理,最后就是调用 ObDereferenceObject。但是 FileObject没有 OkayToCloseProcedure 这个回调啊。这就很奇怪了。为此我还恶补了一下windows的缓存管理。
ObDereferenceObject会在对象的引用计数变为0之后,调用对象的删除回调,也就是DeleteProcedure,对应文件对象的API为IopDeleteFile,然后就没了。那么这个 IopDeleteFile又做了些什么呢?检查一下 FileObject->Flags标记,看看有没有创建句柄,或者取消打开,如果没有FO_HANDLE_CREATED、FO_FILE_OPEN_CANCELLED,就调用IopCloseFile,之后就是构造IRP_MJ_CLOSE的IRP,向设备栈发送,然后就是一些清理操作。再看看 IopCloseFile做了些什么。检查一下文件对象对应的设备对象,是否支持FastIoUnlockAll,如果支持,就调用,不支持,就构造主命令为IRP_MJ_LOCK_CONTROL,次命令为IRP_MN_UNLOCK_ALL的IRP,发送给设备栈。完了就是构造IRP_MJ_CLEANUP的IRP,发送给设备栈。
这一通操作下来,还是有点懵,然后就用FileSpy看看,到底NtClose多了些什么……一看发现就是多了个 IRP_MJ_CLEANUP。发送完这个IRP,后面的清理操作就自动进行了。诶?这有门儿啊,自己构造一个 IRP_MJ_CLEANUP的请求试试……果然,手动发送一次 IRP_MJ_CLEANUP,再销毁文件对象,就可以立即删掉文件了。
今天又研究了一下,NtClose的调用栈如图:
这个调用栈清楚的说明了 IRP_MJ_CLEANUP是何时发送的
总结如下:
1. 读写文件,因为缓存的问题,会导致文件对象的引用计数递增。可以在发送IRP的时候关掉缓存。但这与缓存的设计初衷不符
2. IRP操作文件,最好将文件对象转换为句柄,使用NtClose关掉,可以让操作系统帮我们做很多事情
3. IRP操作文件,读写之后,如果需要立即释放文件对象,可以手动构造 IRP_MJ_CLEANUP,再调用 ObDereferenceObject,或者参考上一条