/ Windows / 98浏览

深入理解 APC

💡 以下代码主要参考 reactos 和 win2k3(泄露代码)x86 代码。Win10 中有部分修改,但原理一致,这里先不进行讨论。

引入

APC(Asynchronous Procedure Calls):异步过程调用

应用程序中经常用到 APC。这里以 ReadFileEx 为例来描述下,Win32 API 中是怎么用 APC 的。

ReadFileEx 是通过 APC 机制来实现异步读取文件的功能的。先看看 ReadFileEx 的代码:

BOOL WINAPI
ReadFileEx(IN HANDLE hFile,
           IN LPVOID lpBuffer,
           IN DWORD nNumberOfBytesToRead OPTIONAL,
           IN LPOVERLAPPED lpOverlapped,
           IN LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine)
{
    LARGE_INTEGER Offset;
    NTSTATUS Status;

    Offset.u.LowPart = lpOverlapped->Offset;
    Offset.u.HighPart = lpOverlapped->OffsetHigh;
    lpOverlapped->Internal = STATUS_PENDING;

    Status = NtReadFile(hFile,
                        NULL,
                        ApcRoutine,
                        lpCompletionRoutine,
                        (PIO_STATUS_BLOCK)lpOverlapped,
                        lpBuffer,
                        nNumberOfBytesToRead,
                        &Offset,
                        NULL);

    if (!NT_SUCCESS(Status))
    {
        BaseSetLastNTError(Status);
        return FALSE;
    }

    return TRUE;
}

其中 lpCompletionRoutine 是指向在读取操作完成且调用线程处于可警报等待状态时调用的完成例程的指针。说人话就是,这是当文件读取完成时的回调。

我们再来看看 NtReadFile ,因为其比较长,就截取关键代码:

VOID WINAPI
ApcRoutine(PVOID ApcContext,
           PIO_STATUS_BLOCK IoStatusBlock,
           ULONG Reserved)
{
    DWORD dwErrorCode;
    LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine =
        (LPOVERLAPPED_COMPLETION_ROUTINE)ApcContext;

    dwErrorCode = RtlNtStatusToDosError(IoStatusBlock->Status);
    lpCompletionRoutine(dwErrorCode,
                        IoStatusBlock->Information,
                        (LPOVERLAPPED)IoStatusBlock);
}

NTSTATUS
NTAPI
NtReadFile(IN HANDLE FileHandle,
           IN HANDLE Event OPTIONAL,
           IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
           IN PVOID ApcContext OPTIONAL,
           OUT PIO_STATUS_BLOCK IoStatusBlock,
           OUT PVOID Buffer,
           IN ULONG Length,
           IN PLARGE_INTEGER ByteOffset OPTIONAL,
           IN PULONG Key OPTIONAL)
{
......
        /* Allocate the IRP */
    Irp = IoAllocateIrp(DeviceObject->StackSize, FALSE);
        ...
        Irp->Overlay.AsynchronousParameters.UserApcRoutine = ApcRoutine;
    Irp->Overlay.AsynchronousParameters.UserApcContext = ApcContext;
        ...
        /* Perform the call */
    return IopPerformSynchronousRequest(DeviceObject,
                                        Irp,
                                        FileObject,
                                        TRUE,
                                        PreviousMode,
                                        Synchronous,
                                        IopReadTransfer);
}

NTSTATUS
NTAPI
IopPerformSynchronousRequest(IN PDEVICE_OBJECT DeviceObject,
                             IN PIRP Irp,
                             IN PFILE_OBJECT FileObject,
                             IN BOOLEAN Deferred,
                             IN KPROCESSOR_MODE PreviousMode,
                             IN BOOLEAN SynchIo,
                             IN IOP_TRANSFER_TYPE TransferType)
{
        ...
        /* Call the driver */
    Status = IoCallDriver(DeviceObject, Irp);
        ....
}

可以看到在 NtReadFile 中,实际上将应用层的 callback 作为 apccontext 来进行的。

ApcRoutine 是预置的一个 APC 例程,实现也比较简单,其实就是对例程的一个封装执行。运行在 APC_LEVEL

NtReadFile 中就是将用户层的 callback 打包到 irp 中,然后发给底层的驱动,待驱动调用 IoCompleteRequest 来完成这个 irp。

再来看看 IoCompleteRequest ,由于 IoCompleteRequestIofCompleteRequestIopCompleteRequest ,所以这里放部分 IopCompleteRequest 的相关代码:

💡 IofCompleteRequestIopCompleteRequest 的区别是:IofCompleteRequestIopCompleteRequest 的快速调用版本,具有更优的性能。IopCompleteRequest 是原始方法。

