小默

(转载)Windows文件系统过滤驱动开发教程 作者,楚狂人自述 (3)

VPB是Volume parameter block.一个数据结构.它的主要作用是把实际存储媒介设备对象和文件系统上的卷设备对象联系起来.

wd_dev_vbp可以让你从一个Storage Device Object得到一个VPB,而wd_vbp_dev这个函数可以得到这个VPB所对应的Volmue设备.

现在首先要得到Storage Device Object.实际上这个东西保存在当前IO_STACK_LOCATION中.

// ------------------wdf.h 中的内容 -----------------------
_inline wd_dev *wd_irpsp_mount_storage(wd_io_stack *irpsp)
{
return irpsp->Parameters.MountVolume.Vpb->RealDevice;
};

那么,从irp出发,我最终可以通过以下的方式得到Volumue设备:

wd_irpsp *irpsp = wd_cur_io_stack(irp);
wd_dev *storage_dev = wd_irpsp_mount_storage(irp);
wd_vpb *vpb = wd_dev_vbp(storage_dev);
wd_dev *volume_dev = wd_vbp_dev(vpb);

不过实际情况并不这么简单.这里的IRP是一个MOUNT请求.而volume设备对象实际上是这个请求完成之后的返回结果.因此,在这个请求还没有完成之前,我们就试图去获得Volume设备对象,当然是竹篮打水一场空了.

这里,你可以直接拷贝当前IO_STACK_LOCATION,然后向下发送请求,但在此之前,要先给irp分配一个完成函数.irp一旦完成,你的完成函数将被调用.这样的话,你可以在完成函数中得到Volume设备,并实施你的绑定过程.

这里要讨论一下中断级别的问题.常常碰到人问某函数只能在Passive Level调用是什么意思.总之我们的任何代码执行的时候,总是处在某个当前的中断级之中.某些系统调用只能在低级别中断级中执行.请注意,如果一个调用可以在高处运行,那么它能在低处运行,反过来则不行.

我们需要知道的只是我们关心Passive Level和Dispatch Level.而且Dispatch Level的中断级较高.一般ddk上都会标明,如果注明irq level>=dispatch,那么你就不能在passive level的代码中调用它们了.

那么你如何判断当前的代码在哪个中断级别中呢?我一般是这么判断的:如果你的代码执行是由于应用程序(或者说上层)的调用而引发的,那么应该在Passive Level.如果你的代码执行是由于下层硬件而引发的,那么则可能在dispatch level.

希望不要机械的理解我的话!以上只是极为粗略的便于记忆的理解方法.实际的应用应该是这样的:所有的dispatch functions由于是上层发来的irp而导致的调用,所以应该都是Passive Level,在其中你可以调用绝大多数系统调用.而如网卡的OnReceive,硬盘读写完毕,返回而导致的完成函数,都有可能在Dispatch级.注意都是有可能,而不是绝对是.但是一旦有可能,我们就应该按就是考虑.

好,现在我们发现,我们已经注册了完成函数,并且这个函数执行中可能是dispatch level.

现在面临的问题是,我们已经决定在完成函数中调用 IoAttachDeviceToDeviceStack来绑定Volume.而DDK说明有:Callers of IoAttachDeviceToDeviceStack must be running at IRQL <= DISPATCH_LEVEL.

实际上前边说过有IoAttachDeviceToDeviceStackSafe,这个调用可以在Dispatch level进行.无奈这个调用仅仅出现在Xp以上的系统中.

超越中断级别的限制有几种方法.第一种是自己生成一个系统线程来完成此事.系统线程将保证在Passive Level中运行.另一种方法就是把自己的任务插入Windows工作者线程,这会使你的任务迟早得到执行.如果你的任务比较小,可以实行第二种方法.对系统来说比较省事,对程序员来说则反正都是麻烦.

我做了以下几个函数专门来插入任务到工作者线程.
//---------------wdf.h 中的内容 ------------------------
typedef WORK_QUEUE_ITEM wd_work_item;
typedef PWORKER_THREAD_ROUTINE wd_work_func;
// 任务的初始化
_inline wd_void wd_work_init(wd_work_item *item,
wd_work_func worker,
wd_void *context)
{
ExInitializeWorkItem(item,worker,context);
}

// 三种任务队列
typedef enum _wd_work_quque_type{
wd_work_crit = CriticalWorkQueue,
wd_work_delay = DelayedWorkQueue,
wd_work_hyper = HyperCriticalWorkQueue
} wd_work_queue_type;

_inline wd_void wd_work_queue(in wd_work_item *item,
in wd_work_queue_type type)
{
ExQueueWorkItem(item,(WORK_QUEUE_TYPE)type);
}

_inline wd_void wd_work_run(in wd_work_item *item)
{
(item->WorkerRoutine)(item->Parameter);
}

任务是一个数据结构,已经被我重定义为wd_work_item,wd_work_init能初始化它.初始化的时候你只需要填写一个你的任务的函数.同时一个context用来记录上下相关参数.(这是个空指针,你可以只想你任何想要的参数类型).

