文件系统的过滤与监控
11.5 文件系统卷设备的绑定
11.5.1 从IRP中获得VPB指针
? 回忆前面说到的文件系统控制请求中的挂载请求。主功能号为 IRP_MJ_FILE_SYSTEM_CONTROL,次功能号为 IRP_MN_MOUNT_VOLUME 的IRP。对这种挂载卷的IRP的处理,调用了函数 SfFsControlMountVolume 这个函数的代码。在其中实现了绑定文件系系统卷的功能。
? 从这个IRP的信息中,如何得到文件系统的卷设备呢? 在 irpSp->Parameters.MountVolume.Vpb->DeviceObject 中。
? VPB是一个卷参数块,一个重要的数据结构。它在这里的主要作用是把实际的存储媒介设备对象和文件系统上的卷设备对象联系起来。
//从VPB中得到一个存储设备对象
storageStackDeviceObject = irpSp->Parameters.MountVolume.Vpb->RealDevice;
? 以后可以从这个存储设备对象再得到原来的VPB。这里己下存储设备,实际上是为了从存储设备中找到VPB,再找回文件系统的卷设备。为什么需要这样一个倒手过程呢?
? 这里的IRP是一个挂载请求,二文件系统的设备对象实际上是这个挂载请求完成之后才可以用的。因此在这个请求没有完成之前,irpSp->Parameters.MountVolume.Vpb->DeviceObject 是没有意义的,必须要等这个请求完成之后。但是在这个IRP传递过程中,VPB可能被修改。因此必须首先获得存储设备对象,再返回去。
11.5.2 设置完成函数并等待IRP完成
? 在前面的几个例子中,都讲过等待IRP完成的方法。首先拷贝当前栈空间,然后向下发送请求,但在此之前,要先给IRP分配一个完成函数。IRP一旦完成,完成函数将被调用。这样就可以在完成函数中得到文件系统的卷设备,并实施绑定过程。
? 完成函数的一个特点是处于Dispatch中断级,这是一个比较高的中断级别。总是,任何代买执行时,总是储在某个当前的中断级之前。某些系统调用只能在低级别的中断级执行。注意,如果一个调用可以在高处运行,那么在低处就可以,反之不行。
? 我们需要关心的只有 Passive 中断级 和 Dispatch 中断级,而且 Dispatch中断级的级别比较高,一般WDK的函数在帮助中都标明,如果标明 “irp level = PASSIVE”,那么就不能再Dispatch中断级的代码中调用他们了。
? 在实际编码中,一般都认为代码执行是由于应用程序或者上层调用而引发的,那么应该在Passive中断级;如果代码执行是由于下层硬件引发的,那么很有可能在Dispatch级。
? 以上只是极为粗略的便于记忆的理解方法,而实际的应用是这样的:所有的分发函数由于是上层发来的IRP而导致的调用,所以应该都在Passive中断级,在其中可以调用绝大多数系统调用。但是如果是网卡的OnReceive、硬盘读写完毕而返回的完成函数都有可能在Dispatch级。
? 下面是 SfFsCountrolMountVolume 的执行过程。这里没有绑定卷设备,而是把IRP发送下去,等待完成之后,再在完成函数中去绑定。
NTSTATUS
SfFsControlMountVolume (IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)
{PSFILTER_DEVICE_EXTENSION devExt = DeviceObject->DeviceExtension;PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation( Irp );PDEVICE_OBJECT newDeviceObject;PDEVICE_OBJECT storageStackDeviceObject;PSFILTER_DEVICE_EXTENSION newDevExt;NTSTATUS status;BOOLEAN isShadowCopyVolume;PFSCTRL_COMPLETION_CONTEXT completionContext;PAGED_CODE();ASSERT(IS_MY_DEVICE_OBJECT( DeviceObject ));ASSERT(IS_DESIRED_DEVICE_TYPE(DeviceObject->DeviceType));// 获得Vpb->RealDevice 保存下来storageStackDeviceObject = irpSp->Parameters.MountVolume.Vpb->RealDevice;//判断是否是卷影,后面再提status = SfIsShadowCopyVolume ( storageStackDeviceObject, &isShadowCopyVolume );//如果不打算绑定卷影,则跳过if (NT_SUCCESS(status) && isShadowCopyVolume &&!FlagOn(SfDebug,SFDEBUG_ATTACH_TO_SHADOW_COPIES)) {IoSkipCurrentIrpStackLocation( Irp );return IoCallDriver( devExt->AttachedToDeviceObject, Irp );}//预先生成过滤设备,虽然现在还不是绑定的时候status = IoCreateDevice( gSFilterDriverObject,sizeof( SFILTER_DEVICE_EXTENSION ) + gUserExtensionSize,NULL,DeviceObject->DeviceType,0,FALSE,&newDeviceObject );if (!NT_SUCCESS( status )) {KdPrint(( "SFilter!SfFsControlMountVolume: Error creating volume device object, status=%08x\n", status ));Irp->IoStatus.Information = 0;Irp->IoStatus.Status = status;IoCompleteRequest( Irp, IO_NO_INCREMENT );return status;}//填写设备拓展。这里将storageStackDeviceObject 保存在扩展中//以便于在完成函数中使用newDevExt = newDeviceObject->DeviceExtension;newDevExt->StorageStackDeviceObject = storageStackDeviceObject;newDevExt->TypeFlag = SFLT_POOL_TAG;RtlInitEmptyUnicodeString( &newDevExt->DeviceName, newDevExt->DeviceNameBuffer, sizeof(newDevExt->DeviceNameBuffer) );SfGetObjectName( storageStackDeviceObject, &newDevExt->DeviceName );
...
}
? 卷影是一种用于磁盘数据回复的特殊设备,对这类设备暂时没有深入的了解。但是知道该设备可以过滤也可以不过滤。
? 当完成函数被调用时,请求接输了。可以向完成函数中传递一个上下文指针来保存我们的信息,以便能够确定哪一次调用对应于哪一次完成。也可以使用一个等待事件。在晚上函数中设置这个事件,而在本函数中则等待这个事件,等待结束时候,这个请求就完成了。对于上一段省略的代码如下。
KEVENT waitEvent;//初始化事件KeInitializeEvent( &waitEvent, NotificationEvent, FALSE );IoCopyCurrentIrpStackLocationToNext ( Irp );//设置完成函数,并把事件的指针当作上下文传入IoSetCompletionRoutine( Irp,SfFsControlCompletion,&waitEvent, //context parameterTRUE,TRUE,TRUE );//发送IRP并等待事件完成status = IoCallDriver( devExt->AttachedToDeviceObject, Irp );if (STATUS_PENDING == status) {status = KeWaitForSingleObject( &waitEvent,Executive,KernelMode,FALSE,NULL );ASSERT( STATUS_SUCCESS == status );}...
? 这里注意设置完成函数的第三个参数,即完成函数中的Context指针。这里设置为事件的指针。上下文是传递信息到完成函数的接口。
NTSTATUS SfFsControlCompletion(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp,IN PVOID Context
)
{
UNREFERENCED_PARAMETER(DeviceObject);UNREFERENCED_PARAMETER(Irp);KeSetEvent((PKEVENT)Context,IO_NO_INCREMENT,FALSE);return STATUS_MORE_PROCESSING_REQUIRED;
}
? UNREFERENCED_PARAMETER 的意义在于去掉C编译器对于没有使用的参数所产生的一个警告。一般地说,内核代码会打开所有的警告以避免比较隐晦的错误。这样,我们一旦通知请求完成,之后就可以执行卷的绑定操作了。
11.5.3 卷挂载IRP完成后的工作
? 之所以不在完成函数里绑定设备,而非要等完成函数设置了事件之后再回来绑定是因为完成函数的中断级别过高。虽然在Dispatch级别可以使用 IoAttachDeviceToDeviceStack,但是sfilter使用了 ExAcquireFastMutex 等待这些不适宜在Dispatch级别执行。
? 用事件等待完成函数是一个通用的方法,但是在老版本可能会出现死锁问题。这里不考虑老版本的问题,因为当下连windows xp 都很少有了。
11.5.5 绑定卷的实现
? SfFsControlMountVolumeComplete 这个函数来最终实现卷的绑定。
SfFsControlMountVolumeComplete (IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp,IN PDEVICE_OBJECT NewDeviceObject)
{PVPB vpb;PSFILTER_DEVICE_EXTENSION newDevExt;PIO_STACK_LOCATION irpSp;PDEVICE_OBJECT attachedDeviceObject;NTSTATUS status;PAGED_CODE();newDevExt = NewDeviceObject->DeviceExtension;irpSp = IoGetCurrentIrpStackLocation( Irp );//获得我们之前保存的VPBvpb = newDevExt->StorageStackDeviceObject->Vpb;if (NT_SUCCESS( Irp->IoStatus.Status )) {//获得一个互斥体,以便我们可以原子地判断是否//绑定过一个卷设备,可以防止我们一个卷绑定两次ExAcquireFastMutex( &gSfilterAttachLock );//判断是否绑定过了if (!SfIsAttachedToDevice( vpb->DeviceObject, &attachedDeviceObject )) {//调用SfAttachToMountedDevice实现真正的绑定status = SfAttachToMountedDevice( vpb->DeviceObject,NewDeviceObject );if (!NT_SUCCESS( status )) { //绑定失败SfCleanupMountedDevice( NewDeviceObject );IoDeleteDevice( NewDeviceObject );}ASSERT( NULL == attachedDeviceObject );} else {//说明之前绑定过了SfCleanupMountedDevice( NewDeviceObject );IoDeleteDevice( NewDeviceObject );}//完成请求status = Irp->IoStatus.Status;IoCompleteRequest( Irp, IO_NO_INCREMENT );return status;
}
? 这个过程看似确实比较复杂,这也使我们意识到。在文件系统这种Windows核心组件上进行的修改,与对串口键盘这样的简单的设备修改不同。即使是一个简单的调用一个内核调用去绑定一个设备,也依然要充分考虑到对整个执行环境的影响。
明日计划
读写操作的过滤