VOID
NTAPI
IopCompleteRequest(IN PKAPC Apc,
                   IN PKNORMAL_ROUTINE* NormalRoutine,
                   IN PVOID* NormalContext,
                   IN PVOID* SystemArgument1,
                   IN PVOID* SystemArgument2)
{
                ...
                /* 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);
        }
                ...
}

IoCompleteRequest 中,将用户层的 callback 和上文提到的 ApcRoutine 塞进去 irp 的 APC 队列中。待返回到用户层之前,进行执行,以调用用户层的 callback 来实现异步回调。

总结下 ReadFileEx ,用户层在调用ReadFileEx 的时候,传入一个 callback,NtReadFile 将 callback 作为 apccontext 塞入 irp 给到底层驱动处理。待底层驱动处理完成后,通过调用 IoCompleteRequest 将 callback 构造成一个 apc,待返回用户层前夕进行调用,以完成异步调用。

APC 关键结构

APC 主要分为内核 APC用户 APC 。内核 APC 就是指在内核空间执行的 APC,用户 APC 就是在用户空间执行的 APC。

内核 APC

先来看看支撑内核 APC 的结构:

APC 跟线程是密不可分的,所以我们先从 KTHREAD 中看看 APC 相关的结构:

KTHREAD 部分结构如下:

Typedef struct _KTHREAD
{
   …
   KAPC_STATE  ApcState;//表示本线程当前使用的APC状态(即apc队列的状态)
   KAPC_STATE  SavedApcState;//表示保存的原apc状态,备份用
   KAPC_STATE* ApcStatePointer[2];//状态数组,包含两个指向APC状态的指针
   UCHAR ApcStateIndex;//0或1,指当前的ApcState在ApcStatePointer数组中的索引位置
   UCHAR ApcQueueable;//指本线程的APC队列是否可插入apc
   ULONG KernelApcDisable;//禁用标志
     //专用于挂起操作的APC(这个函数在线程一得到调度就重新进入等待态,等待挂起计数减到0)
     //Win10 中已经改成 SchedulerApc
   KAPC SuspendApc;
   …   
}KTHREAD;

KAPC_STATE 的结构如下:

Typedef struct _KAPC_STATE //APC队列的状态描述符
{
   LIST_ENTRY  ApcListHead[2];//每个线程有两个apc队列
   PKPROCESS Process;//当前线程所在的进程
   BOOL KernelApcInProgress;//指示本线程是否当前正在 内核apc
   BOOL KernelApcPending;//表示内核apc队列中是否有apc
   BOOL UserApcPending;//表示用户apc队列中是否apc
}

最重要的 _KAPC 的结构如下:

typedef struct _KAPC
{
  UCHAR Type;//结构体的类型
  UCHAR Size;//结构体的大小
  struct _KTHREAD *Thread;//目标线程
  LIST_ENTRY ApcListEntry;//用来挂入目标apc队列
  PKKERNEL_ROUTINE KernelRoutine;//该apc的内核总入口
  PKRUNDOWN_ROUTINE RundownRoutine;
  PKNORMAL_ROUTINE NormalRoutine;//该apc的用户空间总入口或者用户真正的内核apc函数
  PVOID NormalContext;//真正用户提供的用户空间apc函数或者用户真正的内核apc函数的context*
  PVOID SystemArgument1;//挂入时的附加参数1。真正用户apc的context*
  PVOID SystemArgument2;//挂入时的附加参数2
  CCHAR ApcStateIndex;//指要挂入目标线程的哪个状态时的apc队列
  KPROCESSOR_MODE ApcMode;//指要挂入用户apc队列还是内核apc队列
  BOOLEAN Inserted;//表示本apc是否已挂入队列
} KAPC, *PKAPC;

需要注意的是:

  • KernelRoutine :无论是什么 APC,都会首先执行这个函数
  • RundownRoutine :当插入 APC 失败的时候,会执行这个函数
  • NormalRoutine :在内核 APC 下,这是 APC 例程,NormalContext 是其 context;在用户 APC 下,这是统一的例程,上述例子中是 ApcRoutineNormalContext 才是用户的 APC 函数,SystemArgument1 才是 APC 的 context。

APC 函数执行时机

我们先看看从内核返回用户时的流程:

以下是部分伪代码:

KiSystemService()//int 2e的isr,内核服务函数总入口,注意这个函数可以嵌套、递归!!!
{
    SaveTrap();//保存trap现场
        Sti  //开中断
        ---------------上面保存完寄存器等现场后,开始查SST表调用系统服务------------------
        FindTableCall();
        ---------------------------------调用完系统服务函数后------------------------------
        Move  esp,kthread.TrapFrame; //将栈顶回到trap帧结构体处
        Cli  //关中断
        If(上次模式==UserMode)
        {
        Call  KiDeliverApc //遍历执行本线程的内核APC和用户APC队列中的所有APC函数
        //清理Trap帧,恢复寄存器现场
        Iret   //返回用户空间
        }
        Else
        {
           //返回到原call处后面的那条指令处
        }
}

从上述伪代码可以看出,内核代码执行完后切换到用户模式的时候,就会通过 KiDeliverApc 来进行扫描 APC 并执行。

通过搜索KiDeliverApc 调用,可以看到从异常、中断返回、ThreadSwap 都有调用。可以从相关函数中找到(包括但不限于):

  • KiCheckForKernelApcDelivery
  • HalpApcInterruptHandler
  • KiExitV86Trap
  • KiCheckForApcDelivery
  • KiApcInterrupt
  • KiSwapThread
  • KiExitDispatcher

💡 HalpApcInterruptHandler 就是 apc 作为一种软中断的 ISR。

于是就可以来看看最核心的 KiDeliverApc 的调用了(简化了的):

/*++
 * @name KiDeliverApc
 * @implemented @NT4
 *
 *     The KiDeliverApc routine is called from IRQL switching code if the
 *     thread is returning from an IRQL >= APC_LEVEL and Kernel-Mode APCs are
 *     pending.
 *
 * @param DeliveryMode
 *        Specifies the current processor mode.
 *
 * @param ExceptionFrame
 *        Pointer to the Exception Frame on non-i386 builds.
 *
 * @param TrapFrame
 *        Pointer to the Trap Frame.
 *
 * @return None.
 *
 * @remarks First, Special APCs are delivered, followed by Kernel-Mode APCs and
 *          User-Mode APCs. Note that the TrapFrame is only valid if the
 *          delivery mode is User-Mode.
 *          Upon entry, this routine executes at APC_LEVEL.
 *
 *--*/
VOID
NTAPI
KiDeliverApc(IN KPROCESSOR_MODE DeliveryMode,//指要执行哪个apc队列中的函数
             IN PKEXCEPTION_FRAME ExceptionFrame,//传入的是NULL
             IN PKTRAP_FRAME TrapFrame)//即将返回用户空间前的Trap现场帧
{
    PKTHREAD Thread = KeGetCurrentThread();
    PKPROCESS Process = Thread->ApcState.Process;
    OldTrapFrame = Thread->TrapFrame;
    Thread->TrapFrame = TrapFrame;
    Thread->ApcState.KernelApcPending = FALSE;
        if (Thread->SpecialApcDisable) goto Quickie;
        //先固定执行掉内核apc队列中的所有apc函数
    while (!IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode]))
    {
        KiAcquireApcLockAtApcLevel(Thread, &ApcLock);//锁定apc队列
        ApcListEntry = Thread->ApcState.ApcListHead[KernelMode].Flink;//队列头部中的apc
        Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);
        KernelRoutine = Apc->KernelRoutine;//内核总apc函数
        NormalRoutine = Apc->NormalRoutine;//用户自己真正的内核apc函数
        NormalContext = Apc->NormalContext;//真正内核apc函数的context*
        SystemArgument1 = Apc->SystemArgument1;
        SystemArgument2 = Apc->SystemArgument2;
        if (NormalRoutine==NULL) //称为Special Apc,少见
        {
            RemoveEntryList(ApcListEntry);//关键,移除队列
            Apc->Inserted = FALSE;
            KiReleaseApcLock(&ApcLock);
            //执行内核中的总apc函数
            KernelRoutine(Apc,&NormalRoutine,&NormalContext,
                          &SystemArgument1,&SystemArgument2);
        }
        Else //典型,一般程序员都会提供一个自己的内核apc函数
        {
            if ((Thread->ApcState.KernelApcInProgress) || (Thread->KernelApcDisable))
            {
                KiReleaseApcLock(&ApcLock);
                goto Quickie;
            }
            RemoveEntryList(ApcListEntry); //关键,移除队列
            Apc->Inserted = FALSE;
            KiReleaseApcLock(&ApcLock);
//执行内核中的总apc函数
            KernelRoutine(Apc,
                          &NormalRoutine,//注意,内核中的总apc可能会在内部修改NormalRoutine
                          &NormalContext,
                          &SystemArgument1,
                          &SystemArgument2);
            if (NormalRoutine)//如果内核总apc没有修改NormalRoutine成NULL
            {
                Thread->ApcState.KernelApcInProgress = TRUE;//标记当前线程正在执行内核apc
                KeLowerIrql(PASSIVE_LEVEL);
                //直接调用用户提供的真正内核apc函数
                NormalRoutine(NormalContext, SystemArgument1, SystemArgument2);
                KeRaiseIrql(APC_LEVEL, &ApcLock.OldIrql);
            }
            Thread->ApcState.KernelApcInProgress = FALSE;
        }
    }
    //上面的循环,执行掉所有内核apc函数后,下面开始执行用户apc队列中的第一个apc
    if ((DeliveryMode == UserMode) &&
         !(IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])) &&
         (Thread->ApcState.UserApcPending))
    {
        KiAcquireApcLockAtApcLevel(Thread, &ApcLock);//锁定apc队列
        Thread->ApcState.UserApcPending = FALSE;

        ApcListEntry = Thread->ApcState.ApcListHead[UserMode].Flink;//队列头
        Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);
        KernelRoutine = Apc->KernelRoutine; //内核总apc函数
        NormalRoutine = Apc->NormalRoutine; //用户空间的总apc函数
        NormalContext = Apc->NormalContext;//用户真正的用户空间apc函数
        SystemArgument1 = Apc->SystemArgument1;//真正apc的context*
        SystemArgument2 = Apc->SystemArgument2;
        RemoveEntryList(ApcListEntry);//关键,移除队列
        Apc->Inserted = FALSE;
        KiReleaseApcLock(&ApcLock);
        KernelRoutine(Apc,
                      &NormalRoutine,// 注意,内核中的总apc可能会在内部修改NormalRoutine
                      &NormalContext,
                      &SystemArgument1,
                      &SystemArgument2);
        if (!NormalRoutine)
            KeTestAlertThread(UserMode);
        Else //典型,准备提前回到用户空间调用用户空间的总apc函数
        {
            KiInitializeUserApc(ExceptionFrame,//NULL
                                TrapFrame,//Trap帧的地址
                                NormalRoutine, //用户空间的总apc函数
                                NormalContext, //用户真正的用户空间apc函数
                                SystemArgument1, //真正apc的context*
                                SystemArgument2);
        }
    }
