iniwf

风是温柔的,雨是伤心的,云是快乐的,月是多情的,爱是迷失的,恋是醉人的,情是难忘的,天是长久的,地是永恒的

驱动开发之四 --- 过滤驱动 【译文】

转自http://hi.baidu.com/combojiang/blog/item/3afc83eefb90d7feb3fb950b.html

这是第四篇介绍写设备驱动的文章,本篇我们将介绍设备栈的概念以及设备间的交互。我们将使用前面的例子来进行验证。我们将介绍过滤驱动的概念,并且创建一个过滤驱动,挂接到我们的驱动设备栈。

理论:

什么是设备栈?

谈到栈,通常我们可以想象为在一起的一堆物体,一个压另一个的上面。还有就是数据结构中作为一种算法实现的栈,是指出的方法来存储临时对象。这两种描述是有关联的。然而,设备栈既不是指一种算法也不需要和临时对象打交道。因此,用这种摞在一起的一堆物体的简单描述更贴近一些。

设备栈最好的例子就是一堆盘子的类比。盘子一个压在另一个的顶上就像设备栈。另外一个细节要记住的就是我们总是说“设备栈“而不是“驱动栈“。 是否记得在第三篇中,一个驱动中创建了多个设备。这就意味着在一个物理驱动中的所有设备的栈会全部实现。

过滤驱动

一个过滤驱动是指一个驱动挂接在一个设备栈的顶端,在请求到达目标设备之前进行过滤处理。

你可以设想,设备栈中除了最后一个的设备外的其他设备都是过滤器。但这不符合事实。设备栈中的设备除过滤器外,通常都依赖于某个特殊的设备体系。例如,你通常得到靠近设备栈顶层的高层驱动,多数情况下,这些高层的驱动与用户模式的请求进行通讯和交互。

这些设备栈的设备为下层设备中止请求,直到链中的最后一个设备处理这个请求。底层的驱动靠近设备栈底部,就像 “miniport drivers”一样,跟实际的硬件通讯。

最好的例子就是文件系统,顶层的驱动维护着文件和文件系统的概念。他们知道文件存放在磁盘中的位置,底层驱动不知道文件,只是简单的理解请求读取磁盘扇区。他们知道怎样排队这些请求和优化磁盘搜索。但是他们不知道磁盘上的实际内容和怎样解释这些数据。

每一个设备过滤驱动都会被放在设备栈的顶部,就是说,如果在你之后另一个过滤驱动加到设备栈,那么现在它将在你的顶部。你不能保证自己永远在设备栈顶端。

为了加到设备栈,我们需要使用下面的API.

RtlInitUnicodeString(&usDeviceToFilter, L"\\Device\\Example");

NtStatus = IoAttachDevice(pDeviceObject,  

                        &usDeviceToFilter,

                        &pExampleFilterDeviceContext->pNextDeviceInChain);

实际上,这个API会为了贴上设备,它会打开一个设备句柄,然后再关闭这个句柄。当API企图关闭这个句柄时,我们的驱动就会被挂在了设备栈上。所以我们必须确保IRP_MJ_CLEANUP 和 IRP_MJ_CLOSE 能被正确的处理,并且在他们调用的时候不会出现问题。

还有少许其他的API,其中一个是IoAttachDeviceToStack。这个实际上是IoAttachDevice 在打开一个设备句柄后调用的函数。

IRP处理

IRP被创建后,发送到设备栈的第一个设备。然后这个设备处理IRP,并且完成它或者向下传递到设备栈的下一个设备。IRP的一般规则是,你收到IRP后,你拥有它。如果你把它向下传递到下一个设备,你将不再拥有它,也不能再访问它。最后一个处理IRP的设备必须完成它。

在这个例子中,为了演示,我们创建简单的IRP。这个演示非常简单,我们的驱动会收到发来的IRP. 由于我们控制着所有的终点,在我们的实现中这里可以忽略一些方面。这仅仅是一个非常简单的演示,由于实际上我们完全控制了所有的终点,因此这会让我们实现得更加灵活和确保不出问题。

当我们创建一个IRP时,需要遵循以下几个简单步骤。根据irp的处理,这里有些小的变化。我们将一步一步的仔细的检查。

