1. 程式人生 > >談談對APC的一點理解

談談對APC的一點理解

非同步過程呼叫(APCs) 是NT非同步處理體系結構中的一個基礎部分,理解了它,對於瞭解NT怎樣操作和執行幾個核心的系統操作很有幫助。

1) APCs允許使用者程式和系統元件在一個程序的地址空間內某個執行緒的上下文中執行程式碼。
2) I/O管理器使用APCs來完成一個執行緒發起的非同步的I/O操作。例如:當一個裝置驅動呼叫IoCompleteRequest來通知I/O管理器,它已經結束處理一個非同步I/O請求時,I/O管理器排隊一個apc到發起請求的執行緒。然後執行緒在一個較低IRQL級別,來執行APC. APC的作用是從系統空間拷貝I/O操作結果和狀態資訊到執行緒虛擬記憶體空間的一個緩衝中。
3) 使用APC可以得到或者設定一個執行緒的上下文和掛起執行緒的執行。

儘管APCs在nt體系結構下被廣泛使用,但是關於怎樣使用它的文件卻非常的缺乏。本篇我們詳細介紹下nt系統是怎樣處理APCs的,並且記錄匯出的nt函式,方便裝置驅動開發者在他們的程式中使用APCs。我也會展示一個非常可靠的NT的APC排程子程式KiDeliverApc的實現,來幫助你更好的掌握APC排程的內幕。

APC物件

在NT中,有兩種型別的APCs:使用者模式和核心模式。使用者APCs執行在使用者模式下目標執行緒當前上下文中,並且需要從目標執行緒得到許可來執行。特別是,使用者模式的APCs需要目標執行緒處在alertable等待狀態才能被成功的排程執行。通過呼叫下面任意一個函式,都可以讓執行緒進入這種狀態。這些函式是:KeWaitForSingleObject, KeWaitForMultipleObjects, KeWaitForMutexObject, KeDelayExecutionThread。
對於使用者模式下,可以呼叫函式SleepEx, SignalObjectAndWait, WaitForSingleObjectEx, WaitForMultipleObjectsEx,MsgWaitForMultipleObjectsEx 都可以使目標執行緒處於alertable等待狀態,從而讓使用者模式APCs執行,原因是這些函式最終都是呼叫了核心中的KeWaitForSingleObject, KeWaitForMultipleObjects, KeWaitForMutexObject, KeDelayExecutionThread等函式。另外通過呼叫一個未公開的alert-test服務KeTestAlertThread,使用者執行緒可以使使用者模式APCs執行。

當一個使用者模式APC被投遞到一個執行緒,呼叫上面的等待函式,如果返回等待狀態STATUS_USER_APC,在返回使用者模式時,核心轉去控制APC例程,當APC例程完成後,再繼續執行緒的執行.

和使用者模式APCs比較,核心模式APCs執行在核心模式下。可以被劃分為常規的和特殊的兩類。當APCs被投遞到一個特殊的執行緒,特殊的核心模式APCs不需要從執行緒得到許可來執行。然而,常規的核心模式APCs在他們成功執行前,需要有特定的環境。此外,特殊的核心APC被儘可能快地執行,既只要APC_LEVEL級上有可排程的活動。在很多情況下,特殊的核心APC甚至能喚醒阻塞的執行緒。普通的核心APC僅在所有特殊APC都被執行完,並且目標執行緒仍在執行,同時該執行緒中也沒有其它核心模式APC正執行時才執行。使用者模式APC在所有核心模式APC執行完後才執行,並且僅在目標執行緒有alertable屬性時才執行。 

每一個等待執行的APC都存在於一個執行緒執行體,由核心管理的佇列中。系統中的每一個執行緒都包含兩個APC佇列,一個是為使用者模式APCs,另一個是為核心模式APCs的。
NT通過一個成為KAPC的核心控制物件來描述一個APC.儘管DDK中沒有明確的文件化APCs,但是在NTDDK.H中卻非常清楚的定義了APC物件。從下面的KAPC物件的定義看,有些是不需要說明的。像Type和Size。Type表示了這是一個APC核心物件。在nt中,每一個核心物件或者執行體物件都有Type和Size這兩個域。由此處理函式可以確定當前處理的物件。Size表示一個字對齊的結構體的大小。也就是指明瞭物件佔的記憶體空間大小。Spare0看起來有些晦澀難懂,但是它是沒用什麼任何深遠的意義,僅僅是為了記憶體補齊。其他的域將在下面的篇幅中介紹。

