以下是我对APC的一些理解,肯定不全面也不深入,真的是浅浅的,但本着共同学习进步的想法发出来,欢迎各位发表意见多多指导多多拍砖。




APC,异步过程调用。
APC是线程相关的,每个线程都有自己独自的APC链表,因此可以让一段代码在指定的线程上下文中运行。系统中有两种APC,用户层和内核层APC(忽略用户层)。

初始化APC的函数定义如下:
VOID
KeInitializeApc (
    __out PRKAPC Apc,
    __in PRKTHREAD Thread, 
    __in KAPC_ENVIRONMENT Environment,          //APC环境
    __in PKKERNEL_ROUTINE KernelRoutine,         //内核模式APC函数
    __in_opt PKRUNDOWN_ROUTINE RundownRoutine,   //线程退出时且APC链表中非空,若此例程也非空则被调用
    __in_opt PKNORMAL_ROUTINE NormalRoutine,
    __in_opt KPROCESSOR_MODE ApcMode,
    __in_opt PVOID NormalContext
    )
参数详细说明下面介绍。

内核APC又分为两种:普通APC和特殊APC。这两种APC在线程中共用同一个APC链表,但特殊APC总是排在普通APC的前面,即在交付时优先交付特殊APC。APC对象结构中并没有标记自己是一个普通APC还是特殊APC的字段,这种区分是通过参数NormalRoutine来区分的。初始化的时候如果参数NormalRoutine为NULL,则说明这是一个特殊APC,此时忽略参数NormalContext且参数ApcMode默认为KernelMode;如果参数NormalRoutine不为NULL,则这是一个普通
APC,参数NormalContext作为参数NormalRoutine的参数。在交付普通APC时,即执行NormalRoutine,也执行KernelRoutine。

下面说一下参数Environment,这个参数的设定,是为了解决线程Attach到其它进程以及Dettach回来的情况。
在线程的内核对象中,有两个域ApcState和SavedApcState,以及一个包含两个元素的数组ApcStatePointer和保存ApcStatePointer下标的整数索引值ApcStateIndex。APC插入时,总是插入到ApcStatePointer[ApcStateIndex]代表的链表中(也即总是插入到ApcState代表的链表中,因为ApcStatePointer[ApcStateIndex]和ApcState总是指向同一块地址)。例如,当线程在自己的进程中运行时,新的APC被插入到ApcState所代表的链表中,ApcStateIndex为0,ApcStatePointer[0]指向ApcState指向的地址。当线程附加到其它进程中时,系统先将当前ApcStatePointer[ApcStateIndex]指向的内存中的内容(即ApcState指向的内容)保存到SavedApcState中,然后,将ApcStateIndex改为1,令ApcStatePointer[1]指向ApcState指向的内存,ApcStatePointer[0]指向ApcState指向的内存。然后重新初始化ApcStatePointer[ApcStateIndex]指向的内存。为什么这么麻烦呢?直接来两个字段,一个ApcState,一个SavedApcState不就完了吗?当然不行,因为我们可以指定APC被插入到哪个链表中(ApcState还是SavedApcState),而我们正是通过Environment指定的。
先看一下Environment的结构:
typedef enum _KAPC_ENVIRONMENT {
    OriginalApcEnvironment,
    AttachedApcEnvironment,
    CurrentApcEnvironment,
    InsertApcEnvironment
} KAPC_ENVIRONMENT;
下面是对这四个域的解释:
OriginalApcEnvironment:插入到目标线程ApcStatePointer[0]代表的链表中。这样可以保证这个APC只在目标线程在自己的进程中时运行,为什么呢?因为从上面嗦的分析中可以知道,当目标线程没有Attach时,ApcStatePointer[0]指向ApcState,即线程正在使用的APC链表;当目标线程Attach了,则ApcStatePointer[0]指向SavedApcState,它所代表的链表此时是作为备份的,其中的APC不会被交付,只有在Dettach后将其恢复到ApcState后才会被交付。所以喽。
AttachedApcEnvironment:插入到目标线程ApcStatePointer[1]代表的链表中。这个的使用发生在目标线程已经Attach,并且插入的APC也想在这个Attach的进程中运行。因为如果目标线程没有Attach,则ApcStatePointer[1]代表备份,所以APC不会被交付;当目标线程真的Attach的时候,原来的SavedApcState就会被覆盖掉,那这个APC还有什么用?所以不会使用。因此只有目标线程已经Attach了,APC被插入到ApcStatePointer[1]中,此j时ApcStatePointer[1]代表正在被使用的APC链表,因此就被交付啦。
CurrentApcEnvironment:APC被插入到初始化时目标线程的ApcStatePointer[ApcStateIndex]所代表的链表中。使用这个值时,当初始化后和插入之间这
段时间没有Attach或者Detach时,APC肯定会被执行(不能保证是在自己进程中还是Attach进程中)。如果在这之间发生了Attach,则APC等待目标线程Detach后被执行;如果这之间发生了Detach,则APC不会被执行。
InsertApcEnvironment:APC被插入到插入时(调用KeInsertQueueApc时)目标线程的ApcStatePointer[ApcStateIndex]所代表的链表中。使用这个环境值时,APC可以被保证肯定会马上执行(所谓的马上指不需要等待Dettach),因为目标线程无论是Attach还是没有,ApcStatePointer[ApcStateIndex]都代表当前使用的链表。只是不能保证APC是在目标线程自己的进程运行还是在Attach后的进程中运行。

知道了上面的参数后,使用起来就比较简单了,调用KeInitializeApc初始化一个APC后,调用KeInsertApc插入APC就可以了。
别外需要注意的是,APC的函数都是运行在IRQL为APC_LEVEL级别的。


应用:
APC被知道的最多的应用应该是对一些异步操作的支持,如ReadFile的异步方式等。以ReadFile为例,文件系统发现这是一个异步调用,就会标记为Pending,然后立刻返回。等读取操作完成的时候,在将结果拷贝到用户层的内存空间和调用用户层提供的完成函数时,就用到APC。

另外,挂钩APC相关函数可以达到阻止结束目标进程(ZwTerminateProcess)、阻止创建远程线程(CreateRemoteThread)、阻止对其调用GetContext SetContext等。(但直接读写进程空间或者Attach操作是阻止不了的)。

总结:
如果说其它进程或者线程间通信的方式(比如共享一个对象、一块映射内存、甚至套接字等等)是两个国家间的以合作的方式交流:我需要你的什么信息或者要送给你什么,要得到你的同意才行,那么APC就像是间谍:我安插在你选干部的队伍里(插入APC),等你请我去当你的干部。哪一天我当上了(交付),我就可以做我自己想做的事了,我同国外的沟通不用得到同意(目标进程里的代码可能完全不知道我的存在)。