USB开发—自上而下(三)

上一节关于CreateFile函数的调用过程分析中,有一个函数很让我在意,下面代码片段为其核心功能的实现:

NTSTATUS
NTAPI
IopParseDevice(IN PVOID ParseObject,
               IN PVOID ObjectType,
               IN OUT PACCESS_STATE AccessState,
               IN KPROCESSOR_MODE AccessMode,
               IN ULONG Attributes,
               IN OUT PUNICODE_STRING CompleteName,
               IN OUT PUNICODE_STRING RemainingName,
               IN OUT PVOID Context,
               IN PSECURITY_QUALITY_OF_SERVICE SecurityQos OPTIONAL,
               OUT PVOID *Object)
{
    ..................
    PIRP Irp;
    Irp = IoAllocateIrp(DeviceObject->StackSize, TRUE);
    if (!Irp)
    {
        /* Dereference the device and VPB, then fail */
        IopDereferenceDeviceObject(OriginalDeviceObject, FALSE);
        if (Vpb) IopDereferenceVpbAndFree(Vpb);
        return STATUS_INSUFFICIENT_RESOURCES;
    }
    Irp->RequestorMode = AccessMode;
    Irp->Flags = IRP_CREATE_OPERATION |
                 IRP_SYNCHRONOUS_API |
                 IRP_DEFER_IO_COMPLETION;
    Irp->Tail.Overlay.Thread = PsGetCurrentThread();
    Irp->UserIosb = &IoStatusBlock;
    Irp->MdlAddress = NULL;
    Irp->PendingReturned = FALSE;
    Irp->UserEvent = NULL;
    Irp->Cancel = FALSE;
    Irp->CancelRoutine = NULL;
    Irp->Tail.Overlay.AuxiliaryBuffer = NULL;
    SecurityContext.SecurityQos = SecurityQos;
    SecurityContext.AccessState = AccessState;
    SecurityContext.DesiredAccess = AccessState->RemainingDesiredAccess;
    SecurityContext.FullCreateOptions = OpenPacket->CreateOptions;

    /* Get the I/O Stack location */
    StackLoc = (PEXTENDED_IO_STACK_LOCATION)IoGetNextIrpStackLocation(Irp);
    StackLoc->Control = 0;

    /* Check what kind of file this is */
    switch (OpenPacket->CreateFileType)
    {
        /* Normal file */
        case CreateFileTypeNone:

            /* Set the major function and EA Length */
            StackLoc->MajorFunction = IRP_MJ_CREATE;
            StackLoc->Parameters.Create.EaLength = OpenPacket->EaLength;

            /* Set the flags */
            StackLoc->Flags = (UCHAR)OpenPacket->Options;
            StackLoc->Flags |= !(Attributes & OBJ_CASE_INSENSITIVE) ?
                                SL_CASE_SENSITIVE: 0;
            break;

        /* Named pipe */
        case CreateFileTypeNamedPipe:

            /* Set the named pipe MJ and set the parameters */
            StackLoc->MajorFunction = IRP_MJ_CREATE_NAMED_PIPE;
            StackLoc->Parameters.CreatePipe.Parameters =
                OpenPacket->MailslotOrPipeParameters;
            break;

        /* Mailslot */
        case CreateFileTypeMailslot:

            /* Set the mailslot MJ and set the parameters */
            StackLoc->MajorFunction = IRP_MJ_CREATE_MAILSLOT;
            StackLoc->Parameters.CreateMailslot.Parameters =
                OpenPacket->MailslotOrPipeParameters;
            break;
    }

    /* Set the common data */
    Irp->Overlay.AllocationSize = OpenPacket->AllocationSize;
    Irp->AssociatedIrp.SystemBuffer = OpenPacket->EaBuffer;
    StackLoc->Parameters.Create.Options = (OpenPacket->Disposition << 24) |
                                          (OpenPacket->CreateOptions &
                                           0xFFFFFF);
    StackLoc->Parameters.Create.FileAttributes = OpenPacket->FileAttributes;
    StackLoc->Parameters.Create.ShareAccess = OpenPacket->ShareAccess;
    StackLoc->Parameters.Create.SecurityContext = &SecurityContext;

    /* Check if we really need to create an object */
    if (!UseDummyFile)
    {
        /* Create the actual file object */
        InitializeObjectAttributes(&ObjectAttributes,
                                   NULL,
                                   Attributes,
                                   NULL,
                                   NULL);
        Status = ObCreateObject(KernelMode,
                                IoFileObjectType,
                                &ObjectAttributes,
                                AccessMode,
                                NULL,
                                sizeof(FILE_OBJECT),
                                0,
                                0,
                                (PVOID*)&FileObject);
        if (!NT_SUCCESS(Status))
        {
            /* Create failed, free the IRP */
            IoFreeIrp(Irp);

            /* Dereference the device and VPB */
            IopDereferenceDeviceObject(OriginalDeviceObject, FALSE);
            if (Vpb) IopDereferenceVpbAndFree(Vpb);

            /* We failed, return status */
            OpenPacket->FinalStatus = Status;
            return Status;
        }

        /* Clear the file object */
        RtlZeroMemory(FileObject, sizeof(FILE_OBJECT));

        /* Check if this is Synch I/O */
        if (OpenPacket->CreateOptions &
            (FILE_SYNCHRONOUS_IO_ALERT | FILE_SYNCHRONOUS_IO_NONALERT))
        {
            /* Set the synch flag */
            FileObject->Flags |= FO_SYNCHRONOUS_IO;

            /* Check if it's also alertable */
            if (OpenPacket->CreateOptions & FILE_SYNCHRONOUS_IO_ALERT)
            {
                /* It is, set the alertable flag */
                FileObject->Flags |= FO_ALERTABLE_IO;
            }
        }

        /* Check if this is synch I/O */
        if (FileObject->Flags & FO_SYNCHRONOUS_IO)
        {
            /* Initialize the event */
            KeInitializeEvent(&FileObject->Lock, SynchronizationEvent, FALSE);
        }

        /* Check if the caller requested no intermediate buffering */
        if (OpenPacket->CreateOptions & FILE_NO_INTERMEDIATE_BUFFERING)
        {
            /* Set the correct flag for the FSD to read */
            FileObject->Flags |= FO_NO_INTERMEDIATE_BUFFERING;
        }

        /* Check if the caller requested write through support */
        if (OpenPacket->CreateOptions & FILE_WRITE_THROUGH)
        {
            /* Set the correct flag for the FSD to read */
            FileObject->Flags |= FO_WRITE_THROUGH;
        }

        /* Check if the caller says the file will be only read sequentially */
        if (OpenPacket->CreateOptions & FILE_SEQUENTIAL_ONLY)
        {
            /* Set the correct flag for the FSD to read */
            FileObject->Flags |= FO_SEQUENTIAL_ONLY;
        }

        /* Check if the caller believes the file will be only read randomly */
        if (OpenPacket->CreateOptions & FILE_RANDOM_ACCESS)
        {
            /* Set the correct flag for the FSD to read */
            FileObject->Flags |= FO_RANDOM_ACCESS;
        }
    }
    else
    {
        /* Use the dummy object instead */
        DummyFileObject = OpenPacket->DummyFileObject;
        RtlZeroMemory(DummyFileObject, sizeof(DUMMY_FILE_OBJECT));

        /* Set it up */
        FileObject = (PFILE_OBJECT)&DummyFileObject->ObjectHeader.Body;
        DummyFileObject->ObjectHeader.Type = IoFileObjectType;
        DummyFileObject->ObjectHeader.PointerCount = 1;
    }

    /* Setup the file header */
    FileObject->Type = IO_TYPE_FILE;
    FileObject->Size = sizeof(FILE_OBJECT);
    FileObject->RelatedFileObject = OpenPacket->RelatedFileObject;
    FileObject->DeviceObject = OriginalDeviceObject;

    /* Check if this is a direct device open */
    if (DirectOpen) FileObject->Flags |= FO_DIRECT_DEVICE_OPEN;

    /* Check if the caller wants case sensitivity */
    if (!(Attributes & OBJ_CASE_INSENSITIVE))
    {
        /* Tell the driver about it */
        FileObject->Flags |= FO_OPENED_CASE_SENSITIVE;
    }

    /* Now set the file object */
    Irp->Tail.Overlay.OriginalFileObject = FileObject;
    StackLoc->FileObject = FileObject;

    /* Check if the file object has a name */
    if (RemainingName->Length)
    {
        /* Setup the unicode string */
        FileObject->FileName.MaximumLength = RemainingName->Length +
                                             sizeof(WCHAR);
        FileObject->FileName.Buffer = ExAllocatePoolWithTag(PagedPool,
                                                            FileObject->
                                                            FileName.
                                                            MaximumLength,
                                                            TAG_IO_NAME);
        if (!FileObject->FileName.Buffer)
        {
            /* Failed to allocate the name, free the IRP */
            IoFreeIrp(Irp);

            /* Dereference the device object and VPB */
            IopDereferenceDeviceObject(OriginalDeviceObject, FALSE);
            if (Vpb) IopDereferenceVpbAndFree(Vpb);

            /* Clear the FO and dereference it */
            FileObject->DeviceObject = NULL;
            if (!UseDummyFile) ObDereferenceObject(FileObject);

            /* Fail */
            return STATUS_INSUFFICIENT_RESOURCES;
        }
    }

    /* Copy the name */
    RtlCopyUnicodeString(&FileObject->FileName, RemainingName);

    /* Initialize the File Object event and set the FO */
    KeInitializeEvent(&FileObject->Event, NotificationEvent, FALSE);
    OpenPacket->FileObject = FileObject;

    /* Queue the IRP and call the driver */
    IopQueueIrpToThread(Irp);
    Status = IoCallDriver(DeviceObject, Irp);
    if (Status == STATUS_PENDING)
    {
        /* Wait for the driver to complete the create */
        KeWaitForSingleObject(&FileObject->Event,
                              Executive,
                              KernelMode,
                              FALSE,
                              NULL);

        /* Get the new status */
        Status = IoStatusBlock.Status;
    }
    else
    {
        /* We'll have to complete it ourselves */
        ASSERT(!Irp->PendingReturned);
        ASSERT(!Irp->MdlAddress);

        /* Completion happens at APC_LEVEL */
        KeRaiseIrql(APC_LEVEL, &OldIrql);

        /* Get the new I/O Status block ourselves */
        IoStatusBlock = Irp->IoStatus;
        Status = IoStatusBlock.Status;

        /* Manually signal the even, we can't have any waiters */
        FileObject->Event.Header.SignalState = 1;

        /* Now that we've signaled the events, de-associate the IRP */
        IopUnQueueIrpFromThread(Irp);

        /* Check if the IRP had an input buffer */
        if ((Irp->Flags & IRP_BUFFERED_IO) &&
            (Irp->Flags & IRP_DEALLOCATE_BUFFER))
        {
            /* Free it. A driver might've tacked one on */
            ExFreePool(Irp->AssociatedIrp.SystemBuffer);
        }

        /* Free the IRP and bring the IRQL back down */
        IoFreeIrp(Irp);
        KeLowerIrql(OldIrql);
    }

    /* Copy the I/O Status */
    OpenPacket->Information = IoStatusBlock.Information;

    /* The driver failed to create the file */
    if (!NT_SUCCESS(Status))
    {
        /* Check if we have a name */
        if (FileObject->FileName.Length)
        {
            /* Free it */
            ExFreePoolWithTag(FileObject->FileName.Buffer, TAG_IO_NAME);
            FileObject->FileName.Length = 0;
        }

        /* Clear its device object */
        FileObject->DeviceObject = NULL;

        /* Save this now because the FO might go away */
        OpenCancelled = FileObject->Flags & FO_FILE_OPEN_CANCELLED ?
                        TRUE : FALSE;

        /* Clear the file object in the open packet */
        OpenPacket->FileObject = NULL;

        /* Dereference the file object */
        if (!UseDummyFile) ObDereferenceObject(FileObject);

        /* Dereference the device object */
        IopDereferenceDeviceObject(OriginalDeviceObject, FALSE);

        /* Unless the driver cancelled the open, dereference the VPB */
        if (!(OpenCancelled) && (Vpb)) IopDereferenceVpbAndFree(Vpb);

        /* Set the status and return */
        OpenPacket->FinalStatus = Status;
        return Status;
    }
    ...............................
}

