驱动程序开发第四部分:设备堆栈入门






4.83/5 (47投票s)
Mar 5, 2005
11分钟阅读

245575

5356
本文将介绍设备如何相互交互。
引言
这是《编写设备驱动程序》系列的第四版。本文将介绍设备堆栈的概念以及设备如何相互交互。我们将使用之前创建的示例设备驱动程序来演示此主题。为此,我们将介绍“筛选器”驱动程序的概念,我们将创建它以附加到我们自己的驱动程序的设备堆栈。
什么是设备堆栈?
堆栈是一个通用术语,可以将其想象成一堆相互堆叠的物体。还有一种算法实现将堆栈定义为一种存储临时对象的方法,其中最后一个进入的对象是第一个出去的对象(也称为 LIFO)。这两种描述都是相关的。然而,设备堆栈不是算法,也与临时对象无关。因此,一堆相互堆叠的物体的简单描述更相关。
设备堆栈最好的例子就是一叠盘子。盘子相互堆叠,就像一叠设备一样。另一个要记住的细节是,我们说的是“设备堆栈”,而不是“驱动程序堆栈”。在第三个教程中,我们记得,一个驱动程序实际上可以实现多个设备。这意味着一堆设备都可以由一个物理驱动程序实现。然而,本文和许多其他文章都交替使用“设备”和“驱动程序”,尽管它们基本上是独立的但相关的实体。
筛选器驱动程序
这是一个非常常用的流行词,我相信几乎所有程序员都听说过。筛选器驱动程序是一种附加到设备堆栈顶部的驱动程序,旨在在请求到达设备之前“筛选”对设备的请求处理。
您可能会认为设备堆栈中的所有设备(除了最后一个)都是筛选器,但事实并非如此。设备堆栈中的设备,除了筛选器之外,通常取决于该特定设备的架构。例如,您通常在堆栈顶部附近有更高级别的驱动程序。在最一般的情况下,这些更高级别的驱动程序与用户模式请求进行通信和交互。堆栈中的设备开始为下一级设备分解请求,直到链中的最后一个设备处理请求。在设备堆栈底部附近是低级驱动程序,例如“微端口驱动程序”,它们可能会与实际硬件进行通信。
最好的例子可能是文件系统。高级驱动程序维护文件和文件系统的概念。它们可能了解文件存储在磁盘上的位置。低级驱动程序对文件一无所知,只了解读取磁盘扇区的请求。它们还了解如何对这些请求进行排队并优化磁盘查找,但它们不知道磁盘上实际有什么或如何解释数据。
每个附加到设备堆栈的筛选器设备都放在顶部。这意味着,如果另一个筛选器设备在您的设备之后附加到设备堆栈,那么它现在位于您的上方。您永远无法保证处于堆栈的顶部。
要附加到设备堆栈,我们将使用以下 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
这是显而易见的第一步,我们需要创建一个 IRP。这非常简单,您可以直接使用名为 `IoAllocateIrp` 的函数。以下是使用该 API 的简单代码示例。
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);
第二步 – 设置参数
此步骤取决于您想要执行的功能。您需要设置 `FILE_OBJECT`、`IO_STACK_PARAMETER` 以及所有其他内容。在我们的示例中,我们作弊了。我们没有提供 `FILE_OBJECT`,并且设置了最小的参数。为什么?好吧,这只是一个简单的示例,我们拥有所有端点。由于我们控制所有端点,因此我们基本上可以对参数做任何我们想做的事情。但是,如果您阅读了 `IRP_MJ_xxx` 和该驱动程序的特定功能(例如 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 时释放缓冲区。然而,我们只是做了这个快速示例,我们根本不允许 I/O 管理器释放 IRP,我们显然也没有释放 `SystemBuffer`。
第三步 – 下传 IRP
您需要将 IRP 下传给驱动程序。为此,您只需在 `IoCallDriver` API 中指定 `DEVICE_OBJECT` 和 IRP。您基本上可以使用任何您拥有的 `DEVICE_OBJECT`。但是,如果您想从设备堆栈的顶部开始,最好使用 `IoGetRelatedDeviceObject` 等 API 查找顶级设备对象。在我们的示例中,我们有一个调用来获取顶级设备,另一个只是使用我们已有的设备对象。如果您阅读调试输出,您会注意到其中一个我们没有通过筛选器驱动程序。这是因为 `IoCallDriver` 非常简单。它只接受设备对象并查找要调用的适当函数。
NtStatus = IoCallDriver(pFileObject->DeviceObject, MyIrp);
第四步 – 处理并清理 IRP
在我们将 IRP 下传之前,我们做了一件事,就是创建了一个“完成例程”。当 IRP 完成时,这个例程会收到通知。在这种情况下,我们可以做几件事,我们可以允许 IRP 继续,以便我们可以处理其参数,或者我们可以销毁它。我们还可以让 I/O 管理器释放它。这实际上取决于您创建 IRP 的方式。要回答“谁应该释放它”这个问题,您应该阅读 DDK 文档中关于您用于分配 IRP 的 API 的内容。实施错误的方法可能会导致灾难!
这是一个简单的例子,我们只是自己释放它。
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 后处理参数。为此,我们必须捕获完成并阻止其完成。这是因为我们知道较低级别的驱动程序应该并且将完成它。因此,通过设置我们自己的完成例程,我们可以阻止这种情况。这可以通过以下代码完成。
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; }
IRP 将不会完成,因为我们返回给 I/O 管理器,表示需要进行更多处理。现在我们可以在 `IoCallDriver` 之后操作 IRP,但是我们必须在完成后完成它。这是因为我们阻止了 IRP 的完成。请记住,我们的示例没有考虑 `STATUS_PENDING`,因为我们拥有所有端点,并且我们正在尝试使此示例尽可能简单。
筛选器示例
本文中的示例筛选器驱动程序将其自身附加到我们在第 3 篇文章中创建的驱动程序的堆栈。如果您还记得那个实现,我们能够在两个用户模式应用程序之间进行通信。这样做的一个问题是,如果您输入了多个字符串,用户模式应用程序只会打印一个字符串,而它可能已经读取了三个字符串。这很容易在用户模式应用程序中修复,但这有什么乐趣呢?
相反,我们创建了一个筛选器驱动程序,它在读取后拦截 IRP 并操作 IRP 返回参数。它从字符串中删除所有空终止符并用空格替换它们。然后它只是将字符串的末尾用空终止符终止。这显然不是一个完美的例子,因为我们覆盖了最后一个字符,甚至没有尝试查看是否需要这样做,但这只是一个简单的例子。
这些示例只做了最少必要的工作,因此它们可以工作并且尽量不陷入困境(在最简单的情况下)。我宁愿提供一个简单示例的解释,而不是一个包含所有花哨功能的完整示例。这些在 DDK 和 MSDN 上的长文章中已经可以找到,它们一次性解释了所有内容。
使用示例
要使用示例,您只需像第 3 篇文章中那样操作。唯一的区别是,在您已经加载了 *example.sys* 之后,现在还有一个加载器程序可以运行。这个程序将加载 *examplefilter.sys*,并将其附加到 *example.sys*。用户模式程序可以带或不带 *examplefilter.sys* 运行。您可以两种方式运行它并查看差异。所有入口点都有调试语句,因此您可以跟踪代码路径。
结论
在本文中,我们进一步学习了 IRP 处理(为了理解设备堆栈)和设备堆栈。我们还学习了如何实现一个非常简单的筛选器驱动程序。在每篇文章中,我们都将尝试在这些基本思想的基础上进行构建,以便我们能够进一步理解驱动程序的工作原理以及如何开发驱动程序。
本系列的下一篇文章将尝试结合这 4 篇文章中学到的所有内容,并进一步解释 IRP 处理。