//-------------------------------------------------------------------------------------------------------

幾個函式宣告和結構定義:

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;
    //
    // N.B. The following two members MUST be together.
    //
    PVOID SystemArgument1;
    PVOID SystemArgument2;
    CCHAR ApcStateIndex;
    KPROCESSOR_MODE ApcMode;
    BOOLEAN Inserted;
} KAPC, *PKAPC, *RESTRICTED_POINTER PRKAPC;
//------

APC環境

一個執行緒在它執行的任意時刻,假設當前的IRQL是在Passive級,它可能需要臨時在其他的程序上下文中執行程式碼,為了完成這個操作,執行緒呼叫系統功能函式KeAttachProcess,在從這個呼叫返回時,執行緒執行在另一個程序的地址空間。先前所有線上程自己的程序上下文中等待執行的APCs,由於這時其所屬程序的地址空間不是當前可用的,因此他們不能被投遞執行。然而,新的插入到這個執行緒的APCs可以執行在這個新的程序空間。甚至當執行緒最後從新的程序中分離時,新的插入到這個執行緒的APCs還可以在這個執行緒所屬的程序上下文中執行。
為了達到控制APC傳送的這個程度,NT中每個執行緒維護了兩個APC環境或者說是狀態。每一個APC環境包含了使用者模式的APC佇列和核心模式的APC佇列,一個指向當前程序物件的指標和三個控制變數,用於指出:是否有未決的核心模式APCs(KernelApcPending),是否有常規核心模式APC在進行中(KernelApcInProgress),是否有未決的使用者模式的APC(UserApcPending). 這些APC的環境儲存線上程物件的ApcStatePointer域中。這個域是由2個元素組成的陣列。即:+0x138 ApcStatePointer : [2] Ptr32 _KAPC_STATE


typedef struct _KAPC_STATE {
    LIST_ENTRY ApcListHead[MaximumMode];
    struct _KPROCESS *Process;
    BOOLEAN KernelApcInProgress;
    BOOLEAN KernelApcPending;
    BOOLEAN UserApcPending;
} KAPC_STATE, *PKAPC_STATE, *PRKAPC_STATE;

