轉載: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函數,而且在執行完了這個函數之後仍進入內核,而後恢復原來的堆棧,再次返回用戶空間原來的斷點。這樣,對於原來的流程而言,就至關於受到了中斷。