我们知道,DPC是针对处理器的,而APC是针对线程的,同时在IRQL上处于DPC的下层,也属于软件中断。在Windows的内核实现中,APC有很广泛的应用,比如在线程的创建、驱动程序完成端口的实现,甚至是进程的挂靠(Processattach)中,都有APC的身影。APC与DPC在某些方面很有类似之处,两者最重要的行为就是构造、排队(Queue)和投递(Deliver)。所谓构造就是生成对应的APC或者DPC对象体,排队就是将APC或者DPC对象放到队列中去,而投递就是在某些固定的条件下,执行APC或者DPC对象中的例程。如图1所示。
APC是与线程相关的,在描述线程的结构KTHREAD中,可以看到很多带APC字眼的成员。
kddtKTHREAD nt!KTHREAD ……….. +0x028ApcState +0x028ApcStateFill +0x03fApcQueueable ……….. :_KAPC_STATE :UChar :UChar +0x044ApcQueueLock ……….. :Uint4B +0x070KernelApcDisable:Int2B +0x072SpecialApcDisable:Int2B +0x070CombinedApcDisable:Uint4B ……….. +0x11cApcStateIndex ……….. :UChar +0x130ApcStatePointer +0x138SavedApcState :Ptr32_KAPC_STATE :_KAPC_STATE +0x138SavedApcStateFill:UChar ……….. 大约有十几个这样的成员,这些成员中,最重要的是ApcState。 kddt_KAPC_STATE nt!_KAPC_STATE +0x000ApcListHead[2] +0x010Process :_LIST_ENTRY//每个指针指向_KAPC结构 :Ptr32_KPROCESS//当前进程 +0x014KernelApcInProgress:UChar//内核态APC在处理当中 +0x015KernelApcPending +0x016UserApcPending :UChar//有内核态APC等待处理 :UChar//有用户态APC等待处理
上面的文字出现了两种APC:用户态APC和内核态APC,实际上内核态APC还分为普通的和特殊的。_KTHREAD、_KAPC_STATE和各种模式APC的关系如图2所示。
图2
从图中可以看出,_KTHREAD包含了ApcState和SavedApcState两个成员,这两个成员分别指向两个都是_KAPC_STATE结构的变量(ApcState和SavedApcState之间的关系,将在后面进行说明),在_KAPC_STATE结构中,第一个成员为ApcListHead[2],第一个成员指向一个用户模式APC链表,而第二个成员指向内核模式APC链表。如果注意观察,可以看出特殊内核模式APC都排在普通内核模式APC之前。
APC结构
APC对象是Windows内核对象之一,其结构为:
kddt_KAPC nt!_KAPC ; +0x000Type :Uchar//KOBJECTS枚举类型的ApcObject; :Uchar//没有使用; +0x001SpareByte0 +0x002Size :Uchar//KAPC结构的大小; :Uchar//没有使用; +0x003SpareByte1 +0x004SpareLong0 +0x008Thread :Uint4B//没有使用; :Ptr32_KTHREAD;//指向此APC对象所在的线程KTHREAD对象; :_LIST_ENTRY +0x00cApcListEntry//ApcListEntry域是APC对象被加入到线程APC链表中的 节点对象; +0x014KernelRoutine :Ptr32void//一个函数指针,必须的; +0x018RundownRoutine:Ptr32void//一个函数指针,线程终止时,如APC链表 非空,执行这个函数,可选的; +0x01cNormalRoutine +0x020NormalContext :Ptr32void//一个函数指针,可选的; :Ptr32Void//NormalRoutine执行时的上下文 +0x024SystemArgument1:Ptr32Void//KernelRoutine或NormalRoutine函数的 参数 +0x028SystemArgument2:Ptr32Void//KernelRoutine或NormalRoutine函数的参 数 +0x02cApcStateIndex +0x02dApcMode +0x02eInserted :Char :Char :Uchar//指示了它位于线程KTHREAD对象的哪个APC
链表中//说明了APC对象的环境状态,用户态还是内
核态;//该APC对象是否已被插入到线程的APC链表中
这个结构不算长,每个成员的作用在注释中都进行了说明。下面阐述一下在学习APC的过程中经常被绕晕的两个问题:用户模式APC、特殊的内核模式APC、普通的内核模式APC之间的区别;ApcState和SavedApcState之间的区别和使用方法。
1)各种模式APC的区别
欲知用户模式APC、特殊的内核模式APC和普通的内核模式APC之间的区别,请先看下表。
通过上表可以比较清楚的看出这几种模式APC之间的区别,另外有几点需要注意的是:
①如果mode为UserMode,但是NormalRoutine为NULL,那么实际的模式变为Kernelmode,很简单,因为没有用户空间的APC可以使用;
②每种APC都有内核模式的例程;
③特殊的内核模式APC与普通的内核模式APC区别在于Normalroutine是否为空;
④当普通的内核模式APC例程被执行时,先执行KernelRoutine例程,再执行NormalRoutine例程(除非执行过KernelRoutine例程后,NormalRoutine例程被清除为NULL)。
2)ApcState和SavedApcState之间的区别和使用方法
当线程1从进程A挂靠到进程B时,线程所在的地址空间发生了变化,线程原来的APC就不会执行,因为线程1仍然会接到APC请求(在进程B的环境里),所以系统会把在进程A环境里APCState队列的内容转移到进程B环境里的SavedApcState队列中,并使用在进程B的环境里正常使用的ApcState队列,用来在进程B中执行APC,结合图3就会明白。当线程从进程B中脱离时,当前ApcState队列中的APC将被投递,即执行,然后将进程A环境中线程ApcState指向进程B环境中的SaveApcState,恢复挂靠前的状态。
图3
为了操作灵活,在_KTHREAD结构中,还有一个指针数组ApcStatePointer[2]和ApcStateIndex的成员,其使用如下表所示。
ApcStateIndex、ApcStatePointer[0]和ApcStatePointer[1]的使用从图3中也可以看出。
APC机制的使用
APC的实现机制与DPC类似,使用也比较规范,一般来说会是下面几个步骤:APC对象的建立、KeInitializeApc(初始化APC对象)和KeInsertQueueApc(插入APC队列)。我们分别叙述之:
①使用ExAllocatePoolWithTag在非换页区分配一段APC结构大小的内存:
Apc=ExAllocatePoolWithTag(NonPagedPool,sizeof(KAPC),TAG_APC);
②指定针对哪个线程插入APC对象,并设定该APC对象执行的环境、KernelRoutine、RundownRoutine和NormalRoutine等参数。
NTKERNELAPI VOID KeInitializeApc( __outPRKAPCApc, __inPRKTHREADThread, __inKAPC_ENVIRONMENTEnvironment, __inPKKERNEL_ROUTINEKernelRoutine, __in_optPKRUNDOWN_ROUTINERundownRoutine, __in_optPKNORMAL_ROUTINENormalRoutine, __in_optKPROCESSOR_MODEProcessorMode, __in_optPVOIDNormalContext );
③KeInsertQueueApc(插入APC队列)。
NTKERNELAPI BOOLEAN KeInsertQueueApc( __inoutPRKAPCApc, __in_optPVOIDSystemArgument1, __in_optPVOIDSystemArgument2, __inKPRIORITYIncrement );
这个函数完成上面生成的APC对象的插入,在APC具体实现中,主要是依据Apc-ApcMode,如果是UserMode,就插入用户模式队列的最前面,否则就插入内核模式的队列中。在内核模式的队列中,特殊内核模式的APC会置于普通内核模式APC之前,这点我们通过这两种APC的放置策略可以明显看出,当放置一个特殊内核模式的APC,KeInsertQueueApc会从头搜索内核模式队列,找到最后一个特殊内核模式的APC,然后放于之后,如果是放置一个普通内核模式APC,就会直接放于内核模式队列末尾。这种实现方式保证了特殊内核模式APC会在普通内核APC之前执行。
APC的执行
触发APC,一定会调用KiDeliverApc,哪些地方会调用这个函数呢?
①离开临界区或者守护区;
②经过一次线程切换,有内核模式APC需要交付;
③系统服务或异常处理函数返回到用户模式;
④系统将IRQL降到Passive_Level时。
当前线程的APC被交付是在KiDeliverApc函数中完成的,KiDeliverApc函数的调用界面为:
VOIDNTAPIKiDeliverApc(INKPROCESSOR_MODEDeliveryMode,INPKEXCEPTION_FRAME ExceptionFrame,INPKTRAP_FRAMETrapFrame)
其实现可以分为三大部分,伪代码如下:
KiDeliverApc() { 特殊内核模式APC对象的处理; 普通内核模式APC对象的处理; 用户模式APC对象的处理; }
很简单,就是三种APC对象的处理,下面我们对它们的实现分别进行讲解。
1)特殊内核模式APC对象的处理
前面已经说过,特殊内核模式APC对象中并没有NormalRoutine例程,因此我们只需处理每个APC对象中的KernelRoutine例程。其伪代码可以归纳如下:
//处理流程 提升IRQL至Dispatch_Level; 锁住APC链表; 操作APC链表; 解锁APC链表; 恢复IRQL; 执行KernelRoutine例程;
2)内核模式普通APC对象的处理
在这种模式的APC对象中,既有KernelRoutine例程,也有NormalRoutine例程。和上述APC对象的处理略有不同,其伪代码如下:
//处理流程 If(ApcState-KernelApcInProgress==0); If(KernelApcDisable==0);
以NormalRoutine成员指针作为参数,调用APC对象中的KernelRoutine例程,此时该例程工作于APC_LEVEL;
KernelRoutine例程返回后若NormalRoutine非空,则调用NormalRoutine例程,此时该例程工作于PASSIVE_LEVEL;
3)用户模式APC对象的处理
看Windows内核多了,会发现处理用户态的东西相较内核态的要复杂很多,这也是很正常的,因为我们的内核代码工作在内核态,同一个状态的代码当然要较其它状态的代码处理简单一些。
在介绍用户态APC的执行之前,我们要先了解一个概念,什么叫Context,也就是我们经常见到的上下文。按照我的理解,Context指的就是在某个时点,CPU运行的环境,这个环境主要就是那个时点CPU各个寄存器的内容以及堆栈中的内容。
KiDeliverApc()这个函数里有一个重要的参数就是DeliveryMode,表示执行的APC是内核态,还是用户态。内核态APC的执行是没有条件的,而用户态APC的执行是有条件的,条件有三:
①调用参数DeliveryMode为Usermode;
②ApcState中的UserApcPending为TRUE,表示有用户模式的APC等待交付。
③仅当线程在“可报警等待状态”(alertablewaitstage)时,用户态APC才可以交付给该线程。在满足以上三个条件后,正式进入用户态APC对象的处理。
//处理流程
以NormalRoutine成员指针作为参数,调用APC对象中的KernelRoutine例程;
若NormalRoutine不为空,则调用KiInitializeUserApc函数;
在这个时间点上,我们需要很清楚的认识到,目前我们所面临的问题,由于要执行的用户模式APC的NormalRoutine函数是在用户态下,即地址空间在2G空间以下,而目前执行的KiInitializeUserApc函数处于内核态下,如何成功完成跨越,是一个很有技术含量的事情!
下面我们看看KiInitializeUserApc函数是如何解决这一问题的。如图5所示。
当系统进入内核态时,会将进入前CPU执行的环境放于一个叫做TRAP_FRAME(处于内核地址空间)的结构中,从图中可以看到,TRAP_FRAME中很多寄存器的信息,以便返回时恢复当时的CPU环境。如果按照正常的流程,当系统要退出内核态时,会将TRAP_FRAME中的内容恢复到各个寄存器中,但是为了完成我们执行NormalRoutine的目的,KiInitializeUserApc这个函数执行了下面三个步骤,如图6所示。
①通过KeTrapFrameToContext函数将此时的自陷框架(TRAP_FRAME)转化为Context;
②将Context、SystemArgument1、SystemArgument2、Normalroutine、NormalContext复制到用户空间的堆栈上(还包括一个异常结构,用于异常处理);
③修改当前自陷框架中的部分内容,将EIP的值修改为指针KeUserApcDispatcher,修改ESP,使其反映当前用户空间堆栈的变化。
完成以上三个步骤之后,系统从内核态返回用户态,这时系统会执行EIP=KiUserApcDispatcher函数,在这个函数里会调用目前处于堆栈中NormalRoutine例程,实际上就是执行用户投递APC对象中的例程。在NormalRoutine例程执行完成后,调用_ZwContinue,目的是将用户空间的CONTEXT结构拷贝回系统空间的自陷框架(TRAP_FRAME)中,恢复成图5的情形,这时完成一个用户态APC的投递,事情还没有结束,如果用户模式APC队列非空,则继续DeliverAPC,完成上述的①~③步骤,直到APC队列中的例程都得到执行。
小结
实际上,APC的内容还是很多的,仅是其应用可能就要几十页的篇幅并结合实例才能讲明白,本文就是起到抛砖引玉的作用,使大家能够对APC的概念、使用和实现有一个整体的了解。