win驱动中使用IRP注意项及原因分析(后续补充)

MS上有很多关于驱动中使用IRP的守则(跟黄历中的宜忌差不多了),比如:

Q1.“Drivers must not attempt to reuse IRPs issued by the I/O manager. In particular, drivers should not attempt to reuse IRPs created by the IoMakeAssociatedIrp, IoBuildSynchronousFsdRequest, IoBuildAsynchronousFsdRequest, or IoBuildDeviceIoControlRequest routines.”--摘自:https://msdn.microsoft.com/zh-cn/library/ff561107

A1.大意为,在驱动程序中不应该重用IO管理器发来的IRP。这里概述一下IRP在设备栈中传递的背景:应用层调用ReadFile(NtReadFile)时,IO管理器会创建(调用IoAllocateIRP)并初始化(调用IoInitializeIRP)IRP,其中IoAllocateIrp会带一个StackSize的参数,用于创建和这个IRP关联的设备堆栈。一切完成后,IO管理器将IRP发送给下层设备对象,也就是驱动程序中。毋庸置疑,IRP在IO管理器的当前堆栈深度比IRP在驱动的当前堆栈深度至少深一个StackLocation。

    回到IoReuseIrp上,查看他的调用流程可以发现:IoReuseIrp将调用IoInitializeIrp和RtlZeroMemory(Irp,Irp->Size)清空当前IRP。我们来考虑这样一个情景:IO管理器发送IRP前,在自己的StackLocation中设置了一个CompleteRoution,CompleteRoution中调用KeSetEvent唤醒线程(NtReadFile等待在某个eventObj上)。可是设备驱动中调用RtlZeroMemory(Irp,Irp->Size)后,除了清空了自己及设备栈中位于底层设备的StackLocation(此时这些StackLocation中设置的CompleteRountion已经执行完),还顺带着把上层IO管理器的StackLocation一起清空了。不用说,IO管理器设置的CompleteRountion得不到执行,NtReadFile也将因此陷于无限等待中。

    但是,如果是设备驱动自己创建的IRP,调用IoReuseIrp时,并不会过度清除StackLocation(只清除自己及以下的StackLocation),这应该很好的解释了MS的注释项。


Q2.调用IoCompleteRequest后IRP沿设备堆栈返回。如果遇到某层StackLocation的CompleteRountine返回STATUS_MORE_PROCESSING_REQUIRED,停止返回,本层堆栈重新获得IRP的控制。并且该IRP从完成状态变为未完成状态,需要再次调用IoCompleteRequest.--摘自windows驱动开发技术详解P334

A2.这段话信息点有2个:1.返回STATUS_MORE_PROCESSING_REQUIRED时,本层堆栈重新获得IRP的控制权;2.需要再次调用IoCompleteRequest;下面一一分析

1).需要结合IoCompleteRequest的代码说明:

VOID
FASTCALL
IofCompleteRequest(IN PIRP Irp,
                   IN CCHAR PriorityBoost)
{
    ....
    LastStackPtr = (PIO_STACK_LOCATION)(Irp + 1);
    ...
    do
    {
        /* Set Pending Returned */
		//准备异步返回时,会掉用IoMarkIrpPending使StackPtr->Control在该位上置位
        Irp->PendingReturned = StackPtr->Control & SL_PENDING_RETURNED;
        ...
        if ((NT_SUCCESS(Irp->IoStatus.Status) &&
             (StackPtr->Control & SL_INVOKE_ON_SUCCESS)) ||
            (!NT_SUCCESS(Irp->IoStatus.Status) &&
             (StackPtr->Control & SL_INVOKE_ON_ERROR)) ||
            (Irp->Cancel &&
             (StackPtr->Control & SL_INVOKE_ON_CANCEL)))
        {
            /* Clear the stack location */
            IopClearStackLocation(StackPtr);

            /* Check for highest-level device completion routines */
            if (Irp->CurrentLocation == (Irp->StackCount + 1))
            {
                /* Clear the DO, since the current stack location is invalid */
                DeviceObject = NULL;
            }
            else
            {
               ...
            }

            /* Call the completion routine */
            Status = StackPtr->CompletionRoutine(DeviceObject,
                                                 Irp,
                                                 StackPtr->Context);  <=1 调用CompleteRoutine

            /* Don't touch the Packet in this case, since it might be gone! */
            if (Status == STATUS_MORE_PROCESSING_REQUIRED) return;    <=2 退出IoCompleteRequest
        }
		
假设当前设备栈中由上而下以此堆着filterDo->Fdo->Pdo。当Fdo以同步方式IoCallDriver(Pdo)调用Pdo的某个派遣函数,那么直到Pdo显式调用IoCompleteRequest返回,Fdo才会从IoCallDriver返回。退出IoCompleteRequest!while循环有两大条件:1).遍历完所有设备堆栈,从而自然退出;2):遇到break;/return;语句强行退出while,二者条件满足一个就行。CompleteRoutine返回STATUS_MORE_PROCESSING_REQUIRED正好满足条件2),因此上层Fdo从IoCallDriver中全身而退,继续执行Fdo驱动中IoCallDriver之后的代码。这时StackPtr也被拨回指向Fdo所对应的设备栈,因此Fdo可以再次访问该Irp。