Quickie:
    Thread->TrapFrame = OldTrapFrame;
}

可以看到,这个函数既处理内核 APC,也处理用户 APC。一次执行会遍历所有的内核 APC,但只会执行一次的用户 APC。

那其他的用户 APC 就不管了吗?当然不是的,其实每次尝试正式回到用户态的时候都会执行一遍。也就是循环执行,每次循环都会将内核 APC 队列执行完,然后执行一个用户 APC。

所以,总体流程是这样的:

扫描执行内核apc队列所有apc -> 执行用户apc队列中一个apc -> 再次扫描执行内核apc队列所有apc -> 执行用户apc队列中下一个apc -> 再次扫描执行内核apc队列所有apc -> 再次执行用户apc队列中下一个apc如此循环,直到将用户apc队列中的所有apc都执行掉。

上面提到这段代码是在内核模式返回用户模式之前会执行的,也就是内核模式下,那要怎么执行用户模式的代码呢?

嗯,答案就在 KiInitializeUserApc 里面了。我们先来看看它干了什么:

VOID
KiInitializeUserApc(IN PKEXCEPTION_FRAME ExceptionFrame,
                    IN PKTRAP_FRAME TrapFrame,//原真正的断点现场帧
                    IN PKNORMAL_ROUTINE NormalRoutine,
                    IN PVOID NormalContext,
                    IN PVOID SystemArgument1,
                    IN PVOID SystemArgument2)
{
        Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
        //将原真正的Trap帧打包保存在一个Context结构中
    KeTrapFrameToContext(TrapFrame, ExceptionFrame, &Context);
    _SEH2_TRY
    {
        AlignedEsp = Context.Esp & ~3;//对齐4B
                //为用户空间中KiUserApcDisatcher函数的参数腾出空间(4个参数+ CONTEXT + 8B的seh节点)
        ContextLength = CONTEXT_ALIGNED_SIZE + (4 * sizeof(ULONG_PTR));
        Stack = ((AlignedEsp - 8) & ~3) - ContextLength;//8表示seh节点的大小
        //模拟压入KiUserApcDispatcher函数的4个参数
        *(PULONG_PTR)(Stack + 0 * sizeof(ULONG_PTR)) = (ULONG_PTR)NormalRoutine;
        *(PULONG_PTR)(Stack + 1 * sizeof(ULONG_PTR)) = (ULONG_PTR)NormalContext;
        *(PULONG_PTR)(Stack + 2 * sizeof(ULONG_PTR)) = (ULONG_PTR)SystemArgument1;
        *(PULONG_PTR)(Stack + 3 * sizeof(ULONG_PTR)) = (ULONG_PTR)SystemArgument2;
        //将原真正trap帧保存在用户栈的一个CONTEXT结构中,方便以后还原
        RtlCopyMemory( (Stack + (4 * sizeof(ULONG_PTR))),&Context,sizeof(CONTEXT));

        //强制修改当前Trap帧中的返回地址与用户栈地址(偏离原来的返回路线)
        TrapFrame->Eip = (ULONG)KeUserApcDispatcher;//关键,新的返回断点地址
        TrapFrame->HardwareEsp = Stack;//关键,新的用户栈顶
        TrapFrame->SegCs = Ke386SanitizeSeg(KGDT_R3_CODE, UserMode);
        TrapFrame->HardwareSegSs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode);
        TrapFrame->SegDs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode);
        TrapFrame->SegEs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode);
        TrapFrame->SegFs = Ke386SanitizeSeg(KGDT_R3_TEB, UserMode);
        TrapFrame->SegGs = 0;
        TrapFrame->ErrCode = 0;
        TrapFrame->EFlags = Ke386SanitizeFlags(Context.EFlags, UserMode);
        if (KeGetCurrentThread()->Iopl) TrapFrame->EFlags |= EFLAGS_IOPL;
    }
    _SEH2_EXCEPT((RtlCopyMemory(&SehExceptRecord, _SEH2_GetExceptionInformation()->ExceptionRecord, sizeof(EXCEPTION_RECORD)),    EXCEPTION_EXECUTE_HANDLER))
    {
        SehExceptRecord.ExceptionAddress = (PVOID)TrapFrame->Eip;
        KiDispatchException(&SehExceptRecord,ExceptionFrame,TrapFrame,UserMode,TRUE);
    }
    _SEH2_END;
}