lkd> dt _kthread
ntdll!_KTHREAD
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 MutantListHead   : _LIST_ENTRY
   +0x018 InitialStack     : Ptr32 Void
   +0x01c StackLimit       : Ptr32 Void
   +0x020 Teb              : Ptr32 Void
   +0x024 TlsArray         : Ptr32 Void
   +0x028 KernelStack      : Ptr32 Void
   +0x02c DebugActive      : UChar
   +0x02d State            : UChar
   +0x02e Alerted          : [2] UChar
   +0x030 Iopl             : UChar
   +0x031 NpxState         : UChar
   +0x032 Saturation       : Char
   +0x033 Priority         : Char
   +0x034 ApcState         : _KAPC_STATE

   +0x04c ContextSwitches : Uint4B
   +0x050 IdleSwapBlock    : UChar
   +0x051 Spare0           : [3] UChar
   +0x054 WaitStatus       : Int4B
   +0x058 WaitIrql         : UChar
   +0x059 WaitMode         : Char
   +0x05a WaitNext         : UChar
   +0x05b WaitReason       : UChar
   +0x05c WaitBlockList    : Ptr32 _KWAIT_BLOCK
   +0x060 WaitListEntry    : _LIST_ENTRY
   +0x060 SwapListEntry    : _SINGLE_LIST_ENTRY
   +0x068 WaitTime         : Uint4B
   +0x06c BasePriority     : Char
   +0x06d DecrementCount   : UChar
   +0x06e PriorityDecrement : Char
   +0x06f Quantum          : Char
   +0x070 WaitBlock        : [4] _KWAIT_BLOCK
   +0x0d0 LegoData         : Ptr32 Void
   +0x0d4 KernelApcDisable : Uint4B
   +0x0d8 UserAffinity     : Uint4B
   +0x0dc SystemAffinityActive : UChar
   +0x0dd PowerState       : UChar
   +0x0de NpxIrql          : UChar
   +0x0df InitialNode      : UChar
   +0x0e0 ServiceTable     : Ptr32 Void
   +0x0e4 Queue            : Ptr32 _KQUEUE
   +0x0e8 ApcQueueLock     : Uint4B
   +0x0f0 Timer            : _KTIMER
   +0x118 QueueListEntry   : _LIST_ENTRY
   +0x120 SoftAffinity     : Uint4B
   +0x124 Affinity         : Uint4B
   +0x128 Preempted        : UChar
   +0x129 ProcessReadyQueue : UChar
   +0x12a KernelStackResident : UChar
   +0x12b NextProcessor    : UChar
   +0x12c CallbackStack    : Ptr32 Void
   +0x130 Win32Thread      : Ptr32 Void
   +0x134 TrapFrame        : Ptr32 _KTRAP_FRAME
   +0x138 ApcStatePointer : [2] Ptr32 _KAPC_STATE
   +0x140 PreviousMode     : Char
   +0x141 EnableStackSwap : UChar
   +0x142 LargeStack       : UChar
   +0x143 ResourceIndex    : UChar
   +0x144 KernelTime       : Uint4B
   +0x148 UserTime         : Uint4B
   +0x14c SavedApcState    : _KAPC_STATE
   +0x164 Alertable        : UChar
   +0x165 ApcStateIndex    : UChar
   +0x166 ApcQueueable     : UChar
   +0x167 AutoAlignment    : UChar
   +0x168 StackBase        : Ptr32 Void
   +0x16c SuspendApc       : _KAPC
   +0x19c SuspendSemaphore : _KSEMAPHORE
   +0x1b0 ThreadListEntry : _LIST_ENTRY
   +0x1b8 FreezeCount      : Char
   +0x1b9 SuspendCount     : Char
   +0x1ba IdealProcessor   : UChar
   +0x1bb DisableBoost     : UChar

主APC環境是位於執行緒物件的ApcState 域,即:
+0x034 ApcState         : _KAPC_STATE

執行緒中等待在當前程序上下文中執行的APC儲存在ApcState的佇列中。無論何時,NT的APC派發器(dispatcher)和其他系統元件查詢一個執行緒未決的APCs時, 他們都會檢查主APC環境,如果這裡有任何未決的APCs,就會馬上被投遞,或者修改它的控制變數稍後投遞。

第二個APC環境是位於執行緒物件的SavedApcState域,當執行緒臨時掛接到其他程序時,它是用來備份主APC環境的。

當一個執行緒呼叫KeAttachProcess,在另外的程序上下文中執行後續的程式碼時,ApcState域的內容就被拷貝到SavedApcState域。然後ApcState域被清空,它的APC佇列重新初始化,控制變數設定為0,當前程序域設定為新的程序。這些步驟成功的確保先前線上程所屬的程序上下文地址空間中等待的APCs,當執行緒執行在其它不同的程序上下文時,這些APCs不被傳送執行。隨後,ApcStatePointer域陣列內容被更新來反映新的狀態,陣列中第一個元素指向SavedApcState域,第二個元素指向ApcState域,表明執行緒所屬程序上下文的APC環境位於SavedApcState域。執行緒的新的程序上下文的APC環境位於ApcState域。最後,當前程序上下文切換到新的程序上下文。

對於一個APC物件,決定當前APC環境的是ApcStateIndex域。ApcStateIndex域的值作為ApcStatePointer域陣列的索引來得到目標APC環境指標。隨後,目標APC環境指標用來在相應的佇列中存放apc物件.

當執行緒從新的程序中脫離時(KeDetachProcess), 任何在新的程序地址空間中等待執行的未決的核心APCs被派發執行。隨後SavedApcState 域的內容被拷貝回ApcState域。SavedApcState 域的內容被清空,執行緒的ApcStateIndex域被設為OriginalApcEnvironment,ApcStatePointer域更新,當前程序上下文切換到執行緒所屬程序。 