第一步:创建IRP

很明显,第一步我们需要创建IRP。我们可以使用IoAllocateIrp。下面是一个使用它的例句。

MyIrp = IoAllocateIrp(pFileObject->DeviceObject->StackSize, FALSE);

还有其他的api和宏都可以实现为你创建一个IRP.他们可以更加快捷的创建irp并设置参数。需要注意的一点就是在使用这个函数创建IRP时,要确信这个函数可以在你使用的irql级别下被调用。另一个要检查的就是,谁来释放这个IRP. 是I/O管理器负责管理和释放这个irp还是你自己?

下面是一个可以为我们设置参数的例子。

MyIrp = IoBuildAsynchronousFsdRequest(IRP_MJ_INTERNAL_DEVICE_CONTROL,  

                                       pTopOfStackDevice,

                                       NULL,  

                                       0,  

                                       &StartOffset,

                                       &StatusBlock);

步骤2:设置参数

这一步是根据你需要什么样的功能来决定的。你需要设置FILE_OBJECT和IO_STACK_PARAMETER以及其他。在我们这个例子中,我们不提供FILE_OBJECT,我们设置最少的参数。为什么呢?原因就是这是个简单的例子,并且我们拥有所有的终点。既然我们控制了重点,实际上讲,我们能够用参数做任何我们想要做的事情。就像IOCTL一样,当发送IRP时,我们会知道你需要什么设置什么。实际上我们需要遵守这些规则,让其他驱动可以跟我们会话,但是这里仅仅是为了使这个例子看上去简单。

下面的代码是关于我们怎样设置IRP参数的。

PIO_STACK_LOCATION pMyIoStackLocation = IoGetNextIrpStackLocation(MyIrp);

pMyIoStackLocation->MajorFunction = IRP_MJ_INTERNAL_DEVICE_CONTROL;

pMyIoStackLocation->Parameters.DeviceIoControl.IoControlCode =

                                  IOCTL_CREATE_NEW_RESOURCE_CONTEXT;

/*

* METHOD_BUFFERED

*

*     Input Buffer = Irp->AssociatedIrp.SystemBuffer

*     Ouput Buffer = Irp->AssociatedIrp.SystemBuffer

*

*     Input Size    =   Parameters.DeviceIoControl.InputBufferLength

*     Output Size   =   Parameters.DeviceIoControl.OutputBufferLength

*

*     Since we are now doing the same job as the I/O Manager,

*     to follow the rules our IOCTL specified METHOD_BUFFERED

*/   

pMyIoStackLocation->Parameters.DeviceIoControl.InputBufferLength   =                                           sizeof(FILE_OBJECT);

pMyIoStackLocation->Parameters.DeviceIoControl.OutputBufferLength = 0;

                  

/*

* This is not really how you use IOCTL's but   

* this is simply an example using

* an existing implementation.   

* We will simply set our File Object as the SystemBuffer.

* Then the IOCTL handler will  

* know it's a pFileObject and implement the code that we

* had here previously.

*/

                  

MyIrp->AssociatedIrp.SystemBuffer = pFileObject;

MyIrp->MdlAddress                  = NULL;

注意,我们设置SystemBuffer来指向我们的文件对象。这样做是不严谨的。在这里,我们应该分配一个缓冲,然后把数据拷贝进来。那样我们能比较安全的让I/O管理器来释放内存或者当我们注销IRP的时候,我们来释放内存。

第三步:向下发送IRP

你需要向下给驱动发送irp. 你仅仅在IoCallDriver里指定DEVICE_OBJECT和IRP就可以了。

无论是你有怎样的DEVICE_OBJECT都可以用。然而,如果你想要从设备栈顶开始执行,最好使用像IoGetRelatedDeviceObject这样的API来找到顶层的设备对象。在我们的例子中,我有一个这样的调用来得到顶层设备。另一个是直接使用我们已经有的设备对象。如果你看调试输出,你就会注意有一个,我们没有经过过滤驱动。这是因为IoCallDriver是非常简单的,只是获得设备对象并且找出合适的函数进行调用。