一般这个任务会自动执行,但是有时我们也想不插入队列,我们自己执行它.那么调用wd_work_run即可.

然后调用wd_work_queque插入工作者队列,之后会被执行.插入类型这里选择wd_work_delay.

希望你没有被这一串东西搞糊涂.现在我会写一个"设置完成函数"的函数.执行后,自动在Passive Level级执行你的完成函数.希望不会把你搞得晕头转向的:).

// 完成例程上下文。好几个fsctl需要注册完成例程。而例程中的工作可能
// 只能在passive level中运行,因此不得不加入一个work_item,把任务塞
// 入工作线程等待完成
typedef struct _my_fsctl_comp_con
{
wd_work_item work;
wd_dev *dev;
wd_irp *irp;
wd_dev *new_dev; // 这个元素仅仅用于mount的时候。因为我
// 们要生成一个新设备来绑定vdo.
} my_fsctl_comp_con;

wd_bool my_fsctl_set_comp(wd_dev *dev,
wd_irp *irp,
wd_dev *new_dev,
wd_irp_comp_func complete,
wd_work_func work_complete)
{
my_fsctl_comp_con *context;
context = (wdff_fsctl_comp_con *)wd_malloc(wd_false,
sizeof(wdff_fsctl_comp_con));
if(context == NULL)
{
wd_printf0("fsctl set comp: failed to malloc context.rn");
return wd_false;
}

// 初始化工作细节
wd_work_init(&context->work,
work_complete,
context);

context->dev = dev;
context->irp = irp;
context->new_dev = new_dev;

// 设置irp完成例程
wd_irp_comp(irp,complete,context);

return wd_true;
}

// 以下函数作为以上complete的参数被使用
wd_stat my_fsctl_comp(in wd_dev *dev,
in wd_irp *irp,
in wd_void *context)
{
wd_printf0("fsctl_comp: come in!!!rn");
UNREFERENCED_PARAMETER(dev);
UNREFERENCED_PARAMETER(irp);
// 判断当前中断级
if(wd_get_cur_irql() > wd_irql_passive)
{
wd_printf0("fsctl_comp:into quque!!!rn");
// 如果在passive更低的中断级别,必须插入延迟队列中运行
wd_work_queue((wd_work_item *)context,wd_work_delay);
}
else
{
// 否则可以直接执行
wd_printf0("fsctl_comp:run directly!!!rn");
wd_work_run((wd_work_item *)context);
}
return wd_stat_more_processing;
}

我想以上的过程应该已经可以理解了!注册了基本的完成历程complete函数(也就是我最后写的函数my_fsctl_comp后),irp执行完毕回调my_fsctl_comp,而我事先已经把已经做好的任务(wd_work_item)写在上下文指针中(context)中.一回调这个函数,我就wd_work_queque插入队列.结果wd_work_item中记录的work_complete函数显然会在Passive level中执行.我们的系统也将保持稳定.

work_complete函数将从context上下文指针中得到足够的参数,来完成对Volume的绑定.

希望你没有被弄昏头:),我们下回再分解.



8 终于绑定了Volume,读操作的捕获与分析

上文已经讲到绑定Volume之前的关键操作.我们一路逢山开路,逢水架桥,相信你从中也学到了驱动开发的基本方法.以后的工作,无非灵活运用这些方法而已.前边已经举出过绑定FS CDO的操作.那么现在绑定Volume,无非照猫画虎,而以后的教程中,我也不会逐一详尽的列举出细节的代码了.

但是绑定Volume的过程中,还是有些地方需要稍微注意:

1.获得Storage Device Object的问题.前边已经说过,为了得到Vbp,我要先得到Storage Device Object,方法如下:

wd_irpsp *irpsp = wd_cur_io_stack(irp);
wd_dev *storage_dev = wd_irpsp_mount_storage(irpsp);

这是在Irp完成之前,这样调用是对的.但是到完成函数中,情况就不同了.因为这个irpsp下保存的storage_dev可能被修改掉(这并非我自己调试的结果,而是阅读sfilter代码中的注释而得到的信息).既然有这个可能,我们只好先把storage_dev这个东西保存下来.实际上,可以在Device扩展增加一个项目storage_dev.在irp完成之前,生成我要绑定的设备(只是不绑定),并把这个设备指针传入完成函数上下文.

这样在完成函数中,我就能得到这个storage_dev,最终找到Volmue设备的指针,此时进行绑定就可以了.

2.绑定的过程,未必一次就能成功.因为Volmue设备的Initilize标记被清除之前,我的绑定是不能成功的.对这种情况,我抄袭了sfilter中的方法,就是延时500毫秒,然后再尝试.一共尝试8次.

我包装了一个函数用来延时:

