寫在前面
此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統核心的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統核心——簡述 ,方便學習本教程。
看此教程之前,問幾個問題,基礎知識儲備好了嗎?保護模式篇學會了嗎?系統呼叫篇學會了嗎?練習做完了嗎?沒有的話就不要繼續了。
? 華麗的分割線 ?
執行緒切換途徑
上一篇我們介紹了執行緒切換的基本概念並在3環模擬了執行緒切換,在Windows
中是如何切換執行緒的呢?模擬執行緒切換有一個函式SwitchContext
,呼叫它就能實現模擬的執行緒切換,而Windows
中有一個函式SwapContext
用來實現執行緒切換。要想確切地瞭解,我們先知道導致Windows
中執行緒切換的途徑。
主動切換
我們在應用層學習執行緒切換的時候都是說,當執行完某一個執行緒的時候,執行緒都是被切換成另一個執行緒。為什麼我說這一句,是因為這句話的意思就是執行緒是被切換的,是被動的。而事實上,執行緒是主動切換的。
之前所有的學習,為了降低學習難度並防止出現個人難以理解的莫名其妙的情況,我們把虛擬機器調整成單核的。既然執行緒是執行的,而CPU
正是執行程式碼的。多個CPU
想想似乎還有執行緒被切換的可能性,但事實上我們單個也能夠執行的好好的,這就說明執行緒是不可能“被”切換的,因為CPU
拿不出另一個手來幹活了。
Windows
通過SwapContext
用來實現執行緒切換,我們用IDA
來初步認識一下,先查詢一下它的引用:
然後我們看一下引用SwapContext
的函式KiSwapContext
的引用:
然後再看看裡面的唯一引用KiSwapThread
的引用:
然後再看看裡面的一個函式KeWaitForSingleObject
的引用:
可以發現,有大量的函式都會呼叫我們的SwapContext
,這個僅僅是一個小小的縮影。由於篇幅限制就不再展示,自己有興趣可以看看這個函式的交叉引用的數量到底多麼大。Windows
中絕大部分API
都呼叫了SwapContext
函式也就是說,當執行緒只要呼叫了API
,就是導致執行緒切換。
程式碼是在記憶體中的,如果執行緒不屬於一個程式,如果Cr3
不切換的話,明顯是不行的。執行緒切換時會比較是否屬於同一個程式,如果不是,切換Cr3
,Cr3
換了,程式也就切換了。
如果不呼叫API
,就可以一直佔用CPU嗎?
時鐘中斷
絕大部分系統核心函式都會呼叫SwapContext
函式,來實現執行緒的切換,那麼這種切換是執行緒主動呼叫的。那如果當前的執行緒不去呼叫系統API
,作業系統如何實現執行緒切換呢?
我們可以通過中斷
和異常
來實現中斷一個正在執行的程式。其中,時鐘中斷也是一種中斷,中斷號0x30
,Windows
系列作業系統為10-20毫秒。如要獲取當前的時鐘間隔值,可使用GetSystemTimeAdjustment
這個API
進行獲取。如下示意圖就是對時鐘中斷執行時的流程示意圖以供瞭解:
如果一個執行緒不呼叫API
,在程式碼中遮蔽中斷(CLI
指令),並且不會出現異常,那麼當前執行緒將永久佔有CPU
。
時間片管理
時鐘中斷會導致執行緒進行切換,但並不是說只要有時鐘中斷就一定會切換執行緒,時鐘中斷時,如下兩種情況會導致執行緒切換:
1、當前的執行緒CPU
時間片到期
2、有備用執行緒:KPCR.PrcbData.NextThread
時間片
當一個新的執行緒開始執行時,初始化程式會在_KTHREAD.Quantum
賦初始值,該值的大小由_KPROCESS.ThreadQuantum
決定。每次時鐘中斷會呼叫KeUpdateRunTime
函式,該函式每次將當前執行緒Quantum
減少3
個單位,如果減到0
,則將KPCR.PrcbData.QuantumEnd
的值設定為非0
。KiDispatchInterrupt
判斷時間片到期,呼叫KiQuantumEnd
重新設定時間片、找到要執行的執行緒。
存在備用執行緒
這個值被設定時,即使當前執行緒的CPU
時間片沒有到期,仍然會被切換。
綜上所述,本部分先做一個小總結來看看執行緒切換的三種情況:
-
當前執行緒主動呼叫
API
:graph LR API函式 --> KiSwapThread --> KiSwapContext --> SwapContext -
當前執行緒時間片到期:
graph LR KiDispatchInterrupt --> KiQuantumEnd --> SwapContext -
有備用執行緒
KPCR.PrcbData.NextThread
:graph LR KiDispatchInterrupt --> SwapContext
TSS
SwapContext
這個函式是Windows
執行緒切換的核心,無論是主動切換還是系統時鐘導致的執行緒切換,最終都會呼叫這個函式。在這個函式中除了切換堆疊意外,還做了很多其他的事情,瞭解這些細節對我們學習作業系統至關重要,接下來我們看看執行緒切換與TSS
的關係。
上一篇我們進行了執行緒的模擬切換,實現是差不多的,結合之前講解的結構體,我們就能明白執行緒切換堆疊,我們回顧一下:
上面這個圖是用來表示核心堆疊示意圖的,在 系統呼叫篇——0環層面呼叫過程(下) 中提到。
kd> dt _KTrap_Frame
nt!_KTRAP_FRAME
+0x000 DbgEbp : Uint4B
+0x004 DbgEip : Uint4B
+0x008 DbgArgMark : Uint4B
+0x00c DbgArgPointer : Uint4B
+0x010 TempSegCs : Uint4B
+0x014 TempEsp : Uint4B
+0x018 Dr0 : Uint4B
+0x01c Dr1 : Uint4B
+0x020 Dr2 : Uint4B
+0x024 Dr3 : Uint4B
+0x028 Dr6 : Uint4B
+0x02c Dr7 : Uint4B
+0x030 SegGs : Uint4B
+0x034 SegEs : Uint4B
+0x038 SegDs : Uint4B
+0x03c Edx : Uint4B
+0x040 Ecx : Uint4B
+0x044 Eax : Uint4B
+0x048 PreviousMode : Uint4B
+0x04c ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x050 SegFs : Uint4B
+0x054 Edi : Uint4B
+0x058 Esi : Uint4B
+0x05c Ebx : Uint4B
+0x060 Ebp : Uint4B
+0x064 ErrCode : Uint4B
+0x068 Eip : Uint4B
+0x06c SegCs : Uint4B
+0x070 EFlags : Uint4B
+0x074 HardwareEsp : Uint4B
+0x078 HardwareSegSs : Uint4B
+0x07c V86Es : Uint4B
+0x080 V86Ds : Uint4B
+0x084 V86Fs : Uint4B
+0x088 V86Gs : Uint4B
這個結構體是不是很熟悉,對執行緒切換也遵守這個約定的。
之前我們學習過API
呼叫流程,如果忘記的話請到 系統呼叫篇 進行復習。普通呼叫,也就是使用中斷門進入的,通過TSS.ESP0
得到0環
堆疊,而快速呼叫是從MSR
得到一個臨時0環棧,程式碼執行後仍然通過TSS.ESP0
得到當前執行緒0環
堆疊。
Intel
設計TSS
的目的是為了任務切換,在作業系統層面也就是執行緒切換,但Windows
與Linux
並沒有使用,而是採用堆疊來儲存執行緒的各種暫存器。但是一個CPU
只有一個TSS
,但是執行緒很多,如何用一個TSS來儲存所有執行緒的ESP0
呢?本問題將會作為思考題,下一篇進行詳細講述。
FS
FS:[0]
暫存器在3環時指向TEB
,進入0環後FS:[0]
指向KPCR
。系統中同時存在很多個執行緒,這就意味著FS:[0]
在3環時指向的TEB
要有多個,即每個執行緒一份。但在實際的使用中我們發現,當我們在3環檢視不同執行緒的FS
暫存器時,FS
的段選擇子都是相同的,那是如何實現通過一個FS
暫存器指向多個TEB
呢?這一切的一切都在SwapContext
這個函式裡面,逆向此函式作為本篇思考題,下一篇繼續講解。
執行緒優先順序
之前在上一篇,我們簡單介紹了執行緒的等待連結串列和排程連結串列。這部分我們談談執行緒優先順序的事情。
之前講過有三種情況會導致執行緒切換,在KiSwapThread
與KiQuantumEnd
函式中都是通過KiFindReadyThread
來找下一個要切換的執行緒,KiFindReadyThread
是根據什麼條件來選擇下一個要執行的執行緒呢?
排程連結串列有32個,每次都從頭開始查詢效率太低,所以Windows
都過一個DWORD
型別變數的變數來記錄,正好是32位,一個位代表一個連結串列,當向排程連結串列.中掛入或者摘除某個執行緒時,會判斷當前級別的連結串列是否為空,為空將.變數對應位置0
,否則置1
,這個變數就是_kiReadySummary
。多CPU
會隨機尋找KiDispatcherReadyListHead
指向的陣列中的執行緒,執行緒可以繫結某個CPU
,可以使用API
:SetThreadAffinityMask
進行設定。
如果沒有就緒執行緒怎麼辦?CPU
是不可能閒下來的,它會執行一個空閒執行緒,即為IdleThread
,它在_KPRCB
結構體中,通過它就能找到執行的執行緒,如下所示:
kd> dt _KPRCB
ntdll!_KPRCB
+0x000 MinorVersion : Uint2B
+0x002 MajorVersion : Uint2B
+0x004 CurrentThread : Ptr32 _KTHREAD
+0x008 NextThread : Ptr32 _KTHREAD
+0x00c IdleThread : Ptr32 _KTHREAD
本節練習
本節的答案將會在下一節進行講解,務必把本節練習做完後看下一個講解內容。不要偷懶,實驗是學習本教程的捷徑。
俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教程了,越到後面,不做練習的話容易夾生了,開始還明白,後來就真的一點都不明白了。本節練習較多,請保質保量的完成。本篇答案將會在文章正文講解。
1️⃣ SwapContext
有幾個引數,分別是什麼?你是如何判斷出來引數的?在哪裡實現了執行緒切換?
2️⃣ 執行緒切換的時候,會切換Cr3
嗎?切換Cr3
的條件是什麼?
3️⃣ 中斷門提權時,CPU
會從TSS
得到ESP0
和SS0
,TSS
中儲存的一定是當前執行緒的ESP0
和SS0
嗎?如何做到的?
4️⃣ FS:[0]
在3環時指向TEB
但是執行緒有很多,FS:[0]
指向的是哪個執行緒的TEB
如何做到的?
5️⃣ 0環的ExceptionList
在哪裡備份的?
6️⃣ IdleThread
是什麼?什麼時候執行?如何找到這個函式?
7️⃣ 分析KiFindReadyThread
,檢視是怎樣查詢就緒執行緒的。
8️⃣ 模擬執行緒切換與Windows
的執行緒切換有哪些區別?
9️⃣ 走一遍時鐘中斷流程,分析KeUpdateRunTine
函式。
下一篇
程式執行緒篇——總結與提升