这段代码的核心逻辑就是修改 TrapFram 的执行地址(在这里就是返回地址)为用户空间的 KiUserApcDisatcher ,并把原 TrapFram 保存到用户空间下,然后将参数模拟压入这个函数的栈中,这样就可以模拟执行 KiUserApcDisatcher 了。

💡 可以看到这里整个执行块被一个 try 块包起来了,这是因为谁也不知道用户代码会发生什么问题,这里可不能崩溃。

于是,我们再来看看 KiUserApcDisatcher 干了啥:

KiUserApcDisatcher(NormalRoutine,
                   NormalContext,
                   SysArg1,
                   SysArg2
)
{
   Lea eax,[esp+ CONTEXT_ALIGNED_SIZE+16]   //eax指向seh异常节点的地址
   Mov ecx,fs:[TEB_EXCEPTION_LIST]
   Mov edx,offset KiUserApcExceptionHandler
   --------------------------------------------------------------------------------------
   Mov [eax],ecx //seh节点的next指针成员
   Mov [eax+4],edx //she节点的handler函数指针成员
   Mov fs:[TEB_EXCEPTION_LIST],eax
   --------------------上面三条指令在栈中构造一个8B的标准seh节点-----------------------
   Pop eax //eax=NormalRoutine(即IntCallUserApc这个总apc函数)
   Lea edi,[esp+12] //edi=栈中保存的CONTEXT结构的地址
   Call eax //相当于call IntCallUserApc(NormalContext,SysArg1,SysArg2)

   Mov ecx,[edi+ CONTEXT_ALIGNED_SIZE]
   Mov fs:[ TEB_EXCEPTION_LIST],ecx   //撤销栈中的seh节点

   Push TRUE  //表示回到内核后需要继续检测执行用户apc队列中的apc函数
   Push edi  //传入原栈帧的CONTEXT结构的地址给这个函数,以做恢复工作
   Call NtContinue   //调用这个函数重新进入内核(注意这个函数正常情况下是不会返回到下面的)
   ----------------------------------华丽的分割线-------------------------------------------
   Mov esi,eax
   Push esi
   Call RtlRaiseStatus  //若ZwContinue返回了,那一定是内部出现了异常
   Jmp StatusRaiseApc
   Ret 16
}