_inline wd_void wd_delay_milli_se(wd_ulong milli_sec)
{
wd_llong delay = milli_sec*(-10)*1000;
wd_lgint interval;
interval.QuadPart = delay;
KeDelayExecutionThread(KernelMode,FALSE,&interval);
}

这个函数的参数是毫秒,但是我并不清楚有多小的精度.

其他的就不说了,祝愿你的运气足够的好.现在我们处理IRP_MJ_READ,如果你已经绑定了Volume,那么显然,发送给Volume的请求就会先发送给你.处理IRP_MJ_READ,能捕获文件的读操作.

进入你的my_disp_read()函数(假设你注册了这个函数来处理IRP_MJ_READ,请见前面关于分发函数的讲述),首先判断这个Dev是不是绑定Volume的设备.如果是,那么就是一个读文件的操作.

如何判断?记得我们先绑定Volume的时候,在我们的设备扩展中设置了storage_dev,如果不是(比如是FS CDO,我们没设置过),那么这么判断即可:

if(is_my_dev(dev))
{
my_dev_ext *ext = (my_dev_ext *)wd_dev_ext(dev);
if(ext->storage_dev)
{
// ... 到这里说明是对文件的读操作
}
}

其他的情况不需要捕获,请直接传递到下层.

读请求的IRP情况非常复杂,请有足够的心理准备.并不要过于依赖帮助,最好的办法就是自己打印IRP的各个细节,亲自查看文件读操作的完成过程.而我现在所说的东西,换另一我未尝试过的版本的windows是否还正确,我也无法下断言.

不过基本的东西是不会变的.

首先关心被读写的文件.IRP下有一个FileObject指针.这个东西指向一个文件对象.你可以得到文件对象的名字,这个名字是没有盘符的文件全路径.有人要问那么盘符如何获得?因为你已经知道了Volume,前边已经说过盘符不过是Volume的符号连接名,那么想要真正的全路径问题应该也不大了.

我现在偷一个懒,我现在只打印没有盘符的路径名.先写一个函数,从IRP得到FileObject.

_inline wd_file *wd_irp_file(wd_irpsp *irpsp)
{
return irpsp->FileObject;
}

然后写一个函数来获得文件名.这个函数参考的是FileMon的代码.

wd_void wd_file_get_name(in wd_file *file,
in out wd_ustr *name)
{
if( file->FileName.Buffer &&
!(file->Flags & FO_DIRECT_DEVICE_OPEN) )
RtlCopyUnicodeString(name,&file->FileName);
}

接下来有必要得到读文件的偏移量.和vxd的文件系统驱动不同,2000下文件系统得到的偏移量似乎都是从文件起始位置开始计算的.偏移量是一个LARGE_INTEGER.因为现在确实有些文件已经超过了长整型所能表示的大小.

以下函数用来得到偏移量.wd_lgint是经过重定义的LARGE_INTEGER.

_inline wd_lgint wd_irp_read_offset(wd_irpsp *irpsp)
{
return irpsp->Parameters.Read.ByteOffset;
}

注意以上的参数不是irp.是当前IO_STACK_LOCATION,也就是我的wd_irpsp.前面已经讲述过如何获取当前irpsp.

此外我还希望能得到我所读到的数据.这要注意,我们捕获这个请求的时候,这个请求还没有完成.既然没有完成,当然无数据可读.如果要获取,那就设置完成函数,在完成函数中完成请求.

完成Irp的时候忽略还是拷贝当前IO_STACK_LOCATION,返回什么STATUS,以及完成函数中如何结束Irp,是不那么容易搞清楚的一件事情.我想做个总结如下:

1.如果对irp完成之后的事情无兴趣,直接忽略当前IO_STACK_LOCATION,(对我的程序来说,调用wd_ship_cur_io_stack),然后向下传递请求,返回wd_irp_call()所返回的状态.

2.不但对irp完成之后的事情无兴趣,而且我不打算继续传递,打算立刻返回成功或失败.那么我不用忽略或者拷贝当前IO_STACK_LOCATION,填写参数后调用IoCompleteRequest,并返回我想返回的结果.

3.如果对irp完成之后的事情有兴趣,并打算在完成函数中处理,应该首先拷贝当前IO_STACK_LOCATION(wd_copy_cur_io_stack()),然后指定完成函数,并返回wd_irp_call()所返回的status.完成函数中,不需要调用IoCompleteRequest!直接返回Irp的当前状态即可.

4.同3的情况,有时候,会把任务塞入系统工作者线程或者希望在另外的线程中去完成Irp,那么完成函数中应该返回wd_stat_more_processing,此时完成Irp的时候应该调用IoCompleteRequest.另一种类似的情况是在dispatch函数中等待完成函数中设置事件,那么完成函数返回wd_stat_more_processing,dispatch函数在等待结束后调用IoCompleteRequest.

