APC:异步过程调用。这是一种常见的技术。前面进程启动的初始过程就是:主线程在内核构造好运行环境后,从KiThreadStartup开始运行,然后调用PspUserThreadStartup,在该线程的apc队列中插入一个APCLdrInitializeThunk,这样,当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,指当前的ApcStateApcStatePointer数组中的索引位置

  
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。真正用户apccontext*

  PVOID
SystemArgument2;//挂入时的附加参数2

  CCHAR
ApcStateIndex;//指要挂入目标线程的哪个状态时的apc队列

  KPROCESSOR_MODE
ApcMode;//指要挂入用户apc队列还是内核apc队列

  BOOLEAN
Inserted;//表示本apc是否已挂入队列

} KAPC,
*PKAPC;

注意:

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

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

 

 

//下面这个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,//用户自己apccontext*

                 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,此处为用户自己apccontext*

                     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)//有NormalRoutineAPC都插入尾部(用户模式发来的线程终止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没有修改NormalRoutineNULL

            {

                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;//真正apccontext*

        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, //真正apccontext*

                                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 + 8Bseh节点)

        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 IntCallUserApcNormalContextSysArg1SysArg2

  

   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);//也即调用RealApcFuncvoid*
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);

}

 

如上,上面的函数,就把NtContinueTrapFrame强制还原成原来的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要将cpuirqlCMCI_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中断,并且当前irqlPASSIVE_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中断,以在下次降低irqlPASSIVE_LEVEL时处理执行队列中那些阻塞的apc。反之,若将线程的WaitIrql置为PASSIVE_LEVEL,将导致KiSwapContextInternal函数内部在重新切回来后,不会发出apc中断,然后系统会自行显式调用KiDeliverApc给予扫描执行

 

典型情形2

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

 

 

 

Apc是一种软中断,既然是中断,他也有类似的isrApc中断的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);

}

 

打赏