2).虽然Fdo获得了Irp,又随意的再Fdo中直接释放了Irp,而没有执行filterDo对应的设备栈中的CompleteRoutine,那这个完成函数随着Irp的消失,一起得不到执行。如果filterDo正好是异步方式调用IoCallDriver/KeWaitForSingleObject,等待CompleteRoutine中执行KeSetEvent,那很不幸,这个线程就失去了唤醒的机会。因此,某一层重新获得Irp后,还需要再次调用IoCompleteRequest,让它上层的设备栈中设置的CompleteRoutine得到运行的机会。(不能假设上层设备也是通过同步调用进入当前堆栈吧?)


Q3."每当低级驱动完成IRP后,将IRP的堆栈向上回卷时,底层IO堆栈中Control域的SL_PENDING_RETURNED位必须被传播到上一层。如果本层没有设置完成例程,那么这种传播是自动的,即不用程序员指定。否则,这种传播需要程序员自己实现,形如:

NTSTATUS CompletionRoutine(..)
{
    if(Irp->PendingReturned)
    {
        IoMarkIrpPending(Irp);
    }
}

"--摘自Windows驱动开发技术详解P332

A3.完成例程中遇到了Irp->PendingReturned,为什么一定要传播SL_PENDING_RETURNED到上层?这个问题在刚学驱动时困扰我很久,现在结合源码来回答。

切入点是驱动完成Irp后,和完成Irp相关的十有八九是函数IofCompleteRequest,这里择要罗列相关代码:

VOID
FASTCALL
IofCompleteRequest(IN PIRP Irp,
                   IN CCHAR PriorityBoost)
{
    ...
    PIO_STACK_LOCATION StackPtr = IoGetCurrentIrpStackLocation(Irp); <---A
    IoSkipCurrentIrpStackLocation(Irp); <---B
    ...
    do
    {
        /* Set Pending Returned */
		//准备异步返回时,会掉用IoMarkIrpPending使StackPtr->Control在该位上置位
        Irp->PendingReturned = StackPtr->Control & SL_PENDING_RETURNED;  <---B1
    if() <---E
    {
        Status = StackPtr->CompletionRoutine();
    }
    else
        {
            /* Otherwise, check if this is a completed IRP */
            if ((Irp->CurrentLocation <= Irp->StackCount) && <---B2
                (Irp->PendingReturned))
            {
                /* Mark it as pending */
                IoMarkIrpPending(Irp);
            }

            /* Clear the stack location */
            IopClearStackLocation(StackPtr);
        }
    IoSkipCurrentIrpStackLocation(Irp); <---C
        StackPtr++; <---D
}
假设现在设备栈由高往低依次为filterDO->Fdo->Pdo。A处,StackPtr指向Pdo的设备栈;B处,Irp->CurrentLocation已经指向Fdo的设备栈,使得StackPtr和Irp->CurrentLocation形成譬如链表一样的前后关系;如果把Irp->PendingReturned看做临时变量,那么到了B1处,临时变量会装载来自下层Pdo设备StackPtr[Pdo]->Control的值,而到了B2处会以临时变量的值更新到当前Fdo设备StackPtr[Pdo]->Control,说白了就是取链表后一节点的值更新前一节点的值;CD两处依次改变Irp->CurrentLocation和StackPtr指针的值,使得他两维持链表前后关系。如此往复,就像波浪一样将Irp->Currentlocation从堆栈底部传递到顶部。重点来了,如果遇到了完成函数,IofCompleteRequest进入E处的if块,当从if块退出时,错失了"取链表后一节点的值更新前一节点的值"的机会,将波浪的连续性给截流了。因此需要在当前断流处(更新前一节点)StackPtr[Fdo]重新设置Control |= SL_PENDING_RETURNED,填补这个空缺。

你可能感兴趣的:(windows)