转载:http://www.longene.org/techdoc/0562505001224576771.htmlhtml
漫谈兼容内核之十二:数组
Windows的APC机制数据结构
毛德操app
前两篇漫谈中讲到,除ntdll.dll外,在启动一个新进程运行时,PE格式DLL映像的装入和动态链接是由ntdll.dll中的函数LdrInitializeThunk()做为APC函数执行而完成的。这就牵涉到了Windows的APC机制,APC是“异步过程调用(Asyncroneus Procedure Call)”的缩写。从大致上说,Windows的APC机制至关于Linux的Signal机制,实质上是一种对于应用软件(线程)的“软件中断”机制。可是读者将会看到,APC机制至少在形式上与软件中断机制仍是有至关的区别,而称之为“异步过程调用”确实更为贴切。框架
APC与系统调用是密切连系在一块儿的,在这个意义上APC是系统调用界面的一部分。然而APC又与设备驱动有着很密切的关系。例如,ntddk.h中提供“写文件”系统调用ZwWriteFile()、即NtWriteFile()的调用界面为:异步
NTSYSAPIide
NTSTATUS函数
NTAPIui
ZwWriteFile(this
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PVOID Buffer,
IN ULONG Length,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN PULONG Key OPTIONAL
);
这里有个参数ApcRoutine,这是一个函数指针。何时要用到这个指针呢?原来,文件操做有“同步”和“异步”之分。普通的写文件操做是同步写,启动这种操做的线程在内核进行写文件操做期间被“阻塞(blocked)”而进入“睡眠”,直到设备驱动完成了操做之后才又将该线程“唤醒”而从系统调用返回。可是,若是目标文件是按异步操做打开的,即在经过W32的API函数CreateFile()打开目标文件时把调用参数dwFlagsAndAttributes设置成FILE_FLAG_OVERLAPPED,那么调用者就不会被阻塞,而是把事情交给内核、不等实际的操做完成就返回了。可是此时要把ApcRoutine设置成指向某个APC函数。这样,当设备驱动完成实际的操做时,就会使调用者线程执行这个APC函数,就像是发生了一次中断。执行该APC函数时的调用界面为:
typedef
VOID
(NTAPI *PIO_APC_ROUTINE) (IN PVOID ApcContext,
IN PIO_STATUS_BLOCK IoStatusBlock, IN ULONG Reserved);
这里的指针ApcContext就是NtWriteFile()调用界面上传下来的,至于做什么解释、起什么做用,那是包括APC函数在内的用户软件本身的事,内核只是把它传递给APC函数。
在这个过程当中,把ApcRoutine设置成指向APC函数至关于登记了一个中断服务程序,而设备驱动在完成实际的文件操做后就向调用者线程发出至关于中断请求的“APC请求”,使其执行这个APC函数。
从这个角度说,APC机制又应该说是设备驱动框架的一部分。事实上,读者之后还会看到,APC机制与设备驱动的关系比这里所见的还要更加密切。此外,APC机制与异常处理的关系也很密切。
不只内核能够向一个线程发出APC请求,别的线程、乃至目标线程自身也能够发出这样的请求。Windows为应用程序提供了一个函数QueueUserAPC(),就是用于此项目的,下面是ReactOS中这个函数的代码:
DWORD STDCALL
QueueUserAPC(PAPCFUNC pfnAPC, HANDLE hThread, ULONG_PTR dwData)
{
NTSTATUS Status;
Status = NtQueueApcThread(hThread, IntCallUserApc,
pfnAPC, (PVOID)dwData, NULL);
if (Status)
SetLastErrorByStatus(Status);
return NT_SUCCESS(Status);
}
参数pfnAPC是函数指针,这就是APC函数。另外一个参数hThread是指向目标线程对象(已打开)的Handle,这能够是当前线程自己,也能够是同一进程中别的线程,还能够是别的进程中的某个线程。值得注意的是:若是目标线程在另外一个进程中,那么pfnAPC必须是这个函数在目标线程所在用户空间的地址,而不是这个函数在本线程所在空间的地址。最后一个参数dwData则是须要传递给APC函数的参数。
这里的NtQueueApcThread()是个系统调用。“Native API”书中有关于NtQueueApcThread()的一些说明。这个系统调用把一个“用户APC请求”挂入目标线程的APC队列(更确切地说,是把一个带有函数指针的数据结构挂入队列)。注意其第二个参数是须要执行的APC函数指针,本该是pfnAPC,这里却换成了函数IntCallUserApc(),而pfnAPC倒变成了第三个参数,成了须要传递给IntCallUserApc()的参数之一。IntCallUserApc()是kernel32.dll内部的一个函数,可是并未引出,因此不能从外部直接加以调用。
APC是针对具体线程、要求由具体线程加以执行的,因此每一个线程都有本身的APC队列。内核中表明着线程的数据结构是ETHREAD,而ETHREAD中的第一个成分Tcb是KTHREAD数据结构,线程的APC队列就在KTHREAD里面:
typedef struct _KTHREAD
{
. . . . . .
/* Thread state (one of THREAD_STATE_xxx constants below) */
UCHAR State; /* 2D */
BOOLEAN Alerted[2]; /* 2E */
. . . . . .
KAPC_STATE ApcState; /* 34 */
ULONG ContextSwitches; /* 4C */
. . . . . .
ULONG KernelApcDisable; /* D0 */
. . . . . .
PKQUEUE Queue; /* E0 */
KSPIN_LOCK ApcQueueLock; /* E4 */
. . . . . .
PKAPC_STATE ApcStatePointer[2]; /* 12C */
. . . . . .
KAPC_STATE SavedApcState; /* 140 */
UCHAR Alertable; /* 158 */
UCHAR ApcStateIndex; /* 159 */
UCHAR ApcQueueable; /* 15A */
. . . . . .
KAPC SuspendApc; /* 160 */
. . . . . .
} KTHREAD;
Microsoft并不公开这个数据结构的定义,因此ReactOS代码中对这个数据结构的定义带有逆向工程的痕迹,每一行后面的十六进制数值就是相应结构成分在数据结构中的位移。这里咱们最关心的是ApcState,这又是一个数据结构、即KAPC_STATE。能够看出,KAPC_STATE的大小是0x18字节。其定义以下:
typedef struct _KAPC_STATE {
LIST_ENTRY ApcListHead[2];
PKPROCESS Process;
BOOLEAN KernelApcInProgress;
BOOLEAN KernelApcPending;
BOOLEAN UserApcPending;
} KAPC_STATE, *PKAPC_STATE, *__restrict PRKAPC_STATE;
显然,这里的ApcListHead就是APC队列头。不过这是个大小为2的数组,说明实际上(每一个线程)有两个APC队列。这是由于APC函数分为用户APC和内核APC两种,各有各的队列。所谓用户APC,是指相应的APC函数位于用户空间、在用户空间执行;而内核APC,则相应的APC函数为内核函数。
读者也许已经注意到,KTHREAD结构中除ApcState外还有SavedApcState也是KAPC_STATE数据结构。此外还有ApcStatePointer[2]和ApcStateIndex两个结构成分。这是干什么用的呢?原来,在Windows的内核中,一个线程能够暂时“挂靠(Attach)”到另外一个进程的地址空间。比方说,线程T原本是属于进程A的,当这个线程在内核中运行时,若是其活动与用户空间有关(APC就是与用户空间有关),那么当时的用户空间应该就是进程A的用户空间。可是Windows内核容许一些跨进程的操做(例如将ntdll.dll的映像装入新创进程B的用户空间并对其进行操做),因此有时候须要把当时的用户空间切换到别的进程(例如B) 的用户空间,这就称为“挂靠(Attach)”,对此我将另行撰文介绍。在当前线程挂靠在另外一个进程的期间,既然用户空间是别的进程的用户空间,挂在队列中的APC请求就变成“牛头不对马嘴”了,因此此时要把这些队列转移到别的地方,以避免乱套,而后在回到原进程的用户空间时再于恢复。那么转移到什么地方呢?就是SavedApcState。固然,还要有状态信息说明本线程当前是处于“原始环境”仍是“挂靠环境”,这就是ApcStateIndex的做用。代码中为SavedApcState的值定义了一种枚举类型:
typedef enum _KAPC_ENVIRONMENT
{
OriginalApcEnvironment,
AttachedApcEnvironment,
CurrentApcEnvironment
} KAPC_ENVIRONMENT;
实际可用于ApcStateIndex的只是OriginalApcEnvironment和AttachedApcEnvironment,即0和1。读者也许又要问,在挂靠环境下原来的APC队列确实不适用了,但不去用它就是,何须要把它转移呢?再说,APC队列转移之后,ApcState不是空下来不用了吗?问题在于,在挂靠环境下也可能会有(针对所挂靠进程的)APC请求(不过固然不是来自用户空间),因此须要有用于两种不一样环境的APC队列,因而便有了ApcState和SavedApcState。进一步,为了提供操做上的灵活性,又增长了一个KAPC_STATE指针数组ApcStatePointer[2],就用ApcStateIndex的当前值做为下标,而数组中的指针则根据状况能够分别指向两个APC_STATE数据结构中的一个。
这样,以ApcStateIndex的当前数值为下标,从指针数组ApcStatePointer[2]中就能够获得指向ApcState或SavedApcState的指针,而要求把一个APC请求挂入队列时则能够指定是要挂入哪个环境的队列。实际上,当ApcStateIndex的值为OriginalApcEnvironment、即0时,使用的是ApcState;为AttachedApcEnvironment、即1时,则用的是SavedApcState。
每当要求挂入一个APC函数时,无论是用户APC仍是内核APC,内核都要为之准备好一个KAPC数据结构,并将其挂入相应的队列。
typedef struct _KAPC
{
CSHORT Type;
CSHORT Size;
ULONG Spare0;
struct _KTHREAD* Thread;
LIST_ENTRY ApcListEntry;
PKKERNEL_ROUTINE KernelRoutine;
PKRUNDOWN_ROUTINE RundownRoutine;
PKNORMAL_ROUTINE NormalRoutine;
PVOID NormalContext;
PVOID SystemArgument1;
PVOID SystemArgument2;
CCHAR ApcStateIndex;
KPROCESSOR_MODE ApcMode;
BOOLEAN Inserted;
} KAPC, *PKAPC;
结构中的ApcListEntry就是用来将KAPC结构挂入队列的。注意这个数据结构中有三个函数指针,即KernelRoutine、RundownRoutine、NormalRoutine。其中只有NormalRoutine才指向(执行)APC函数的请求者所提供的函数,其他两个都是辅助性的。以NtQueueApcThread()为例,其请求者(调用者)QueueUserAPC()所提供的函数是IntCallUserApc(),因此NormalRoutine应该指向这个函数。注意真正的请求者实际上是QueueUserAPC()的调用者,真正的目标APC函数也并不是IntCallUserApc(),而是前面的函数指针pfnAPC所指向的函数,而IntCallUserApc()起着相似于“门户”的做用。
如今咱们能够往下看系统调用NtQueueApcThread()的实现了。
NTSTATUS
STDCALL
NtQueueApcThread(HANDLE ThreadHandle, PKNORMAL_ROUTINE ApcRoutine,
PVOID NormalContext, PVOID SystemArgument1, PVOID SystemArgument2)
{
PKAPC Apc;
PETHREAD Thread;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
NTSTATUS Status;
/* Get ETHREAD from Handle */
Status = ObReferenceObjectByHandle(ThreadHandle, THREAD_SET_CONTEXT,
PsThreadType, PreviousMode, (PVOID)&Thread, NULL);
. . . . . .
/* Allocate an APC */
Apc = ExAllocatePoolWithTag(NonPagedPool, sizeof(KAPC), TAG('P', 's', 'a', 'p'));
. . . . . .
/* Initialize and Queue a user mode apc (always!) */
KeInitializeApc(Apc, &Thread->Tcb, OriginalApcEnvironment,
KiFreeApcRoutine, NULL, ApcRoutine, UserMode, NormalContext);
if (!KeInsertQueueApc(Apc, SystemArgument1, SystemArgument2,
IO_NO_INCREMENT))
{
Status = STATUS_UNSUCCESSFUL;
} else {
Status = STATUS_SUCCESS;
}
/* Dereference Thread and Return */
ObDereferenceObject(Thread);
return Status;
}
先看调用参数。第一个参数是表明着某个已打开线程的Handle,这说明所要求的APC函数的执行者、即目标线程、能够是另外一个线程,而没必要是请求者线程自己。第二个参数不言自明。第三个参数NormalContext,以及后面的两个参数,则是准备传递给APC函数的参数,至于怎样解释和使用这几个参数是APC函数的事。看一下前面QueueUserAPC()的代码,就能够知道这里的APC函数是IntCallUserApc(),而准备传给它的参数分别为pfnAPC、dwData、和NULL,前者是真正的目标APC函数指针,后二者是要传给它的参数。
根据Handle找到目标线程的ETHREAD数据结构之后,就为APC函数分配一个KAPC数据结构,并经过KeInitializeApc()加以初始化。
[NtQueueApcThread() > KeInitializeApc()]
VOID
STDCALL
KeInitializeApc(IN PKAPC Apc,
IN PKTHREAD Thread,
IN KAPC_ENVIRONMENT TargetEnvironment,
IN PKKERNEL_ROUTINE KernelRoutine,
IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
IN PKNORMAL_ROUTINE NormalRoutine,
IN KPROCESSOR_MODE Mode,
IN PVOID Context)
{
. . . . . .
/* Set up the basic APC Structure Data */
RtlZeroMemory(Apc, sizeof(KAPC));
Apc->Type = ApcObject;
Apc->Size = sizeof(KAPC);
/* Set the Environment */
if (TargetEnvironment == CurrentApcEnvironment) {
Apc->ApcStateIndex = Thread->ApcStateIndex;
} else {
Apc->ApcStateIndex = TargetEnvironment;
}
/* Set the Thread and Routines */
Apc->Thread = Thread;
Apc->KernelRoutine = KernelRoutine;
Apc->RundownRoutine = RundownRoutine;
Apc->NormalRoutine = NormalRoutine;
/* Check if this is a Special APC, in which case we use KernelMode and no Context */
if (ARGUMENT_PRESENT(NormalRoutine)) {
Apc->ApcMode = Mode;
Apc->NormalContext = Context;
} else {
Apc->ApcMode = KernelMode;
}
}
这段代码自己很简单,可是有几个问题须要结合前面NtQueueApcThread()的代码再做些说明。
首先,从NtQueueApcThread()传下来的KernelRoutine是KiFreeApcRoutine(),顾名思义这是在为未来释放PKAPC数据结构作好准备,而RundownRoutine是NULL。
其次,参数TargetEnvironment说明要求挂入哪种环境下的APC队列。实际传下来的值是OriginalApcEnvironment,表示是针对原始环境、即当前线程所属(而不是所挂靠)进程的。注意代码中所设置的是Apc->ApcStateIndex、即PKAPC数据结构中的ApcStateIndex字段,而不是KTHREAD结构中的ApcStateIndex字段。另外一方面,ApcStateIndex的值只能是OriginalApcEnvironment或AttachedApcEnvironment,若是所要求的是CurrentApcEnvironment就要从Thread->ApcStateIndex获取当前的环境值。
最后,APC请求的模式Mode是UserMode。可是有个例外,那就是:若是指针NormalRoutine为0,那么实际的模式变成了KernelMode。这是由于在这种状况下没有用户空间APC函数能够执行,惟一将获得执行的是KernelRoutine,在这里是KiFreeApcRoutine()。这里的宏操做ARGUMENT_PRESENT定义为:
#define ARGUMENT_PRESENT(ArgumentPointer) \
((BOOLEAN) ((PVOID)ArgumentPointer != (PVOID)NULL))
回到NtQueueApcThread()代码中,下一步就是根据Apc->ApcStateIndex、Apc->Thread、和Apc->ApcMode把准备好的KAPC结构挂入相应的队列。根据APC请求的具体状况,有时候要插在队列的前头,通常则挂在队列的尾部。限于篇幅,咱们在这里就不看KeInsertQueueApc()的代码了;虽然这段代码中有一些特殊的处理,但都不是咱们此刻所特别关心的。
若是跟Linux的Signal机制做一类比,那么NtQueueApcThread()至关于设置Signal处理函数(或中断服务程序)。在Linux里面,Signal处理函数的执行须要受到某种触发,例如收到了别的线程或某个内核成分发来的信号;而执行Signal处理函数的时机则是在CPU从内核返回目标线程的用户空间程序的前夕。但是Windows的APC机制与此有所不一样,通常来讲,只要把APC请求挂入了队列,就再也不须要触发,而只是等待执行的时机。对于用户APC请求,这时机一样也是在CPU从内核返回目标线程用户空间程序的前夕(对于内核APC则有所不一样)。因此,在某种意义上,把一个APC请求挂入队列,就同时意味着受到了触发。对于系统调用NtQueueApcThread(),咱们能够理解为是把APC函数的设置与触发合在了一块儿。而对于异步的文件读写,则APC函数的设置与触发是分开的,内核先把APC函数记录在别的数据结构中,等实际的文件读写完成之后才把APC请求挂入队列,此时实际上只是触发其运行。不过那已经是属于设备驱动框架的事了。因此,一旦把APC请求挂入队列,就只是等待执行时机的问题了。从这个意义上说,“异步过程调用”还真不失为贴切的称呼。
下面就来看执行APC的时机,那是在(系统调用、中断、或异常处理以后)从内核返回用户空间的途中。
_KiServiceExit:
/* Get the Current Thread */
cli
movl %fs:KPCR_CURRENT_THREAD, %esi
/* Deliver APCs only if we were called from user mode */
testb $1, KTRAP_FRAME_CS(%esp)
je KiRosTrapReturn
/* And only if any are actually pending */
cmpb $0, KTHREAD_PENDING_USER_APC(%esi)
je KiRosTrapReturn
/* Save pointer to Trap Frame */
movl %esp, %ebx
/* Raise IRQL to APC_LEVEL */
movl $1, %ecx
call @KfRaiseIrql@4
/* Save old IRQL */
pushl %eax
/* Deliver APCs */
sti
pushl %ebx
pushl $0
pushl $UserMode
call _KiDeliverApc@12
cli
/* Return to old IRQL */
popl %ecx
call @KfLowerIrql@4
. . . . . .
这是内核中处理系统调用返回和中断/异常返回的代码。在返回前夕,这里先经过%fs:KPCR_CURRENT_THREAD取得指向当前线程的ETHREAD(从而KTHREAD)的指针,而后依次检查:
l 即将返回的是否用户空间。
l 是否有用户APC请求正在等待执行(KTHREAD_PENDING_USER_APC是ApcState.KernelApcPending在KTHREAD数据结构中的位移)。
要是经过了这两项检查,执行针对当前线程的APC请求的时机就到了,因而就调用KiDeliverApc()去“投递”APC函数,这跟Linux中对Signal的处理又是十分类似的。注意在调用这个函数的先后还分别调用了KfRaiseIrql()和KfLowerIrql(),这是为了在执行KiDeliverApc()期间让内核的“中断请求级别”处于APC_LEVEL,执行完之后再予恢复。咱们如今暂时不关心“中断请求级别”,之后会回到这个问题上。
前面讲过,KTHREAD中有两个KAPC_STATE数据结构,一个是ApcState,另外一个是SavedApcState,两者都有APC队列,可是要投递的只是ApcState中的队列。
注意在call指令前面压入堆栈的三个参数,特别是首先压入堆栈的%ebx,它指向(系统空间)堆栈上的“中断现场”、或称“框架”,即CPU进入本次中断或系统调用时各寄存器的值,这就是下面KiDeliverApc()的调用参数TrapFrame。
下面咱们看KiDeliverApc()的代码。
[KiDeliverApc()]
VOID
STDCALL
KiDeliverApc(KPROCESSOR_MODE DeliveryMode,
PVOID Reserved,
PKTRAP_FRAME TrapFrame)
{
PKTHREAD Thread = KeGetCurrentThread();
. . . . . .
ASSERT_IRQL_EQUAL(APC_LEVEL);
/* Lock the APC Queue and Raise IRQL to Synch */
KeAcquireSpinLock(&Thread->ApcQueueLock, &OldIrql);
/* Clear APC Pending */
Thread->ApcState.KernelApcPending = FALSE;
/* Do the Kernel APCs first */
while (!IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode])) {
/* Get the next Entry */
ApcListEntry = Thread->ApcState.ApcListHead[KernelMode].Flink;
Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);
/* Save Parameters so that it's safe to free the Object in Kernel Routine*/
NormalRoutine = Apc->NormalRoutine;
KernelRoutine = Apc->KernelRoutine;
NormalContext = Apc->NormalContext;
SystemArgument1 = Apc->SystemArgument1;
SystemArgument2 = Apc->SystemArgument2;
/* Special APC */
if (NormalRoutine == NULL) {
/* Remove the APC from the list */
Apc->Inserted = FALSE;
RemoveEntryList(ApcListEntry);
/* Go back to APC_LEVEL */
KeReleaseSpinLock(&Thread->ApcQueueLock, OldIrql);
/* Call the Special APC */
DPRINT("Delivering a Special APC: %x\n", Apc);
KernelRoutine(Apc, &NormalRoutine, &NormalContext,
&SystemArgument1, &SystemArgument2);
/* Raise IRQL and Lock again */
KeAcquireSpinLock(&Thread->ApcQueueLock, &OldIrql);
} else {
/* Normal Kernel APC */
if (Thread->ApcState.KernelApcInProgress || Thread->KernelApcDisable)
{
/*
* DeliveryMode must be KernelMode in this case, since one may not
* return to umode while being inside a critical section or while
* a regular kmode apc is running (the latter should be impossible btw).
* -Gunnar
*/
ASSERT(DeliveryMode == KernelMode);
KeReleaseSpinLock(&Thread->ApcQueueLock, OldIrql);
return;
}
/* Dequeue the APC */
RemoveEntryList(ApcListEntry);
Apc->Inserted = FALSE;
/* Go back to APC_LEVEL */
KeReleaseSpinLock(&Thread->ApcQueueLock, OldIrql);
/* Call the Kernel APC */
DPRINT("Delivering a Normal APC: %x\n", Apc);
KernelRoutine(Apc,
&NormalRoutine,
&NormalContext,
&SystemArgument1,
&SystemArgument2);
/* If There still is a Normal Routine, then we need to call this at PASSIVE_LEVEL */
if (NormalRoutine != NULL) {
/* At Passive Level, this APC can be prempted by a Special APC */
Thread->ApcState.KernelApcInProgress = TRUE;
KeLowerIrql(PASSIVE_LEVEL);
/* Call and Raise IRQ back to APC_LEVEL */
DPRINT("Calling the Normal Routine for a Normal APC: %x\n", Apc);
NormalRoutine(&NormalContext, &SystemArgument1, &SystemArgument2);
KeRaiseIrql(APC_LEVEL, &OldIrql);
}
/* Raise IRQL and Lock again */
KeAcquireSpinLock(&Thread->ApcQueueLock, &OldIrql);
Thread->ApcState.KernelApcInProgress = FALSE;
}
} //end while
参数DeliveryMode表示须要“投递”哪种APC,能够是UserMode,也能够是KernelMode。不过,KernelMode确实表示只要求执行内核APC,而UserMode却表示在执行内核APC以外再执行用户APC。这里所谓“执行内核APC”是执行内核APC队列中的全部请求,而“执行用户APC”却只是执行用户APC队列中的一项。
因此首先检查内核模式APC队列,只要非空就经过一个while循环处理其全部的APC请求。队列中的每一项(若是队列非空的话)、即每个APC请求都是KAPC结构,结构中有三个函数指针,可是这里只涉及其中的两个。一个是NormalRoutine,若为非0就是指向一个实质性的内核APC函数。另外一个是KernelRoutine,指向一个辅助性的内核APC函数,这个指针不会是0,不然这个KAPC结构就不会在队列中了(注意KernelRoutine与内核模式NormalRoutine的区别)。NormalRoutine为0是一种特殊的状况,在这种状况下KernelRoutine所指的内核函数无条件地获得调用。可是,若是NormalRoutine非0,那么首先获得调用的是KernelRoutine,而指针NormalRoutine的地址是做为参数传下去的。KernelRoutine的执行有可能改变这个指针的值。这样,若是执行KernelRoutine之后NormalRoutine仍为非0,那就说明须要加以执行,因此经过这个函数指针予以调用。不过,内核APC函数的执行是在PASSIVE_LEVEL级别上执行的,因此对NormalRoutine的调用前有KeLowerIrql()、后有KeRaiseIrql(),前者将CPU的运行级别调整为PASSIVE_LEVEL,后者则将其恢复为APC_LEVEL。
执行完内核APC队列中的全部请求之后,若是调用参数DeliveryMode为UserMode的话,就轮到用户APC了。咱们继续往下看:
[KiDeliverApc()]
/* Now we do the User APCs */
if ((!IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])) &&
(DeliveryMode == UserMode) && (Thread->ApcState.UserApcPending == TRUE)) {
/* It's not pending anymore */
Thread->ApcState.UserApcPending = FALSE;
/* Get the APC Object */
ApcListEntry = Thread->ApcState.ApcListHead[UserMode].Flink;
Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);
/* Save Parameters so that it's safe to free the Object in Kernel Routine*/
NormalRoutine = Apc->NormalRoutine;
KernelRoutine = Apc->KernelRoutine;
NormalContext = Apc->NormalContext;
SystemArgument1 = Apc->SystemArgument1;
SystemArgument2 = Apc->SystemArgument2;
/* Remove the APC from Queue, restore IRQL and call the APC */
RemoveEntryList(ApcListEntry);
Apc->Inserted = FALSE;
KeReleaseSpinLock(&Thread->ApcQueueLock, OldIrql);
DPRINT("Calling the Kernel Routine for for a User APC: %x\n", Apc);
KernelRoutine(Apc,
&NormalRoutine,
&NormalContext,
&SystemArgument1,
&SystemArgument2);
if (NormalRoutine == NULL) {
/* Check if more User APCs are Pending */
KeTestAlertThread(UserMode);
}else {
/* Set up the Trap Frame and prepare for Execution in NTDLL.DLL */
DPRINT("Delivering a User APC: %x\n", Apc);
KiInitializeUserApc(Reserved,
TrapFrame,
NormalRoutine,
NormalContext,
SystemArgument1,
SystemArgument2);
}
} else {
/* Go back to APC_LEVEL */
KeReleaseSpinLock(&Thread->ApcQueueLock, OldIrql);
}
}
固然,执行用户APC是有条件的。首先天然是用户APC队列非空,同时调用参数DeliveryMode必须是UserMode;而且ApcState中的UserApcPending为TRUE,表示队列中的请求确实是要求尽快加以执行的。
读者也许已经注意到,比以内核APC队列,对用户APC队列的处理有个显著的不一样,那就是对用户APC队列并非经过一个while循环处理队列中的全部请求,而是每次进入KiDeliverApc()只处理用户APC队列中的第一个请求。一样,这里也是只涉及两个函数指针,即NormalRoutine和KernelRoutine,也是先执行KernelRoutine,而且KernelRoutine能够对指针NormalRoutine做出修正。可是再往下就不一样了。
首先,若是执行完KernelRoutine(所指的函数)之后指针NormalRoutine为0,这里要执行KeTestAlertThread()。这又是跟设备驱动有关的事(Windows术语中的Alert至关于Linux术语中的“唤醒”),咱们在这里暂不关心。
反之,若是指针NormalRoutine仍为非0,那么这里执行的是KiInitializeUserApc(),而不是直接调用NormalRoutine所指的函数,由于NormalRoutine所指的函数是在用户空间,要等CPU回到用户空间才能执行,这里只是为其做好安排和准备。
[KiDeliverApc() > KiInitializeUserApc()]
VOID
STDCALL
KiInitializeUserApc(IN PVOID Reserved,
IN PKTRAP_FRAME TrapFrame,
IN PKNORMAL_ROUTINE NormalRoutine,
IN PVOID NormalContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2)
{
PCONTEXT Context;
PULONG Esp;
. . . . . .
/*
* Save the thread's current context (in other words the registers
* that will be restored when it returns to user mode) so the
* APC dispatcher can restore them later
*/
Context = (PCONTEXT)(((PUCHAR)TrapFrame->Esp) - sizeof(CONTEXT));
RtlZeroMemory(Context, sizeof(CONTEXT));
Context->ContextFlags = CONTEXT_FULL;
Context->SegGs = TrapFrame->Gs;
Context->SegFs = TrapFrame->Fs;
Context->SegEs = TrapFrame->Es;
Context->SegDs = TrapFrame->Ds;
Context->Edi = TrapFrame->Edi;
Context->Esi = TrapFrame->Esi;
Context->Ebx = TrapFrame->Ebx;
Context->Edx = TrapFrame->Edx;
Context->Ecx = TrapFrame->Ecx;
Context->Eax = TrapFrame->Eax;
Context->Ebp = TrapFrame->Ebp;
Context->Eip = TrapFrame->Eip;
Context->SegCs = TrapFrame->Cs;
Context->EFlags = TrapFrame->Eflags;
Context->Esp = TrapFrame->Esp;
Context->SegSs = TrapFrame->Ss;
/*
* Setup the trap frame so the thread will start executing at the
* APC Dispatcher when it returns to user-mode
*/
Esp = (PULONG)(((PUCHAR)TrapFrame->Esp) -
(sizeof(CONTEXT) + (6 * sizeof(ULONG))));
Esp[0] = 0xdeadbeef;
Esp[1] = (ULONG)NormalRoutine;
Esp[2] = (ULONG)NormalContext;
Esp[3] = (ULONG)SystemArgument1;
Esp[4] = (ULONG)SystemArgument2;
Esp[5] = (ULONG)Context;
TrapFrame->Eip = (ULONG)LdrpGetSystemDllApcDispatcher();
TrapFrame->Esp = (ULONG)Esp;
}
这个函数的名字取得很差,很容易让人把它跟前面的KeInitializeApc()相连系,实际上却彻底是两码事。参数TrapFrame是由KiDeliverApc()传下来的一个指针,指向用户空间堆栈上的“中断现场”。这里要作的事情就是在原有现场的基础上“注水”,伪造出一个新的现场,使得CPU返回用户空间时误认为中断(或系统调用)发生于进入APC函数的前夕,从而转向APC函数。
怎么伪造呢?首先使用户空间的堆栈指针Esp下移一个CONTEXT数据结构的大小,外加6个32位整数的位置(注意堆栈是由上向下伸展的)。换言之就是在用户空间堆栈上扩充出一个CONTEXT数据结构和6个32位整数。注意,TrapFrame是在系统空间堆栈上,而TrapFrame->Esp的值是用户空间的堆栈指针,所指向的是用户空间堆栈。因此这里扩充的是用户空间堆栈。这样,原先的用户堆栈下方是CONTEXT数据结构Context,再往下就是那6个32位整数。而后把TrapFrame的内容保存在这个CONTEXT数据结构中,并设置好6个32位整数,那是要做为调用参数传递的。接着就把保存在TrapFrame中的Eip映像改为指向用户空间的一个特殊函数,具体的地址经过LdrpGetSystemDllApcDispatcher()获取。这样,当CPU返回到用户空间时,就会从这个特殊函数“继续”执行。固然,也要调整TrapFrame中的用户空间堆栈指针Esp。
LdrpGetSystemDllApcDispatcher()只是返回一个(内核)全局量SystemDllApcDispatcher的值,这个值是个函数指针,指向ntdll.dll中的一个函数,是在映射ntdll.dll映像时设置好的。
PVOID LdrpGetSystemDllApcDispatcher(VOID)
{
return(SystemDllApcDispatcher);
}
与全局变量SystemDllApcDispatcher类似的函数指针有:
l SystemDllEntryPoint,指向LdrInitializeThunk()。
l SystemDllApcDispatcher,指向KiUserApcDispatcher()。
l SystemDllExceptionDispatcher,指向KiUserExceptionDispatcher()。
l SystemDllCallbackDispatcher,指向KiUserCallbackDispatcher()。
l SystemDllRaiseExceptionDispatcher,指向KiRaiseUserExceptionDispatcher()。
这些指针都是在LdrpMapSystemDll()中获得设置的。给定一个函数名的字符串,就能够经过一个函数LdrGetProcedureAddress()从(已经映射的)DLL映像中获取这个函数的地址(若是这个函数被引出的话)。
因而,CPU从KiDeliverApc()回到_KiServiceExit之后会继续完成其返回用户空间的行程,只是一到用户空间就栽进了圈套,那就是KiUserApcDispatcher(),而不是回到原先的断点上。关于原先断点的现场信息保存在用户空间堆栈上、并造成一个CONTEXT数据结构,可是“深埋”在6个32位整数的后面。而这6个32位整数的做用则为:
l Esp[0]的值为0xdeadbeef,用来模拟KiUserApcDispatcher()的返回地址。固然,这个地址是无效的,因此KiUserApcDispatcher()其实是不会返回的。
l Esp[1]的值为NormalRoutine,在咱们这个情景中指向“门户”函数IntCallUserApc()。
l Esp[2]的值为NormalContext,在咱们这个情景中是指向实际APC函数的指针。
l 余类推。其中Esp[5]指向(用户)堆栈上的CONTEXT数据结构。
总之,用户堆栈上的这6个32位整数模拟了一次CPU在进入KiUserApcDispatcher()尚未来得及执行其第一条指令以前就发生了中断的假象,使得CPU在结束了KiDeliverApc()的执行、回到_KiServiceExit中继续前行、并最终回到用户空间时就进入KiUserApcDispatcher()执行其第一条指令。
另外一方面,对于该线程原来的上下文而言,则又好像是刚回到用户空间就发生了中断,而KiUserApcDispatcher()则至关于中断相应程序。
VOID STDCALL
KiUserApcDispatcher(PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext,
PIO_STATUS_BLOCK Iosb, ULONG Reserved, PCONTEXT Context)
{
/* Call the APC */
ApcRoutine(ApcContext, Iosb, Reserved);
/* Switch back to the interrupted context */
NtContinue(Context, 1);
}
这里的第一个参数ApcRoutine指向IntCallUserApc(),第二个参数ApcContext指向真正的(目标)APC函数。
[KiUserApcDispatcher() > IntCallUserApc()]
static void CALLBACK
IntCallUserApc(PVOID Function, PVOID dwData, PVOID Argument3)
{
PAPCFUNC pfnAPC = (PAPCFUNC)Function;
pfnAPC((ULONG_PTR)dwData);
}
可见,IntCallUserApc()其实并没有必要,在KiUserApcDispatcher()中直接调用目标APC函数也无不可,这样作只是为未来可能的修改扩充提供一些方便和灵活性。从IntCallUserApc()回到KiUserApcDispatcher(),下面紧接着是系统调用NtContinue()。
KiUserApcDispatcher()是不返回的。它之因此不返回,是由于对NtContinue()的调用不返回。正如代码中的注释所述,NtContinue()的做用是切换回被中断了的上下文,不过其实还不止于此,下面读者就会看到它还起着循环执行整个用户APC请求队列的做用。
[KiUserApcDispatcher() > NtContinue()]
NTSTATUS STDCALL
NtContinue (IN PCONTEXT Context, IN BOOLEAN TestAlert)
{
PKTHREAD Thread = KeGetCurrentThread();
PKTRAP_FRAME TrapFrame = Thread->TrapFrame;
PKTRAP_FRAME PrevTrapFrame = (PKTRAP_FRAME)TrapFrame->Edx;
PFX_SAVE_AREA FxSaveArea;
KIRQL oldIrql;
DPRINT("NtContinue: Context: Eip=0x%x, Esp=0x%x\n", Context->Eip, Context->Esp );
PULONG Frame = 0;
__asm__("mov %%ebp, %%ebx" : "=b" (Frame) : );
. . . . . .
/*
* Copy the supplied context over the register information that was saved
* on entry to kernel mode, it will then be restored on exit
* FIXME: Validate the context
*/
KeContextToTrapFrame ( Context, TrapFrame );
/* Put the floating point context into the thread's FX_SAVE_AREA
* and make sure it is reloaded when needed.
*/
FxSaveArea = (PFX_SAVE_AREA)((ULONG_PTR)Thread->InitialStack –
sizeof(FX_SAVE_AREA));
if (KiContextToFxSaveArea(FxSaveArea, Context))
{
Thread->NpxState = NPX_STATE_VALID;
KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);
if (KeGetCurrentPrcb()->NpxThread == Thread)
{
KeGetCurrentPrcb()->NpxThread = NULL;
Ke386SetCr0(Ke386GetCr0() | X86_CR0_TS);
}
else
{
ASSERT((Ke386GetCr0() & X86_CR0_TS) == X86_CR0_TS);
}
KeLowerIrql(oldIrql);
}
/* Restore the user context */
Thread->TrapFrame = PrevTrapFrame;
__asm__("mov %%ebx, %%esp;\n" "jmp _KiServiceExit": : "b" (TrapFrame));
return STATUS_SUCCESS; /* this doesn't actually happen */
}
注意从KiUserApcDispatcher()到NtContinue()并非普通的函数调用,而是系统调用,这中间经历了空间的切换,也从用户空间堆栈切换到了系统空间堆栈。CPU进入系统调用空间后,在_KiSystemServicex下面的代码中把指向中断现场的框架指针保存在当前线程的KTHREAD数据结构的TrapFrame字段中。这样,很容易就能够找到系统空间堆栈上的调用框架。固然,如今的框架是由于系统调用而产生的框架;而要想回到当初、即在执行用户空间APC函数以前的断点,就得先恢复当初的框架。那么当初的框架在哪里呢?它保存在用户空间的堆栈上,就是前面KiInitializeUserApc()保存的CONTEXT数据结构中。因此,这里经过KeContextToTrapFrame()把当初保存的信息拷贝回来,从而恢复了当初的框架。
下面的KiContextToFxSaveArea()等语句与浮点处理器有关,咱们在这里并不关心。
最后,汇编指令“jmp _KiServiceExit”使CPU跳转到了返回用户空间途中的_KiServiceExit处(见前面的代码)。在这里,CPU又会检查APC请求队列中是否有APC请求等着要执行,若是有的话又会进入KiDeliverApc()。前面讲过,每次进入KiDeliverApc()只会执行一个用户APC请求,因此若是用户APC队列的长度大于1的话就得循环着屡次走过上述的路线,即:
1. 从系统调用、中断、或异常返回途径_KiServiceExit,若是APC队列中有等待执行的APC请求,就调用KiDeliverApc()。
2. KiDeliverApc(),从用户APC队列中摘下一个APC请求。
3. 在KiInitializeUserApc()中保存当前框架,并伪造新的框架。
4. 回到用户空间。
5. 在KiUserApcDispatcher()中调用目标APC函数。
6. 经过系统调用NtContinue()进入系统空间。
7. 在NtContinue()中恢复当初保存的框架。
8. 从NtContinue()返回、途径_KiServiceExit时,若是APC队列中还有等待执行的APC请求,就调用KiDeliverApc()。因而转回上面的第二步。
这个过程一直要循环到APC队列中再也不有须要执行的请求。注意这里每一次循环中保存和恢复的都是同一个框架,就是原始的、开始处理APC队列以前的那个框架,表明着原始的用户空间程序断点。一旦APC队列中再也不有等待执行的APC请求,在_KiServiceExit下面就再也不调用KiDeliverApc(),因而就直接返回用户空间,此次是返回到原始的程序断点了。因此,系统调用neContinue()的做用不只仅是切换回到被中断了的上下文,还包括执行用户APC队列中的下一个APC请求。
对于KiUserApcDispatcher()而言,它对NtContinue()的调用是不返回的。由于在NtContinue()中CPU不是“返回”到对于KiUserApcDispatcher()的另外一次调用、从而对另外一个APC函数的调用;就是返回到原始的用户空间程序断点,这个断点既多是由于中断或异常而造成的,也多是由于系统调用而造成的。
理解了常规的APC请求和执行机制,咱们不妨再看看启动执行PE目标映像时函数的动态链接。之前讲过,PE格式EXE映像与(除ntdll.dll外的)DLL的动态链接、包括这些DLL的装入,是由ntdll.dll中的一个函数LdrInitializeThunk()做为APC函数执行而完成的,因此这也是对APC机制的一种变通使用。
要启动一个EXE映像运行时,首先要建立进程,再把目标EXE映像和ntdll.dll的映像都映射到新进程的用户空间,而后经过系统调用NtCreateThread()建立这个进程的第一个线程、或称“主线程”。而LdrInitializeThunk()做为APC函数的执行,就是在NtCreateThread()中安排好的。
NtCreateThread(OUT PHANDLE ThreadHandle, IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle, OUT PCLIENT_ID ClientId,
IN PCONTEXT ThreadContext, IN PINITIAL_TEB InitialTeb,
IN BOOLEAN CreateSuspended)
{
HANDLE hThread;
. . . . . .
. . . . . .
/*
* Queue an APC to the thread that will execute the ntdll startup
* routine.
*/
LdrInitApc = ExAllocatePool(NonPagedPool, sizeof(KAPC));
KeInitializeApc(LdrInitApc, &Thread->Tcb, OriginalApcEnvironment,
LdrInitApcKernelRoutine,
LdrInitApcRundownRoutine,
LdrpGetSystemDllEntryPoint(), UserMode, NULL);
KeInsertQueueApc(LdrInitApc, NULL, NULL, IO_NO_INCREMENT);
/*
* The thread is non-alertable, so the APC we added did not set UserApcPending to TRUE.
* We must do this manually. Do NOT attempt to set the Thread to Alertable before the call,
* doing so is a blatant and erronous hack.
*/
Thread->Tcb.ApcState.UserApcPending = TRUE;
Thread->Tcb.Alerted[KernelMode] = TRUE;
. . . . . .
. . . . . .
}
NeCreateThread()要作的事固然不少,可是其中很重要的一项就是安排好APC函数的执行。这里的KeInitializeApc()和KeInsertQueueApc读者都已经熟悉了,因此咱们只关心调用参数中的三个函数指针,特别是其中的KernelRoutine和NormalRoutine。前者十分简单:
VOID STDCALL
LdrInitApcKernelRoutine(PKAPC Apc, PKNORMAL_ROUTINE* NormalRoutine,
PVOID* NormalContext, PVOID* SystemArgument1, PVOID* SystemArgument2)
{
ExFreePool(Apc);
}
而NormalRoutine,这里是经过LdrpGetSystemDllEntryPoint()获取的,它只是返回全局量SystemDllEntryPoint的值:
PVOID LdrpGetSystemDllEntryPoint(VOID)
{
return(SystemDllEntryPoint);
}
前面已经讲到,全局量SystemDllEntryPoint是在LdrpMapSystemDll()时获得设置的,指向已经映射到用户空间的ntdll.dll映像中的LdrInitializeThunk()。注意这APC请求是挂在新线程的队列中,而不是当前进程的队列中。事实上,新线程和当前进程处于不一样的进程,于是不在同一个用户空间中。还要注意,这里的NormalRoutine直接就是LdrInitializeThunk(),而不像前面经过QueueUserAPC()发出的APC请求那样中间还有一层IntCallUserApc()。至于KiUserApcDispatcher(),那是由KeInitializeApc()强制加上的,正是这个函数保证了对NtContinue()的调用。
此后的流程原本无需细说了,可是因为情景的特殊性仍是须要加一些简要的说明。由NtCreateProcess()建立的进程并不是一个能够调度运行的实体,而NtCreateThread()建立的线程倒是。因此,在NtCreateProcess()返回的前夕,系统中已经多了一个线程。这个新增线程的“框架”是伪造的,目的在于让这个线程一开始在用户空间运行就进入预约的程序入口。从NtCreateProcess()返回是回到当前线程、而不是新增线程,而刚才的APC请求是挂在新增线程的队列中,因此在从NtCreateThread()返回的途中不会去执行这个APC请求。但是,当新增线程受调度运行时,首先就是按伪造的框架和堆栈模拟一个从系统调用返回的过程,因此也要途径_KiServiceExit。这时候,这个APC请求就要获得执行了(由KiUserApcDispatcher()调用LdrInitializeThunk())。而后,在用户空间执行完APC函数LdrInitializeThunk()之后,一样也是经过NtContinue()回到内核中,而后又按原先的伪造框架“返回”到用户空间,这才真正开始了新线程在用户空间的执行。
最后,咱们不妨比较一下APC机制和Unix/Linux的Signal机制。
Unix/Linux的Signal机制基本上是对硬件中断机制的软件模拟,具体表如今如下几个方面:
1) 现代的硬件中断机制通常都是“向量中断”机制,而Signal机制中的Signal序号(例如SIG_KILL)就是对中断向量序号的模拟。
2) 做为操做系统对硬件中断机制的支持,通常都会提供“设置中断向量”一类的内核函数,使特定序号的中断向量指向某个中断服务程序。而系统调用signal()就至关因而这一类的函数。只不过前者在内核中、通常只是供其它内核函数调用,然后者是系统调用、供用户空间的程序调用。
3) 在硬件中断机制中,“中断向量”的设置只是为某类异步事件、及中断的发生作好了准备,可是并不意味着某个特定时间的发生。若是一直没有中断请求,那么所设置的中断向量就一直得不到执行,而中断的发生只是触发了中断服务程序的执行。在Signal机制中,向某个进程发出“信号”、即Signal、就至关于中断请求。
相比之下,APC机制就不能说是对于硬件中断机制的模拟了。首先,经过NtQueueApcThread()设置一个APC函数跟经过signal()设置一个“中断向量”有所不一样。将一个APC函数挂入APC队列中时,对于这个函数的获得执行、以及大约在何时获得执行,其实是预知的,只是这获得执行的条件要过一回儿才会成熟。而“中断”则不一样,中断向量的设置只是说若是发生某种中断则如何如何,可是对于其到底是否会发生、什么时候发生则经常是没法预测的。因此,从这个意义上说,APC函数只是一种推迟执行、异步执行的函数调用,所以称之为“异步过程调用”确实更为贴切。
还有,signal机制的signal()所设置的“中断服务程序”都是用户空间的程序,而APC机制中挂入APC队列的函数却能够是内核函数。
可是,尽管如此,它们的(某些方面的)实质仍是同样的。“中断”原本就是一种异步执行的机制。再说,(用户)APC与Signal的执行流程几乎彻底同样,都是在从内核返回用户空间的前夕检查是否有这样的函数须要加以执行,若是是就临时修改堆栈,偏离原来的执行路线,使得返回用户空间后进入APC函数,而且在执行完了这个函数之后仍进入内核,而后恢复原来的堆栈,再次返回用户空间原来的断点。这样,对于原来的流程而言,就至关于受到了中断。