每当要执行一个用户空间apc时,都会‘提前’偏离原来的路线返回用户空间的这个函数处去执行用户的 apc。在执行这个函数前,会先构造一个 seh 节点,也即相当于把这个函数的调用放在 try 块中保护。这个函数内部会调用IntCallUserApc,执行完真正的用户 apc 函数后,调用NtContinue重返内核。

💡 这里的 IntCallUserApc 跟前面提到的 ApcRoutine 是一致的。

NtContinue 代码如下:

NTSTATUS NtContinue(CONTEXT* Context, //原真正的TraFrame 
                    BOOL TestAlert  //指示是否继续执行用户apc队列中的apc函数
)
{
   Push ebp  //此时ebp=本系统服务自身的TrapFrame地址
   Mov ebx,PCR[KPCR_CURRENT_THREAD] //ebx=当前线程的KTHREAD对象地址
   Mov edx,[ebp+KTRAP_FRAME_EDX] //注意TrapFrame中的这个edx字段不是用来保存edx的
   Mov [ebx+KTHREAD_TRAP_FRAME],edx //将当前的TrapFrame改为上一个TrapFrame的地址
   Mov ebp,esp
   Mob eax,[ebp] //eax=本系统服务自身的TrapFrame地址
   Mov ecx,[ebp+8] /本函数的第一个参数,即Context
   Push eax
   Push NULL
   Push ecx
   Call KiContinue  //call KiContinue(Context*,NULL,TrapFrame*)
   Or eax,eax
   Jnz error
   Cmp dword ptr[ebp+12],0 //检查TestAlert参数的值
   Je DontTest
   Mov al,[ebx+KTHREAD_PREVIOUS_MODE]
   Push eax
   Call KeTestAlertThread  //检测用户apc队列是否为空
   DontTest:
   Pop ebp
   Mov esp,ebp
   Jmp KiServiceExit2 //返回用户空间(返回前,又会去扫描执行apc队列中的下一个用户apc)
}

KiContinue 代码如下:

