• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            小默

            Windows APC機(jī)制zzz

            前兩篇漫談中講到,除ntdll.dll外,在啟動一個新進(jìn)程運行時,PE格式DLL映像的裝入和動態(tài)連接是由ntdll.dll中的函數(shù) LdrInitializeThunk()作為APC函數(shù)執(zhí)行而完成的。這就牽涉到了Windows的APC機(jī)制,APC是“異步過程調(diào)用 (Asyncroneus Procedure Call)”的縮寫。從大體上說,Windows的APC機(jī)制相當(dāng)于Linux的Signal機(jī)制,實質(zhì)上是一種對于應(yīng)用軟件(線程)的“軟件中斷”機(jī)制。但是讀者將會看到,APC機(jī)制至少在形式上與軟件中斷機(jī)制還是有相當(dāng)?shù)膮^(qū)別,而稱之為“異步過程調(diào)用”確實更為貼切。
            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

            評論

            # re: Windows APC機(jī)制zzz 2011-01-18 10:29 a7031x

            寫得非常詳細(xì),感謝作者。  回復(fù)  更多評論   

            # re: Windows APC機(jī)制zzz 2012-09-19 10:44 徐宏豐

            和操作系統(tǒng)內(nèi)核那本書上一模一樣,不知道是誰抄誰的。  回復(fù)  更多評論   

            # re: Windows APC機(jī)制zzz 2013-06-02 14:53 zssure

            這么好的文章,才看到,慚愧  回復(fù)  更多評論   

            導(dǎo)航

            統(tǒng)計

            留言簿(13)

            隨筆分類(287)

            隨筆檔案(289)

            漏洞

            搜索

            積分與排名

            最新評論

            閱讀排行榜

            久久久久亚洲精品日久生情| 久久国产乱子伦免费精品| 五月丁香综合激情六月久久| 久久久精品久久久久久| 狠狠色丁香婷综合久久| 国产高潮国产高潮久久久| 久久久久99精品成人片欧美| 久久久久久综合网天天| 亚洲国产精品无码久久一区二区 | 欧美国产成人久久精品| 久久久久久综合一区中文字幕| 777米奇久久最新地址| 国产成人无码精品久久久久免费| 久久香蕉国产线看观看精品yw| 久久青青草原精品国产| 久久精品国产69国产精品亚洲| 成人a毛片久久免费播放| 久久精品国产只有精品66| 欧美亚洲国产精品久久| 漂亮人妻被中出中文字幕久久 | 一级a性色生活片久久无| 久久国产精品无| 亚洲αv久久久噜噜噜噜噜| 精品久久久久久无码专区| 国产2021久久精品| 久久夜色精品国产噜噜亚洲a| 亚洲精品无码久久千人斩| 久久精品国产亚洲综合色| 婷婷久久综合九色综合绿巨人| 亚洲精品无码久久一线| 99久久精品国产一区二区蜜芽 | 精品久久久中文字幕人妻| 国产精品岛国久久久久| 色婷婷久久综合中文久久一本| 久久精品无码专区免费东京热 | 久久超碰97人人做人人爱| 国产精品亚洲综合专区片高清久久久 | 久久综合久久鬼色| 国产高潮国产高潮久久久| 久久99九九国产免费看小说| 久久久久一区二区三区|