[6]Windows内核情景分析 --APC

APC:异步过程调用。这是一种常见的技术。前面进程启动的初始过程就是:主线程在内核构造好运行环境后,从KiThreadStartup开始运行,然后调用PspUserThreadStartup,在该线程的apc队列中插入一个APC:LdrInitializeThunk,这样,当PspUserThreadStartup返回后,正式退回用户空间的总入口BaseProcessStartThunk前,会执行中途插入的那个apc,完成进程的用户空间初始化工作(链接dll的加载等)

可见:APC的执行时机之一就是从内核空间返回用户空间的前夕。也即在返回用户空间前,会“中断”那么一下。因此,APC就是一种软中断。

除了这种APC用途外,应用程序中也经常使用APC。如Win32 API ReadFileEx就可以使用APC机制来实现异步读写文件的功能。

BOOL   //源码

ReadFileEx(IN HANDLE hFile,

           IN LPVOID lpBuffer,

           IN DWORD nNumberOfBytesToRead  OPTIONAL,

           IN LPOVERLAPPED lpOverlapped,//完成结果

           IN LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine)//预置APC将调用的完成例程

{

   LARGE_INTEGER Offset;

   NTSTATUS Status;

   Offset.u.LowPart = lpOverlapped->Offset;

   Offset.u.HighPart = lpOverlapped->OffsetHigh;

   lpOverlapped->Internal = STATUS_PENDING;

   Status = NtReadFile(hFile,

                       NULL, //Event=NULL

                       ApcRoutine,//这个是内部预置的APC例程

                       lpCompletionRoutine,//APC的Context

                       (PIO_STATUS_BLOCK)lpOverlapped,

                       lpBuffer,

                       nNumberOfBytesToRead,

                       &Offset,

                       NULL);//Key=NULL

   if (!NT_SUCCESS(Status))

   {

 SetLastErrorByStatus(Status);//

 return FALSE;

   }

   return TRUE;

}

 

VOID  ApcRoutine(PVOID ApcContext,//指向用户提供的完成例程

_IO_STATUS_BLOCK* IoStatusBlock,//完成结果

            ULONG Reserved)

{

LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine = ApcContext;

DWORD dwErrorCode = RtlNtStatusToDosError(IoStatusBlock->Status);

     //调用用户提供的完成例程

lpCompletionRoutine(dwErrorCode,

IoStatusBlock->Information, 

(LPOVERLAPPED)IoStatusBlock);

}

 

 

因此,应用层的用户提供的完成例程实际上是作为APC函数进行的,它运行在APC_LEVEL irql

 

NTSTATUS

NtReadFile(IN HANDLE FileHandle,

           IN HANDLE Event OPTIONAL,

           IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,//内置的APC

           IN PVOID ApcContext OPTIONAL,//应用程序中用户提供的完成例程

           OUT PIO_STATUS_BLOCK IoStatusBlock,

           OUT PVOID Buffer,

           IN ULONG Length,

           IN PLARGE_INTEGER ByteOffset OPTIONAL,

           IN PULONG Key OPTIONAL)

{

   …

   Irp = IoAllocateIrp(DeviceObject->StackSize, FALSE);//分配一个irp

   Irp->Overlay.AsynchronousParameters.UserApcRoutine = ApcRoutine;//记录

   Irp->Overlay.AsynchronousParameters.UserApcContext = ApcContext;//记录

   …

   Status = IoCallDriver(DeviceObject, Irp);//把这个构造的irp发给底层驱动

   …

}

 

当底层驱动完成这个irp后,会调用IoCompleteRequest完成掉这个irp,这个IoCompleteRequest实际上内部最终调用IopCompleteRequest来做一些完成时的工作

VOID

IopCompleteRequest(IN PKAPC Apc,

                   IN PKNORMAL_ROUTINE* NormalRoutine,

                   IN PVOID* NormalContext,

                   IN PVOID* SystemArgument1,

                   IN PVOID* SystemArgument2)