这段代码节选自ReactOSV03-15,代码比较长,我节选了其中最重要的一部分。代码的一开头首先初始化一个IRP结构指针,IRP由IRP头部与附加结构IO_STACK_LOCATION组成,参考以下代码:


IopParseDevice->IoAllocateIrp
PIRP NTAPI IoAllocateIrp(IN CCHAR StackSize,IN BOOLEAN ChargeQuota)
{
    PIRP Irp = NULL;
    USHORT Size = IoSizeOfIrp(StackSize);
    PKPRCB Prcb;
    UCHAR Flags = 0;
    PNPAGED_LOOKASIDE_LIST List = NULL;
    PP_NPAGED_LOOKASIDE_NUMBER ListType = LookasideSmallIrpList;

    /* Set Charge Quota Flag */
    if (ChargeQuota) Flags |= IRP_QUOTA_CHARGED;

    /* FIXME: Implement Lookaside Floats */

    /* Figure out which Lookaside List to use */
    if ((StackSize <= 8) && (ChargeQuota == FALSE))
    {
        /* Set Fixed Size Flag */
        Flags = IRP_ALLOCATED_FIXED_SIZE;

        /* See if we should use big list */
        if (StackSize != 1)
        {
            Size = IoSizeOfIrp(8);
            ListType = LookasideLargeIrpList;
        }

        /* Get the PRCB */
        Prcb = KeGetCurrentPrcb();

        /* Get the P List First */
        List = (PNPAGED_LOOKASIDE_LIST)Prcb->PPLookasideList[ListType].P;

        /* Attempt allocation */
        List->L.TotalAllocates++;
        Irp = (PIRP)InterlockedPopEntrySList(&List->L.ListHead);

        /* Check if the P List failed */
        if (!Irp)
        {
            /* Let the balancer know */
            List->L.AllocateMisses++;

            /* Try the L List */
            List = (PNPAGED_LOOKASIDE_LIST)Prcb->PPLookasideList[ListType].L;
            List->L.TotalAllocates++;
            Irp = (PIRP)InterlockedPopEntrySList(&List->L.ListHead);
        }
    }

    /* Check if we have to use the pool */
    if (!Irp)
    {
        /* Did we try lookaside and fail? */
        if (Flags & IRP_ALLOCATED_FIXED_SIZE) List->L.AllocateMisses++;

        /* Check if we should charge quota */
        if (ChargeQuota)
        {
            /* Irp = ExAllocatePoolWithQuotaTag(NonPagedPool, Size, TAG_IRP); */
            /* FIXME */
            Irp = ExAllocatePoolWithTag(NonPagedPool, Size, TAG_IRP);
        }
        else
        {
            /* Allocate the IRP With no Quota charge */
            Irp = ExAllocatePoolWithTag(NonPagedPool, Size, TAG_IRP);
        }

        /* Make sure it was sucessful */
        if (!Irp) return(NULL);
    }
    else
    {
        /* In this case there is no charge quota */
        Flags &= ~IRP_QUOTA_CHARGED;
    }

    /* Now Initialize it */
    IoInitializeIrp(Irp, Size, StackSize);

    /* Set the Allocation Flags */
    Irp->AllocationFlags = Flags;

    /* Return it */
    IOTRACE(IO_IRP_DEBUG,
            "%s - Allocated IRP %p with allocation flags %lx\n",
            __FUNCTION__,
            Irp,
            Flags);
    return Irp;
}

