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