NTSTATUS
KiContinue(IN PCONTEXT Context,//原来的断点现场
           IN PKEXCEPTION_FRAME ExceptionFrame,
           IN PKTRAP_FRAME TrapFrame) //NtContinue自身的TrapFrame地址
{
    NTSTATUS Status = STATUS_SUCCESS;
    KIRQL OldIrql = APC_LEVEL;
    KPROCESSOR_MODE PreviousMode = KeGetPreviousMode();
if (KeGetCurrentIrql() < APC_LEVEL) 
KeRaiseIrql(APC_LEVEL, &OldIrql);
    _SEH2_TRY
    {
        if (PreviousMode != KernelMode)
            KiContinuePreviousModeUser(Context,ExceptionFrame,TrapFrame);//恢复成原TrapFrame
        else
        {
            KeContextToTrapFrame(Context,ExceptionFrame,TrapFrame,Context->ContextFlags,
                                 KernelMode); //恢复成原TrapFrame
        }
    }
    _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
    {
        Status = _SEH2_GetExceptionCode();
    }
    _SEH2_END;
if (OldIrql < APC_LEVEL)
 KeLowerIrql(OldIrql);
    return Status;
}

KiContinuePreviousModeUser 代码如下:

VOID
KiContinuePreviousModeUser(IN PCONTEXT Context,//原来的断点现场
                           IN PKEXCEPTION_FRAME ExceptionFrame,
                           IN PKTRAP_FRAME TrapFrame)//NtContinue自身的TrapFrame地址
{
    CONTEXT LocalContext;
    ProbeForRead(Context, sizeof(CONTEXT), sizeof(ULONG));
    RtlCopyMemory(&LocalContext, Context, sizeof(CONTEXT));
Context = &LocalContext;
//看到没,将原Context中的成员填写到NtContinue系统服务的TrapFrame帧中(也即修改成原来的TrapFrame)
    KeContextToTrapFrame(&LocalContext,ExceptionFrame,TrapFrame,
                         LocalContext.ContextFlags,UserMode);
}

KeTestAlertThread 代码如下:

BOOLEAN  KeTestAlertThread(IN KPROCESSOR_MODE AlertMode)
{
    PKTHREAD Thread = KeGetCurrentThread();
    KiAcquireApcLock(Thread, &ApcLock);
    OldState = Thread->Alerted[AlertMode];
    if (OldState)
        Thread->Alerted[AlertMode] = FALSE;
    else if ((AlertMode != KernelMode) &&
 (!IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])))
    {
        Thread->ApcState.UserApcPending = TRUE;//关键。又标记为不空,从而又去执行用户apc
    }
    KiReleaseApcLock(&ApcLock);
    return OldState;
}

如上,上面的函数,就把NtContinue的TrapFrame强制还原成原来的TrapFrame,以好‘正式’返回到用户空间的真正断点处。

💡 不过在返回用户空间前,又要去扫描用户 apc 队列,若仍有用户 apc 函数,就先执行掉内核 apc 队列中的所有 apc 函数,然后又偏离原来的返回路线,【提前】返回到用户空间的KiUserApcDispatcher函数去执行用户 apc,这是一个不断循环的过程。可见,NtContinue这个函数不仅含有继续回到原真正用户空间断点处的意思,还含有继续执行用户 apc 队列中下一个 apc 函数的意思。


特别地,在 KeLowerIrql 也会执行 apc,这个函数在从当前irql降到目标irql时,会按irql高低顺序执行各个软中断的isr。

#define PASSIVE_LEVEL           0
#define APC_LEVEL               1
#define DISPATCH_LEVEL          2
#define CMCI_LEVEL              5
比如,当调用KfLowerIrql要将 cpu 的 irql 从 CMCI_LEVEL 降低到 PASSIVE_LEVEL 时,这个函数中途会先看看当前 cpu 是否收到了CMCI_LEVEL级的软中断,若有,就调用那个软中断的 isr 处理之。然后,再检查是否收到有 DISPATCH_LEVEL级的软中断,若有,调用那个软中断的isr处理之,然后,检查是否有 APC 中断,若有,同样处理之。最后,降到目标 irql,即PASSIVE_LEVEL。 换句话说,在 irql 的降低过程中会一路检查、处理中途的软中断。Cpu 数据结构中有一个 IRR 字段,即表示当前 cpu 累积收到了哪些级别的软中断。

APC 如何插入

下面这个 Win32 API 可以用来手动插入一个 apc 到指定线程的用户 apc 队列中。

DWORD 
QueueUserAPC(PAPCFUNC pfnAPC, HANDLE hThread, ULONG_PTR dwData)
{
  NTSTATUS Status;
  //调用对应的系统服务
  Status = NtQueueApcThread(hThread,//目标线程
 IntCallUserApc,//用户空间中的总apc入口
 pfnAPC,//用户自己真正提供的apc函数
(PVOID)dwData,//SysArg1=context*
 NULL);//SysArg2=NULL
  if (!NT_SUCCESS(Status))
  {
    SetLastErrorByStatus(Status);
    return 0;
  }
  return 1;
}