前边已经提到过设备的DO_BUFFERED_IO,DO_DIRECT_IO这两个标记.情况是3种:要么是两个标记中其中一个,要么是一个都没有.Volume设备出现DO_BUFFERED的情况几乎没有,我碰到的都是一个标记都没有.DO_DIRECT_IO表示数据应该返回到Irp->MdlAddress所指向的MDL所指向的内存.在无标记的情况下,表明数据读好,请返回到Irp->UseBuffer中即可.

UseBuffer是一个只在当前线程上下文才有效的地址.如果你打算按这个地址获得数据,你最好在当前线程上下文中.完成函数与my_disp_read并非同一个线程.所以在完成函数中按这个地址去获取数据是不对的.如何回到当前线程?我采用简单的办法.在my_disp_read中设置一个事件,调用wd_irp_call(即ddk中的IoCallDriver)之后开始等待这个事件.而在完成函数中设置这个事件.这样等待结束的时候,刚好Irp已经完成,我也回到了我的my_disp_read原来的线程.

wd_stat my_disp_read(in wd_dev *dev,in wd_pirp irp)
{
my_dev_ext *ext;
wd_dev *attached_dev;
wd_irpsp *irpsp = wd_cur_io_stack(irp);
wd_stat status;
wd_file *file = wd_irp_file(irpsp);
wd_lgint offset = wd_irp_read_offset(irpsp);
wd_size length = wd_irp_read_length(irpsp);
wd_wchar name_buf[512];
wd_ustr name;
wd_event event;

// 检查是否我的设备
if(!is_my_dev(dev))
return wd_irp_failed(irp,wd_stat_invalid_dev_req);

ext = (wdff_dev_ext *)wd_dev_ext(dev);
attached_dev = wdff_dev_attached(dev);

// 到这里判断得到这是对一个被绑定了的卷的读操作
if(ext->storage_dev == NULL)
{
wd_skip_io_stack(irp);
return wd_irp_call(attached_dev,irp);
}

// 到了这里,确认是对文件的读
wd_ustr_init_em(&name,name_buf,512);
if(file)
wd_file_get_name((wd_void *)file,&name);
else
{
wd_skip_io_stack(irp);
return wd_irp_call(attached_dev,irp);
}

wd_printf1("xxx irp flag = %xrn",wd_irp_flags(irp));
wd_printf1("xxx file read: %wZ rn",&name);
wd_printf1("xxx read offset = %ld ",offset);
wd_printf1("xxx read length = %ldrn",length);

// 以上我已经打印了读请求的相关参数,下面我希望得到读出的内容
wd_event_init(&event);
// 先拷贝当前io_stack,然后再指定完成例程

wd_copy_io_stack(irp);
wd_irp_set_comp(irp,my_disp_read_comp,&event);

// 对实际设备呼叫irp
status = wd_irp_call(attached_dev,irp);
if(status == wd_stat_pending)
wd_event_wait(&event);

wd_printf1("test read end:status = %x rn",status);
// 如果此时status = 0,那么内容应该就在Irp->UserBuffer中,请自己打印...
wd_printf1("test read end:read length = %ldrn",wd_irp_infor(irp));

return wd_irp_over(irp);
}

然后是my_disp_read_comp的内容,可以看见只是简单的设置事件,然后返回wd_stat_more_processing.

wd_stat my_disp_read_comp(in wd_dev *dev,
in wd_irp *irp,
in wd_void *context)
{
wd_event * event=
(wd_event *)context;
UNREFERENCED_PARAMETER(dev);
UNREFERENCED_PARAMETER(irp);
wd_event_set(event);
return wd_stat_more_processing;
}

尽管已经写了很多,尽管我们得到了读过程的所有参数和结果,我们依然不知道如果自己写一个文件系统,该如何完成读请求,或者过滤驱动中,如何修改读请求等等.我们下一节继续讨论读操作.



9 完成读操作

除非是一个完整的文件系统,完成读操作似乎是不必要的。过滤驱动一般只需要把请求交给下层的实际文件系统来完成。但是有时候比如加解密操作,我希望从下层读到数据,解密后,我自己来完成这一IRP请求。

这里要谈到IRP的minor function code.以前已经讨论到如果major function code 是IRP_MJ_READ则是Read请求。实际上有些主功能号下面有一些子功能号,如果是IRP_MJ_READ,检查其MINOR,应该有几种情况:IRP_MN_NORMAL,IRP_MN_MDL,IRP_MN_MDL|IRP_COMPLETE(这个其实就是IRP_MN_MDL_COMPLETE).还有其他几种情况,资料上有解释,但是我没自己调试过,也就不胡说了。只拿自己调试过的几种情况来说说。

先写个函数,得到次功能号。

_inline wd_uchar wd_irpsp_minor(wd_io_stack *irpsp)
{
return irpsp->MinorFunction;
}

enum {
wd_mn_mdl = IRP_MN_MDL,
wd_mn_mdl_comp = IRP_MN_MDL_COMPLETE,
wd_mn_normal = IRP_MN_NORMAL
};

希望你喜欢我重新定义过的次功能号码。

