Windows APC機(jī)制zzz
APC與系統(tǒng)調(diào)用是密切連系在一起的,在這個意義上APC是系統(tǒng)調(diào)用界面的一部分。然而APC又與設(shè)備驅(qū)動有著很密切的關(guān)系。例如,ntddk.h中提供“寫文件”系統(tǒng)調(diào)用ZwWriteFile()、即NtWriteFile()的調(diào)用界面為:
CODE:
NTSYSAPI
NTSTATUS
NTAPI
ZwWriteFile(
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
);
這里有個參數(shù)ApcRoutine,這是一個函數(shù)指針。什么時候要用到這個指針呢?原來,文件操作有“同步”和“異步”之分。普通的寫文件操作是同步寫,啟動這種操作的線程在內(nèi)核進(jìn)行寫文件操作期間被“阻塞(blocked)”而進(jìn)入“睡眠”,直到設(shè)備驅(qū)動完成了操作以后才又將該線程“喚醒”而從系統(tǒng)調(diào)用返回。但是,如果目標(biāo)文件是按異步操作打開的,即在通過W32的API函數(shù)CreateFile()打開目標(biāo)文件時把調(diào)用參數(shù) dwFlagsAndAttributes設(shè)置成FILE_FLAG_OVERLAPPED,那么調(diào)用者就不會被阻塞,而是把事情交給內(nèi)核、不等實際的操作完成就返回了。但是此時要把ApcRoutine設(shè)置成指向某個APC函數(shù)。這樣,當(dāng)設(shè)備驅(qū)動完成實際的操作時,就會使調(diào)用者線程執(zhí)行這個APC函數(shù),就像是發(fā)生了一次中斷。執(zhí)行該APC函數(shù)時的調(diào)用界面為:
CODE:
typedef
VOID
(NTAPI *PIO_APC_ROUTINE) (IN PVOID ApcContext,
IN PIO_STATUS_BLOCK IoStatusBlock, IN ULONG Reserved);
這里的指針ApcContext就是NtWriteFile()調(diào)用界面上傳下來的,至于作什么解釋、起什么作用,那是包括APC函數(shù)在內(nèi)的用戶軟件自己的事,內(nèi)核只是把它傳遞給APC函數(shù)。
在這個過程中,把ApcRoutine設(shè)置成指向APC函數(shù)相當(dāng)于登記了一個中斷服務(wù)程序,而設(shè)備驅(qū)動在完成實際的文件操作后就向調(diào)用者線程發(fā)出相當(dāng)于中斷請求的“APC請求”,使其執(zhí)行這個APC函數(shù)。
從這個角度說,APC機(jī)制又應(yīng)該說是設(shè)備驅(qū)動框架的一部分。事實上,讀者以后還會看到,APC機(jī)制與設(shè)備驅(qū)動的關(guān)系比這里所見的還要更加密切。此外,APC機(jī)制與異常處理的關(guān)系也很密切。
不僅內(nèi)核可以向一個線程發(fā)出APC請求,別的線程、乃至目標(biāo)線程自身也可以發(fā)出這樣的請求。Windows為應(yīng)用程序提供了一個函數(shù)QueueUserAPC(),就是用于此項目的,下面是ReactOS中這個函數(shù)的代碼:
CODE:
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);
}
參數(shù)pfnAPC是函數(shù)指針,這就是APC函數(shù)。另一個參數(shù)hThread是指向目標(biāo)線程對象(已打開)的Handle,這可以是當(dāng)前線程本身,也可以是同一進(jìn)程中別的線程,還可以是別的進(jìn)程中的某個線程。值得注意的是:如果目標(biāo)線程在另一個進(jìn)程中,那么pfnAPC必須是這個函數(shù)在目標(biāo)線程所在用戶空間的地址,而不是這個函數(shù)在本線程所在空間的地址。最后一個參數(shù)dwData則是需要傳遞給APC函數(shù)的參數(shù)。
這里的NtQueueApcThread()是個系統(tǒng)調(diào)用。“Native API”書中有關(guān)于NtQueueApcThread()的一些說明。這個系統(tǒng)調(diào)用把一個“用戶APC請求”掛入目標(biāo)線程的APC隊列(更確切地說,是把一個帶有函數(shù)指針的數(shù)據(jù)結(jié)構(gòu)掛入隊列)。注意其第二個參數(shù)是需要執(zhí)行的APC函數(shù)指針,本該是pfnAPC,這里卻換成了函數(shù) IntCallUserApc(),而pfnAPC倒變成了第三個參數(shù),成了需要傳遞給IntCallUserApc()的參數(shù)之一。 IntCallUserApc()是kernel32.dll內(nèi)部的一個函數(shù),但是并未引出,所以不能從外部直接加以調(diào)用。
APC是針對具體線程、要求由具體線程加以執(zhí)行的,所以每個線程都有自己的APC隊列。內(nèi)核中代表著線程的數(shù)據(jù)結(jié)構(gòu)是ETHREAD,而ETHREAD中的第一個成分Tcb是KTHREAD數(shù)據(jù)結(jié)構(gòu),線程的APC隊列就在KTHREAD里面:
CODE:
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 并不公開這個數(shù)據(jù)結(jié)構(gòu)的定義,所以ReactOS代碼中對這個數(shù)據(jù)結(jié)構(gòu)的定義帶有逆向工程的痕跡,每一行后面的十六進(jìn)制數(shù)值就是相應(yīng)結(jié)構(gòu)成分在數(shù)據(jù)結(jié)構(gòu)中的位移。這里我們最關(guān)心的是ApcState,這又是一個數(shù)據(jù)結(jié)構(gòu)、即KAPC_STATE。可以看出,KAPC_STATE的大小是0x18字節(jié)。其定義如下:
CODE:
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的數(shù)組,說明實際上(每個線程)有兩個APC隊列。這是因為APC函數(shù)分為用戶 APC和內(nèi)核APC兩種,各有各的隊列。所謂用戶APC,是指相應(yīng)的APC函數(shù)位于用戶空間、在用戶空間執(zhí)行;而內(nèi)核APC,則相應(yīng)的APC函數(shù)為內(nèi)核函數(shù)。
讀者也許已經(jīng)注意到,KTHREAD結(jié)構(gòu)中除ApcState外還有SavedApcState也是KAPC_STATE數(shù)據(jù)結(jié)構(gòu)。此外還有 ApcStatePointer[2]和ApcStateIndex兩個結(jié)構(gòu)成分。這是干什么用的呢?原來,在Windows的內(nèi)核中,一個線程可以暫時 “掛靠(Attach)”到另一個進(jìn)程的地址空間。比方說,線程T本來是屬于進(jìn)程A的,當(dāng)這個線程在內(nèi)核中運行時,如果其活動與用戶空間有關(guān)(APC就是與用戶空間有關(guān)),那么當(dāng)時的用戶空間應(yīng)該就是進(jìn)程A的用戶空間。但是Windows內(nèi)核允許一些跨進(jìn)程的操作(例如將ntdll.dll的映像裝入新創(chuàng)進(jìn)程B的用戶空間并對其進(jìn)行操作),所以有時候需要把當(dāng)時的用戶空間切換到別的進(jìn)程(例如B) 的用戶空間,這就稱為“掛靠(Attach)”,對此我將另行撰文介紹。在當(dāng)前線程掛靠在另一個進(jìn)程的期間,既然用戶空間是別的進(jìn)程的用戶空間,掛在隊列中的APC請求就變成“牛頭不對馬嘴”了,所以此時要把這些隊列轉(zhuǎn)移到別的地方,以免亂套,然后在回到原進(jìn)程的用戶空間時再于恢復(fù)。那么轉(zhuǎn)移到什么地方呢?就是SavedApcState。當(dāng)然,還要有狀態(tài)信息說明本線程當(dāng)前是處于“原始環(huán)境”還是“掛靠環(huán)境”,這就是ApcStateIndex的作用。代碼中為SavedApcState的值定義了一種枚舉類型:
CODE:
typedef enum _KAPC_ENVIRONMENT
{
OriginalApcEnvironment,
AttachedApcEnvironment,
CurrentApcEnvironment
} KAPC_ENVIRONMENT;
實際可用于ApcStateIndex的只是OriginalApcEnvironment和AttachedApcEnvironment,即0和1。讀者也許又要問,在掛靠環(huán)境下原來的APC隊列確實不適用了,但不去用它就是,何必要把它轉(zhuǎn)移呢?再說,APC隊列轉(zhuǎn)移以后,ApcState不是空下來不用了嗎?問題在于,在掛靠環(huán)境下也可能會有(針對所掛靠進(jìn)程的)APC請求(不過當(dāng)然不是來自用戶空間),所以需要有用于兩種不同環(huán)境的APC隊列,于是便有了ApcState和SavedApcState。進(jìn)一步,為了提供操作上的靈活性,又增加了一個KAPC_STATE指針數(shù)組 ApcStatePointer[2],就用ApcStateIndex的當(dāng)前值作為下標(biāo),而數(shù)組中的指針則根據(jù)情況可以分別指向兩個APC_STATE 數(shù)據(jù)結(jié)構(gòu)中的一個。
這樣,以ApcStateIndex的當(dāng)前數(shù)值為下標(biāo),從指針數(shù)組ApcStatePointer[2]中就可以得到指向ApcState或 SavedApcState的指針,而要求把一個APC請求掛入隊列時則可以指定是要掛入哪一個環(huán)境的隊列。實際上,當(dāng)ApcStateIndex的值為 OriginalApcEnvironment、即0時,使用的是ApcState;為AttachedApcEnvironment、即1時,則用的是 SavedApcState。
每當(dāng)要求掛入一個APC函數(shù)時,不管是用戶APC還是內(nèi)核APC,內(nèi)核都要為之準(zhǔn)備好一個KAPC數(shù)據(jù)結(jié)構(gòu),并將其掛入相應(yīng)的隊列。
CODE:
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;
結(jié)構(gòu)中的ApcListEntry就是用來將KAPC結(jié)構(gòu)掛入隊列的。注意這個數(shù)據(jù)結(jié)構(gòu)中有三個函數(shù)指針,即KernelRoutine、 RundownRoutine、NormalRoutine。其中只有NormalRoutine才指向(執(zhí)行)APC函數(shù)的請求者所提供的函數(shù),其余兩個都是輔助性的。以NtQueueApcThread()為例,其請求者(調(diào)用者)QueueUserAPC()所提供的函數(shù)是 IntCallUserApc(),所以NormalRoutine應(yīng)該指向這個函數(shù)。注意真正的請求者其實是QueueUserAPC()的調(diào)用者,真正的目標(biāo)APC函數(shù)也并非IntCallUserApc(),而是前面的函數(shù)指針pfnAPC所指向的函數(shù),而IntCallUserApc()起著類似于“門戶”的作用。
現(xiàn)在我們可以往下看系統(tǒng)調(diào)用NtQueueApcThread()的實現(xiàn)了。
CODE:
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;
}
先看調(diào)用參數(shù)。第一個參數(shù)是代表著某個已打開線程的Handle,這說明所要求的APC函數(shù)的執(zhí)行者、即目標(biāo)線程、可以是另一個線程,而不必是請求者線程本身。第二個參數(shù)不言自明。第三個參數(shù)NormalContext,以及后面的兩個參數(shù),則是準(zhǔn)備傳遞給APC函數(shù)的參數(shù),至于怎樣解釋和使用這幾個參數(shù)是 APC函數(shù)的事。看一下前面QueueUserAPC()的代碼,就可以知道這里的APC函數(shù)是IntCallUserApc(),而準(zhǔn)備傳給它的參數(shù)分別為pfnAPC、dwData、和NULL,前者是真正的目標(biāo)APC函數(shù)指針,后兩者是要傳給它的參數(shù)。
根據(jù)Handle找到目標(biāo)線程的ETHREAD數(shù)據(jù)結(jié)構(gòu)以后,就為APC函數(shù)分配一個KAPC數(shù)據(jù)結(jié)構(gòu),并通過KeInitializeApc()加以初始化。
CODE:
[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;
}
}
這段代碼本身很簡單,但是有幾個問題需要結(jié)合前面NtQueueApcThread()的代碼再作些說明。
首先,從NtQueueApcThread()傳下來的KernelRoutine是KiFreeApcRoutine(),顧名思義這是在為將來釋放PKAPC數(shù)據(jù)結(jié)構(gòu)做好準(zhǔn)備,而RundownRoutine是NULL。
其次,參數(shù)TargetEnvironment說明要求掛入哪一種環(huán)境下的APC隊列。實際傳下來的值是OriginalApcEnvironment,表示是針對原始環(huán)境、即當(dāng)前線程所屬(而不是所掛靠)進(jìn)程的。注意代碼中所設(shè)置的是Apc->ApcStateIndex、即PKAPC數(shù)據(jù)結(jié)構(gòu)中的ApcStateIndex字段,而不是KTHREAD結(jié)構(gòu)中的ApcStateIndex字段。另一方面,ApcStateIndex的值只能是 OriginalApcEnvironment或AttachedApcEnvironment,如果所要求的是 CurrentApcEnvironment就要從Thread->ApcStateIndex獲取當(dāng)前的環(huán)境值。
最后,APC請求的模式Mode是UserMode。但是有個例外,那就是:如果指針NormalRoutine為0,那么實際的模式變成了 KernelMode。這是因為在這種情況下沒有用戶空間APC函數(shù)可以執(zhí)行,唯一將得到執(zhí)行的是KernelRoutine,在這里是 KiFreeApcRoutine()。這里的宏操作ARGUMENT_PRESENT定義為:
CODE:
#define ARGUMENT_PRESENT(ArgumentPointer) \
((BOOLEAN) ((PVOID)ArgumentPointer != (PVOID)NULL))
回到NtQueueApcThread()代碼中,下一步就是根據(jù)Apc->ApcStateIndex、Apc->Thread、和Apc- >ApcMode把準(zhǔn)備好的KAPC結(jié)構(gòu)掛入相應(yīng)的隊列。根據(jù)APC請求的具體情況,有時候要插在隊列的前頭,一般則掛在隊列的尾部。限于篇幅,我們在這里就不看KeInsertQueueApc()的代碼了;雖然這段代碼中有一些特殊的處理,但都不是我們此刻所特別關(guān)心的。
如果跟Linux的Signal機(jī)制作一類比,那么NtQueueApcThread()相當(dāng)于設(shè)置Signal處理函數(shù)(或中斷服務(wù)程序)。在 Linux里面,Signal處理函數(shù)的執(zhí)行需要受到某種觸發(fā),例如收到了別的線程或某個內(nèi)核成分發(fā)來的信號;而執(zhí)行Signal處理函數(shù)的時機(jī)則是在 CPU從內(nèi)核返回目標(biāo)線程的用戶空間程序的前夕。可是Windows的APC機(jī)制與此有所不同,一般來說,只要把APC請求掛入了隊列,就不再需要觸發(fā),而只是等待執(zhí)行的時機(jī)。對于用戶APC請求,這時機(jī)同樣也是在CPU從內(nèi)核返回目標(biāo)線程用戶空間程序的前夕(對于內(nèi)核APC則有所不同)。所以,在某種意義上,把一個APC請求掛入隊列,就同時意味著受到了觸發(fā)。對于系統(tǒng)調(diào)用NtQueueApcThread(),我們可以理解為是把APC函數(shù)的設(shè)置與觸發(fā)合在了一起。而對于異步的文件讀寫,則APC函數(shù)的設(shè)置與觸發(fā)是分開的,內(nèi)核先把APC函數(shù)記錄在別的數(shù)據(jù)結(jié)構(gòu)中,等實際的文件讀寫完成以后才把APC 請求掛入隊列,此時實際上只是觸發(fā)其運行。不過那已是屬于設(shè)備驅(qū)動框架的事了。所以,一旦把APC請求掛入隊列,就只是等待執(zhí)行時機(jī)的問題了。從這個意義上說,“異步過程調(diào)用”還真不失為貼切的稱呼。
下面就來看執(zhí)行APC的時機(jī),那是在(系統(tǒng)調(diào)用、中斷、或異常處理之后)從內(nèi)核返回用戶空間的途中。
CODE:
_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
. . . . . .
這是內(nèi)核中處理系統(tǒng)調(diào)用返回和中斷/異常返回的代碼。在返回前夕,這里先通過%fs:KPCR_CURRENT_THREAD取得指向當(dāng)前線程的ETHREAD(從而KTHREAD)的指針,然后依次檢查:
● 即將返回的是否用戶空間。
● 是否有用戶APC請求正在等待執(zhí)行(KTHREAD_PENDING_USER_APC是 ApcState.KernelApcPending在KTHREAD數(shù)據(jù)結(jié)構(gòu)中的位移)。
要是通過了這兩項檢查,執(zhí)行針對當(dāng)前線程的APC請求的時機(jī)就到了,于是就調(diào)用KiDeliverApc()去“投遞”APC函數(shù),這跟Linux中對 Signal的處理又是十分相似的。注意在調(diào)用這個函數(shù)的前后還分別調(diào)用了KfRaiseIrql()和KfLowerIrql(),這是為了在執(zhí)行 KiDeliverApc()期間讓內(nèi)核的“中斷請求級別”處于APC_LEVEL,執(zhí)行完以后再予恢復(fù)。我們現(xiàn)在暫時不關(guān)心“中斷請求級別”,以后會回到這個問題上。
前面講過,KTHREAD中有兩個KAPC_STATE數(shù)據(jù)結(jié)構(gòu),一個是ApcState,另一個是SavedApcState,二者都有APC隊列,但是要投遞的只是ApcState中的隊列。
注意在call指令前面壓入堆棧的三個參數(shù),特別是首先壓入堆棧的%ebx,它指向(系統(tǒng)空間)堆棧上的“中斷現(xiàn)場”、或稱“框架”,即CPU進(jìn)入本次中斷或系統(tǒng)調(diào)用時各寄存器的值,這就是下面KiDeliverApc()的調(diào)用參數(shù)TrapFrame。
下面我們看KiDeliverApc()的代碼。
CODE:
[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
參數(shù)DeliveryMode表示需要“投遞”哪一種APC,可以是UserMode,也可以是KernelMode。不過,KernelMode確實表示只要求執(zhí)行內(nèi)核APC,而UserMode卻表示在執(zhí)行內(nèi)核APC之外再執(zhí)行用戶APC。這里所謂“執(zhí)行內(nèi)核APC”是執(zhí)行內(nèi)核APC隊列中的所有請求,而“執(zhí)行用戶APC”卻只是執(zhí)行用戶APC隊列中的一項。
所以首先檢查內(nèi)核模式APC隊列,只要非空就通過一個while循環(huán)處理其所有的APC請求。隊列中的每一項(如果隊列非空的話)、即每一個APC請求都是KAPC結(jié)構(gòu),結(jié)構(gòu)中有三個函數(shù)指針,但是這里只涉及其中的兩個。一個是NormalRoutine,若為非0就是指向一個實質(zhì)性的內(nèi)核APC函數(shù)。另一個是KernelRoutine,指向一個輔助性的內(nèi)核APC函數(shù),這個指針不會是0,否則這個KAPC結(jié)構(gòu)就不會在隊列中了(注意 KernelRoutine與內(nèi)核模式NormalRoutine的區(qū)別)。NormalRoutine為0是一種特殊的情況,在這種情況下 KernelRoutine所指的內(nèi)核函數(shù)無條件地得到調(diào)用。但是,如果NormalRoutine非0,那么首先得到調(diào)用的是 KernelRoutine,而指針NormalRoutine的地址是作為參數(shù)傳下去的。KernelRoutine的執(zhí)行有可能改變這個指針的值。這樣,如果執(zhí)行KernelRoutine以后NormalRoutine仍為非0,那就說明需要加以執(zhí)行,所以通過這個函數(shù)指針予以調(diào)用。不過,內(nèi)核 APC函數(shù)的執(zhí)行是在PASSIVE_LEVEL級別上執(zhí)行的,所以對NormalRoutine的調(diào)用前有KeLowerIrql()、后有 KeRaiseIrql(),前者將CPU的運行級別調(diào)整為PASSIVE_LEVEL,后者則將其恢復(fù)為APC_LEVEL。
執(zhí)行完內(nèi)核APC隊列中的所有請求以后,如果調(diào)用參數(shù)DeliveryMode為UserMode的話,就輪到用戶APC了。我們繼續(xù)往下看:
CODE:
[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);
}
}
當(dāng)然,執(zhí)行用戶APC是有條件的。首先自然是用戶APC隊列非空,同時調(diào)用參數(shù)DeliveryMode必須是UserMode;并且ApcState中的UserApcPending為TRUE,表示隊列中的請求確實是要求盡快加以執(zhí)行的。
讀者也許已經(jīng)注意到,比之內(nèi)核APC隊列,對用戶APC隊列的處理有個顯著的不同,那就是對用戶APC隊列并不是通過一個while循環(huán)處理隊列中的所有請求,而是每次進(jìn)入KiDeliverApc()只處理用戶APC隊列中的第一個請求。同樣,這里也是只涉及兩個函數(shù)指針,即NormalRoutine 和KernelRoutine,也是先執(zhí)行KernelRoutine,并且KernelRoutine可以對指針NormalRoutine作出修正。但是再往下就不同了。
首先,如果執(zhí)行完KernelRoutine(所指的函數(shù))以后指針NormalRoutine為0,這里要執(zhí)行KeTestAlertThread ()。這又是跟設(shè)備驅(qū)動有關(guān)的事(Windows術(shù)語中的Alert相當(dāng)于Linux術(shù)語中的“喚醒”),我們在這里暫不關(guān)心。
反之,如果指針NormalRoutine仍為非0,那么這里執(zhí)行的是KiInitializeUserApc(),而不是直接調(diào)用 NormalRoutine所指的函數(shù),因為NormalRoutine所指的函數(shù)是在用戶空間,要等CPU回到用戶空間才能執(zhí)行,這里只是為其作好安排和準(zhǔn)備。
CODE:
[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;
}
這個函數(shù)的名字取得不好,很容易讓人把它跟前面的KeInitializeApc()相連系,實際上卻完全是兩碼事。參數(shù)TrapFrame是由 KiDeliverApc()傳下來的一個指針,指向用戶空間堆棧上的“中斷現(xiàn)場”。這里要做的事情就是在原有現(xiàn)場的基礎(chǔ)上“注水”,偽造出一個新的現(xiàn)場,使得CPU返回用戶空間時誤認(rèn)為中斷(或系統(tǒng)調(diào)用)發(fā)生于進(jìn)入APC函數(shù)的前夕,從而轉(zhuǎn)向APC函數(shù)。
怎么偽造呢?首先使用戶空間的堆棧指針Esp下移一個CONTEXT數(shù)據(jù)結(jié)構(gòu)的大小,外加6個32位整數(shù)的位置(注意堆棧是由上向下伸展的)。換言之就是在用戶空間堆棧上擴(kuò)充出一個CONTEXT數(shù)據(jù)結(jié)構(gòu)和6個32位整數(shù)。注意,TrapFrame是在系統(tǒng)空間堆棧上,而TrapFrame-> Esp的值是用戶空間的堆棧指針,所指向的是用戶空間堆棧。所以這里擴(kuò)充的是用戶空間堆棧。這樣,原先的用戶堆棧下方是CONTEXT數(shù)據(jù)結(jié)構(gòu) Context,再往下就是那6個32位整數(shù)。然后把TrapFrame的內(nèi)容保存在這個CONTEXT數(shù)據(jù)結(jié)構(gòu)中,并設(shè)置好6個32位整數(shù),那是要作為調(diào)用參數(shù)傳遞的。接著就把保存在TrapFrame中的Eip映像改成指向用戶空間的一個特殊函數(shù),具體的地址通過 LdrpGetSystemDllApcDispatcher()獲取。這樣,當(dāng)CPU返回到用戶空間時,就會從這個特殊函數(shù)“繼續(xù)”執(zhí)行。當(dāng)然,也要調(diào)整TrapFrame中的用戶空間堆棧指針Esp。
LdrpGetSystemDllApcDispatcher()只是返回一個(內(nèi)核)全局量SystemDllApcDispatcher的值,這個值是個函數(shù)指針,指向ntdll.dll中的一個函數(shù),是在映射ntdll.dll映像時設(shè)置好的。
CODE:
PVOID LdrpGetSystemDllApcDispatcher(VOID)
{
return(SystemDllApcDispatcher);
}
與全局變量SystemDllApcDispatcher相似的函數(shù)指針有:
● SystemDllEntryPoint,指向LdrInitializeThunk()。
● SystemDllApcDispatcher,指向KiUserApcDispatcher()。
● SystemDllExceptionDispatcher,指向KiUserExceptionDispatcher()。
● SystemDllCallbackDispatcher,指向KiUserCallbackDispatcher()。
● SystemDllRaiseExceptionDispatche r,指向KiRaiseUserExceptionDispatcher()。
這些指針都是在LdrpMapSystemDll()中得到設(shè)置的。給定一個函數(shù)名的字符串,就可以通過一個函數(shù)LdrGetProcedureAddress()從(已經(jīng)映射的)DLL映像中獲取這個函數(shù)的地址(如果這個函數(shù)被引出的話)。
于是,CPU從KiDeliverApc()回到_KiServiceExit以后會繼續(xù)完成其返回用戶空間的行程,只是一到用戶空間就栽進(jìn)了圈套,那就是KiUserApcDispatcher(),而不是回到原先的斷點上。關(guān)于原先斷點的現(xiàn)場信息保存在用戶空間堆棧上、并形成一個CONTEXT數(shù)據(jù)結(jié)構(gòu),但是“深埋”在6個32位整數(shù)的后面。而這6個32位整數(shù)的作用則為:
● Esp[0]的值為0xdeadbeef,用來模擬KiUserApcDispatcher()的返回地址。當(dāng)然,這個地址是無效的,所以KiUserApcDispatcher()實際上是不會返回的。
● Esp[1]的值為NormalRoutine,在我們這個情景中指向“門戶”函數(shù)IntCallUserApc()。
● Esp[2]的值為NormalContext,在我們這個情景中是指向?qū)嶋HAPC函數(shù)的指針。
● 余類推。其中Esp[5]指向(用戶)堆棧上的CONTEXT數(shù)據(jù)結(jié)構(gòu)。
總之,用戶堆棧上的這6個32位整數(shù)模擬了一次CPU在進(jìn)入KiUserApcDispatcher()還沒有來得及執(zhí)行其第一條指令之前就發(fā)生了中斷的假象,使得CPU在結(jié)束了KiDeliverApc()的執(zhí)行、回到_KiServiceExit中繼續(xù)前行、并最終回到用戶空間時就進(jìn)入 KiUserApcDispatcher()執(zhí)行其第一條指令。
另一方面,對于該線程原來的上下文而言,則又好像是剛回到用戶空間就發(fā)生了中斷,而KiUserApcDispatcher()則相當(dāng)于中斷相應(yīng)程序。
CODE:
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);
}
這里的第一個參數(shù)ApcRoutine指向IntCallUserApc(),第二個參數(shù)ApcContext指向真正的(目標(biāo))APC函數(shù)。
CODE:
[KiUserApcDispatcher() > IntCallUserApc()]
static void CALLBACK
IntCallUserApc(PVOID Function, PVOID dwData, PVOID Argument3)
{
PAPCFUNC pfnAPC = (PAPCFUNC)Function;
pfnAPC((ULONG_PTR)dwData);
}
可見,IntCallUserApc()其實并無必要,在KiUserApcDispatcher()中直接調(diào)用目標(biāo)APC函數(shù)也無不可,這樣做只是為將來可能的修改擴(kuò)充提供一些方便和靈活性。從IntCallUserApc()回到KiUserApcDispatcher(),下面緊接著是系統(tǒng)調(diào)用 NtContinue()。
KiUserApcDispatcher()是不返回的。它之所以不返回,是因為對NtContinue()的調(diào)用不返回。正如代碼中的注釋所述, NtContinue()的作用是切換回被中斷了的上下文,不過其實還不止于此,下面讀者就會看到它還起著循環(huán)執(zhí)行整個用戶APC請求隊列的作用。
CODE:
[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()并不是普通的函數(shù)調(diào)用,而是系統(tǒng)調(diào)用,這中間經(jīng)歷了空間的切換,也從用戶空間堆棧切換到了系統(tǒng)空間堆棧。CPU進(jìn)入系統(tǒng)調(diào)用空間后,在_KiSystemServicex下面的代碼中把指向中斷現(xiàn)場的框架指針保存在當(dāng)前線程的 KTHREAD數(shù)據(jù)結(jié)構(gòu)的TrapFrame字段中。這樣,很容易就可以找到系統(tǒng)空間堆棧上的調(diào)用框架。當(dāng)然,現(xiàn)在的框架是因為系統(tǒng)調(diào)用而產(chǎn)生的框架;而要想回到當(dāng)初、即在執(zhí)行用戶空間APC函數(shù)之前的斷點,就得先恢復(fù)當(dāng)初的框架。那么當(dāng)初的框架在哪里呢?它保存在用戶空間的堆棧上,就是前面 KiInitializeUserApc()保存的CONTEXT數(shù)據(jù)結(jié)構(gòu)中。所以,這里通過KeContextToTrapFrame()把當(dāng)初保存的信息拷貝回來,從而恢復(fù)了當(dāng)初的框架。
下面的KiContextToFxSaveArea()等語句與浮點處理器有關(guān),我們在這里并不關(guān)心。
最后,匯編指令“jmp _KiServiceExit”使CPU跳轉(zhuǎn)到了返回用戶空間途中的_KiServiceExit處(見前面的代碼)。在這里,CPU又會檢查APC請求隊列中是否有APC請求等著要執(zhí)行,如果有的話又會進(jìn)入KiDeliverApc()。前面講過,每次進(jìn)入KiDeliverApc()只會執(zhí)行一個用戶 APC請求,所以如果用戶APC隊列的長度大于1的話就得循環(huán)著多次走過上述的路線,即:
1. 從系統(tǒng)調(diào)用、中斷、或異常返回途徑_KiServiceExit,如果APC隊列中有等待執(zhí)行的APC請求,就調(diào)用KiDeliverApc()。
2. KiDeliverApc(),從用戶APC隊列中摘下一個APC請求。
3. 在KiInitializeUserApc()中保存當(dāng)前框架,并偽造新的框架。
4. 回到用戶空間。
5. 在KiUserApcDispatcher()中調(diào)用目標(biāo)APC函數(shù)。
6. 通過系統(tǒng)調(diào)用NtContinue()進(jìn)入系統(tǒng)空間。
7. 在NtContinue()中恢復(fù)當(dāng)初保存的框架。
8. 從NtContinue()返回、途徑_KiServiceExit時,如果APC隊列中還有等待執(zhí)行的APC請求,就調(diào)用KiDeliverApc()。于是轉(zhuǎn)回上面的第二步。
這個過程一直要循環(huán)到APC隊列中不再有需要執(zhí)行的請求。注意這里每一次循環(huán)中保存和恢復(fù)的都是同一個框架,就是原始的、開始處理APC隊列之前的那個框架,代表著原始的用戶空間程序斷點。一旦APC隊列中不再有等待執(zhí)行的APC請求,在_KiServiceExit下面就不再調(diào)用 KiDeliverApc(),于是就直接返回用戶空間,這次是返回到原始的程序斷點了。所以,系統(tǒng)調(diào)用neContinue()的作用不僅僅是切換回到被中斷了的上下文,還包括執(zhí)行用戶APC隊列中的下一個APC請求。
對于KiUserApcDispatcher()而言,它對NtContinue()的調(diào)用是不返回的。因為在NtContinue()中CPU不是“返回”到對于KiUserApcDispatcher()的另一次調(diào)用、從而對另一個APC函數(shù)的調(diào)用;就是返回到原始的用戶空間程序斷點,這個斷點既可能是因為中斷或異常而形成的,也可能是因為系統(tǒng)調(diào)用而形成的。
理解了常規(guī)的APC請求和執(zhí)行機(jī)制,我們不妨再看看啟動執(zhí)行PE目標(biāo)映像時函數(shù)的動態(tài)連接。以前講過,PE格式EXE映像與(除ntdll.dll外的) DLL的動態(tài)連接、包括這些DLL的裝入,是由ntdll.dll中的一個函數(shù)LdrInitializeThunk()作為APC函數(shù)執(zhí)行而完成的,所以這也是對APC機(jī)制的一種變通使用。
要啟動一個EXE映像運行時,首先要創(chuàng)建進(jìn)程,再把目標(biāo)EXE映像和ntdll.dll的映像都映射到新進(jìn)程的用戶空間,然后通過系統(tǒng)調(diào)用 NtCreateThread()創(chuàng)建這個進(jìn)程的第一個線程、或稱“主線程”。而LdrInitializeThunk()作為APC函數(shù)的執(zhí)行,就是在 NtCreateThread()中安排好的。
CODE:
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 ()要做的事當(dāng)然很多,但是其中很重要的一項就是安排好APC函數(shù)的執(zhí)行。這里的KeInitializeApc()和KeInsertQueueApc 讀者都已經(jīng)熟悉了,所以我們只關(guān)心調(diào)用參數(shù)中的三個函數(shù)指針,特別是其中的KernelRoutine和NormalRoutine。前者十分簡單:
CODE:
VOID STDCALL
LdrInitApcKernelRoutine(PKAPC Apc, PKNORMAL_ROUTINE* NormalRoutine,
PVOID* NormalContext, PVOID* SystemArgument1, PVOID* SystemArgument2)
{
ExFreePool(Apc);
}
而NormalRoutine,這里是通過LdrpGetSystemDllEntryPoint()獲取的,它只是返回全局量SystemDllEntryPoint的值:
CODE:
PVOID LdrpGetSystemDllEntryPoint(VOID)
{
return(SystemDllEntryPoint);
}
前面已經(jīng)講到,全局量SystemDllEntryPoint是在LdrpMapSystemDll()時得到設(shè)置的,指向已經(jīng)映射到用戶空間的 ntdll.dll映像中的LdrInitializeThunk()。注意這APC請求是掛在新線程的隊列中,而不是當(dāng)前進(jìn)程的隊列中。事實上,新線程和當(dāng)前進(jìn)程處于不同的進(jìn)程,因而不在同一個用戶空間中。還要注意,這里的NormalRoutine直接就是LdrInitializeThunk(),而不像前面通過QueueUserAPC()發(fā)出的APC請求那樣中間還有一層IntCallUserApc()。至于 KiUserApcDispatcher(),那是由KeInitializeApc()強(qiáng)制加上的,正是這個函數(shù)保證了對NtContinue()的調(diào)用。
此后的流程本來無需細(xì)說了,但是由于情景的特殊性還是需要加一些簡要的說明。由NtCreateProcess()創(chuàng)建的進(jìn)程并非一個可以調(diào)度運行的實體,而NtCreateThread()創(chuàng)建的線程卻是。所以,在NtCreateProcess()返回的前夕,系統(tǒng)中已經(jīng)多了一個線程。這個新增線程的“框架”是偽造的,目的在于讓這個線程一開始在用戶空間運行就進(jìn)入預(yù)定的程序入口。從NtCreateProcess()返回是回到當(dāng)前線程、而不是新增線程,而剛才的APC請求是掛在新增線程的隊列中,所以在從NtCreateThread()返回的途中不會去執(zhí)行這個APC請求。可是,當(dāng)新增線程受調(diào)度運行時,首先就是按偽造的框架和堆棧模擬一個從系統(tǒng)調(diào)用返回的過程,所以也要途徑_KiServiceExit。這時候,這個APC請求就要得到執(zhí)行了(由KiUserApcDispatcher()調(diào)用LdrInitializeThunk())。然后,在用戶空間執(zhí)行完APC函數(shù) LdrInitializeThunk()以后,同樣也是通過NtContinue()回到內(nèi)核中,然后又按原先的偽造框架“返回”到用戶空間,這才真正開始了新線程在用戶空間的執(zhí)行。
最后,我們不妨比較一下APC機(jī)制和Unix/Linux的Signal機(jī)制。
Unix/Linux的Signal機(jī)制基本上是對硬件中斷機(jī)制的軟件模擬,具體表現(xiàn)在以下幾個方面:
1) 現(xiàn)代的硬件中斷機(jī)制一般都是“向量中斷”機(jī)制,而Signal機(jī)制中的Signal序號(例如SIG_KILL)就是對中斷向量序號的模擬。
2)作為操作系統(tǒng)對硬件中斷機(jī)制的支持,一般都會提供“設(shè)置中斷向量”一類的內(nèi)核函數(shù),使特定序號的中斷向量指向某個中斷服務(wù)程序。而系統(tǒng)調(diào)用signal ()就相當(dāng)于是這一類的函數(shù)。只不過前者在內(nèi)核中、一般只是供其它內(nèi)核函數(shù)調(diào)用,而后者是系統(tǒng)調(diào)用、供用戶空間的程序調(diào)用。
3)在硬件中斷機(jī)制中,“中斷向量”的設(shè)置只是為某類異步事件、及中斷的發(fā)生做好了準(zhǔn)備,但是并不意味著某個特定時間的發(fā)生。如果一直沒有中斷請求,那么所設(shè)置的中斷向量就一直得不到執(zhí)行,而中斷的發(fā)生只是觸發(fā)了中斷服務(wù)程序的執(zhí)行。在Signal機(jī)制中,向某個進(jìn)程發(fā)出“信號”、即Signal、就相當(dāng)于中斷請求。
相比之下,APC機(jī)制就不能說是對于硬件中斷機(jī)制的模擬了。首先,通過NtQueueApcThread()設(shè)置一個APC函數(shù)跟通過signal()設(shè)置一個“中斷向量”有所不同。將一個APC函數(shù)掛入APC隊列中時,對于這個函數(shù)的得到執(zhí)行、以及大約在什么時候得到執(zhí)行,實際上是預(yù)知的,只是這得到執(zhí)行的條件要過一回兒才會成熟。而“中斷”則不同,中斷向量的設(shè)置只是說如果發(fā)生某種中斷則如何如何,但是對于其究竟是否會發(fā)生、何時發(fā)生則常常是無法預(yù)測的。所以,從這個意義上說,APC函數(shù)只是一種推遲執(zhí)行、異步執(zhí)行的函數(shù)調(diào)用,因此稱之為“異步過程調(diào)用”確實更為貼切。
還有,signal機(jī)制的signal()所設(shè)置的“中斷服務(wù)程序”都是用戶空間的程序,而APC機(jī)制中掛入APC隊列的函數(shù)卻可以是內(nèi)核函數(shù)。
但是,盡管如此,它們的(某些方面的)實質(zhì)還是一樣的。“中斷”本來就是一種異步執(zhí)行的機(jī)制。再說,(用戶)APC與Signal的執(zhí)行流程幾乎完全一樣,都是在從內(nèi)核返回用戶空間的前夕檢查是否有這樣的函數(shù)需要加以執(zhí)行,如果是就臨時修改堆棧,偏離原來的執(zhí)行路線,使得返回用戶空間后進(jìn)入APC函數(shù),并且在執(zhí)行完了這個函數(shù)以后仍進(jìn)入內(nèi)核,然后恢復(fù)原來的堆棧,再次返回用戶空間原來的斷點。這樣,對于原來的流程而言,就相當(dāng)于受到了中斷。
posted on 2010-03-18 15:09 小默 閱讀(2938) 評論(3) 編輯 收藏 引用 所屬分類: Windows