NTSTATUS
NtQueueApcThread(IN HANDLE ThreadHandle,//目标线程
                 IN PKNORMAL_ROUTINE ApcRoutine,//用户空间中的总apc
                 IN PVOID NormalContext,//用户自己真正的apc函数
                 IN PVOID SystemArgument1,//用户自己apc的context*
                 IN PVOID SystemArgument2)//其它
{
    PKAPC Apc;
    PETHREAD Thread;
    NTSTATUS Status = STATUS_SUCCESS;
    Status = ObReferenceObjectByHandle(ThreadHandle,THREAD_SET_CONTEXT,PsThreadType,
                                       ExGetPreviousMode(), (PVOID)&Thread,NULL);
    //分配一个apc结构,这个结构最终在PspQueueApcSpecialApc中释放
    Apc = ExAllocatePoolWithTag(NonPagedPool |POOL_QUOTA_FAIL_INSTEAD_OF_RAISE,
                                sizeof(KAPC),TAG_PS_APC);
    //构造一个apc
    KeInitializeApc(Apc,
                    &Thread->Tcb,//目标线程
                    OriginalApcEnvironment,//目标apc状态(此服务固定为OriginalApcEnvironment)
                    PspQueueApcSpecialApc,//内核apc总入口
                    NULL,//Rundown Rounine=NULL
                    ApcRoutine,//用户空间的总apc
                    UserMode,//此系统服务固定插入到用户apc队列
                    NormalContext);//用户自己真正的apc函数
    //插入到目标线程的用户apc队列
    KeInsertQueueApc(Apc,
                     SystemArgument1,//插入时的附加参数1,此处为用户自己apc的context*
                     SystemArgument2, //插入时的附加参数2
                     IO_NO_INCREMENT)//表示不予调整目标线程的调度优先级
    return Status;
}

//这个函数用来构造一个要插入指定目标队列的apc对象
VOID
KeInitializeApc(IN PKAPC Apc,
                IN PKTHREAD Thread,//目标线程
                IN KAPC_ENVIRONMENT TargetEnvironment,//目标线程的目标apc状态
                IN PKKERNEL_ROUTINE KernelRoutine,//内核apc总入口
                IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
                IN PKNORMAL_ROUTINE NormalRoutine,//用户空间的总apc
                IN KPROCESSOR_MODE Mode,//要插入用户apc队列还是内核apc队列
                IN PVOID Context) //用户自己真正的apc函数
{
    Apc->Type = ApcObject;
    Apc->Size = sizeof(KAPC);
    if (TargetEnvironment == CurrentApcEnvironment)//CurrentApcEnvironment表示使用当前apc状态
        Apc->ApcStateIndex = Thread->ApcStateIndex;
    else
        Apc->ApcStateIndex = TargetEnvironment;
    Apc->Thread = Thread;
    Apc->KernelRoutine = KernelRoutine;
    Apc->RundownRoutine = RundownRoutine;
    Apc->NormalRoutine = NormalRoutine;
    if (NormalRoutine)//if 提供了用户空间总apc入口
    {
        Apc->ApcMode = Mode;
        Apc->NormalContext = Context;
    }
    Else//若没提供,肯定是内核模式
    {
        Apc->ApcMode = KernelMode;
        Apc->NormalContext = NULL;
    }
    Apc->Inserted = FALSE;//表示初始构造后,尚未挂入apc队列
}

BOOLEAN
KeInsertQueueApc(IN PKAPC Apc,IN PVOID SystemArgument1,IN PVOID SystemArgument2,
                 IN KPRIORITY PriorityBoost)
{
    PKTHREAD Thread = Apc->Thread;
    KLOCK_QUEUE_HANDLE ApcLock;
    BOOLEAN State = TRUE;
    KiAcquireApcLock(Thread, &ApcLock);//插入过程需要独占队列
    if (!(Thread->ApcQueueable) || (Apc->Inserted))//检查队列是否可以插入apc
        State = FALSE;
    else
    {
        Apc->SystemArgument1 = SystemArgument1;//记录该apc的附加插入时的参数
        Apc->SystemArgument2 = SystemArgument2; //记录该apc的附加插入时的参数
        Apc->Inserted = TRUE;//标记为已插入队列
       //插入目标线程的目标apc队列(如果目标线程正处于睡眠状态,可能会唤醒它)
        KiInsertQueueApc(Apc, PriorityBoost); 
    }
    KiReleaseApcLockFromDpcLevel(&ApcLock);
    KiExitDispatcher(ApcLock.OldIrql);//可能引发一次线程切换,以立即切换到目标线程执行apc
    return State;
}