首先来看宏定义IoSizeOfIrp

#define IoSizeOfIrp(_StackSize) \
  ((USHORT) (sizeof(IRP) + ((_StackSize) * (sizeof(IO_STACK_LOCATION)))))

也就是说,初始化IRP同时,IRP头部与IO_STACK_LOCATION会同时创建,这里有一个技巧:如果IRP的StackCount=1,则标记为LookasideSmallIrpList,如果StackCount在1-8之间,则标记为LookasideLargeIrpList,并且都按StackSize=8处理,在这两种情况下,操作系统为我们准备了一份缓存,直接去缓存中取相应大小的内存就可以了,一般的PNP驱动都不会超过8层,这种设计能够满足大部分的应用场景,当StackSize>8时,就要去内核堆中动态申请一份内存就可以了。


IRP创建之后,紧跟在后面的是IRP的初始化:



Type与Size为结构体的头部,注意这个Size是包括IO_STACK_LOCATION的大小;MdlAddress指向内存描述表(MDL),如果创建设备指定设备Flags为DO_DIRECT_IO时,这个MDL就会派上大用场;AssociatedIRP是一个union,AssociatedIRP.SystemBuffer指向一个缓冲区,通过这个缓冲区可以与User模式交互数据,前提是设备Flags为DO_BUFFERED_IO;ThreadListEntry在IRP挂接到线程Thread时使用;IoStatus是一个结构体,用于IRP消息的返回;RequestMode用于指定请求来源是UserMode还是KernelMode,在不同模式下会做不同级别的检查;PenddingReturned标识IRP请求是否返回STATUS_PENDING,稍后我们还会继续做分析;StackCount表示栈的层数;CurrentLocation表示当前处于栈的哪一层;Cancel标识是否针对IRP调用了函数IoCancelIrp();CancelIrql是一个IRQL值;ApcEnviroment是一个枚举,跟APC函数的调用有关,这里我们不做过多表述;AllocationFlags是一个标志符,常见的标志有IRP_ALLOCATED_FIXED_SIZE、IRP_LOOKASIDE_ALLOCATION等,UserIosb指向结构体IO_STATUS_BLOCK,具体作用我还没弄明白,我们先不去管它;UserEvent是一个指向KEVENT的指针,这个KEVENT就是异步调用时,通过Overlap结构传递的Event,Overlay又是一个union,其中UserApcRoutine与UserApcContext对APC实现机制非常重要;CancelRoutine是驱动程序取消例程的地址,后面我们再讨论;UserBuffer用在非DIRECT非BUFFER的场合,不建议使用;最后的Tail也是一个union,其中的CurrentStackLocation是一个IO_STACK_LOCATION的指针,表示当前栈的位置,在后面的驱动程序分析过程当会经常碰到。


IoGetNextIrpStackLocation的代码我直接贴出来,就不多做分析了,需要说明的是,IRP初始创建时CurrentLocation的地址是指向最上层STACK_LOCATION+1的位置的,也就是一个无效地址,通过IoGetNextIrpStackLocation就将指针指向正确的地方。