NtStatus = IoCallDriver(pFileObject->DeviceObject, MyIrp);

第四步:处理和清除IRP

在我们向下发送irp之前有一件事情需要做,那就是创建一个“Completion Routine“。这个例程当IRP完成时,会得到通知。在这个情况下,我可以做一些事情。我们可以允许irp继续,以至于我们可以处理它的参数或者我们注销它。我们也可以让I/O管理器来释放它。这需要依赖我们创建irp的方式。为了回答“谁来释放它“的问题,你需要阅读关于分配IRP的API的ddk文档。错误地实现方法会造成灾难!

这是一个简单的例子,我们自己直接释放它。

IoSetCompletionRoutine(MyIrp, Example_SampleCompletionRoutine,  

                                            NULL, TRUE, TRUE, TRUE);

...

NTSTATUS Example_SampleCompletionRoutine(PDEVICE_OBJECT DeviceObject,  

                                PIRP Irp, PVOID   Context)

{

     DbgPrint("Example_SampleCompletionRoutine \n");

     IoFreeIrp(Irp);

     return STATUS_MORE_PROCESSING_REQUIRED;

}

或许你注意到,有时你看代码检查“STATUS_PENDING”和等待事件发生。在例子中,我们拥有所有的端点,不会发生这个事情。这也就是为什么讲一些细节被忽略了。在下篇文章,我们将详述这些概念。重要的是正好一次消化一部分。

在驱动中处理irp

一旦你得到IRP,你就拥有了这个IRP. 你就能够使用它做任何你想要的事情。 如果你处理它,当你处理完时你就必须要么完成它,要么把它向下传给另外的驱动。如果你把它传给其他的驱动,你就必须忘记它。你传递给的驱动现在负责完成它。

这个例子过滤驱动,我们地实现有些不同,在我们提供irp给例子驱动后,它想要处理参数。

这样,我们必须要捕获完成时并停止它。因为我们知道底层驱动将会完成它。所以,我们设置我们自己的completion routine, 我们就能停止它。下面就是我们的实现代码。

pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);

IoCopyCurrentIrpStackLocationToNext(Irp);

IoSetCompletionRoutine(Irp,  

       PIO_COMPLETION_ROUTINE) ExampleFilter_CompletionRoutine, NULL,  

       TRUE, TRUE, TRUE);

/*

* IoCallDriver() simply calls the  

* appropriate entry point in the driver object associated

* with the device object.   This is  

* how drivers are basically "chained" together, they must know

* that there are lower driver so they  

* can perform the appropriate action and send down the IRP.

*

* They do not have to send the IRP down  

* they could simply process it completely themselves if they wish.

*/

NtStatus = IoCallDriver(

           pExampleFilterDeviceContext->pNextDeviceInChain, Irp);

/*

* Please note that our  

* implementation here is a simple one.   We do not take into account  

* PENDING IRP's oranything complicated.   We assume that once we get   

* to this locaiton the IRP has alreadybeen completed and our completetion   

* routine was called or it wasn't completed and we are still able  

* to complete it here.

* Our completetion routine makes sure that the IRP is still valid here.

*

*/