wd_mn_normal的情况完全与上一节同(读者可能要骂了,上一节明明说就是这样的,结果还有其他的情况,那不是骗人么?但是微软的东西例外就是多,我也实在拿他没办法... ).

注意如上节所叙述,wd_mn_normal的情况,既有可能是在Irp->MdlAddress中返回数据,也可能是在Irp->UserBuffer中返回数据,这个取决于Device的标志.

但是如果次功能号为wd_mn_mdl则完全不是这个意思。这种irp一进来看数据,就赫然发现Irp->MdlAddress和Irp->UserBuffer都为空。那你得到数据后把数据往哪里拷贝呢?

wd_mn_mdl的意思是请自己分配一个mdl,然后把mdl指向你的数据所在的空间,然后返回给上层。自然mdl是要释放的,换句话说事业使用完毕要归还,所以又有wd_mn_mdl_comp,意思是一个mdl已经使用完毕,可以释放了。

mdl用于描述内存的位置。据说和NDIS_BUFFER用的是同一个结构。这里不深究,我写一些函数来分配和释放mdl,并把mdl指向内存位置或者得到mdl所指向的内存:

// 释放mdl
_inline wd_void wd_mdl_free(wd_mdl *mdl)
{
IoFreeMdl(mdl);
}

// 这个这个东西分配mdl,缓冲必须是非分页的。可以在dispatch level跑。
_inline wd_mdl *wd_mdl_alloc(wd_void* buf,
wd_ulong length)
{
wd_mdl * pmdl = IoAllocateMdl(buf,length,wd_false,wd_false,NULL);
if(pmdl == NULL)
return NULL;
MmBuildMdlForNonPagedPool(pmdl);
return pmdl;
}

// 这个函数分配一个mdl,并且带有一片内存
_inline wd_mdl *wd_mdl_malloc(wd_ulong length)
{
wd_mdl *mdl;
wd_void *point = wd_malloc(wd_false,length);
if(point == NULL)
return NULL;
mdl = wd_mdl_alloc(point,length);
if(mdl == NULL)
{
wd_free(point);
return NULL;
}
return mdl;
}

// 这个函数释放mdl并释放mdl所带的内存。
_inline wd_void wd_mdl_mfree(wd_mdl *mdl)
{
wd_void *point = wd_mdl_vaddr(mdl);
wd_mdl_free(mdl);
wd_free(point);
}

// 得到地址。如果想往一个MdlAddress里边拷贝数据 ...
_inline wd_void *wd_mdl_vaddr(wd_mdl *mdl)
{
return MmGetSystemAddressForMdlSafe(mdl,NormalPagePriority);
}

另外我通过这两个函数来设置和获取Irp上的mdl.

_inline wd_mdl *wd_irp_mdl(wd_irp *irp)
{
return irp->MdlAddress;
}

_inline wd_void wd_irp_mdl_set(wd_irp *irp,
wd_mdl *mdl)
{
irp->MdlAddress = mdl;
}

一个函数获得UserBuffer.
_inline wd_void * wd_irp_user_buf(wd_irp *irp)
{
return irp->UserBuffer;
}

要完成请求还有一个问题。就是irp->IoStatus.Information.在这里你必须填上实际读取得到的字节数字。不然上层不知道有多少数据返回。这个数字不一定与你的请求的长度等同(其实我认为几乎只要是成功,就应该都是等同的,唯一的例外是读取到文件结束的地方,长度不够了的情况)。我用下边两个函数来获取和设置这个数值:

_inline wd_void wd_irp_infor_set(wd_irp *irp,
wd_ulong infor)
{
irp->IoStatus.Information = infor;
}

_inline wd_ulong wd_irp_infor(wd_irp *irp)
{
return irp->IoStatus.Information;
}


也许你都烦了,但是还有事情要做。作为读文件的情况,如果你是自己完成请求,不能忘记移动一下文件指针。否则操作系统会不知道文件指针移动了而反复读同一个地方永远找不到文件尾,我碰到过这样的情况。

一般是这样的,如果文件读取失败,请保持原来的文件指针位置不要变。如果文件读取成功,请把文件指针指到“读请求偏移量+成功读取长度”的位置。

这个所谓的指针是指Irp->FileObject->CurrentByteOffset.

我跟踪过正常的windows文件系统的读行为,我认为并不一定是向我上边说的这样做。情况很复杂,有时动,有时不动(说复杂当然是因为我不理解),但是按我上边说的方法来完成,我还没有发现过错误。

我用以下函数来设置和获取这个。

_inline wd_void wd_file_offset_add(wd_file *file,wd_ulong len)
{
file->CurrentByteOffset.QuadPart += len;
}

_inline wd_void wd_file_offset_set(wd_file *file,wd_lgint offset)
{
file->CurrentByteOffset = offset;
}

_inline wd_lgint wd_file_offset(wd_file *file)
{
return file->CurrentByteOffset;
}