PIO_STACK_LOCATION IoGetNextIrpStackLocation(_In_ PIRP Irp)
{
  ASSERT(Irp->CurrentLocation > 0);
#ifdef NONAMELESSUNION
  return ((Irp)->Tail.Overlay.s.u.CurrentStackLocation - 1 );
#else
  return ((Irp)->Tail.Overlay.CurrentStackLocation - 1 );
#endif
}


回到IopParseDevice,发现内核对象MailSlot与NamedPipe的创建于常规Device有点不太一样,这个也不是我们讨论的范围

紧接着是IO_STACK_LOCATION的赋值,其结构体定义如下:


MajorFunction与MinorFunction指定了当前IRP的请求代号,驱动程序会根据代号做不同的响应;Control常用于函数IoMarkIrpPending,有机会我会再重点说明这个参数的用法,Parameter是对IRP请求参数的统称,其结构是一个很大的结构体,具体内容请查找相关书籍,DeviceObject指向当前驱动设备对象,该对象由上层驱动程序指定;FileObject指向内核文件对象的地址,CreateFile返回的句柄对应的文件对象就是它;CompleteRoutine与Context分别指向完成例程的函数地址与参数,主要用于异步过程调用。


跳过后面文件操作的那一部分细节,我们直接来到IRP请求的发送这一节,首先可以看到的操作是将IRP插入到当前线程:

VOID IopQueueIrpToThread(IN PIRP Irp)
{
    KIRQL OldIrql;

    /* Raise to APC Level */
    KeRaiseIrql(APC_LEVEL, &OldIrql);

    /* Insert it into the list */
    InsertHeadList(&Irp->Tail.Overlay.Thread->IrpList, &Irp->ThreadListEntry);

    /* Lower irql */
    KeLowerIrql(OldIrql);
}

接着就是重头戏,IoCallDriver的调用,IoCallDriver通过IofCallDriver实现:

NTSTATUS IofCallDriver(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)
{
    PDRIVER_OBJECT DriverObject;
    PIO_STACK_LOCATION StackPtr;

    /* Make sure this is a valid IRP */
    ASSERT(Irp->Type == IO_TYPE_IRP);

    /* Get the Driver Object */
    DriverObject = DeviceObject->DriverObject;

    /* Decrease the current location and check if */
    Irp->CurrentLocation--;
    if (Irp->CurrentLocation <= 0)
    {
        /* This IRP ran out of stack, bugcheck */
        KeBugCheckEx(NO_MORE_IRP_STACK_LOCATIONS, (ULONG_PTR)Irp, 0, 0, 0);
    }

    /* Now update the stack location */
    StackPtr = IoGetNextIrpStackLocation(Irp);
    Irp->Tail.Overlay.CurrentStackLocation = StackPtr;

    /* Get the Device Object */
    StackPtr->DeviceObject = DeviceObject;

    /* Call it */
    return DriverObject->MajorFunction[StackPtr->MajorFunction](DeviceObject,Irp);
}
这个函数功能很简单:获取下层驱动对象,并调用驱动函数。


再来看IoCallDriver后面的这一段if{}.. else{}..。如果IoCallDriver返回值Status为STATUS_PENDING,则表示返回过程是异步的,需要静静的等待FileObject->Event变成有信号,函数才能返回(CreateFile本身是不存在异步的)。如果Status返回非STATUS_PENDING,那就是操作成功了,这时候需要将当前IRQL提升到APC Level,并将IRP从对应的线程中摘除,并释放IRP内存。最后就是返回值的复制与其他善后工作了。


调用DriverObject->MajorFunction[StackPtr->MajorFunction](DeviceObject,Irp),进入驱动模块的设计范畴,OnCreate代码具体代码怎么写看个人的喜好,比较常见的做法是:

IO_STATUS OnCreate(DEVICE_OBJECT *device_object, IRP *irp)
{
    ...........
    irp->IoStatus.Status = status;
    irp->IoStatus.Information = info;
    IoCompleteRequest(irp, IO_NO_INCREMENT);
    return status;
}

这里的IoCompleteRequest又是一个需要重点注意的函数,让我们来看看它的实现:

 #define IoCompleteRequest IofCompleteRequest