if(NT_SUCCESS(NtStatus)

{       /*

          * Data was read?

          */

     if(Irp->IoStatus.Information)

     {

       /*

        * Our filter device is dependent upon the compliation settings of  

        * how we compiled example.sys

        * That means we need to dynamically figure out if we're  

        * using Direct, Buffered or Neither.

        */

       if(DeviceObject->Flags & DO_BUFFERED_IO)

       {

          DbgPrint("ExampleFilter_Read - Use Buffered I/O \r\n");

          /*

           * Implementation for Buffered I/O

           */

          pReadDataBuffer = (PCHAR)Irp->AssociatedIrp.SystemBuffer;

                 

          if(pReadDataBuffer  

              && pIoStackIrp->Parameters.Read.Length > 0)

          {                              

              ExampleFilter_FixNullString(pReadDataBuffer,  

                            (UINT)Irp->IoStatus.Information);

          }

        }

        else

        {

           if(DeviceObject->Flags  

                & DO_DIRECT_IO)

           {

               DbgPrint("ExampleFilter_Read - Use Direct I/O \r\n");

                /*

                 * Implementation for Direct I/O

                 */

                if(pIoStackIrp && Irp->MdlAddress)

                {

                  pReadDataBuffer = MmGetSystemAddressForMdlSafe(

                          Irp->MdlAddress, NormalPagePriority);    

                  if(pReadDataBuffer &&  

                      pIoStackIrp->Parameters.Read.Length)

                  {                              

                      ExampleFilter_FixNullString(pReadDataBuffer,

                                      (UINT)Irp->IoStatus.Information);

                  }

                 }

            }

            else

            {

               DbgPrint("ExampleFilter_Read - Use Neither I/O \r\n");

               /* Implementation for Neither I/O

                */

               __try {

         

                       if(pIoStackIrp->Parameters.Read.Length >  

                                                 0 && Irp->UserBuffer)

                       {

                     

                         ProbeForWrite(Irp->UserBuffer,

                              IoStackIrp->Parameters.Read.Length,  

                              TYPE_ALIGNMENT(char));

                         pReadDataBuffer = Irp->UserBuffer;

                     

                         ExampleFilter_FixNullString(pReadDataBuffer,

                                        (UINT)Irp->IoStatus.Information);

                        }

                     

                } __except( EXCEPTION_EXECUTE_HANDLER ) {

                     

                     NtStatus = GetExceptionCode();      

                }

            }

        }

     }

}

/*

* Complete the IRP

*

*/

Irp->IoStatus.Status = NtStatus;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

....

NTSTATUS ExampleFilter_CompletionRoutine(

            PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context)

{

     

DbgPrint("ExampleFilter_CompletionRoutine Called \r\n");

/*

   * We need to return

   * "STATUS_MORE_PROCESSING_REQUIRED" so that we can  

   * use the IRP in our driver.If we complete this here we  

   * would not be able to use it and the IRP would be completed.   This

   * also means that our driver

   * must also complete the IRP since it has not been completed yet.

   */

   return STATUS_MORE_PROCESSING_REQUIRED;

}

由于我们返回STATUS_MORE_PROCESSING_REQUIRED给I/O管理器,IRP没有被完成。现在你可以在IoCallDriver后操作IRP. 然而现在当我们用完后,我们必须要完成它。这是因为我们停止了IRP地完成。记住,我们的例子中不要考虑STATUS_PENDING ,因为我们拥有所有的端点,并且我们尽力想让例子简单。


过滤例子

本篇过滤驱动例子,把自己挂在了我们在第三篇中创建的设备栈上。如果你记得实现,我们能在两个用户模式进程间进行交互。一个问题是,如果你输入大量的字符串,当读它的时候,用户模式应用程序仅仅显示一条字符串。这个问题,即便在用户模式应用中改正起来很简单,然而又有多大的意思呢?

相反,我们创建一个过滤驱动程序在读后直接截取IRP,并且操作IRP返回参数。他去掉字符串中所有的终止符(null字符),用空格替换。然后在字符串最后无效终止。显然这不是个完美的例子,我们覆盖了最后的字符,却不去检查是否需要。毕竟这仅仅是个简单的例子。

使用例子

为了使用这个例子,你需要做的事情跟第三篇中的一样,唯一不同的是,现在有另一种加载程序,你可在加载example.sys后运行它。它会加载examplefilter.sys 并且会附加到example.sys,不过是否加载examplefilter.sys用户模式程序都可以运行。你可以在两种情况下运行看看他们的不同。入口点都有调试信息,你可以跟踪这些代码信息看看。

结束语

在本篇文章中,我们更多的学习了一些处理IRP和设备栈的知识。另外,我们还学习了如何实现一个简单的过滤驱动。在每一篇文章中,我们都努力在这些基本概念上构建驱动,所以我们可以更进一步的理解驱动是怎样工作的?以及如何开发驱动?

posted on 2009-03-23 22:02 iniwf 阅读(984) 评论(0)  编辑 收藏 引用 所属分类: 驱动


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


导航

统计

常用链接

留言簿(2)

随笔分类

随笔档案

收藏夹

IT技术

积分与排名

最新评论

阅读排行榜

评论排行榜