{

   …

   if (Irp->Overlay.AsynchronousParameters.UserApcRoutine)//上面传入的APC

   {

      //构造一个APC

      KeInitializeApc(&Irp->Tail.Apc,KeGetCurrentThread(),CurrentApcEnvironment,

       IopFreeIrpKernelApc,

                   IopAbortIrpKernelApc,

                  (PKNORMAL_ROUTINE)Irp->Overlay.AsynchronousParameters.UserApcRoutine,

                  Irp->RequestorMode,

                  Irp->Overlay.AsynchronousParameters.UserApcContext);//应用层的完成例程

      //插入到APC队列

      KeInsertQueueApc(&Irp->Tail.Apc, Irp->UserIosb, NULL, 2);

    }//end if

   …

}

 

如上,ReadFileEx函数的异步APC机制是:在这个请求完成后,IO管理器会将一个APC插入队列中,然后

在返回用户空间前夕调用那个内置APC,最终调用应用层用户提供的完成例程。

 

明白了APC大致原理后,现在详细看一下APC的工作原理。

APC分两种,用户APC、内核APC。前者指在用户空间执行的APC,后者指在内核空间执行的APC。

先看一下内核为支持APC机制提供的一些基础结构设施。

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)

   KAPC SuspendApc;

   …   

}KTHREAD;

 

Typedef struct _KAPC_STATE //APC队列的状态描述符

{

   LIST_EBTRY  ApcListHead[2];//每个线程有两个apc队列

   PKPROCESS Process;//当前线程所在的进程

   BOOL KernelApcInProgress;//指示本线程是否当前正在 内核apc

   BOOL KernelApcPending;//表示内核apc队列中是否有apc

   BOOL UserApcPending;//表示用户apc队列中是否apc

}

Typedef enum _KAPC_ENVIRONMENT

{

   OriginalApcEnvironment,//0,状态数组索引

   AttachedApcEnvironment;//1,状态数组索引

   CurrentApc Environment;//2,表示使用当前apc状态

   CurrentApc Environment;//3,表示使用插入apc时那时的线程的apc状态

}

 

一个线程可以挂靠到其他进程的地址空间中,因此,一个线程的状态分两种:常态、挂靠态。

常态下,状态数组中0号元素指向ApcState(即当前apc状态),1号元素指向SavedApcState(非当前apc状态);挂靠态下,两个元素的指向刚好相反。但无论如何,KTHREAD结构中的ApcStateIndex总是指当前状态的位置,ApcState则总是表示线程当前使用的apc状态。

于是有:

#define PsGetCurrentProcess  IoGetCurrentProces

PEPROCESS  IoGetCurrentProces()

{

   Return PsGetCurrentThread()->Tcb.ApcState.Process;//ApcState中的进程字段总是表示当前进程

}

不管当前线程是处于常态还是挂靠态下,它都有两个apc队列,一个内核,一个用户。把apc插入对应的队列后就可以在恰当的时机得到执行。注意:每当一个线程挂靠到其他进程时,挂靠初期,两个apc队列都会变空。下面看下每个apc本身的结构

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;

注意:

若这个apc是内核apc,那么NormalRoutine表示用户自己提供的内核apc函数,NormalContext则是该apc函数的context*,SystemArgument1与SystemArgument2表示插入队列时的附加参数

若这个apc是用户apc,那么NormalRoutine表示该apc的用户空间总apc函数,NormalContext才是真正用户自己提供的用户空间apc函数,SystemArgument1则表示该真正apc的context*。(一切错位了)

 

 

//下面这个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时,可能会唤醒目标线程。

 

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处后面的那条指令处

}

}

不光是从系统调用返回用户空间要扫描执行apc,从异常和中断返回用户空间也同样需要扫描执行。

现在我们只看从系统调用返回时apc的执行过程。

上面是伪代码,实际的从Cli后面的代码,是下面这样的。

Test dword ptr[ebp+KTRAP_FRAME_EFLAGS], EFLAGS_V86_MASK   //检查eflags是否标志运行在V86模式

Jnz 1  //若运行在V86模式,那么上次模式肯定是从用户空间进入内核的,跳过下面的检查