VOID IofCompleteRequest(IN PIRP Irp,IN CCHAR PriorityBoost)
{
    PIO_STACK_LOCATION StackPtr, LastStackPtr;
    PDEVICE_OBJECT DeviceObject;
    PFILE_OBJECT FileObject;
    PETHREAD Thread;
    NTSTATUS Status;
    PMDL Mdl, NextMdl;
    ULONG MasterCount;
    PIRP MasterIrp;
    ULONG Flags;
    NTSTATUS ErrorCode = STATUS_SUCCESS;
    IOTRACE(IO_IRP_DEBUG,
            "%s - Completing IRP %p\n",
            __FUNCTION__,
            Irp);

    /* Make sure this IRP isn't getting completed twice or is invalid */
    if ((Irp->CurrentLocation) > (Irp->StackCount + 1))
    {
        /* Bugcheck */
        KeBugCheckEx(MULTIPLE_IRP_COMPLETE_REQUESTS, (ULONG_PTR)Irp, 0, 0, 0);
    }

    /* Some sanity checks */
    ASSERT(Irp->Type == IO_TYPE_IRP);
    ASSERT(!Irp->CancelRoutine);
    ASSERT(Irp->IoStatus.Status != STATUS_PENDING);
    ASSERT(Irp->IoStatus.Status != (NTSTATUS)0xFFFFFFFF);

    /* Get the last stack */
    LastStackPtr = (PIO_STACK_LOCATION)(Irp + 1);
    if (LastStackPtr->Control & SL_ERROR_RETURNED)
    {
        /* Get the error code */
        ErrorCode = PtrToUlong(LastStackPtr->Parameters.Others.Argument4);
    }

    /*
     * Start the loop with the current stack and point the IRP to the next stack
     * and then keep incrementing the stack as we loop through. The IRP should
     * always point to the next stack location w.r.t the one currently being
     * analyzed, so completion routine code will see the appropriate value.
     * Because of this, we must loop until the current stack location is +1 of
     * the stack count, because when StackPtr is at the end, CurrentLocation is +1.
     */
    for (StackPtr = IoGetCurrentIrpStackLocation(Irp),
         Irp->CurrentLocation++,
         Irp->Tail.Overlay.CurrentStackLocation++;
         Irp->CurrentLocation <= (Irp->StackCount + 1);
         StackPtr++,
         Irp->CurrentLocation++,
         Irp->Tail.Overlay.CurrentStackLocation++)
    {
        /* Set Pending Returned */
        Irp->PendingReturned = StackPtr->Control & SL_PENDING_RETURNED;

        /* Check if we failed */
        if (!NT_SUCCESS(Irp->IoStatus.Status))
        {
            /* Check if it was changed by a completion routine */
            if (Irp->IoStatus.Status != ErrorCode)
            {
                /* Update the error for the current stack */
                ErrorCode = Irp->IoStatus.Status;
                StackPtr->Control |= SL_ERROR_RETURNED;
                LastStackPtr->Parameters.Others.Argument4 = UlongToPtr(ErrorCode);
                LastStackPtr->Control |= SL_ERROR_RETURNED;
            }
        }

        /* Check if there is a Completion Routine to Call */
        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
            {
                /* Otherwise, return the real one */
                DeviceObject = IoGetCurrentIrpStackLocation(Irp)->DeviceObject;
            }

            /* Call the completion routine */
            Status = StackPtr->CompletionRoutine(DeviceObject,
                                                 Irp,
                                                 StackPtr->Context);

            /* Don't touch the Packet in this case, since it might be gone! */
            if (Status == STATUS_MORE_PROCESSING_REQUIRED) return;
        }
        else
        {
            /* Otherwise, check if this is a completed IRP */
            if ((Irp->CurrentLocation <= Irp->StackCount) &&
                (Irp->PendingReturned))
            {
                /* Mark it as pending */
                IoMarkIrpPending(Irp);
            }

            /* Clear the stack location */
            IopClearStackLocation(StackPtr);
        }
    }

    /* Check if the IRP is an associated IRP */
    if (Irp->Flags & IRP_ASSOCIATED_IRP)
    {
        /* Get the master IRP and count */
        MasterIrp = Irp->AssociatedIrp.MasterIrp;
        MasterCount = InterlockedDecrement(&MasterIrp->AssociatedIrp.IrpCount);

        /* Free the MDLs */
        for (Mdl = Irp->MdlAddress; Mdl; Mdl = NextMdl)
        {
            /* Go to the next one */
            NextMdl = Mdl->Next;
            IoFreeMdl(Mdl);
        }

        /* Free the IRP itself */
        IoFreeIrp(Irp);

        /* Complete the Master IRP */
        if (!MasterCount) IofCompleteRequest(MasterIrp, PriorityBoost);
        return;
    }

    /* We don't support this yet */
    ASSERT(Irp->IoStatus.Status != STATUS_REPARSE);

    /* Check if we have an auxiliary buffer */
    if (Irp->Tail.Overlay.AuxiliaryBuffer)
    {
        /* Free it */
        ExFreePool(Irp->Tail.Overlay.AuxiliaryBuffer);
        Irp->Tail.Overlay.AuxiliaryBuffer = NULL;
    }

    /* Check if this is a Paging I/O or Close Operation */
    if (Irp->Flags & (IRP_PAGING_IO | IRP_CLOSE_OPERATION))
    {
        /* Handle a Close Operation or Sync Paging I/O */
        if (Irp->Flags & (IRP_SYNCHRONOUS_PAGING_IO | IRP_CLOSE_OPERATION))
        {
            /* Set the I/O Status and Signal the Event */
            Flags = Irp->Flags & (IRP_SYNCHRONOUS_PAGING_IO | IRP_PAGING_IO);
            *Irp->UserIosb = Irp->IoStatus;
            KeSetEvent(Irp->UserEvent, PriorityBoost, FALSE);

            /* Free the IRP for a Paging I/O Only, Close is handled by us */
            if (Flags) IoFreeIrp(Irp);
        }
        else
        {
#if 0
            /* Page 166 */
            KeInitializeApc(&Irp->Tail.Apc
                            &Irp->Tail.Overlay.Thread->Tcb,
                            Irp->ApcEnvironment,
                            IopCompletePageWrite,
                            NULL,
                            NULL,
                            KernelMode,
                            NULL);
            KeInsertQueueApc(&Irp->Tail.Apc,
                             NULL,
                             NULL,
                             PriorityBoost);
#else
            /* Not implemented yet. */
            UNIMPLEMENTED_DBGBREAK("Not supported!\n");
#endif
        }

        /* Get out of here */
        return;
    }

    /* Unlock MDL Pages, page 167. */
    Mdl = Irp->MdlAddress;
    while (Mdl)
    {
		MmUnlockPages(Mdl);
        Mdl = Mdl->Next;
    }

    /* Check if we should exit because of a Deferred I/O (page 168) */
    if ((Irp->Flags & IRP_DEFER_IO_COMPLETION) && !(Irp->PendingReturned))
    {
        /*
         * Return without queuing the completion APC, since the caller will
         * take care of doing its own optimized completion at PASSIVE_LEVEL.
         */
        return;
    }

    /* Get the thread and file object */
    Thread = Irp->Tail.Overlay.Thread;
    FileObject = Irp->Tail.Overlay.OriginalFileObject;

    /* Make sure the IRP isn't canceled */
    if (!Irp->Cancel)
    {
        /* Initialize the APC */
        KeInitializeApc(&Irp->Tail.Apc,
                        &Thread->Tcb,
                        Irp->ApcEnvironment,
                        IopCompleteRequest,
                        NULL,
                        NULL,
                        KernelMode,
                        NULL);

        /* Queue it */
        KeInsertQueueApc(&Irp->Tail.Apc,
                         FileObject,
                         NULL, /* This is used for REPARSE stuff */
                         PriorityBoost);
    }
    else
    {
        /* The IRP just got canceled... does a thread still own it? */
        if (Thread)
        {
            /* Yes! There is still hope! Initialize the APC */
            KeInitializeApc(&Irp->Tail.Apc,
                            &Thread->Tcb,
                            Irp->ApcEnvironment,
                            IopCompleteRequest,
                            NULL,
                            NULL,
                            KernelMode,
                            NULL);

            /* Queue it */
            KeInsertQueueApc(&Irp->Tail.Apc,
                             FileObject,
                             NULL, /* This is used for REPARSE stuff */
                             PriorityBoost);
        }
        else
        {
            /* Nothing left for us to do, kill it */
            ASSERT(Irp->Cancel);
            IopCleanupIrp(Irp, FileObject);
        }
    }
}
跳过前面的安全检查,来到后面for循环的这一段代码,这个循环遍历的对象是:从最底层IO_STACK_LOCATION到最上层IO_STACK_LOCATION,下面我们来逐步分析:

        /* Set Pending Returned */
        Irp->PendingReturned = StackPtr->Control & SL_PENDING_RETURNED;