使用APCs

裝置驅動程式使用兩個主要函式來利用APCs, 第一個是KeInitializeApc,用來初始化APC物件。這個函式接受一個驅動分配的APC物件,一個目標執行緒物件指標,APC環境索引(指出APC物件存放於哪個APC環境),APC的kernel,rundown和normal例程指標,APC型別(使用者模式或者核心模式)和一個上下文引數。 函式宣告如下:

NTKERNELAPI
VOID
KeInitializeApc (
    IN PRKAPC Apc,
    IN PKTHREAD Thread,
    IN KAPC_ENVIRONMENT Environment,
    IN PKKERNEL_ROUTINE KernelRoutine,
    IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
    IN PKNORMAL_ROUTINE NormalRoutine OPTIONAL,
    IN KPROCESSOR_MODE ApcMode,
    IN PVOID NormalContext
    );

typedef enum _KAPC_ENVIRONMENT {
    OriginalApcEnvironment,
    AttachedApcEnvironment,
    CurrentApcEnvironment
} KAPC_ENVIRONMENT;

KeInitializeApc 首先設定APC物件的Type和Size域一個適當的值,然後檢查引數Environment的值,如果是CurrentApcEnvironment,那麼ApcStateIndex域設定為目標執行緒的ApcStateIndex域。否則,ApcStateIndex域設定為引數Environment的值。隨後,函式直接用引數設定APC物件Thread,RundownRoutine,KernelRoutine域的值。為了正確地確定APC的型別,KeInitializeApc 檢查引數NormalRoutine的值,如果是NULL,ApcMode域的值設定為KernelMode,NormalContext域設定為NULL。如果NormalRoutine的值不是NULL,這時候它一定指向一個有效的例程,就用相應的引數來設定ApcMode域和NormalContext域。最後,KeInitializeApc 設定Inserted域為FALSE.然而初始化APC物件,並沒有把它存放到相應的APC佇列中。

從這個解釋看,你可以瞭解到APCs物件如果缺少有效的NormalRoutine,就會被當作核心模式APCs.尤其是它們會被認為是特殊的核心模式APCs.
實際上,I/O管理器就是用這類的APC來完成非同步I/O操作。相反地,APC物件定義了有效的NormalRoutine,並且ApcMode域是KernelMode,就會被當作常規的核心模式APCs,否則就會被當作是使用者模式APCs. NTDDK.H中KernelRoutine, RundownRoutine, and NormalRoutine 的定義如下:
typedef
VOID
(*PKKERNEL_ROUTINE) (
    IN struct _KAPC *Apc,
    IN OUT PKNORMAL_ROUTINE *NormalRoutine,
    IN OUT PVOID *NormalContext,
    IN OUT PVOID *SystemArgument1,
    IN OUT PVOID *SystemArgument2
    );

typedef
VOID
(*PKRUNDOWN_ROUTINE) (
    IN struct _KAPC *Apc
    );

typedef
VOID
(*PKNORMAL_ROUTINE) (
    IN PVOID NormalContext,
    IN PVOID SystemArgument1,
    IN PVOID SystemArgument2
    );
//------------------

通常,無論是什麼型別,每個APC物件必須要包含一個有效的KernelRoutine 函式指標。當這個APC被NT的APC dispatcher傳送執行時,這個例程首先被執行。使用者模式的APCs必須包含一個有效的NormalRoutine 函式指標,這個函式必須在使用者記憶體區域。同樣的,常規核心模式APCs也必須包含一個有效的NormalRoutine,但是它就像KernelRoutine一樣執行在核心模式。作為可選擇的,任意型別的APC都可以定義一個有效的RundownRoutine,這個例程必須在核心記憶體區域,並且僅僅當系統需要釋放APC佇列的內容時,才被呼叫。例如執行緒退出時,在這種情況下,KernelRoutine和NormalRoutine都不執行,只有RundownRoutine執行。沒有這個例程的APC物件會被刪除。

記住,投遞APCs到一個執行緒的動作,僅僅是作業系統呼叫KiDeliverApc完成的。執行APC實際上就是呼叫APC內的例程。