VOID FASTCALL
KiInsertQueueApc(IN PKAPC Apc,IN KPRIORITY PriorityBoost)//唤醒目标线程后的优先级增量
{
    PKTHREAD Thread = Apc->Thread;
    BOOLEAN RequestInterrupt = FALSE;
    if (Apc->ApcStateIndex == InsertApcEnvironment) //if要动态插入到当前的apc状态队列
        Apc->ApcStateIndex = Thread->ApcStateIndex; 
    ApcState = Thread->ApcStatePointer[(UCHAR)Apc->ApcStateIndex];//目标状态
ApcMode = Apc->ApcMode;
//先插入apc到指定位置
    /* 插入位置的确定:分三种情形
     * 1) Kernel APC with Normal Routine or User APC : Put it at the end of the List
     * 2) User APC which is PsExitSpecialApc : Put it at the front of the List
     * 3) Kernel APC without Normal Routine : Put it at the end of the No-Normal Routine Kernel APC list
    */
    if (Apc->NormalRoutine)//有NormalRoutine的APC都插入尾部(用户模式发来的线程终止APC除外)
    {
        if ((ApcMode == UserMode) && (Apc->KernelRoutine == PsExitSpecialApc))
        {
            Thread->ApcState.UserApcPending = TRUE;
            InsertHeadList(&ApcState->ApcListHead[ApcMode],&Apc->ApcListEntry);
        }
        else
            InsertTailList(&ApcState->ApcListHead[ApcMode],&Apc->ApcListEntry);
    }
    Else //无NormalRoutine的特殊类APC(内核APC),少见
    {
        ListHead = &ApcState->ApcListHead[ApcMode];
        NextEntry = ListHead->Blink;
        while (NextEntry != ListHead)
        {
            QueuedApc = CONTAINING_RECORD(NextEntry, KAPC, ApcListEntry);
            if (!QueuedApc->NormalRoutine) break;
            NextEntry = NextEntry->Blink;
        }
        InsertHeadList(NextEntry, &Apc->ApcListEntry);//插在这儿
    }

    //插入到相应的位置后,下面检查Apc状态是否匹配
    if (Thread->ApcStateIndex == Apc->ApcStateIndex)//if 插到了当前apc状态的apc队列中
    {
        if (Thread == KeGetCurrentThread())//if就是给当前线程发送的apc
        {
            ASSERT(Thread->State == Running);//当前线程肯定没有睡眠,这不废话吗?
            if (ApcMode == KernelMode)
            {
                Thread->ApcState.KernelApcPending = TRUE;
                if (!Thread->SpecialApcDisable)//发出一个apc中断,待下次降低irql时将执行apc
                    HalRequestSoftwareInterrupt(APC_LEVEL); //关键
            }
        }
        Else //给其他线程发送的内核apc
        {
            KiAcquireDispatcherLock();
            if (ApcMode == KernelMode)
            {
                Thread->ApcState.KernelApcPending = TRUE;
                if (Thread->State == Running)
                    RequestInterrupt = TRUE;//需要给它发出一个apc中断
                else if ((Thread->State == Waiting) && (Thread->WaitIrql == PASSIVE_LEVEL) &&
                         !(Thread->SpecialApcDisable) && (!(Apc->NormalRoutine) ||
                         (!(Thread->KernelApcDisable) &&
                         !(Thread->ApcState.KernelApcInProgress))))
                {
                    Status = STATUS_KERNEL_APC;
                    KiUnwaitThread(Thread, Status, PriorityBoost);//临时唤醒目标线程执行apc
                }
                else if (Thread->State == GateWait) …
            }
            else if ((Thread->State == Waiting) && (Thread->WaitMode == UserMode) &&
                     ((Thread->Alertable) || (Thread->ApcState.UserApcPending)))
            {
                Thread->ApcState.UserApcPending = TRUE;
                Status = STATUS_USER_APC;
                KiUnwaitThread(Thread, Status, PriorityBoost);//强制唤醒目标线程
            }
            KiReleaseDispatcherLockFromDpcLevel();
            KiRequestApcInterrupt(RequestInterrupt, Thread->NextProcessor);
        }
    }
}

这个函数既可以给当前线程发送 apc,也可以给目标线程发送 apc。若给当前线程发送内核 apc 时,会立即请求发出一个 apc 中断。若给其他线程发送 apc 时,可能会唤醒目标线程。

如何通过 C++ 实时监听 ETW 事件
如何通过 C++ 实时监听 ETW 事件
【译】调查并确定 Windows 运行速度变慢问题
【译】调查并确定 Windows 运行速度变慢问题
【译】丢失的 WPA 文档 —— 磁盘使用
【译】丢失的 WPA 文档 —— 磁盘使用
【译】丢失的 WPA 文档 —— CPU 调度
【译】丢失的 WPA 文档 —— CPU 调度
【译】丢失的 WPA 文档 —— CPU 采样
【译】丢失的 WPA 文档 —— CPU 采样
如何通过 PDH(Performance Data Helper) 获取性能计数器的值

0

  1. This post has no comment yet

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注