如果当前层的Control字段能够找到SL_PENDING_RETURN标记,则Irp的PendingReturned字段置为1,否则置为0,很显然,如果最下面一层堆叠是异步返回的,Control标记中肯定能找到SL_PENDING_RETURN标记,Irp的PendingReturned就会等于1,但这是在一个循环当中,所以在上层堆叠的循环过程中,这个函数又会被执行一次,上层Control是一个什么状态这个很不确定,这不就将最下层设置好的PendingReturned给覆盖了吗?

是的,为了解决这种问题,驱动函数中,驱动函数通常需要如下操作:

NTSTATUS pass_irp_down(libusb_device_t *dev, IRP *irp,
                       PIO_COMPLETION_ROUTINE completion_routine,
                       void *context)
{
    if (completion_routine)
    {
        IoCopyCurrentIrpStackLocationToNext(irp);
        IoSetCompletionRoutine(irp, completion_routine, context,
                               TRUE, TRUE, TRUE);
    }
    else
    {
        IoSkipCurrentIrpStackLocation(irp);
    }
    return IoCallDriver(dev->next_stack_device, irp);
}
上层堆叠向下层堆叠传递IRP时,如果有完成函数completion_routine,则将当前IO_STACK_LOCATION的参数复制到下一层,并设置完成函数;如果没有完成函数,则需要将堆叠指针+1,让下一层与本层共用一个IO_STACK_LOCATION。

当上下层没有共用IO_STACK_LOCATION的时候,completion_routine的实现体中通常需要进行如下操作:

static NTSTATUS DDKAPI on_xx_complete(DEVICE_OBJECT *device_object, IRP *irp, void *context)
{
    ........................
    if (irp->PendingReturned)
    {
        IoMarkIrpPending(irp);
    }
    ........................
    return STATUS_SUCCESS;
}

这样一来,Irp->PendingReturned = StackPtr->Control & SL_PENDING_RETURNED操作就说得通了:下层驱动对象将irp->PendingReturned置为1,并调用上一层的完成函数complete_routine,完成函数又会将上一层的StackPtr->Control的SL_PENDING_RETURNED置位,循环到达上一层堆叠之后,又会再一次调用Irp->PendingReturned = StackPtr->Control & SL_PENDING_RETURNED,如此往复,就好像接力棒传递一样,最终到达IO管理器,管理器看到的结果就是Irp->PendingReturned = 1。

为什么要如此煞费苦心,直接返回irp->PendingReturned = 1不好吗?我想设计这套系统的人是不太愿意我们直接操作Irp这个数据结构的,每一层驱动都只能在自己的IO_STACK_LOCATION范围内活动,这样就避免了开发中的混乱。

另外,有一个小技巧就是:虽然完成函数是在当前堆叠中实现的,不过函数指针确实通过下一层堆叠保存的,StackPtr->CompletionRoutine调用的实际上是上一层的完成函数

VOID IoSetCompletionRoutine(
  _In_ PIRP Irp,
  _In_opt_ PIO_COMPLETION_ROUTINE CompletionRoutine,
  _In_opt_ __drv_aliasesMem PVOID Context,
  _In_ BOOLEAN InvokeOnSuccess,
  _In_ BOOLEAN InvokeOnError,
  _In_ BOOLEAN InvokeOnCancel)
{
  PIO_STACK_LOCATION irpSp;
  ASSERT( (InvokeOnSuccess || InvokeOnError || InvokeOnCancel) ? (CompletionRoutine != NULL) : TRUE );
  irpSp = IoGetNextIrpStackLocation(Irp);
  irpSp->CompletionRoutine = CompletionRoutine;
  irpSp->Context = Context;
  irpSp->Control = 0;

  if (InvokeOnSuccess) {
    irpSp->Control = SL_INVOKE_ON_SUCCESS;
  }

  if (InvokeOnError) {
    irpSp->Control |= SL_INVOKE_ON_ERROR;
  }

  if (InvokeOnCancel) {
    irpSp->Control |= SL_INVOKE_ON_CANCEL;
  }
}

让我们再回到IofCompleteRequest函数,可以看到一条语句:

if (Status == STATUS_MORE_PROCESSING_REQUIRED) return;
如果完成函数返回STATUS_SUCCESS,则传递irp->PendingReturned=X(0,1)的接力棒会一直传递下去,如果返回值是STATUS_MORE_PROCESSING_REQUIRED,则传递终止,本函数也执行结束。

一些书籍对此的解释是:完成函数返回STATUS_MORE_PROCESSING_REQUIRED时为了将IRP控制权交还给当前堆叠,并由当前堆叠去完成它。这么说好像有点抽象了,我们还是以代码进行佐证。

完成函数返回STATUS_MORE_PROCESSING_REQUIRED的代码模板:

static NTSTATUS DDKAPI on_xx_complete(DEVICE_OBJECT *device_object, IRP *irp, void *context)
{
    
    ........................
    KeSetEvent((KEVENT *) context, IO_NO_INCREMENT, FALSE);

    return STATUS_MORE_PROCESSING_REQUIRED;
}

调用完成函数的模板:

ULONG OnMajor_xx(IN PDEVICE_EXTENSION deviceExtension, IN PIRP Irp)
{
    .....................
    KeInitializeEvent(&event,NotificationEvent,FALSE);
    IoSetCompletionRoutine(Irp,on_usbd_complete,&event,TRUE,TRUE,TRUE);
    IoCallDriver(deviceExtension->target_device, Irp);
    KeWaitForSingleObject((PVOID) &event,Executive,KernelMode,FALSE,NULL);
    
    status = irp->IoStatus.Status;
    IoCompleteRequest(irp, IO_NO_INCREMENT);
    return status;
}
很明白了吧,调用函数中创建一个等待事件对象,设置完成函数,然后调用IocallDriver,触发下层堆叠的操作,紧接着对事件等待;下层堆叠可能同步完成,也可能异步完成,不管怎么样,最终都会调用IofCompleteRequest,开启一个循环,循环遍历堆叠,碰到完成函数就执行,完成函数SetEvent,等待事件的堆叠对应的线程开始运作,最后再次调用IoCompleteRequest完成IRP操作。