一旦APC物件完成初始化後,裝置驅動呼叫KeInsertQueueApc來將APC物件存放到目標執行緒的相應的APC佇列中。這個函式接受一個由KeInitializeApc完成初始化的APC物件指標,兩個系統引數和一個優先順序增量。跟傳遞給KeInitializeApc函式的引數context 一樣,這兩個系統引數只是在APC的例程執行時,簡單的傳遞給APC的例程。

NTKERNELAPI
BOOLEAN
KeInsertQueueApc (
    IN PRKAPC Apc,
    IN PVOID SystemArgument1,
    IN PVOID SystemArgument2,
    IN KPRIORITY Increment
    );
//-----------------
在KeInsertQueueApc 將APC物件存放到目標執行緒相應的APC佇列之前,它首先檢查目標執行緒是否是APC queueable。如果不是,函式立即返回FALSE.如果是,函式直接用引數設定SystemArgument1域和SystemArgument2 域,隨後,函式呼叫KiInsertQueueApc來將APC物件存放到相應的APC佇列。

KiInsertQueueApc 僅僅接受一個APC物件和一個優先順序增量。這個函式首先得到執行緒APC佇列的spinlock並且持有它,防止其他執行緒修改當前執行緒的APC結構。隨後,檢查APC物件的Inserted 域。如果是TRUE,表明這個APC物件已經存放到APC佇列中了,函式立即返回FALSE.如果APC物件的Inserted 域是FALSE.函式通過ApcStateIndex域來確定目標APC環境,然後把APC物件存放到相應的APC佇列中,即將APC物件中的ApcListEntry 域鏈入到APC環境的ApcListHead域中。鏈入的位置由APC的型別決定。常規的核心模式APC,使用者模式APC都是存放到相應的APC佇列的末端。相反的,如果佇列中已經存放了一些APC物件,特殊的核心模式APC存放到佇列中第一個常規核心模式APC物件的前面。如果是核心定義的一個當執行緒退出時使用的使用者APC,它也會被放在相應的佇列的前面。然後,執行緒的主APC環境中的UserApcPending域杯設定為TRUE。這時KiInsertQueueApc 設定APC物件的Inserted 域為TRUE,表明這個APC物件已經存放到APC佇列中了。接下來,檢查這個APC物件是否被排隊到執行緒的當前程序上下文APC環境中,如果不是,函式立即返回TRUE。如果這是一個核心模式APC,執行緒主APC環境中的KernelApcPending域設定為TRUE。

在WIN32 SDK文件中是這樣描述APCs的: 當一個APC被成功的存放到它的佇列後,發出一個軟中斷,APC將會線上程被排程執行的下一個時間片執行。然而這不是完全正確的。這樣一個軟中斷,僅僅是當一個核心模式的APC(無論是常規的核心模式APC還是特殊的核心模式APC)針對於呼叫執行緒時,才會發出。隨後函式返回TRUE。 

1)如果APC不是針對於呼叫執行緒,目標執行緒在Passive許可權等級處在等待狀態;
2)這是一個常規核心模式APC
3)這個執行緒不再臨界區
4)沒有其他的常規核心模式APC仍然在進行中
那麼這個執行緒被喚醒,返回狀態是STATUS_KERNEL_APC。但是等待狀態沒有aborted。 如果這是一個使用者模式APC,KiInsertQueueApc檢查判斷目標執行緒是否是alertable等待狀態,並且WaitMode域等於UserMode。如果是,主APC環境的UserApcPending 域設定為TRUE。等待狀態返回STATUS_USER_APC,最後,函式釋放spinlock,返回TRUE,表示APC物件已經被成功放入佇列。
早期作為APC管理函式的補充,裝置驅動開發者可以使用未公開的系統服務NtQueueApcThread來直接將一個使用者模式的APC投遞到某個執行緒。
這個函式內部實際上是呼叫了KeInitializeApc 和KeInsertQueueApc 來完成這個任務。

NTSYSAPI
NTSTATUS
NTAPI
NtQueueApcThread (
IN HANDLE Thread,
IN PKNORMAL_ROUTINE NormalRoutine,
IN PVOID NormalContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
);