Test byte ptr[ebp+KTRAP_FRAME_CS],1

Je 2 //若上次模式不是用户模式,跳过下面的流程,不予扫描apc

1:

Mov ebx,PCR[KPCR_CURRENT_THREAD]  //ebx=KTHREAD*(当前线程对象的地址)

Mov byte ptr[ebx+KTHREAD_ALERTED],0 //kthread.Alert修改为不可提醒

Cmp byte ptr[ebx+KTHREAD_PENDING_USER_APC],0

Je 2 //如果当前线程的用户apc队列为空,直接跳过

Mov ebx,ebp //ebx=TrapFrame帧的地址

Mov [ebx,KTRAP_FRAME_EAX],eax //保存

Mov ecx,APC_LEVEL

Call KfRaiseIrql  //call KfRaiseIrql(APC_LEVEL)

Push eax //保存提升irql之前的irql

Sti

Push ebx //TrapFrame帧的地址

Push NULL

Push UserMode

Call KiDeliverApc   //call KiDeliverApc(UserMode, NULL, TrapFrame*) 

Pop ecx // ecx=之前的irql

Call KfLowerIrql  //call KfLowerIrql(之前的irql)

Move eax, [ebx,KTRAP_FRAME_EAX] //恢复eax

Cli

Jmp 1 //再次跳回1处循环,扫描apc队列

 

关键的函数是KiDeliverApc,这个函数用来真正扫描apc队列执行所有apc,我们看:

VOID

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不同,因为用户apc队列中的apc函数自然是要在用户空间中执行的,而KiDeliverApc这个函数本身位于内核空间,因此,不能直接调用用户apc函数,需要‘提前’回到用户空间去执行队列中的每个用户apc,然后重新返回内核,再次扫描整个内核apc队列,再执行用户apc队列中遗留的下一个用户apc。如此循环,直至执行完所有用户apc后,才‘正式’返回用户空间。

 

 

 

 

下面的函数就是用来为执行用户apc做准备的。

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;

}

至于为什么要放在一个try块中保护,是因为用户空间中的栈地址,谁也无法保证会不会出现崩溃。

如上,这个函数修改返回地址,回到用户空间中的KiUserApcDisatcher函数处去。然后把原trap帧保存在用户栈中。由于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函数后,调用ZwContinue重返内核。 

 

 

Void CALLBACK  //用户空间的总apc函数

IntCallUserApc(void* RealApcFunc, void* SysArg1,void* SysArg2)

{

   (*RealApcFunc)(SysArg1);//也即调用RealApcFunc(void* context)

}

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)

}

 

 

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;

}

 

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);

}

 

如上,上面的函数,就把NtContinue的TrapFrame强制还原成原来的TrapFrame,以好‘正式’返回到用户空间的真正断点处(不过在返回用户空间前,又要去扫描用户apc队列,若仍有用户apc函数,就先执行掉内核apc队列中的所有apc函数,然后又偏离原来的返回路线,‘提前’返回到用户空间的KiUserApcDispatcher函数去执行用户apc,这是一个不断循环的过程。可见,NtContinue这个函数不仅含有继续回到原真正用户空间断点处的意思,还含有继续执行用户apc队列中下一个apc函数的意思)

 

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;

}

上面这个函数的关键工作是检测到用户apc队列不为空,就又将UserApcPending标志置于TRUE。

 

 

 

前面我们看到的是用户apc队列的执行机制与时机,那是用户apc唯一的执行时机。内核apc队列中的apc执行时机是不相同的,而且有很多执行时机。

内核apc的执行时机主要有:

1、 每次返回用户空间前,每执行一个用户apc前,就会扫描执行整个内核apc队列

2、 每当调用KeLowerIrql,从APC_LEVEL以上(不包括APC_LEVEL) 降到 APC_LEVEL以下(不包括APC_LEVEL)前,中途会检查是否有阻塞的apc中断请求,若有就扫描执行内核apc队列

3、 每当线程重新得到调度,开始运行前,会扫描执行内核apc队列 或者 发出apc中断请求

内核apc的执行时机:【调度、返、降】apc

 

 