现在看看怎么完成这些请求,假设我已经有数据了。现在假设本设备缓冲标记为0,即正常情况采用Irp->UserBuffer返回数据. 这些当然都是在my_disp_read中或者是其他想完成这个irp的地方做的(希望你还记得我们是如何来到这里),假设其他必要的判断都已经做了:

wd_irpsp *irpsp = wd_irp_cur_io_stack(irp);
switch(wd_irpsp_minor(irpsp))
{
// 我先保留文件的偏移位置
case wd_mn_normal:
{
wd_void *point = wd_irp_user_buf(irp);

// 如果有数据,就往point ...里边拷贝 ...

wd_irp_infor_set(irp,length);
wd_irp_status_set(irp,wd_suc);
wd_file_offset_set(wd_irp_file(irp),offset+length);

return wd_irp_over(irp);
}
case wd_mn_mdl:
{
wd_void *mdl = wd_mdl_malloc(length); // 情况比上边的复杂,请先分配mdl
if(mdl == NULL)
{
// ... 返回资源不足 ...
}

wd_irp_mdl_set(irp,mdl);
wd_irp_infor_set(irp,length);
wd_irp_status_set(irp,wd_suc);

wd_file_offset_set(wd_irp_file(irp),offset+length);

return wd_irp_over(irp);

}
case wd_mn_mdl_comp:
{
// 没有其他任务,就是释放mdl
wd_mdl_mfree(wd_irp_mdl(irp));
wd_irp_mdl_set(irp,0);
wd_irp_infor_set(irp,0);
wd_irp_status_set(irp,wd_status_suc);

return wd_irp_over(irp);
}
default:
{
// 我认为其他的情况不过滤比较简单 ...
}
}

重要提醒:wd_mn_mdl的情况,需要分配一个mdl,并且这个mdl所带有的内存是有一定长度的,这个长度必须与后来的irp->IoStatus.Information相同!似乎上层并不以irp->IoStatus.Information返回的长度为准。比如明明只读了50个字节,但是你返回了一个mdl指向内存长度为60字节,则操作系统则认为已经读了60个字节!这非常糟糕。

最后提一下文件是如何结尾的。如果到某一处,返回成功,但是实际读取到的数据没有请求的数据长,这时还是返回wd_status_suc(STATUS_SUCCESS),但是此后操作系统会马上发irp来读最后一个位置,此时返回长度为0,返回状态STATUS_FILE_END即可。



10 自己发送Irp完成读请求

关于这个有一篇文档解释得很详细,不过我认为示例的代码有点太简略了,这篇文档在IFS所附带的OSR文档中,请自己寻找。

为何要自己发送Irp?在一个文件过滤驱动中,如果你打算读写文件,可以试用ZwReadFile.但是这有一些问题。Zw系列的Native API使用句柄。而句柄是有线程环境限制的。此外也有中断级别的限制。再说,Zw系列函数来读写文件,最终还是要发出Irp,又会被自己的过滤驱动捕获到。结果带来判断上的困难。对资源也是浪费。那么最应该的办法是什么呢?当然是直接对卷设备发Irp了。

但是Irp是非常复杂的数据结构,而且又被微软所构造的很多未空开的部件所处理。所以自己发irp并不是一件简单的事情。

比较万能的方法是IoAllocateIrp,分配后自己逐个填写。问题是细节实在太多,很多无文档可寻。有兴趣的应该看看我上边所提及的那篇文章“Rolling Your Own”。

有意思的是这篇文章后来提到了捷径,就是利用三个函数:

IoBuildAsynchronousFsdRequest(...)
IoBuildSynchronousFsdRequest(...)
IoBuildDeviceIoControlRequest(...)

于是我参考了他这方面的示例代码,发现运行良好,程序也很简单。建议怕深入研究的选手就可以使用我下边提供的方法了。

首先的建议是使用IoBuildAsynchronousFsdRequest(),而不要使用同步的那个。使用异步的Irp使irp和线程无关。而你的过滤驱动一般很难把握当前线程(如果你开一个系统线程来专门读取文件那例外)。此时,你可以轻松的在Irp的完成函数中删除你分配过的Irp,避免去追究和线程相关的事情。

但是这个方法有局限性。文档指出,这个方法仅仅能用于IRP_MJ_READ,IRP_MJ_WRITE,IRP_MJ_FLUSH_BUFFERS,和IRP_MJ_SHUTDOWN.刚好我这里仅仅要求完成文件读。

用Irp完成文件读需要一个FILE_OBJECT.FileObject是比Zw系列所用的句柄更好的东西。因为这个FileObject是和线程无关的。你可以放心的在未知的线程中使用他。

自己要获得一个FILE_OBJECT必须自己发送IRP_MJ_CREATE的IRP.这又不是一件轻松的事情。不过我跳过了这个问题。因为我是文件系统过滤驱动,所以我从上面发来的IRP中得到FILE_OBJECT,然后再构造自己的IRP使用这个FILE_OBJECT,我发现运行很好。