NT的APC派發器

NT檢查是否執行緒有未決的APCs. 然後APC派發器子程式KiDeliverApc在這個執行緒上下文執行來開始將未決的APC執行。注意,這個行為中斷了執行緒的正常執行流程,首先將控制權給APC派發器,隨後當KiDeliverApc完成後,繼續執行緒的執行。
例如:當一個執行緒被排程執行時,最後一步,上下文切換函式 SwapContext 用來檢查是否新的執行緒有未決的核心APCs.如果是,SwapContext要麼(1)請求一個APC級別的軟中斷來開始APC執行,由於新執行緒執行在低的IRQL(Passive級別。
或者(2)返回TRUE,表示新的執行緒有未決的核心APCs。

究竟是執行(1)還是(2)取決於新執行緒所處的IRQL級別. 如果它的許可權級別高於Passive級,SwapContext 執行(1),如果它是在Passive級,則選擇執行(2).
SwapContext的返回值僅僅是特定系統函式可用的,這些系統函式呼叫SwapContext來強制切換執行緒上下文到另一個執行緒. 然後,當這些系統函式經過一段時間再繼續時,他們通常檢查SwapContext 的返回值,如果是TRUE,他們就會呼叫APC派發器來投遞核心APCs到當前的執行緒. 例如:
系統函式KiSwapThread被等待服務用來放棄處理器,直到等待結束。這個函式內部呼叫SwapContext。當等待結束,繼續從呼叫SwapContext處執行時,就會檢查SwapContext的返回值。如果是TRUE,KiSwapThread會降低IRQL級別到APC級,然後呼叫KiDeliverApc來在當前執行緒執行核心APCs.
對於使用者APCs, 核心呼叫APC派發器僅僅是當執行緒回到使用者模式,並且執行緒的主APC環境的UserApcPending域為TRUE時。例如:當系統服務派發器KiSystemService完成一個系統服務請求正打算回到使用者模式時,它會檢查是否有未決的使用者APCs。在執行上,KiDeliverApc呼叫使用者APC的KernelRoutine. 隨後,KiInitializeUserApc函式被呼叫,用來設定執行緒的陷阱幀。所以從核心模式退出時,執行緒開始在使用者模式下執行
。KiInitializeUserApc的函式的作用是拷貝當前執行緒先前的執行狀態(當進入核心模式時,這個狀態儲存線上程核心棧建立的陷阱幀裡),從核心棧到執行緒的使用者模式棧,初始化使用者模式APC。APC派發器子程式KiUserApcDispatcher在Ntdll.dll內。最後,載入陷阱幀的EIP暫存器和Ntdll.dll中KiUserApcDispatcher的地址。當陷阱幀最後釋放時,核心將控制轉交給KiUserApcDispatcher,這個函式呼叫APC的NormalRoutine例程,NormalRoutine函式地址以及引數都在棧中,當例程完成時,它呼叫NtContinue來讓執行緒利用在棧中先前的上下文繼續執行,彷彿什麼事情也沒有發生過。


當核心呼叫KiDeliverApc來執行一個使用者模式APC時,執行緒中的PreviousMode域被設為UserMode. TrapFrame域指向執行緒的陷阱幀。當核心呼叫KiDeliverApc來執行核心APCs時,執行緒中的PreviousMode域被設為KernelMode. TrapFrame域指向NULL。
注意,無論何時只要KernelRoutine被呼叫,傳遞給它的指標是一個區域性的APC屬性的副本,由於APC物件已經脫離了佇列,所以可以安全的在KernelRoutine中釋放APC記憶體。此外,這個例程在它的引數被傳遞給其他例程之前,有一個最後的機會來修改這些引數。

結論:
APC提供了一個非常有用的機制,允許在特定的執行緒上下文中非同步的執行程式碼。作為一個裝置驅動開發者,你可以依賴APCs在某個特定的執行緒上下文中執行一個例程,而不需要執行緒的許可和干涉。對於使用者應用程式,使用者模式APCs可以用來有效地實現一些回撥通知機制。