还有一点需要说明的是:设置irp->PendingReturned时,完成函数必须返回STATUS_SUCCESS,STATUS_MORE_PROCESSING_REQUIRED通常与事件同步使用。


再次回到IofCompleteRequest函数的分析中来,if (Irp->Flags & IRP_ASSOCIATED_IRP),如果这是一个关联IRP,在释放自己的同时,还需要对被关联IRP进行操作。

if (Irp->Flags & (IRP_PAGING_IO | IRP_CLOSE_OPERATION))这一段,如果这是一个同步的Paging I/O或者是一个关闭操作,还需要对用户定义的Event进行触发,这样,在UserMode下面调用的WaitForSingleObject等函数才得以返回。

最后,如果不是IRP_DEFER_IO_COMPLETION的IRP,就需要初始化一个APC,并将其挂接到FileObject相应的域,在合适的时机,善后函数IopCompleteRequest(与函数IofCompleteRequest仅一个字母之差)函数会被调用。

VOID IopCompleteRequest(IN PKAPC Apc,
                   IN PKNORMAL_ROUTINE* NormalRoutine,
                   IN PVOID* NormalContext,
                   IN PVOID* SystemArgument1,
                   IN PVOID* SystemArgument2)
{
    PFILE_OBJECT FileObject;
    PIRP Irp;
    PMDL Mdl, NextMdl;
    PVOID Port = NULL, Key = NULL;
    BOOLEAN SignaledCreateRequest = FALSE;

    /* Get data from the APC */
    FileObject = (PFILE_OBJECT)*SystemArgument1;
    Irp = CONTAINING_RECORD(Apc, IRP, Tail.Apc);
    IOTRACE(IO_IRP_DEBUG,
            "%s - Completing IRP %p for %p\n",
            __FUNCTION__,
            Irp,
            FileObject);

    /* Sanity check */
    ASSERT(Irp->IoStatus.Status != (NTSTATUS)0xFFFFFFFF);

    /* Check if we have a file object */
    if (*SystemArgument2)
    {
        /* Check if we're reparsing */
        if ((Irp->IoStatus.Status == STATUS_REPARSE) &&
            (Irp->IoStatus.Information == IO_REPARSE_TAG_MOUNT_POINT))
        {
            /* We should never get this yet */
            UNIMPLEMENTED_DBGBREAK("Reparse support not yet present!\n");
            return;
        }
    }

    /* Handle Buffered case first */
    if (Irp->Flags & IRP_BUFFERED_IO)
    {
        /* Check if we have an input buffer and if we succeeded */
        if ((Irp->Flags & IRP_INPUT_OPERATION) &&
            (Irp->IoStatus.Status != STATUS_VERIFY_REQUIRED) &&
            !(NT_ERROR(Irp->IoStatus.Status)))
        {
            /* Copy the buffer back to the user */
            RtlCopyMemory(Irp->UserBuffer,
                          Irp->AssociatedIrp.SystemBuffer,
                          Irp->IoStatus.Information);
        }

        /* Also check if we should de-allocate it */
        if (Irp->Flags & IRP_DEALLOCATE_BUFFER)
        {
            /* Deallocate it */
            ExFreePool(Irp->AssociatedIrp.SystemBuffer);
        }
    }

    /* Now we got rid of these two... */
    Irp->Flags &= ~(IRP_BUFFERED_IO | IRP_DEALLOCATE_BUFFER);

    /* Check if there's an MDL */
    for (Mdl = Irp->MdlAddress; Mdl; Mdl = NextMdl)
    {
        /* Free it */
        NextMdl = Mdl->Next;
        IoFreeMdl(Mdl);
    }

    /* No MDLs left */
    Irp->MdlAddress = NULL;

    /*
     * Check if either the request was completed without any errors
     * (but warnings are OK!), or if it was completed with an error, but
     * did return from a pending I/O Operation and is not synchronous.
     */
    if (!(NT_ERROR(Irp->IoStatus.Status)) ||
         (NT_ERROR(Irp->IoStatus.Status) &&
          (Irp->PendingReturned) &&
          !(IsIrpSynchronous(Irp, FileObject))))
    {
        /* Get any information we need from the FO before we kill it */
        if ((FileObject) && (FileObject->CompletionContext))
        {
            /* Save Completion Data */
            Port = FileObject->CompletionContext->Port;
            Key = FileObject->CompletionContext->Key;
        }

        /* Use SEH to make sure we don't write somewhere invalid */
        _SEH2_TRY
        {
            /*  Save the IOSB Information */
            *Irp->UserIosb = Irp->IoStatus;
        }
        _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
        {
            /* Ignore any error */
        }
        _SEH2_END;

        /* Check if we have an event or a file object */
        if (Irp->UserEvent)
        {
            /* At the very least, this is a PKEVENT, so signal it always */
            KeSetEvent(Irp->UserEvent, 0, FALSE);

            /* Check if we also have a File Object */
            if (FileObject)
            {
                /* Check if this is an Asynch API */
                if (!(Irp->Flags & IRP_SYNCHRONOUS_API))
                {
                    /* Dereference the event */
                    ObDereferenceObject(Irp->UserEvent);
                }

                /*
                 * Now, if this is a Synch I/O File Object, then this event is
                 * NOT an actual Executive Event, so we won't dereference it,
                 * and instead, we will signal the File Object
                 */
                if ((FileObject->Flags & FO_SYNCHRONOUS_IO) &&
                    !(Irp->Flags & IRP_OB_QUERY_NAME))
                {
                    /* Signal the file object and set the status */
                    KeSetEvent(&FileObject->Event, 0, FALSE);
                    FileObject->FinalStatus = Irp->IoStatus.Status;
                }

                /*
                 * This could also be a create operation, in which case we want
                 * to make sure there's no APC fired.
                 */
                if (Irp->Flags & IRP_CREATE_OPERATION)
                {
                    /* Clear the APC Routine and remember this */
                    Irp->Overlay.AsynchronousParameters.UserApcRoutine = NULL;
                    SignaledCreateRequest = TRUE;
                }
            }
        }
        else if (FileObject)
        {
            /* Signal the file object and set the status */
            KeSetEvent(&FileObject->Event, 0, FALSE);
            FileObject->FinalStatus = Irp->IoStatus.Status;

            /*
            * This could also be a create operation, in which case we want
            * to make sure there's no APC fired.
            */
            if (Irp->Flags & IRP_CREATE_OPERATION)
            {
                /* Clear the APC Routine and remember this */
                Irp->Overlay.AsynchronousParameters.UserApcRoutine = NULL;
                SignaledCreateRequest = TRUE;
            }
        }

        /* Update transfer count for everything but create operation */
        if (!(Irp->Flags & IRP_CREATE_OPERATION))
        {
            if (Irp->Flags & IRP_WRITE_OPERATION)
            {
                /* Update write transfer count */
                IopUpdateTransferCount(IopWriteTransfer,
                                       (ULONG)Irp->IoStatus.Information);
            }
            else if (Irp->Flags & IRP_READ_OPERATION)
            {
                /* Update read transfer count */
                IopUpdateTransferCount(IopReadTransfer,
                                       (ULONG)Irp->IoStatus.Information);
            }
            else
            {
                /* Update other transfer count */
                IopUpdateTransferCount(IopOtherTransfer,
                                       (ULONG)Irp->IoStatus.Information);
            }
        }

        /* Now that we've signaled the events, de-associate the IRP */
        IopUnQueueIrpFromThread(Irp);

        /* Now check if a User APC Routine was requested */
        if (Irp->Overlay.AsynchronousParameters.UserApcRoutine)
        {
            /* Initialize it */
            KeInitializeApc(&Irp->Tail.Apc,
                            KeGetCurrentThread(),
                            CurrentApcEnvironment,
                            IopFreeIrpKernelApc,
                            IopAbortIrpKernelApc,
                            (PKNORMAL_ROUTINE)Irp->
                            Overlay.AsynchronousParameters.UserApcRoutine,
                            Irp->RequestorMode,
                            Irp->
                            Overlay.AsynchronousParameters.UserApcContext);

            /* Queue it */
            KeInsertQueueApc(&Irp->Tail.Apc, Irp->UserIosb, NULL, 2);
        }
        else if ((Port) &&
                 (Irp->Overlay.AsynchronousParameters.UserApcContext))
        {
            /* We have an I/O Completion setup... create the special Overlay */
            Irp->Tail.CompletionKey = Key;
            Irp->Tail.Overlay.PacketType = IopCompletionPacketIrp;
            KeInsertQueue(Port, &Irp->Tail.Overlay.ListEntry);
        }
        else
        {
            /* Free the IRP since we don't need it anymore */
            IoFreeIrp(Irp);
        }

        /* Check if we have a file object that wasn't part of a create */
        if ((FileObject) && !(SignaledCreateRequest))
        {
            /* Dereference it, since it's not needed anymore either */
            ObDereferenceObjectDeferDelete(FileObject);
        }
    }
    else
    {
        /*
         * Either we didn't return from the request, or we did return but this
         * request was synchronous.
         */
        if ((Irp->PendingReturned) && (FileObject))
        {
            /* So we did return with a synch operation, was it the IRP? */
            if (Irp->Flags & IRP_SYNCHRONOUS_API)
            {
                /* Yes, this IRP was synchronous, so return the I/O Status */
                *Irp->UserIosb = Irp->IoStatus;

                /* Now check if the user gave an event */
                if (Irp->UserEvent)
                {
                    /* Signal it */
                    KeSetEvent(Irp->UserEvent, 0, FALSE);
                }
                else
                {
                    /* No event was given, so signal the FO instead */
                    KeSetEvent(&FileObject->Event, 0, FALSE);
                }
            }
            else
            {
                /*
                 * It's not the IRP that was synchronous, it was the FO
                 * that was opened this way. Signal its event.
                 */
                FileObject->FinalStatus = Irp->IoStatus.Status;
                KeSetEvent(&FileObject->Event, 0, FALSE);
            }
        }

        /* Now that we got here, we do this for incomplete I/Os as well */
        if ((FileObject) && !(Irp->Flags & IRP_CREATE_OPERATION))
        {
            /* Dereference the File Object unless this was a create */
            ObDereferenceObjectDeferDelete(FileObject);
        }

        /*
         * Check if this was an Executive Event (remember that we know this
         * by checking if the IRP is synchronous)
         */
        if ((Irp->UserEvent) &&
            (FileObject) &&
            !(Irp->Flags & IRP_SYNCHRONOUS_API))
        {
            /* This isn't a PKEVENT, so dereference it */
            ObDereferenceObject(Irp->UserEvent);
        }

        /* Now that we've signaled the events, de-associate the IRP */
        IopUnQueueIrpFromThread(Irp);

        /* Free the IRP as well */
        IoFreeIrp(Irp);
    }
}
这个函数并没有太大难度,我们只需要抓住主线看它做了哪几件重要的事情就可以了:

数据的拷贝,返回用户数据

RtlCopyMemory(Irp->UserBuffer,  Irp->AssociatedIrp.SystemBuffer, Irp->IoStatus.Information);

设置事件,对应UserMode下的Event

KeSetEvent(Irp->UserEvent, 0, FALSE);

设置事件,对应FileObject->Event,还记得IopParseDevice最后的那个KeWaitForSingleObject吗

KeSetEvent(&FileObject->Event, 0, FALSE);

将IRP从创建线程中移除,见IopParseDevice->IopQueueIrpToThread

IopUnQueueIrpFromThread(Irp);


好了,终于将这两个庞大的主题给讲完了,其中有太多的知识点我不便展开,需要各位自己花时间阅读一些基本书籍,每写一篇,我都会将我读到的参考资料贴出来,以供大家参考,书上有的我一般就没有多讲,我所列出来的都是我开始接触windows驱动比较困惑的地方,希望对每一个开始入门的同志有所帮助,我也是刚开始接触这一块,有什么不对的地方希望大家批评指正,呵呵~


参考资料:

《windows内核情景分析》

《Windows驱动开发技术详解》

http://blog.csdn.net/whatday/article/details/7106721


















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值