但是又出现一个问题,如果IRP的irp->Flags中有IRP_PAGING(或者说是Cache管理器所发来的IRP)标记,则其FileObject我获得并使用后,老是返回错误。阅读网上的经验表明,带有IRP_PAGINGE的FileObject不可以使用.于是我避免使用这时的FileObject.我总是使用不带IRP_PAGING的Irp(认为是用户程序发来的读请求)的FileObject。

好,现在废话很多了,现在来看看构造irp的代码:

_inline wd_irp *wd_irp_fsd_read_alloc(wd_dev *dev,
wd_void *buf,
wd_ulong length,
wd_lgint *offset,
wd_io_stat_block *io_stat)
{
return IoBuildAsynchronousFsdRequest(IRP_MJ_READ,dev,
buf,length,
offset,
io_stat);
}

io_stat我不知道是做何用,我一般填写NULL.类型是PIO_STATUS_BLOCK.buf则是缓冲。在Irp中被用做UserBuffer接收数据。offset是这次读的偏移量。

以上函数构造一个读irp.请注意,此时您还没有设置FileObject.实际上我是这样发出请求的:

irp = wd_irp_fsd_read_alloc(dev,buf,len,&start,NULL);
if(irp == NULL)
return;
irpsp = wd_irp_next_sp(irp);
wd_irpsp_file_set(irpsp,file);
wd_irp_comp(irp,my_req_comp,context);

请注意wd_irp_next_sp,我是得到了这个irp的下一个IO_STACK_LOCATION,然后我设置了FileObject.接下来应该设置了完成后,我就可以发送请求了!请求发送完毕后,一旦系统完成请求就会调用你的my_req_comp.

再看看my_req_comp如何收场:

wd_stat my_req_comp(in wd_dev *dev,
in wd_irp *irp,
in wd_void *context)
{

// 请注意,无论成功还是失败,我都得在这里彻底销毁irp
wd_irp_send_by_myself_free(irp);

// 返回这个,仅仅是因为irp我已经销毁,不想让系统再次销毁它而已。
return wd_stat_more_processing;
}

wd_stat_more_prcessing就是STATUS_MORE_PROCESSING_REQUIRED。之所以返回这个,是因为我已经把irp给删除了。我不想windows系统再对这个irp做什么。所以干脆说我还要继续处理,这样避免了io管理器再去动用这个irp的可能。

最后是那个irp删除函数的代码:

_inline wd_void wd_irp_send_by_myself_free(wd_irp *irp)
{
if (irp->MdlAddress)
{
MmUnmapLockedPages(MmGetSystemAddressForMdl(irp->MdlAddress),
irp->MdlAddress);
MmUnlockPages(irp->MdlAddress);
IoFreeMdl(irp->MdlAddress);
}
IoFreeIrp(irp);
};

因为我自己没有分配过MdlAddress下去过。所以如果下边返回了MDL,我得附带清理它。

好了,祝愿你心情愉快。如果你何我一样懒惰的话,不妨就用上边的简单方法自己发irp来读文件了。



11.文件和目录的生成打开,关闭与删除

我们已经分析了读,写与读类似。文件系统还有其他的操作。比如文件或目录的打开(打开已经存在的或者创建新的),关闭。文件或目录的移动,删除。

实际上FILE_OBJECT并不仅仅指文件对象。在windows文件系统中,目录和文件都是用FileObject来抽象的。这里产生一个问题,对于一个已经有的FileObject,我如何判断这是一个目录还是一个文件呢?

对于一个已经存在的FileObject,我没有找到除了发送IRP来向卷设备询问这个FileObject的信息之外更好的办法。自己发送IRP很麻烦。不是我很乐意做的那种事情。但是FileObject都是在CreateFile的时候诞生的。在诞生的过程中,确实有机会得到这个即将诞生的FileObject,是一个文件还是一个目录。

Create的时候,获得当前IO_STACK_LOCATION,假设为irpsp,那么irpsp->Parameters.Create的结构为:

struct {
PIO_SECURITY_CONTEXT SecurityContext;
ULONG Options;
USHORT FileAttributes;
USHORT ShareAccess;
ULONG EaLength;
};

这个结构中的参数是与CreateFile这个api中的参数对应的,请自己研究。我先写一些函数包装来方便读取irpsp.

_inline wd_ulong wd_irpsp_file_opts(wd_irpsp *irpsp)
{
return irpsp->Parameters.Create.Options;
}

_inline wd_ushort wd_irpsp_file_attrs(wd_irpsp *irpsp)
{
return irpsp->Parameters.Create.FileAttributes;
}

enum {wd_file_opt_dir = FILE_DIRECTORY_FILE};
enum {wd_file_attr_dir = FILE_ATTRIBUTE_DIRECTORY};

然后我们搞清上边Options和FileAttributes的意思。是不是Options里边有FILE_DIRECTORY_FILE标记就表示这是一个目录?实际上,CreateOpen是一种尝试性的动作。无论如何,我们只有当CreateOpen成功的时候,判断FileObject才有意义。否则是空谈。

