目錄
1. 相關閱讀材料 2. 《加密與解密3》 3. [經典文章翻譯]A_Crash_Course_on_the_Depths_of_Win32_Structured_Exception_Handling.pdf 4. 《0 DAY安全: 軟體漏洞分析技術》
2. 資料結構分析
二. KTHREAD
KTHREAD(核心層執行緒物件)。再次重複說明一點:
windows核心中的執行體(ETHREAD, EPROCESS) 負責各種與管理和策略相關的功能,而核心層(或微核心)(KTHREAD, KPROCESS)實現了作業系統的核心機制。
程式和執行緒在這兩層上都有對應的資料結構
之所以多次提到這一點是因為我發現在研究一項技術或者一個機制、資料結構之前,如果腦袋中能建立起對這個事物的功能上的深刻認識,知道這個技術將用在哪裡,將實現哪些功能,到具體深入學習的時候就能有很深的體會。所以小瀚這裡也建議朋友們也能先深刻理解為什麼windows在核心中要有兩套"類似"的資料結構來標識程式和執行緒,以及它們的聯絡和區別、側重點都在哪裡。
另外,我們知道執行體執行緒塊RTHREAD的第一個成員域就是KTHREAD Tcb,這個Tcb指的就是KPROCESS。這點和KPROCESS是EPROCESS的第一個成員域是一樣的
nt!_ETHREAD +0x000 Tcb : _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 .....
在WRK的原始碼 base\ntos\inc\ke.h 中,我們也可以找到KTHREAD的資料結構定義
typedef struct _KTHREAD { DISPATCHER_HEADER Header; LIST_ENTRY MutantListHead; PVOID InitialStack; PVOID StackLimit; PVOID KernelStack; KSPIN_LOCK ThreadLock; union { KAPC_STATE ApcState; struct { UCHAR ApcStateFill[KAPC_STATE_ACTUAL_LENGTH]; BOOLEAN ApcQueueable; volatile UCHAR NextProcessor; volatile UCHAR DeferredProcessor; UCHAR AdjustReason; SCHAR AdjustIncrement; }; }; KSPIN_LOCK ApcQueueLock; ULONG ContextSwitches;
UCHAR State;
UCHAR NpxState; BOOLEAN Alertable;
UCHAR WaitIrql; BOOLEAN WaitNext; UCHAR WaitReason;
CHAR WaitMode; SCHAR Priority;
UCHAR BasePriority UCHAR EnableStackSwap; volatile UCHAR SwapBusy; BOOLEAN Alerted[MaximumMode]; union { LIST_ENTRY WaitListEntry; SINGLE_LIST_ENTRY SwapListEntry; }; PRKQUEUE Queue;
ULONG WaitTime; PVOID Teb; union { KTIMER Timer; struct { UCHAR TimerFill[KTIMER_ACTUAL_LENGTH]; union { struct { LONG AutoAlignment : 1; LONG DisableBoost : 1; LONG ReservedFlags : 30; }; LONG ThreadFlags; }; }; }; union { KWAIT_BLOCK WaitBlock[THREAD_WAIT_OBJECTS + 1]; struct { UCHAR WaitBlockFill0[KWAIT_BLOCK_OFFSET_TO_BYTE0]; BOOLEAN SystemAffinityActive; }; struct { UCHAR WaitBlockFill1[KWAIT_BLOCK_OFFSET_TO_BYTE1]; CCHAR PreviousMode; }; struct { UCHAR WaitBlockFill2[KWAIT_BLOCK_OFFSET_TO_BYTE2]; UCHAR ResourceIndex; }; struct { UCHAR WaitBlockFill3[KWAIT_BLOCK_OFFSET_TO_BYTE3]; UCHAR LargeStack; }; }; LIST_ENTRY QueueListEntry; PKTRAP_FRAME TrapFrame; PVOID CallbackStack; PVOID ServiceTable; UCHAR ApcStateIndex; UCHAR IdealProcessor; BOOLEAN Preempted; BOOLEAN ProcessReadyQueue; BOOLEAN KernelStackResident; SCHAR BasePriority; SCHAR PriorityDecrement; CHAR Saturation; KAFFINITY UserAffinity; PKPROCESS Process; KAFFINITY Affinity; PKAPC_STATE ApcStatePointer[2]; union { KAPC_STATE SavedApcState; struct { UCHAR SavedApcStateFill[KAPC_STATE_ACTUAL_LENGTH]; CCHAR FreezeCount; CCHAR SuspendCount; UCHAR UserIdealProcessor; UCHAR CalloutActive; }; }; PVOID Win32Thread; PVOID StackBase; union { KAPC SuspendApc; struct { UCHAR SuspendApcFill0[KAPC_OFFSET_TO_SPARE_BYTE0]; SCHAR Quantum; }; struct { UCHAR SuspendApcFill1[KAPC_OFFSET_TO_SPARE_BYTE1]; UCHAR QuantumReset; }; struct { UCHAR SuspendApcFill2[KAPC_OFFSET_TO_SPARE_LONG]; ULONG KernelTime; }; struct { UCHAR SuspendApcFill3[KAPC_OFFSET_TO_SYSTEMARGUMENT1]; PVOID TlsArray; }; struct { UCHAR SuspendApcFill4[KAPC_OFFSET_TO_SYSTEMARGUMENT2]; PVOID BBTData; }; struct { UCHAR SuspendApcFill5[KAPC_ACTUAL_LENGTH]; UCHAR PowerState; ULONG UserTime; }; }; union { KSEMAPHORE SuspendSemaphore; struct { UCHAR SuspendSemaphorefill[KSEMAPHORE_ACTUAL_LENGTH]; ULONG SListFaultCount; }; }; LIST_ENTRY ThreadListEntry; PVOID SListFaultAddress; } KTHREAD, *PKTHREAD, *PRKTHREAD;
接下來我們來一條一條地學習這個資料結構,並盡我的最大的能力給出擴充套件延伸的知識。
由於在windows中,執行緒是系統處理器排程的"基本單元",而且執行緒排程是在核心層完成的,所以,KTHREAD的許多域都跟windows的執行緒排程機制有關。我們在學習的過程中要時刻集合
windows的執行緒排程機制來進行理解。
1. DISPATCHER_HEADER Header
Header域是一個DISPATCHER_HEADER結構,說明了核心層的執行緒物件"也"是一個分發器物件,這裡之所以用"也",是因為KPROCESS的第一個成員域同樣是一個DISPATCHER_HEADER結構體物件,這說明執行緒也可以被等待,當被等待的執行緒結束時,之前等待在該物件上的"等待"被滿足,觸發訊號signal,使阻塞等待解除,這樣就達到了同步的效果。
typedef struct _DISPATCHER_HEADER { union { struct { UCHAR Type; union { UCHAR Abandoned; UCHAR Absolute; UCHAR NpxIrql; UCHAR Signalling; }; union { UCHAR Size; UCHAR Hand; }; union { UCHAR Inserted; UCHAR DebugActive; UCHAR DpcActive; }; }; LONG Lock; }; LONG SignalState; LIST_ENTRY WaitListHead; } DISPATCHER_HEADER, *PDISPATCHER_HEADER;
2. LIST_ENTRY MutantListHead
MutantListHead域指向一個連結串列頭,該連結串列中包含了所有屬於"該執行緒"的"突變體物件(mutan,對應於API中的互斥體mutex物件)"。由於突變體物件是有"所有權"的,一旦被某個執行緒等到(暫時獲得),則其所有權歸該執行緒所有,它也被連線到MutanListHead連結串列中
3. 核心棧的維護
PVOID InitialStack;
PVOID StackLimit;
PVOID KernelStack;
PVOID StackBase
注意,核心棧是從高地址向低地址方向變化的.
InitialStack: 記錄了原始的棧位置(高地址)
StackLimit: 記錄了棧的低地址
KernelStack: 記錄了真正核心呼叫棧的開始位置
由於在核心棧的"頂部區域"還記錄了浮點處理器儲存區和一個異常陷阱幀,所以,KernelStack的位置比InitialStack要低一些(核心棧從高往低發展): KTRAP_FRAME_LENGTH + sizeof(FX_SAVE_AREA)
StackBase: 指向了原始的核心棧高地址
線上程初始化的時候,StackBase和InitialStack是相等的,都指向原始的核心棧高地址。
4. KSPIN_LOCK ThreadLock
ThreadLock域是一個自旋鎖,用於保護執行緒資料成員。
typedef struct { volatile unsigned int lock; } spinlock_t;
5. KAPC_STATE ApcState
ApcState域是一個結構成員,指定了一個執行緒的APC(Asynchronous Procedure Call)資訊,包括APC連結串列、是否正在處理APC或者是否有核心APC或使用者APC正在等待資訊。
typedef struct _KAPC_STATE { LIST_ENTRY ApcListHead[2]; PKPROCESS Process; //指向當前執行緒所屬的程式的KPROCESS結構 UCHAR KernelApcInProgress; UCHAR KernelApcPending; UCHAR UserApcPending; } KAPC_STATE, *PKAPC_STATE;
ApcState所在的是一個union,除了ApcState之外,還有其他的結構
union { KAPC_STATE ApcState; struct { UCHAR ApcStateFill[KAPC_STATE_ACTUAL_LENGTH]; BOOLEAN ApcQueueable; volatile UCHAR NextProcessor; volatile UCHAR DeferredProcessor; UCHAR AdjustReason; SCHAR AdjustIncrement; }; };
ApcQueueable: 指示是否可以插入APC
NextProcessor、DeferredProcessor: 關於處理器排程的選擇
AdjustReason: 優先順序調整原因
AdjustIncrement: 優先順序調整的調整量(調整多少,加還是減)
6. KSPIN_LOCK ApcQueueLock
ApcQueueLock域是一個自旋鎖,用於保護APC佇列的操作
7. ULONG ContextSwitches
ContextSwitches域記錄了該執行緒進行了多少次環境切換
8. UCHAR State
State域反映了該執行緒當前的狀態,關於執行緒的狀態,在 base\ntos\inc\ke.h 中的巨集定義 KTHREAD_STATE
typedef enum _KTHREAD_STATE { Initialized, Ready, Running, Standby, Terminated, Waiting, Transition, DeferredReady, GateWait } KTHREAD_STATE;
可以看到,這就是我們在作業系統課上學的執行緒的狀態轉義的相關知識,這涉及到執行緒排程演算法。這裡擴充套件一下學習:
每個執行緒從初始化開始一直到最後終止,其狀態既可能隨著系統狀態而變化,也可能隨自身的程式碼邏輯而發生變化,並非總是一直執行下去直到結束,這是一個動態的過程。
1) 已初始化(Initialized): 說明一個執行緒物件的內部狀態已經初始化,這是執行緒建立過程中的一個內部狀態,此時執行緒尚未加入到程式的連結串列中(EPROCESS->ThreadListHead、
KPROCESS->ThreadListHead),也沒有啟動 2) 就緒(Ready): 代表該執行緒已經準備就緒,等待被排程執行。當"執行緒排程器"選擇一個執行緒來執行時,它只考慮處於"就緒"狀態的執行緒。所以,我們要讓一個執行緒執行,就必須將這個執行緒的
狀態設定為Ready,這個執行緒才會被加入某個處理器的就緒執行緒連結串列中(這裡之所以說某個是因為執行緒被哪個處理器執行和這個執行緒的"親和性affinity" KTHREAD->affinity
有關) 3) 執行(Running): 執行緒正在執行。該執行緒一直佔有處理器,直到分到的時限結束,或者被一個更高優先順序的執行緒搶佔,或者執行緒終止,或者主動放棄處理器執行權,或者進入等待狀態(正常情況
下,每個執行緒的執行時間為20ms,這個數值不是絕對的,根據執行緒優先順序,執行情況會上下波動)。 4) 備用(Standby): 處於備用狀態的執行緒已經被選中作為某個處理器上下一個要執行的執行緒。對於系統中的每個處理器,只能有一個執行緒可以處於備用狀態。然而,一個處於備用狀態的執行緒在
真正被執行以前,有可能被更高優先順序的執行緒搶佔這個備用的"位置"(執行緒的優先順序是不不斷動態變化的) 5) 已終止(Terminated): 表示執行緒已經完成任務,正在進行資源回收。KeTerminatedThread()函式用於設定此狀態 6) 等待(Waiting): 表示一個執行緒正在等待某個條件,比如等待一個分發起物件變成有訊號狀態(signaled)。也可以等待多個物件,當等待的條件滿足時,執行緒或者立即開始執行,或者回到就
緒狀態 7) 轉移(Transition): 處於轉移狀態的執行緒已經準備好執行,但是它的核心棧不再記憶體中。一旦它的核心被換入記憶體,則該執行緒進入就緒狀態 8) 延遲的就緒(DeferredReady): 處於延遲的就緒狀態的執行緒也已經準備好可以執行了,但是,與就緒狀態不同的是,它尚未確定在哪個處理器上執行。當有機會被排程時,或者直接轉入備用
狀態,或者轉到就緒狀態。因此,此狀態是為了多處理器而引入的,對於單處理器系統沒有意義 9) 門等待(GateWait): 執行緒正在等待一個門物件。此狀態與等待狀態類似,只不過它是專門針對門物件而涉及的
回到我們的主線上來,State域反映了該執行緒當前的狀態
9. UCHAR NpxState
NpxState域反映了浮點處理器的狀態
10. BOOLEAN Alertable
Alertable域說明了一個執行緒是否可以被喚醒,當一個執行緒正在等待時,如果它的Alertable值為TRUE,則它是可以被喚醒的。
Alertable是一個布林值,TRUE值表示這個執行緒馬上要呼叫一個核心等待函式,它的用途是,在發出了一個訊號(比如釋放一個訊號量物件)以後,接下來該執行緒會馬上呼叫等待函式,所以,它不必解除執行緒排程器鎖。
11. IRQL
UCHAR WaitIrql;
BOOLEAN WaitNext;
WaitIrql: 記錄了原先的IRQL值
WaitNext: 記錄了下一個I/O請求的IRQL的值
12. UCHAR WaitReason
WaitReason域記錄了一個執行緒等待的理由,在 base\ntos\inc\ke.h 中有關於這個值的列舉巨集定義
typedef enum _KWAIT_REASON { Executive, FreePage, PageIn, PoolAllocation, DelayExecution, Suspended, UserRequest, WrExecutive, WrFreePage, WrPageIn, WrPoolAllocation, WrDelayExecution, WrSuspended, WrUserRequest, WrEventPair, WrQueue, WrLpcReceive, WrLpcReply, WrVirtualMemory, WrPageOut, WrRendezvous, Spare2, Spare3, Spare4, Spare5, Spare6, WrKernel, WrResource, WrPushLock, WrMutex, WrQuantumEnd, WrDispatchInt, WrPreempted, WrYieldExecution, WrFastMutex, WrGuardedMutex, WrRundown, MaximumWaitReason } KWAIT_REASON;
WaitReason基本上只是記錄了等待的理由,而並不參與到執行緒排程或決策中。
13. CHAR WaitMode
WaitMode域記錄了當前執行緒等待時的處理器模式,即核心模式或使用者模式的等待。
NTSTATUS KeWaitForSingleObject( IN PVOID Object, IN KWAIT_REASON WaitReason, IN KPROCESSOR_MODE WaitMode, //指定等待的模式(使用者/核心) IN BOOLEAN Alertable, IN PLARGE_INTEGER Timeout OPTIONAL );
14. LONG_PTR WaitStatus
WaitStatus域記錄了等待的結果狀態
15. PKWAIT_BLOCK WaitBlockList
WaitBlockList成員指向一個以KWAIT_BLOCK為元素的連結串列,其中的KWAIT_BLOCK物件指明瞭哪個執行緒在等待哪個分發器物件。對於一個執行緒而言:
1) WaitBlockList域以及每個KWAIT_BLOCK物件中的WaitBlockList域構成了一個雙連結串列,指明瞭該執行緒正在等待哪些分發器物件。 2) 而對於每個分發器物件而言,它又有另一個KWAIT_BLOCK連結串列指明瞭哪些執行緒正在等待它(二維交叉關係)
typedef struct _KWAIT_BLOCK { LIST_ENTRY WaitListEntry; //該執行緒等待的分發器物件 PKTHREAD Thread; //哪些執行緒在等待這個分發器 PVOID Object; PKWAIT_BLOCK NextWaitBlock; WORD WaitKey; UCHAR WaitType; UCHAR SpareByte; } KWAIT_BLOCK, *PKWAIT_BLOCK;
關於執行緒間排程、同步、分發器的知識請參閱《windows 核心原理與實現》5.4 節
16. PKGATE GateObject
GateObject域記錄了正在等待的"門物件(Gate Object,也是一種分發器物件)"。關於門物件和門等待,我們這裡擴充一下。
首先要明白,門等待屬於執行緒排程的同步機制的知識範疇,基於執行緒排程的同步機制有:
1) 執行緒進入等待: 通過等待"同步物件"或"分發器物件"來進行執行緒的的排程 2) 事件 3) 突變體 4) 訊號量 5) 定時器 6) 推鎖 7) 執行體資源
這些都是windows的等待機制以及各種分發器物件。顯然,這一套機制足夠靈活,可以滿足核心和應用程式的同步、非同步程式設計的需要。"除此之外",windows還提供了一套更加"輕量"的等待機制: 門等待機制。
我們要明白,門等待機制也是執行緒同步機制的範疇裡的一種而已,只不是是一個更輕量級的機制,我們在學習"門等待"機制的時候要學會把它和其他的執行緒間同步機制進行類比,加深理解。
門等待是對windows標準等待機制的一個簡化,它避免了一個執行緒從進入等待函式到離開函式過程中的許多步驟。喚醒一個處於"門等待"的執行緒的方式很簡單,只要呼叫KiDeferredReadyThread()函式將該執行緒分發到某個處理器上準備執行。
門等待中的分發器物件是KGATE,它只有一個DISPATCHER_HEADER結構成員
typedef struct _KGATE { DISPATCHER_HEADER Header; } KGATE, *PKGATE;
它支援多個執行緒同時等待一個KGATE物件,每次有訊號狀態時只喚醒"一個"執行緒,所以,它不是通知型別的分發器物件,而是同步型別的分發器物件。另外,等待門物件的執行緒也不會等待多個分發器物件,也沒有超時設定,所以,執行緒進入門等待時的處理也相對簡單高效很多。
因此,處於門等待狀態的執行緒一旦它所等待的門物件變成有訊號狀態,則可以以最快的速度被排程和執行。這也使得門等待比普通的等待要高效一些,在核心中用於一些效能敏感的同步任務。
回到我們的主線上來,GateObject域記錄了正在等待的"門物件
17. SCHAR Priority
Priority域包含了該執行緒的優先順序值,這是指它的動態優先順序,即線上程執行過程中可能由於某些原因而調整過的優先順序。
(請結合學習筆記(2)中的EPROCESS->PriorityClass,以及學習筆記(3)中的KPROCESS->BasePriority 中的關於執行緒/程式執行中的優先順序動態變化的相關知識)
18. UCHAR BasePriority
BasePriority域是執行緒的靜態優先順序,其初始值是所屬程式的BasePriority值(KPROCESS->BasePriority),以後可以通過KeSetBasePriorityThread()函式重新設定
關於優先順序的動態調整,有一點要注意: 1) 0~15是"普通執行緒"的優先順序 2) 16~31是"實時執行緒"的優先順序 3) 無論執行緒的優先順序怎麼調整,它們的值不會跨過這個區域,即普通執行緒的優先順序不能超過15,實時執行緒的優先順序不能低於16(高富帥永遠是高富帥,屌絲永遠是屌絲)
19. UCHAR EnableStackSwap
EnableStackSwap域是一個布林值,說明本執行緒的核心棧是否允許被換出到外存的swap記憶體對映檔案中。
20. volatile UCHAR SwapBusy
SwapBusy域也是一個布林值,指定了本執行緒當前是否正在進行上下文環境切換(context swap),其用法是,在將執行環境切換到其他執行緒(執行緒上下文切換,即執行緒排程)"以前"設定SwapBusy域為TRUE,切換完成以後再設定回FALSE。
21. BOOLEAN Alerted[MaximumMode]
kthread.alerted[mode] 代表的是指定模式下是否為"已經被喚醒"
wrk: NT_Design_Workbook\alerts
22. 執行緒處於等待狀態
union
{
LIST_ENTRY WaitListEntry;
SINGLE_LIST_ENTRY SwapListEntry;
};
1) WaitListEntry: 它是一個雙連結串列節點,當一個執行緒正在等待"被執行"時,WaitListEntry作為一個執行緒節點加入到一個連結串列中,結合我們之前學習KPROCESS時的KPROCESS->ReadyListHead。
我們知道,在程式被換入記憶體過程中,就緒狀態的執行緒將被加入到以程式的ReadyListHead域為連結串列頭的雙連結串列中,連結串列中的節點即為執行緒的WaitListEntry域。
(請參考學習筆記(3))
2) SwapListEntry: 它是一個單連結串列節點,它被用於當執行緒的"核心棧"需要被換入時,插入到以全域性變數KiStackInSwapListHead為連結串列頭的單連結串列中。另外,當一個執行緒處於
DeferredReady(延遲的就緒)狀態時,其SwapListEntry將被插入到某個處理器的DeferredReadyListHead連結串列中。
23. PRKQUEUE Queue
Queue域是一個佇列分發器物件,如果不為NULL,則表示當前程式正在處理此佇列物件中的項
24. ULONG WaitTime
WaitTime域記錄了一個執行緒進入等待時刻的時間點(時鐘滴答值的低32位),主要用於"平衡集管理器"根據一個執行緒的等待時間的先後來做一些決策,這裡涉及到一些執行緒排程的知識,我們擴充套件一下。
平衡集管理器 平衡集管理器(balance set manager)是在系統初始化時建立的。它是一個系統程式,其優先順序為16,最低的"實時優先順序",因此比普通的非實時執行緒的優先順序高。平衡集管理器的實際用途是
維持系統記憶體資源的平衡,當多個程式發生資源競爭時,平衡集管理器負責從系統全域性來排程"可用的實體記憶體"以及調整每個程式的"工作集"。 平衡集是指所有具備資源分配資格的程式的集合,當然也包括System程式。當系統記憶體緊缺時,它一方面把擁有較多記憶體的程式從它們的工作集中換出一些頁面,另一方面把不滿足執行條件的
程式排除在平衡集外(即不再給它分配記憶體)。這些被排除在平衡集以外的程式,只有當其中的執行緒滿足執行條件並且系統有了足夠空閒記憶體時,才會被重新加入到平衡集中。這就是平衡集的
基礎概念。
我們現在知道了,"平衡集管理器"在對執行緒進行排程的時候的確需要使用到"執行緒已等待的累積時間"這一引數,那它到底是怎麼來排程的呢?這裡面的排程演算法又是怎樣的呢?
我們首先看一下如何評價一個排程演算法。衡量排程演算法的準則包括多個方面
1) 公平性: 排程演算法在選擇下一個執行的執行緒時,要考慮到同等地位的執行緒必須擁有相同的機會獲得處理器執行權 2) CPU的有效利用,只要有程式或執行緒還在等待執行(就緒態),就不能讓CPU空閒著 3) 不同型別的作業系統對排程演算法會有不同的需求: 實時作業系統對響應時間有"最低要求",有時候在"吞吐量最大化"和"最小響應時間時間"之間可能無法兼顧
(我們要牢記這些基本思想,因為我們會發現,不管作業系統的排程演算法怎麼變,它的核心思想就是從這3條裡衍生而來的)
從大的分類上來說,排程演算法可以分為"非搶佔式"和搶佔式(這只是個大分類,實際的作業系統排程演算法中並沒有這兩種名字的演算法)。而是從這兩種演算法的衍生出來的演算法。
1) 先到先服務演算法 在"非搶佔式演算法"中,這一演算法比較簡單易懂,用FIFO佇列來實現排隊,此演算法簡單,易於實現,但是,如果每個執行緒執行任務單元所需要的時間長短不一的話,則演算法的實際效果可能非常
不"公平"(不符合我們的基本原則,所以在windows中並沒有採用,或者說沒有完全採用這種演算法) 2) 時間片輪轉排程演算法 顧名思義,處理器的時間被分成了最大長度不超過某個值的時間片段(一般是20ms),稱為"時間片",然後,用輪轉方法分配給每一個執行緒。當一個執行緒獲得了CPU處理的執行權之後,按照自身的
邏輯執行下去,直到時間片用完,或者自己主動放棄執行權(比如要等待一下訊號量)。這個時候,執行緒把控制權又交還給系統,系統在獲得了處理器的控制權之後,用"輪轉"的方法找到
下一個"正在等待執行(就緒態)"的執行緒(CPU總是去排程就緒佇列中的執行緒),讓它繼續執行。這種策略雖然存在上下文的切換開銷,但是卻非常有效和靈活,windows中的排程演算法雖然不是完全
採用了這種策略,但是也汲取了很多的思想從中 3) 優先順序排程演算法 我們思考時間片輪轉法的一個缺點,即它的輪轉時間是同等的,即我們假設所有的執行緒都同等重要,但實際這是不可能的,所以,優先順序排程演算法是對時間片輪轉排程演算法的一種改進(注意,
是改進,不是推翻,在優先順序排程演算法中也有時間片的思想),每個執行緒都有一個優先順序值,高優先順序的執行緒總是優先被考慮在CPU上執行。作業系統在管理執行緒時,可以讓每個優先順序(0~31)
用一個"佇列"來存放所有滿足執行條件的執行緒,這樣,當一個執行緒用完了它的時間片或者自動放棄處理器的執行權(等待一個訊號量,等待一個鎖等)時,執行緒會交出CPU的控制權,系統在獲得
了CPU的控制權後,會選擇一個優先順序"最高"的執行緒作為下一個要執行的執行緒。 每一個執行緒在優先順序佇列中的位置是由它的優先順序來決定的,同等優先順序的執行緒使用"輪轉"或"先到先服務"的策略,這也體現了技術的相容和進步
那說了這三種,有一個問題了,windows中到底採用了是什麼排程策略?前面說的記錄當前執行緒已等待時間的資訊用在哪裡呢?接下來就引出了最後一個知識點:
windows的排程演算法是一個"搶佔式"的、支援"多處理器"的"優先順序排程演算法",可以說是集大成者,幾乎綜合了前面所說的三種技術的優點。
windows為每個處理器定義了一個連結串列陣列,相同優先順序的執行緒掛在同一個連結串列中(這就相當於優先順序佇列),不同優先順序的執行緒分別屬於不同的連結串列。當一個執行緒滿足了執行條件時,它首先被掛到當前處理器的一個待分配的連結串列(稱為延遲的就緒連結串列),也就是所謂的延遲的就緒(DeferredReady)態,然後排程器會在適當的時候(當它獲得了某個CPU控制權時)把"待分配"連結串列上的這個執行緒分配到某個處理器(這個執行緒獲得的處理器)對應的優先順序的執行緒連結串列中。當這個處理器(剛剛被掛載了新執行緒的處理器)在選擇下一個要執行的執行緒時,會根據"優先順序準則"選擇此執行緒(如果沒有同等或更高優先順序的執行緒也在等待的話,如果有更高優先順序的執行緒,則可以立刻搶佔,體現了搶佔式的思想)。
另外,windows中的執行緒優先順序還有一個"靜態優先順序(KTHREAD->BasePriority)"和"動態優先順序(KTHREAD->Priority)"的概念,所謂動態優先順序是在靜態優先順序的"基礎上"根據某些特定的條件提升或降低執行緒的優先順序,
windows系統中的真實排程應該是按照動態優先順序來進行的,這點一定要注意,
windows中的執行緒優先順序調整也考慮到了很多因素:
1) 前臺執行緒(輕微提升) 2) 等待I/O完成後的執行緒(輕微提升) 3) 當前執行緒已等待時間(隨等待時間增加而輕微提升優先順序) 4) 當前執行緒已執行時間(隨執行時間增加了輕微降低優先順序)
回到我們的主線上來,WaitTime域記錄了一個執行緒進入等待時刻的時間點(我們已經知道了它存在的目的)
25. PVOID Teb
Teb域是一個特殊的域,它指向程式地址空間的一個TEB(執行緒環境塊)結構。TEB結構包含了在使用者地址空間中需要訪問的各種資訊,例如與執行緒相關的GDI資訊、系統支援的異常(SEH)、甚至還有WinSock的資訊等等,關於TEB的詳細學習,我們將放到之後專門專題學習,這裡暫且不表
26. 執行緒的定時器相關
union { KTIMER Timer; struct { UCHAR TimerFill[KTIMER_ACTUAL_LENGTH]; union { struct { LONG AutoAlignment : 1; LONG DisableBoost : 1; LONG ReservedFlags : 30; }; LONG ThreadFlags; }; }; };
1) Timer域是一個附在一個執行緒上的定時器,當一個執行緒在執行過程中需要定時器時,比如實現可超時的等待函式(KeWaitForSingleObject、KeWaitForMultipleObjects)
NTSTATUS KeWaitForMultipleObjects(
IN ULONG Count,
IN PVOID Object[],
IN WAIT_TYPE WaitType,
IN KWAIT_REASON WaitReason,
IN KPROCESSOR_MODE WaitMode,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Timeout OPTIONAL,
IN PKWAIT_BLOCK WaitBlockArray OPTIONAL
);
NTSTATUS KeWaitForSingleObject(
IN PVOID Object,
IN KWAIT_REASON WaitReason,
IN KPROCESSOR_MODE WaitMode,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Timeout OPTIONAL
);
就會用到此KTHREAD中的這個定時器物件。
2) AutoAlignment 和 DisableBoost 這兩個域直接繼承自所屬程式的同名標記(請參考學習筆記(3)中關於KPROCESS->AutoAlignment, KPROCESS->DisableBoost)
27. 執行緒的等待定時器物件(分發器物件)
union { KWAIT_BLOCK WaitBlock[THREAD_WAIT_OBJECTS + 1]; struct { UCHAR WaitBlockFill0[KWAIT_BLOCK_OFFSET_TO_BYTE0]; BOOLEAN SystemAffinityActive; }; struct { UCHAR WaitBlockFill1[KWAIT_BLOCK_OFFSET_TO_BYTE1]; CCHAR PreviousMode; }; struct { UCHAR WaitBlockFill2[KWAIT_BLOCK_OFFSET_TO_BYTE2]; UCHAR ResourceIndex; }; struct { UCHAR WaitBlockFill3[KWAIT_BLOCK_OFFSET_TO_BYTE3]; UCHAR LargeStack; }; };
1) WaitBlock域是一個包含4個KWAIT_BLOCK成員的陣列,它們表示可等待的定時器物件。前面介紹WaitBlockList域成員的時候說到,KWAIT_BLOCK結構代表了一個執行緒正在等待一個"分發器物件",或者說是一個分發器物件正在被一個執行緒等待,它會被同時加入到兩個雙連結串列結構中。
WaitBlock域是一個內建的陣列,核心在實現等待功能的時候:
1. 如果一個執行緒所等待的物件數量小於4(即3個分發器物件加上一個定時器物件),則核心無需另外分KWAIT_BLOCK物件記憶體(即不需要加入到那個雙連結串列中),只需直接使用WaitBlock中的陣列
成員即可。 2. 而如果等待的物件數量大於4,則核心必需分配額外的KWAIT_BLOCK物件記憶體。
由於等待操作在核心中非常頻繁,所以,利用靜態陣列來滿足大多數情況下的記憶體需要,這一優化非常有意義(這也體現了以空間換時間的基本思想)
28. LIST_ENTRY QueueListEntry
QueueListEntry域記錄了執行緒在處理一個佇列項時加入到佇列物件的執行緒連結串列中的節點地址
29. PKTRAP_FRAME TrapFrame
TrapFrame域是執行緒中很關鍵的一個成員域,在windows中,執行緒是系統排程的基礎,它代表了一個程式中的一個"控制流"。當一個執行緒離開執行狀態時,其當前的執行狀態,比如現在的指令指標(EIP)在哪裡,各個暫存器的值(EAX,EBX,..ELAGS)是什麼,等等,都必須儲存下來,以便下次再"輪"到這個執行緒執行時(再次獲得CPU執行權),可以恢復原來的執行狀態。
TrapFrame域記錄了"控制流狀態"的資料結構,它是一個指向KTRAP_FRAME型別的指標, 在 base\ntos\inc\i386.h 中
typedef struct _KTRAP_FRAME { ULONG DbgEbp; // Copy of User EBP set up so KB will work. ULONG DbgEip; // EIP of caller to system call, again, for KB. ULONG DbgArgMark; // Marker to show no args here. ULONG DbgArgPointer; // Pointer to the actual args ULONG TempSegCs; ULONG TempEsp; ULONG Dr0; ULONG Dr1; ULONG Dr2; ULONG Dr3; ULONG Dr6; ULONG Dr7; ULONG SegGs; ULONG SegEs; ULONG SegDs; ULONG Edx; ULONG Ecx; ULONG Eax; ULONG PreviousPreviousMode; PEXCEPTION_REGISTRATION_RECORD ExceptionList; ULONG SegFs; ULONG Edi; ULONG Esi; ULONG Ebx; ULONG Ebp; ULONG ErrCode; ULONG Eip; ULONG SegCs; ULONG EFlags; ULONG HardwareEsp; // WARNING - segSS:esp are only here for stacks ULONG HardwareSegSs; // that involve a ring transition. ULONG V86Es; // these will be present for all transitions from ULONG V86Ds; // V86 mode ULONG V86Fs; ULONG V86Gs; } KTRAP_FRAME;
它包含了Intel x86的所有常用的暫存器
30. PVOID CallbackStack
CallbackStack域包含了執行緒的回撥(callback)棧地址,此棧在該執行緒從"核心模式呼叫"到"使用者模式"時使用(即從核心模式返回到使用者模式的執行緒時觸發)。
31. PVOID ServiceTable
ServiceTable域指向該執行緒使用的"系統服務表(全域性變數KeServiceDescriptorTable)",如果這是一個圖形使用者介面(GUI)執行緒,此域將指向另一個"影子系統服務表(全域性變數KeServieceDescriptorTableShadow)"。這裡涉及到windows的系統服務相關的知識,我們擴充延伸一下。
windows系統服務 在windows的系統結構中,核心提供的服務都通過ntdll.dll模組被應用程式提供。windows應用程式呼叫一組系統DLL(kernel32.dll, user.dll等)中的API函式,"間接"地通過ntdll.dll
中的存根函式來呼叫核心提供的系統服務。例如,NtCreateFile是windows核心中的"建立檔案"服務例程,它執行在處理器的核心模式下,而應用程式的程式碼執行在使用者模式下,所以,應用程式
為了呼叫"建立檔案"服務,必須將處理器從使用者模式切換到核心模式下。
當然,模式切換工作不需要由應用程式自己來完成,windows提供了一個系統模組ntdll.dll,已經實現了所有系統服務的模式切換工作。為了說明這個概念,我們來看幾張圖:
使用VC++6的Dependency可以檢視kernel32.dll的匯出表。可以看到我們常見的win32呼叫函式。
可以看到,ntdll.dll完成了所謂的"穿越"任務,從使用者模式"穿越"到核心模式。Intel x86處理器提供了4種處理器執行模式(硬體),Ring0~Ring3,windows僅使用兩種模式: Ring0(核心模式)和Ring3(使用者模式)。
另外,Intel x86處理器還提供了"門(gate)"機制允許可執行程式碼從Ring3進入Ring0模式,以及從Ring0返回到Ring3模式。注意,我們所說的"模式穿越"就是指的這個"門機制",這些概念要重點記憶。
Intel x86支援4種"門描述符":
1) 呼叫門(call gate) 2) 陷阱門(trap gate) //當系統發生異常時,也會進行模式切換(請參考《深入理解計算機系統(原書第2版)》8.1節 異常的相關知識 ) 3) 中斷門(iinterrupt gate) //windows使用中斷門來實現"模式切換",其中斷號為int 2e(請參考學習筆記(3)關於IDT的知識) 4) 任務門(task gate)
回到我們最開始的問題,我們講到"系統服務表(全域性變數KeServiceDescriptorTable)"的概念,那系統服務表是什麼呢?
我們知道在ntdll.dll中的系統服務存根函式指定了一個系統服務號,比如NtCreateFile的系統服務號為27H。在核心模式下,_KiSystemServiceRepeat根據此係統服務號,就知道該呼叫核心中的哪個系統服務例程(是不是和IDT的思想很想,我們可以類比起來),以及從使用者棧拷貝多少資料到核心棧中。這一過程被稱為"系統服務分發(System Service Dispatching)"
windows實現系統服務分發的關鍵是一個稱為"SDT(Service Descriptor Table) 服務描述表"的表(windows中的這類表的名詞,要注意總結和區分,也有叫SSDT的,都是一個東西)。windows支援多個SDT,ntdll.dll中過的系統存根函式指定的"系統服務號",即eax暫存器的值,包含了表的索引和表內的服務索引。
我們說windows支援多個SDT,是因為核心全域性變數 KeServiceDescriptorTable是一個陣列(這個全域性變數指向這個陣列的基地址),陣列的每個元素指向一個SDT:
kd> dd KeServiceDescriptorTable 80553fa0 80502b8c 00000000 0000011c 80503000 80553fb0 00000000 00000000 00000000 00000000 80553fc0 00000000 00000000 00000000 00000000 80553fd0 00000000 00000000 00000000 00000000 80553fe0 00002710 bf80c0b6 00000000 00000000 80553ff0 f8172a80 f7d81b60 82814328 806e2f40 80554000 00000000 00000000 46aeb095 000000ec 80554010 11f26a55 01ceefe0 00000000 00000000
這是用winDbg對XP進行dump得到的資料。這個資料要這麼理解,因為它是對記憶體的直接dump,所以我們把要每行看成一個整體,即KeServiceDescriptorTable這個陣列的每個成員都是一個結構體
這裡面每個地址都對應著一個SDT(KSERVICE_TABLE_DESCRIPTOR_TABLES資料結構)
typedef struct _KSERVICE_TABLE_DESCRIPTOR_TABLES { PULONG_PTR Base; //System Service Dispatch Table(SDT) 這個SDT的基地址。 PULONG Count; // array of usage counters ULONG Limit; // number of table entries PUCHAR Number; // array of byte counts } KSERVICE_TABLE_DESCRIPTOR_TABLES, *PKSERVICE_TABLE_DESCRIPTOR_TABLES;
SDT結構並沒有儲存"系統服務描述表"的真實內容,知識儲存了指標,在第一個成員域中儲存了指向一個地址陣列,每4個位元組為單位,即Base指向的記憶體空間每4個位元組代表一個"服務例程"的入口地址(或者說是函式入口地址)。我們使用winDbg來dump驗證一下這個概念:
所以,總結一下:
1. 系統根據系統服務號,首先定位到以KeServiceDescriptorTable為基址的陣列中的某個元素: KeServiceDescriptorTable[index] 2. 得到某個SDT(KSERVICE_TABLE_DESCRIPTOR_TABLES)後,然後定址到這個SDT表的基地址(注意,這是一個表哦,即這個SDT表中還有很多的項) 3. 在SDT表中根據系統服務號進行"系統服務例程"的定位。例如: 服務號為0x27說明NtCreateFile服務在KeServiceDescriptorTable陣列第0個表的第39個項
Ps: 關於系統服務的概念,我在學習的時候,發現有很多種名詞,從資料上來看,它們都是指同一個東西,我這裡寫的名詞是按照WRK上來的。
1. SDT和SSDT指的是同一個東西,它們都是系統服務描述符表 2. 全域性變數KeServiceDescriptorTable的陣列成員結構: _SERVICE_DESCRIPTOR_TABLE 和 KSERVICE_TABLE_DESCRIPTOR_TABLES指的都是同一個東西,它們的結構成員都是4個欄位,
意義是一樣,不知道為啥會出現這種現象
好,回到我們的主線上來,ServiceTable域指向該執行緒使用的"系統服務表(全域性變數KeServiceDescriptorTable)"
32. UCHAR IdealProcessor
IdealProcessor域指明瞭多處理器的機器上該執行緒的理想處理器。這個執行緒的處理器"親和性"概念
33. BOOLEAN Preempted
Preempted域是一個布林值,說明這個執行緒是否被高優先順序的執行緒搶佔了,只有當一個執行緒正在執行或者正在等待執行而被高優先順序執行緒搶佔的時候,此值才會被設定成TRUE。在其他情況下,該值總是為FALSE/
34. BOOLEAN ProcessReadyQueue
ProcessReadyQueue域也是一個布林值,說明一個執行緒是否在所屬的程式KPROCESS物件的ReadyListHead連結串列中,TRUE表示在此連結串列中,FALSE表示不在。
(請參考學習筆記(3)中關於ReadyListHead的說明)
35. BOOLEAN KernelStackResident
KernelStackResident域也是一個布林值,說明該執行緒的核心棧是否駐留在記憶體中,當核心棧被換出記憶體時,該值被置成FALSE,該核心棧被換入記憶體時,該值被置成TRUE。
36. KAFFINITY Affinity
Affinity域指定了執行緒的處理器親和性,此值初始時繼承自程式物件的Affinity值。為執行緒指定的處理器集合必須是其程式的親和性處理器集合的子集。
線上程執行過程中,其Affinity值可能有兩種設定:
1. 一是系統親和性,當該執行緒執行系統任務時通過KeSetSystemAffinityThread()函式來設定 2. 二是執行緒本身的親和性,稱為使用者親和性,通過KeRevertToUserAffinityThread()函式來設定
37. KAFFINITY UserAffinity
UserAffinity域是"執行緒的使用者親和性",此值初始時也繼承子程式物件的Affinity值,以後可通過核心函式KeSetAffinityThread改變。
38. PKPROCESS Process
Process域指向執行緒的所屬的程式物件(KPROCESS),線上程初始化時指定,在此總結一下執行緒和程式的關係
1) EPROCESS->ThreadListHead: 指向當前程式所屬執行緒的ETHREAD連結串列 2) KPROCESS->ThreadListHead: 指向當前程式所屬執行緒的KTHREAD連結串列 3) ETHREAD->ThreadsProcess: 指向當前執行緒所屬的程式的EPROCESS連結串列 4) KTHREAD->Process: 指向當前執行緒所屬的程式的KPROCESS連結串列
39.APC相關
PKAPC_STATE ApcStatePointer[2] UCHAR ApcStateIndex
union
{
KAPC_STATE SavedApcState;
struct
{
UCHAR SavedApcStateFill[KAPC_STATE_ACTUAL_LENGTH];
CCHAR FreezeCount;
CCHAR SuspendCount;
UCHAR UserIdealProcessor;
UCHAR CalloutActive;
};
};
union
{
KAPC_STATE ApcState;
struct
{
UCHAR ApcStateFill[KAPC_STATE_ACTUAL_LENGTH];
BOOLEAN ApcQueueable;
volatile UCHAR NextProcessor;
volatile UCHAR DeferredProcessor;
UCHAR AdjustReason;
SCHAR AdjustIncrement;
};
};
1) ApcStateIndex域是一個索引值,它指明瞭當前APC狀態在ApcStatePointer域中的索引。由於ApcStatePointer是一個只有兩個元素的陣列,所以,ApcStateIndex的值為0或1.
2) ApcStatePointer陣列元素的型別是指向KAPC_STATE的指標:
typedef struct _KAPC_STATE { LIST_ENTRY ApcListHead[2]; PKPROCESS Process; UCHAR KernelApcInProgress; UCHAR KernelApcPending; UCHAR UserApcPending; } KAPC_STATE, *PKAPC_STATE;
ApcStatePointer[0]: 指向ApcState
ApcStatePointer[1]: 指向SavedApcState
(關於APC的相關知識請參考學習筆記(4))
40. PVOID Win32Thread
Win32Thread域是一個指標,指向由windows子系統管理的區域。
41. 和APC相關的執行緒間同步
union { KAPC SuspendApc; struct { UCHAR SuspendApcFill0[KAPC_OFFSET_TO_SPARE_BYTE0]; SCHAR Quantum; }; struct { UCHAR SuspendApcFill1[KAPC_OFFSET_TO_SPARE_BYTE1]; UCHAR QuantumReset; }; struct { UCHAR SuspendApcFill2[KAPC_OFFSET_TO_SPARE_LONG]; ULONG KernelTime; }; struct { UCHAR SuspendApcFill3[KAPC_OFFSET_TO_SYSTEMARGUMENT1]; PVOID TlsArray; }; struct { UCHAR SuspendApcFill4[KAPC_OFFSET_TO_SYSTEMARGUMENT2]; PVOID BBTData; }; struct { UCHAR SuspendApcFill5[KAPC_ACTUAL_LENGTH]; UCHAR PowerState; ULONG UserTime; }; }; union { KSEMAPHORE SuspendSemaphore; struct { UCHAR SuspendSemaphorefill[KSEMAPHORE_ACTUAL_LENGTH]; ULONG SListFaultCount; }; };
SuspendApc這個域被初始化成一個專門的APC。當該APC被插入並交付時,KiSuspendThread函式被執行,其執行結果是線上程的SuspendSemaphore訊號量上等待,直到該訊號量物件有訊號,然後執行緒被喚醒並繼續執行。
執行緒的掛起(suspend)操作正是通過這一機制來實現的。自然的,執行緒的恢復(resume)操作則是通過控制SyspendSemaphore訊號量的計數來實現的。
(關於執行緒的掛起和恢復請參閱 base\ntos\ke\thredobj.c 中的KeSuspendThread和KeFreezeAllThreads函式)
42. LIST_ENTRY ThreadListEntry
ThreadListEntry域代表了一個雙連結串列上的節點,當一個執行緒被建立時,它會被加入到程式物件的ThreadListHead連結串列中,回想前面學習KPROCESS->ThreadListHead域的相關知識。
43. PVOID SListFaultAddress
SListFaultAddress域與使用者模式互鎖單連結串列POP操作(KeUserPopEntrySListFault函式)的錯誤處理有關,它記錄了上一次使用者模式的互鎖單連結串列POP操作操作發生頁面錯誤的地址
至此,我們對程式和執行緒的相關資料結構的學習全部結束,我i也長吁一口氣,雖然下面還有一個TEB要繼續學習,但這裡先對EPROCESS/KPROCESS/ETHREAD/KTHREAD做一個概括性的總結吧:
1. 核心層的程式和執行緒物件只包含了系統資源管理和多控制流併發執行所涉及的基本資訊,而沒有包含與應用程式相關聯的資訊(如程式影像檔案和執行緒的啟動函式地址等) 2. 由於windows的執行緒排程演算法比較複雜(在搶佔式排程演算法的基礎上,還支援優先順序區域性調整等),且需要支援某些硬體結構特性,所以,自然的在KPROCESS和KTHREAD結構,有些成員的引入
直接跟這些特性有關 3. 程式物件提供了執行緒的基本執行環境,包括程式地址空間和一組程式範圍內公用的引數。執行緒物件提供了為參與執行緒排程而必須的各種資訊及其維護控制流的狀態 4. 因此,不同於核心層的程式和執行緒物件偏重於基本的功能和機制,執行體層的程式和執行緒物件更側重於管理和策略
二 TEB
在學習TEB的資料結構之前,我們還是先了解幾點
1. 和PEB一樣,TEB位於程式地址空間中 2. 定址到TEB的方式有很多: 2.1 FS:[0] -> TEB 2.2 KTHREAD->Teb 3. KPRCR和TEB指的同一個東西。這是我的理解,因為它們的地址都相同,而且用winDbg進行dump也發現它們是重合的。在WindowsXP中,許多作業系統的系統變數地址值儲存在以KPCR開始
的資料結構中
這裡提到一個FS暫存器,擴充學習一下。《看雪》上這篇帖子講的非常好,感謝作者分享: http://bbs.pediy.com/showthread.php?t=159935
kd> dt -r _kpcr 0xFFDFF000 nt!_KPCR +0x000 NtTib : _NT_TIB +0x000 ExceptionList : 0x8054a4b0 _EXCEPTION_REGISTRATION_RECORD +0x000 Next : 0x8054aabc _EXCEPTION_REGISTRATION_RECORD +0x004 Handler : 0x80536e40 _EXCEPTION_DISPOSITION nt!_except_handler3+0 +0x004 StackBase : 0x8054acf0 +0x008 StackLimit : 0x80547f00 +0x00c SubSystemTib : (null) +0x010 FiberData : (null) +0x010 Version : 0 +0x014 ArbitraryUserPointer : (null) +0x018 Self : (null) +0x01c SelfPcr : 0xffdff000 _KPCR +0x000 NtTib : _NT_TIB +0x000 ExceptionList : 0x8054a4b0 _EXCEPTION_REGISTRATION_RECORD +0x004 StackBase : 0x8054acf0 +0x008 StackLimit : 0x80547f00 +0x00c SubSystemTib : (null) +0x010 FiberData : (null) +0x010 Version : 0 +0x014 ArbitraryUserPointer : (null) +0x018 Self : (null) +0x01c SelfPcr : 0xffdff000 _KPCR +0x000 NtTib : _NT_TIB ..+0xb30 PowerState : _PROCESSOR_POWER_STATE +0x024 Irql : 0 '' +0x028 IRR : 0 +0x02c IrrActive : 0 +0x030 IDR : 0xffffffff +0x034 KdVersionBlock : 0x80546ab8 +0x038 IDT : 0x8003f400 _KIDTENTRY +0x000 Offset : 0xf19c +0x002 Selector : 8 +0x004 Access : 0x8e00 +0x006 ExtendedOffset : 0x8053 +0x03c GDT : 0x8003f000 _KGDTENTRY +0x000 LimitLow : 0 +0x002 BaseLow : 0 +0x004 HighWord : __unnamed +0x040 TSS : 0x80042000 _KTSS +0x000 Backlink : 0x8b24 ..+0x208c IntDirectionMap : [32] "???" +0x044 MajorVersion : 1 +0x046 MinorVersion : 1 +0x048 SetMember : 1 +0x04c StallScaleFactor : 0x8f7 +0x050 DebugActive : 0 '' +0x051 Number : 0 '' +0x052 Spare0 : 0 '' +0x053 SecondLevelCacheAssociativity : 0 '' +0x054 VdmAlert : 0 +0x058 KernelReserved : [14] 0 +0x090 SecondLevelCacheSize : 0 +0x094 HalReserved : [16] 0 +0x0d4 InterruptMode : 0 +0x0d8 Spare1 : 0 '' +0x0dc KernelReserved2 : [17] 0 +0x120 PrcbData : _KPRCB +0x000 MinorVersion : 1 ..+0x020 Prcb : 0xffdff120 _KPRCB +0x000 MinorVersion : 1 +0x002 MajorVersion : 1 +0x004 CurrentThread : 0x80553740 _KTHREAD +0x000 Header : _DISPATCHER_HEADER .. +0x1bb DisableBoost : 0 '' +0x008 NextThread : (null) +0x00c IdleThread : 0x80553740 _KTHREAD +0x000 Header : _DISPATCHER_HEADER .. +0x1bb DisableBoost : 0 '' +0x010 Number : 0 '' +0x011 Reserved : 0 '' +0x012 BuildType : 2 +0x014 SetMember : 1 +0x018 CpuType : 6 '' +0x019 CpuID : 1 '' +0x01a CpuStep : 0x2a07 +0x01c ProcessorState : _KPROCESSOR_STATE +0x000 ContextFrame : _CONTEXT +0x2cc SpecialRegisters : _KSPECIAL_REGISTERS +0x33c KernelReserved : [16] 0 +0x37c HalReserved : [16] 0 +0x3bc PrcbPad0 : [92] "" +0x418 LockQueue : [16] _KSPIN_LOCK_QUEUE +0x000 Next : (null) +0x004 Lock : 0x8054bd54 -> 0 +0x498 PrcbPad1 : [8] "" +0x4a0 NpxThread : 0x8256e440 _KTHREAD +0x000 Header : _DISPATCHER_HEADER .. +0x1bb DisableBoost : 0 '' +0x4a4 InterruptCount : 0x61f0d6 +0x4a8 KernelTime : 0x55a5fa +0x4ac UserTime : 0x6228 +0x4b0 DpcTime : 0x10f +0x4b4 DebugDpcTime : 0 +0x4b8 InterruptTime : 0x4aa +0x4bc AdjustDpcThreshold : 0x14 +0x4c0 PageColor : 0 +0x4c4 SkipTick : 0 +0x4c8 MultiThreadSetBusy : 0 '' +0x4c9 Spare2 : [3] "" +0x4cc ParentNode : 0x80553e00 _KNODE +0x000 ProcessorMask : 1 +0x004 Color : 0 +0x008 MmShiftedColor : 0 +0x00c FreeCount : [2] 0 +0x018 DeadStackList : _SLIST_HEADER +0x020 PfnDereferenceSListHead : _SLIST_HEADER +0x028 PfnDeferredList : (null) +0x02c Seed : 0 '' +0x02d Flags : _flags +0x4d0 MultiThreadProcessorSet : 1 +0x4d4 MultiThreadSetMaster : (null) +0x4d8 ThreadStartCount : [2] 0 +0x4e0 CcFastReadNoWait : 0 +0x4e4 CcFastReadWait : 0 +0x4e8 CcFastReadNotPossible : 0 +0x4ec CcCopyReadNoWait : 0 +0x4f0 CcCopyReadWait : 0 +0x4f4 CcCopyReadNoWaitMiss : 0 +0x4f8 KeAlignmentFixupCount : 0 +0x4fc KeContextSwitches : 0x654342d +0x500 KeDcacheFlushCount : 0 +0x504 KeExceptionDispatchCount : 0xf11e +0x508 KeFirstLevelTbFills : 0 +0x50c KeFloatingEmulationCount : 0 +0x510 KeIcacheFlushCount : 0 +0x514 KeSecondLevelTbFills : 0 +0x518 KeSystemCalls : 0x1652d035 +0x51c SpareCounter0 : [1] 0 +0x520 PPLookasideList : [16] _PP_LOOKASIDE_LIST +0x000 P : 0x82da5d30 _GENERAL_LOOKASIDE +0x004 L : 0x8054d000 _GENERAL_LOOKASIDE +0x5a0 PPNPagedLookasideList : [32] _PP_LOOKASIDE_LIST +0x000 P : 0x82db3000 _GENERAL_LOOKASIDE +0x004 L : 0x8054fb00 _GENERAL_LOOKASIDE +0x6a0 PPPagedLookasideList : [32] _PP_LOOKASIDE_LIST +0x000 P : 0x82db3080 _GENERAL_LOOKASIDE +0x004 L : 0x8054eb00 _GENERAL_LOOKASIDE +0x7a0 PacketBarrier : 0 +0x7a4 ReverseStall : 0 +0x7a8 IpiFrame : (null) +0x7ac PrcbPad2 : [52] "" +0x7e0 CurrentPacket : [3] (null) +0x7ec TargetSet : 0 +0x7f0 WorkerRoutine : (null) +0x7f4 IpiFrozen : 0 +0x7f8 PrcbPad3 : [40] "" +0x820 RequestSummary : 0 +0x824 SignalDone : (null) +0x828 PrcbPad4 : [56] "" +0x860 DpcListHead : _LIST_ENTRY [ 0x80553da4 - 0x80553da4 ] +0x000 Flink : 0x80553da4 _LIST_ENTRY [ 0xffdff980 - 0xffdff980 ] +0x004 Blink : 0x80553da4 _LIST_ENTRY [ 0xffdff980 - 0xffdff980 ] +0x868 DpcStack : 0xf8a46000 +0x86c DpcCount : 0x86cf8 +0x870 DpcQueueDepth : 1 +0x874 DpcRoutineActive : 0 +0x878 DpcInterruptRequested : 0 +0x87c DpcLastCount : 0x86cf8 +0x880 DpcRequestRate : 0 +0x884 MaximumDpcQueueDepth : 1 +0x888 MinimumDpcRate : 3 +0x88c QuantumEnd : 0 +0x890 PrcbPad5 : [16] "" +0x8a0 DpcLock : 0 +0x8a4 PrcbPad6 : [28] "" +0x8c0 CallDpc : _KDPC +0x000 Type : 19 +0x002 Number : 0x20 ' ' +0x003 Importance : 0x2 '' +0x004 DpcListEntry : _LIST_ENTRY [ 0x0 - 0x0 ] +0x00c DeferredRoutine : (null) +0x010 DeferredContext : (null) +0x014 SystemArgument1 : (null) +0x018 SystemArgument2 : (null) +0x01c Lock : (null) +0x8e0 ChainedInterruptList : (null) +0x8e4 LookasideIrpFloat : 1240 +0x8e8 SpareFields0 : [6] 0 +0x900 VendorString : [13] "GenuineIntel" +0x90d InitialApicId : 0 '' +0x90e LogicalProcessorsPerPhysicalProcessor : 0x1 '' +0x910 MHz : 0x8f6 +0x914 FeatureBits : 0x20033fff +0x918 UpdateSignature : _LARGE_INTEGER 0x17`00000000 +0x000 LowPart : 0 +0x004 HighPart : 23 +0x000 u : __unnamed +0x000 QuadPart : 98784247808 +0x920 NpxSaveArea : _FX_SAVE_AREA +0x000 U : __unnamed +0x208 NpxSavedCpu : 0 +0x20c Cr0NpxState : 0 +0xb30 PowerState : _PROCESSOR_POWER_STATE +0x000 IdleFunction : 0x80525bf2 void nt!PopProcessorIdle+0 ..+0x114 PerfSetThrottle : 0xf87fa53c long *** ERROR: Module load completed but symbols could not be loaded for intelppm.sys intelppm+53c +0x118 LastC3KernelUserTime : 0 +0x11c LastPackageIdleTime : 0 +0x024 Irql : 0 '' +0x028 IRR : 0 +0x02c IrrActive : 0 +0x030 IDR : 0xffffffff +0x034 KdVersionBlock : 0x80546ab8 +0x038 IDT : 0x8003f400 _KIDTENTRY +0x000 Offset : 0xf19c +0x002 Selector : 8 +0x004 Access : 0x8e00 +0x006 ExtendedOffset : 0x8053 +0x03c GDT : 0x8003f000 _KGDTENTRY +0x000 LimitLow : 0 +0x002 BaseLow : 0 +0x004 HighWord : __unnamed +0x000 Bytes : __unnamed +0x000 Bits : __unnamed +0x040 TSS : 0x80042000 _KTSS +0x000 Backlink : 0x8b24 .. .. +0x11c LastPackageIdleTime : 0
執行緒執行在RING0(系統地址空間)和RING3(使用者地址空間)時,FS段暫存器分別指向不同記憶體段的。
1. 執行緒執行在RING0下,FS段值是0x3B(WindowsXP下值,在Windows2000下值為0x38)(在GDT中的編號); 2. 執行在RING3下時,FS段暫存器值是0x30(在GDT中的編號)。
FS暫存器值的改變是在程式從Ring3進入Ring0後和從Ring0退回到Ring3前完成的,也就是說:都是在Ring0下給FS賦不同值的。
當執行緒執行在Ring3下時,FS指向的段是GDT中的0x30段。該段的長度為4K,基地址為當前執行緒的執行緒環境塊(TEB),所以該段也被稱為"TEB段"。
因為Windows中執行緒是不停切換的,所以該段的基地址值將隨執行緒切換而改變的。
Windows2000中程式環境塊(PEB)的地址為0X7FFDF000,該程式的
第一個執行緒的TEB地址為0X7FFDE000,
第二個TEB的地址為0X7FFDD000…..
但是在WindowsXP SP2 下這些結構的地址都是隨機對映的。所以總的來說,程式的PEB,執行緒的TEB的地址只能通過FS的方式來動態獲取了。
接下來來看看TEB的資料結構的定義,我結合我能找到的資料來作說明,還有一樣的情況,在google上並沒有找到對TEB的全部資料結構欄位的詳細分析,對SEH,PEB的這一類的文章倒是不少,希望知道的朋友能分享一些好的資料,不勝感激。
typedef struct _TEB { NT_TIB Tib; PVOID EnvironmentPointer; CLIENT_ID Cid; PVOID ActiveRpcInfo; PVOID ThreadLocalStoragePointer; PPEB Peb; ULONG LastErrorValue; ULONG CountOfOwnedCriticalSections; PVOID CsrClientThread; PVOID Win32ThreadInfo; ULONG Win32ClientInfo[0x1F]; PVOID WOW32Reserved; ULONG CurrentLocale; ULONG FpSoftwareStatusRegister; PVOID SystemReserved1[0x36]; PVOID Spare1; ULONG ExceptionCode; ULONG SpareBytes1[0x28]; PVOID SystemReserved2[0xA]; ULONG GdiRgn; ULONG GdiPen; ULONG GdiBrush; CLIENT_ID RealClientId; PVOID GdiCachedProcessHandle; ULONG GdiClientPID; ULONG GdiClientTID; PVOID GdiThreadLocaleInfo; PVOID UserReserved[5]; PVOID GlDispatchTable[0x118]; ULONG GlReserved1[0x1A]; PVOID GlReserved2; PVOID GlSectionInfo; PVOID GlSection; PVOID GlTable; PVOID GlCurrentRC; PVOID GlContext; NTSTATUS LastStatusValue; UNICODE_STRING StaticUnicodeString; WCHAR StaticUnicodeBuffer[0x105]; PVOID DeallocationStack; PVOID TlsSlots[0x40]; LIST_ENTRY TlsLinks; PVOID Vdm; PVOID ReservedForNtRpc; PVOID DbgSsReserved[0x2]; ULONG HardErrorDisabled; PVOID Instrumentation[0x10]; PVOID WinSockData; ULONG GdiBatchCount; ULONG Spare2; ULONG Spare3; ULONG Spare4; PVOID ReservedForOle; ULONG WaitingOnLoaderLock; PVOID StackCommit; PVOID StackCommitMax; PVOID StackReserved; } TEB, *PTEB;
1. NT_TIB Tib
第一個成員域Tib很重要,它是一個指向NT_TIB結構體的指標,它和SEH的定址有很大關係。(題外話,結構體的第一個成員域和結構體本身的地址是重合的,一定要注意)
typedef struct _NT_TIB { struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; PVOID StackBase; //執行緒堆疊頂部 PVOID StackLimit; //執行緒堆疊底部 PVOID SubSystemTib; union { PVOID FiberData; DWORD Version; }; PVOID ArbitraryUserPointer; struct _NT_TIB *Self; } NT_TIB; typedef NT_TIB *PNT_TIB;
1) NT_TIB結構體的第一個成員ExceptionList指向一個_EXCEPTION_REGISTRATION_RECORD結構體
typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Next; PEXCEPTION_ROUTINE Handler; } EXCEPTION_REGISTRATION_RECORD;
Next: 指向一個和自身相同的結構(SEH鏈)
Handler: 指向SEH鏈上當前這個SEH的處理函式的入口地址
nt!_KPCR +0x000 NtTib : _NT_TIB +0x000 ExceptionList : 0x8054a4b0 _EXCEPTION_REGISTRATION_RECORD +0x000 Next : 0x8054aabc _EXCEPTION_REGISTRATION_RECORD +0x004 Handler : 0x80536e40 _EXCEPTION_ROUTINE ...
所以,我們可以使用: FS[0x04] 來獲取當前SEH鏈的最頂部的那個處理函式的入口地址。我們在逆向或者shellcode的編寫中會經常看到以FS:[XX]這種格式的程式碼,這個時候一定要從資料結構出發,去了解程式碼到底是在定址什麼。
更多關於SEH的相關知識請參閱:
1. 《加密與解密3》 11.2 SEH 2. [經典文章翻譯]A_Crash_Course_on_the_Depths_of_Win32_Structured_Exception_Handling.pdf 3. 《0 DAY安全: 軟體漏洞分析技術》 6.1 windows異常處理機制
2) NT_TIB結構體的最後一個成員Self也很重要,它也可以被用來獲取DLL模組的基址,在學習筆記(3)中我們已經談過了"通過PEB(FS:[30])獲取KERNEL32.DLL基地址"
這裡再次介紹一種獲取KERNEL32.DLL的方法,即利用這個Self成員域。
首先明白,Self位於TEB的0x18偏移位置,所以是FS:[0x18]
本地執行緒的棧裡偏移18H的指標指向kernel32.dll內部,而FS:[ 0x18 ] 指向當前執行緒而且往裡四個位元組指向執行緒棧,結合棧頂指標進行對齊遍歷,找到PE檔案頭(DLL的檔案格式)
的"MZ"MSDOS標誌,就拿到了kernel32.dll基址。
下面給出程式碼:
xor esi, esi mov esi, FS:[ esi + 0x18 ] // TEB mov eax, [ esi + 4 ] // 這個是需要的棧頂 mov eax, [ eax - 0x1c ] // 指向Kernel32.dll內部 find_kernel32_base: dec eax // 開始地毯式搜尋Kernel32空間 xor ax, ax cmp word ptr [ eax ], 0x5a4d // "MZ" jne find_kernel32_base // 迴圈遍歷 ,找到則返回 eax
在編寫shellcode的時候會經常用到這種技術
3) NT_TIB結構體的第一個成員ExceptionList除了指向一個_EXCEPTION_REGISTRATION_RECORD結構體,它還可以用來列舉DLL模組的基址。
注意:FS:[ 0 ] 指向的是SHE,它指向kernel32.dll內部鏈,這樣就可以順藤摸瓜了。 FS:[ 0 ] 指向的是SHE的內層鏈,為了找到頂層異常處理,我們向外遍歷找到prev成員等於 0xffffffff 的EXCEPTION_REGISTER結構,該結構的handler值就是系統 預設的處理例程。 這裡有個細節,DLL的裝載是64K邊界對齊的,所以需要利用遍歷到的指向最後的異常處理的指標進行頁查詢,再結合PE檔案MSDOS標誌部分,只要在每個64K邊界查詢"MZ"字元就能
找到kernel32.dll基址。
彙編程式碼如下:
xor ecx, ecx mov esi, FS : [ ecx ] find_seh: mov eax, [ esi ] mov esi, eax cmp [ eax ], ecx jns find_seh // 0xffffffff mov eax, [ eax + 0x04 ] // handler find_kernel32_base: dec eax xor ax, ax cmp word ptr [ eax ], 0x5a4d jne find_kernel32_base
2. 其他
PVOID EnvironmentPointer;
CLIENT_ID Cid;
PVOID ActiveRpcInfo;
PVOID ThreadLocalStoragePointer;
不詳..
3. PPEB Peb
Peb域指向當前執行緒所屬的程式的PEB結構體的基址。也就是說,我們要獲取程式的PEB,如果在使用者模式下(即ring3應用程式)就必須先走TEB這條道路
4. 其他
剩下的欄位全部都不詳,我既沒有找到相關的資料也沒有找到利用這些其他的欄位來達到一些"特殊"目的的用法,也許是還沒開發出來吧,不過更有可能是因為我水平不夠,對這些不瞭解
下面是網上找到的一張圖:
後記:
至此,我的《寒江獨釣》學習筆記全部結束,我們完成了對程式和執行緒的資料結構的學習,在文章中,我盡我最大的能力為大家擴充套件了知識點並在自己不太清楚的地方給出了相關書籍的參考章節,windows系統和核心確實是博大精深,我不敢100%甚至不敢80%保證我寫的都是對的,一定會有理解錯誤的地方,但我願意和大家一起共同學習,共同討論,我相信這也會是一個良好的開端,因為,我發現學習windows的資料結構能夠很好的"擴充"到windows的方方面面,不僅能讓我們積累大量的基礎知識,還能讓我們對windows有一個整體的概念。
接下來準備系統的學習潘老師的《windows 核心原理與實現》,對windows的執行緒和程式建立做原始碼級的研究,也希望有共同愛好的朋友和我共同討論,共同學習