寫在前面
此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的復雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章后面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看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
本節練習
本節的答案將會在下一節進行講解,務必把本節練習做完后看下一個講解內容。不要偷懶,實驗是學習本教程的捷徑。
俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教程了,越到后面,不做練習的話容易夾生了,開始還明白,后來就真的一點都不明白了。本節練習較多,請保質保量的完成。本篇答案將會在文章正文講解。
0️⃣ 進程線程篇——線程切換(上) 模擬的線程切換模擬了什么切換,它們是什么?試分析。並實現模擬線程的掛起和恢復函數。
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
函數。