成功有两种可能,一是已经打开了原有的文件或者目录,另一种是新建立了文件或者目录。Options里边带有FILE_DIRECTORY_FILE表示打开或者生成的对象是一个目录。那么,如果在Create的完成函数中,证实生成或者打开是成功的,那么返回得到的FILE_OBJECT,确实应该是一个目录。

当我经常要使用我过滤时得到的文件或者目录对象的时候,我一般在Create成功的的时候捕获他们,并把他们记录在一个“集合”中。这时你得写一个用来表示“集合”的数据结构。你可以用链表或者数组,只是注意保证多线程安全性。因为Create的时候已经得到了属性表示FileObject是否是目录,你就没有必要再发送IRP来询问FileObject的Attribute了。

对了上边有FileAttributes。但是这个东西并不可靠。因为在生成或者打开的时候,你只需要设置Options。我认为这个字段并无法说明你打开的文件对象是目录。

这你需要设置一下Create的完成函数。如果设置这里不再重复,请参考上边对文件读操作。

wd_stat my_create_comp(in wd_dev *dev,
in wd_irp *irp,
in wd_void *context)
{
wd_irpsp *irpsp = wd_irp_cur_sp(irp);
wd_file *file = wd_irpsp_file(irpsp);

UNREFERENCED_PARAMETER(dev);

if(wd_suc(wd_irp_status(irp))
{
// 如果成功了,把这个FileObject记录到集合里,这是一个
// 刚刚打开或者生成的目录
if(file &&
(wd_irpsp_file_opts(irpsp) & wd_file_opt_dir))
add_obj_to_set(file);
}
return wd_irp_status(irp);
}

这里顺便解释一下UNREFERENCED_PARAMETER宏。我曾经不理解这个宏的意思。其实就是因为本函数传入了三个参数,这些参数你未必会用到。如果你不用的话,大家知道c编译器会发出一条警告。一般认为驱动应该去掉所有的警告,所以用了这个宏来“使用”一下没有用到过的参数。你完全可以不用他们。

现在所有的目录都被你记录。那么得到一个FileObject的时候,判断一下这个FileObject在不在你的集合里,如果在,就说明是目录,反之是文件。

当这个FileObject被关闭的时候你应该把它从你的集合中删除。你可以捕获Close的IRP来做这个。记得本教程很早以前,我们已经安装过my_close函数的来处理IRP(请回忆或者翻阅第3节的内容),那么很简单了,在该函数中从你的集合中删除该FileObject即可。作为保险的做法,应该观察一下关闭是否成功。如果成功的话,再进行你的从集合中删除元素工作。

因为判断FileObject是文件还是目录的问题,我们已经见识了文件的打开和关闭工作。现在看一下文件是如何被删除的。

删除的操作,第一步是打开文件,打开文件的时候必须设置为可以删除。如果打开失败,则直接导致无法删除文件。第二步设置文件属性为用于删除,第三步关闭文件即可。关闭的时候,文件被系统删除。

不过请注意这里的“删除”并非把文件删除到回收站。如果要测试,你必须按住shift彻底删除文件。文件删除到回收站只是一种改名操作。改名操作我们留到以后再讨论。

第一步是打开文件,我应该可以在文件被打开的时候,捕获到的irpsp的参数,记得前边的参数结构,中间有:

PIO_SECURITY_CONTEXT SecurityContext;

相关的结构如下:

typedef struct _IO_SECURITY_CONTEXT {
PSECURITY_QUALITY_OF_SERVICE SecurityQos;
PACCESS_STATE AccessState;
ACCESS_MASK DesiredAccess;
ULONG FullCreateOptions;
} IO_SECURITY_CONTEXT, *PIO_SECURITY_CONTEXT;

注意其中的DesiredAccess,其中必须有DELETE标记,才可以删除文件。

第二步是设置为”关闭时删除”。这是通过发送一个IRP(Set Information)来设置的。捕获主功能码为IRP_MJ_SET_INFORMATION的IRP后:
首先,IrpSp->Parameters.SetFile.FileInformationClass应该为FileDispositionInformation。

然后,Irp->AssociatedIrp.SystemBuffer指向一个如下的结构:

typedef struct _FILE_DISPOSITION_INFORMATION {
BOOLEAN DeleteFile;
} FILE_DISPOSITION_INFORMATION;

如果DeleteFile为TRUE,那么这是一个删除文件的操作。文件将在这个FileObject Close的时候被删除。

以上的我都未实际调试,也不再提供示例的代码。有兴趣的读者请自己完成。

posted on 2009-12-24 20:52 小默 阅读(551) 评论(0)  编辑 收藏 引用 所属分类: Windows


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


导航

统计

留言簿(13)

随笔分类(287)

随笔档案(289)

漏洞

搜索

积分与排名

最新评论

阅读排行榜