KeLowerIrql实质上是下面的函数:

VOID FASTCALL

KfLowerIrql(IN KIRQL OldIrql)

{

    ULONG EFlags;

    ULONG PendingIrql, PendingIrqlMask;

    PKPCR Pcr = KeGetPcr();

    PIC_MASK Mask;

    EFlags = __readeflags();//保存原eflags

    _disable();//关中断

Pcr->Irql = OldIrql;//降到目标irql

//检测是否有高于目标irql的阻塞中的软中断

    PendingIrqlMask = Pcr->IRR & FindHigherIrqlMask[OldIrql];

    if (PendingIrqlMask)//若有

    {

        BitScanReverse(&PendingIrql, PendingIrqlMask);//找到最高级别的软中断

        if (PendingIrql > DISPATCH_LEVEL)

        {

            Mask.Both = Pcr->IDR;

            __outbyte(PIC1_DATA_PORT, Mask.Master);

            __outbyte(PIC2_DATA_PORT, Mask.Slave);

            Pcr->IRR ^= (1 << PendingIrql);

        }

        SWInterruptHandlerTable[PendingIrql]();//处理阻塞的软中断(即扫描执行队列中的函数)

    }

    __writeeflags(EFlags);//恢复原eflags

}

 

这个函数在从当前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累积收到了哪些级别的软中断。

 

 

下面的函数可用于模拟硬件,向cpu发出任意irql级别的软中断,请求cpu处理执行那种中断。

VOID FASTCALL

HalRequestSoftwareInterrupt(IN KIRQL Irql)//Irql一般是APC_LEVEL/DPC_LEVEL

{

    ULONG EFlags;

    PKPCR Pcr = KeGetPcr();

    KIRQL PendingIrql;

    EFlags = __readeflags();//保存老的eflags寄存器

    _disable();//关中断

    Pcr->IRR |= (1 << Irql);//关键。标志向cpu发出了一个对应irql级的软中断

PendingIrql = SWInterruptLookUpTable[Pcr->IRR & 3];//IRR后两位表示是否有阻塞的apc中断

//若有阻塞的apc中断,并且当前irql是PASSIVE_LEVEL,立即执行apc。也即在PASSIVE_LEVEL级时发出任意软中断后,会立即检查执行现有的apc中断。

if (PendingIrql > Pcr->Irql)

 SWInterruptHandlerTable[PendingIrql]();//调用执行apc中断的isr,处理apc中断

    __writeeflags(EFlags);//恢复原eflags寄存器

}

 

那么什么时候,系统会调用这个函数,向cpu发出apc中断呢?

典型的情形1:

在切换线程时,若将线程的WaitIrql置为APC_LEVEL,将导致KiSwapContextInternal函数内部在重新切回来后,立即自动发出一个apc中断,以在下次降低irql到PASSIVE_LEVEL时处理执行队列中那些阻塞的apc。反之,若将线程的WaitIrql置为PASSIVE_LEVEL,将导致KiSwapContextInternal函数内部在重新切回来后,不会发出apc中断,然后系统会自行显式调用KiDeliverApc给予扫描执行

 

典型情形2:

在给自身线程发送一个内核apc时,在apc进队的同时,会发出apc中断,以请求cpu在下次降低irql时,扫描执行apc。

 

 

 

Apc是一种软中断,既然是中断,他也有类似的isr。Apc中断的isr最终进入 HalpApcInterruptHandler

VOID FASTCALL

HalpApcInterruptHandler(IN PKTRAP_FRAME TrapFrame)

{

    //模拟硬件中断压入保存的寄存器

    TrapFrame->EFlags = __readeflags();

    TrapFrame->SegCs = KGDT_R0_CODE;

    TrapFrame->Eip = TrapFrame->Eax;

    KiEnterInterruptTrap(TrapFrame);//构造Trap现场帧

    扫描执行当前线程的内核apc队列,略…

    KiEoiHelper(TrapFrame); 

}

posted @ 2018-12-17 22:01  jadeshu  阅读(1233)  评论(0编辑  收藏  举报