• <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機制zzz

            前兩篇漫談中講到,除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()的調用界面為:

             

            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
            );

                   這里有個參數ApcRoutine,這是一個函數指針。什么時候要用到這個指針呢?原來,文件操作有“同步”和“異步”之分。普通的寫文件操作是同步寫,啟動這種操作的線程在內核進行寫文件操作期間被“阻塞(blocked)”而進入“睡眠”,直到設備驅動完成了操作以后才又將該線程“喚醒”而從系統調用返回。但是,如果目標文件是按異步操作打開的,即在通過W32的API函數CreateFile()打開目標文件時把調用參數 dwFlagsAndAttributes設置成FILE_FLAG_OVERLAPPED,那么調用者就不會被阻塞,而是把事情交給內核、不等實際的操作完成就返回了。但是此時要把ApcRoutine設置成指向某個APC函數。這樣,當設備驅動完成實際的操作時,就會使調用者線程執行這個APC函數,就像是發生了一次中斷。執行該APC函數時的調用界面為:

             

            CODE:
            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中這個函數的代碼:

             

            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);
            }

                  參數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里面:

             

            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 并不公開這個數據結構的定義,所以ReactOS代碼中對這個數據結構的定義帶有逆向工程的痕跡,每一行后面的十六進制數值就是相應結構成分在數據結構中的位移。這里我們最關心的是ApcState,這又是一個數據結構、即KAPC_STATE。可以看出,KAPC_STATE的大小是0x18字節。其定義如下:

             

            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的數組,說明實際上(每個線程)有兩個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的值定義了一種枚舉類型:

             

            CODE:
            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數據結構,并將其掛入相應的隊列。

             

            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;

            結構中的ApcListEntry就是用來將KAPC結構掛入隊列的。注意這個數據結構中有三個函數指針,即KernelRoutine、 RundownRoutine、NormalRoutine。其中只有NormalRoutine才指向(執行)APC函數的請求者所提供的函數,其余兩個都是輔助性的。以NtQueueApcThread()為例,其請求者(調用者)QueueUserAPC()所提供的函數是 IntCallUserApc(),所以NormalRoutine應該指向這個函數。注意真正的請求者其實是QueueUserAPC()的調用者,真正的目標APC函數也并非IntCallUserApc(),而是前面的函數指針pfnAPC所指向的函數,而IntCallUserApc()起著類似于“門戶”的作用。

            現在我們可以往下看系統調用NtQueueApcThread()的實現了。

             

            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;
            }

            先看調用參數。第一個參數是代表著某個已打開線程的Handle,這說明所要求的APC函數的執行者、即目標線程、可以是另一個線程,而不必是請求者線程本身。第二個參數不言自明。第三個參數NormalContext,以及后面的兩個參數,則是準備傳遞給APC函數的參數,至于怎樣解釋和使用這幾個參數是 APC函數的事。看一下前面QueueUserAPC()的代碼,就可以知道這里的APC函數是IntCallUserApc(),而準備傳給它的參數分別為pfnAPC、dwData、和NULL,前者是真正的目標APC函數指針,后兩者是要傳給它的參數。
            根據Handle找到目標線程的ETHREAD數據結構以后,就為APC函數分配一個KAPC數據結構,并通過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;
            }
            }

            這段代碼本身很簡單,但是有幾個問題需要結合前面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定義為:

             

            CODE:
            #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的時機,那是在(系統調用、中斷、或異常處理之后)從內核返回用戶空間的途中。

             

            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
            . . . . . .

            這是內核中處理系統調用返回和中斷/異常返回的代碼。在返回前夕,這里先通過%fs:KPCR_CURRENT_THREAD取得指向當前線程的ETHREAD(從而KTHREAD)的指針,然后依次檢查:
            ● 即將返回的是否用戶空間。
            ● 是否有用戶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()的代碼。

             

            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

            參數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了。我們繼續往下看:

             

            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);
            }
            }

            當然,執行用戶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回到用戶空間才能執行,這里只是為其作好安排和準備。

             

            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;
            }

            這個函數的名字取得不好,很容易讓人把它跟前面的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映像時設置好的。

             

            CODE:
            PVOID LdrpGetSystemDllApcDispatcher(VOID)
            {
            return(SystemDllApcDispatcher);
            }

            與全局變量SystemDllApcDispatcher相似的函數指針有:
            ● SystemDllEntryPoint,指向LdrInitializeThunk()。
            ● SystemDllApcDispatcher,指向KiUserApcDispatcher()。
            ● SystemDllExceptionDispatcher,指向KiUserExceptionDispatcher()。
            ● SystemDllCallbackDispatcher,指向KiUserCallbackDispatcher()。
            ● SystemDllRaiseExceptionDispatche r,指向KiRaiseUserExceptionDispatcher()。
            這些指針都是在LdrpMapSystemDll()中得到設置的。給定一個函數名的字符串,就可以通過一個函數LdrGetProcedureAddress()從(已經映射的)DLL映像中獲取這個函數的地址(如果這個函數被引出的話)。
            于是,CPU從KiDeliverApc()回到_KiServiceExit以后會繼續完成其返回用戶空間的行程,只是一到用戶空間就栽進了圈套,那就是KiUserApcDispatcher(),而不是回到原先的斷點上。關于原先斷點的現場信息保存在用戶空間堆棧上、并形成一個CONTEXT數據結構,但是“深埋”在6個32位整數的后面。而這6個32位整數的作用則為:
            ● Esp[0]的值為0xdeadbeef,用來模擬KiUserApcDispatcher()的返回地址。當然,這個地址是無效的,所以KiUserApcDispatcher()實際上是不會返回的。
            ● Esp[1]的值為NormalRoutine,在我們這個情景中指向“門戶”函數IntCallUserApc()。
            ● Esp[2]的值為NormalContext,在我們這個情景中是指向實際APC函數的指針。
            ● 余類推。其中Esp[5]指向(用戶)堆棧上的CONTEXT數據結構。
            總之,用戶堆棧上的這6個32位整數模擬了一次CPU在進入KiUserApcDispatcher()還沒有來得及執行其第一條指令之前就發生了中斷的假象,使得CPU在結束了KiDeliverApc()的執行、回到_KiServiceExit中繼續前行、并最終回到用戶空間時就進入 KiUserApcDispatcher()執行其第一條指令。
            另一方面,對于該線程原來的上下文而言,則又好像是剛回到用戶空間就發生了中斷,而KiUserApcDispatcher()則相當于中斷相應程序。

             

            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);
            }

            這里的第一個參數ApcRoutine指向IntCallUserApc(),第二個參數ApcContext指向真正的(目標)APC函數。

             

            CODE:
            [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請求隊列的作用。

             

            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()并不是普通的函數調用,而是系統調用,這中間經歷了空間的切換,也從用戶空間堆棧切換到了系統空間堆棧。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()中安排好的。

             

            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 ()要做的事當然很多,但是其中很重要的一項就是安排好APC函數的執行。這里的KeInitializeApc()和KeInsertQueueApc 讀者都已經熟悉了,所以我們只關心調用參數中的三個函數指針,特別是其中的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);
            }

            前面已經講到,全局量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函數,并且在執行完了這個函數以后仍進入內核,然后恢復原來的堆棧,再次返回用戶空間原來的斷點。這樣,對于原來的流程而言,就相當于受到了中斷。

            posted on 2010-03-18 15:09 小默 閱讀(2938) 評論(3)  編輯 收藏 引用 所屬分類: Windows

            評論

            # re: Windows APC機制zzz 2011-01-18 10:29 a7031x

            寫得非常詳細,感謝作者。  回復  更多評論   

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

            和操作系統內核那本書上一模一樣,不知道是誰抄誰的。  回復  更多評論   

            # re: Windows APC機制zzz 2013-06-02 14:53 zssure

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

            導航

            統計

            留言簿(13)

            隨筆分類(287)

            隨筆檔案(289)

            漏洞

            搜索

            積分與排名

            最新評論

            閱讀排行榜

            伊人色综合久久天天人手人婷 | 伊人 久久 精品| 亚洲?V乱码久久精品蜜桃| 久久99这里只有精品国产| 久久不见久久见免费视频7| 伊人久久综在合线亚洲2019 | 久久国产亚洲精品无码| 国产精品久久久久乳精品爆| 亚洲欧美国产精品专区久久| 91精品国产色综合久久| 久久久久免费精品国产| 久久久久久久综合日本亚洲| 久久国产劲爆AV内射—百度| 99久久亚洲综合精品成人| 亚洲va国产va天堂va久久| 久久精品中文字幕有码| 久久精品国产亚洲AV电影| 香蕉99久久国产综合精品宅男自| 2021精品国产综合久久| 一本一道久久综合狠狠老 | 一本久久精品一区二区| 久久精品无码一区二区三区| 国产69精品久久久久久人妻精品| 精品久久久久久久中文字幕| 国产人久久人人人人爽| 久久人人爽人人人人爽AV| 久久精品中文字幕一区| 精品国产热久久久福利| 精品一区二区久久久久久久网站| 久久国产欧美日韩精品| 久久久亚洲裙底偷窥综合| 日本五月天婷久久网站| 久久精品人人做人人爽电影| 无码8090精品久久一区 | 日韩精品久久久肉伦网站 | 久久青青草原国产精品免费 | 亚洲人成无码久久电影网站| 久久久久久亚洲精品不卡| 久久精品无码专区免费| 日日狠狠久久偷偷色综合0| 一本久久免费视频|