程式執行緒篇——執行緒切換(下)

寂靜的羽夏發表於2022-01-11

寫在前面

  此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統核心的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統核心——簡述 ,方便學習本教程。

  看此教程之前,問幾個問題,基礎知識儲備好了嗎?保護模式篇學會了嗎?系統呼叫篇學會了嗎?練習做完了嗎?沒有的話就不要繼續了。


? 華麗的分割線 ?


執行緒切換途徑

  上一篇我們介紹了執行緒切換的基本概念並在3環模擬了執行緒切換,在Windows中是如何切換執行緒的呢?模擬執行緒切換有一個函式SwitchContext,呼叫它就能實現模擬的執行緒切換,而Windows中有一個函式SwapContext用來實現執行緒切換。要想確切地瞭解,我們先知道導致Windows中執行緒切換的途徑。

主動切換

  我們在應用層學習執行緒切換的時候都是說,當執行完某一個執行緒的時候,執行緒都是被切換成另一個執行緒。為什麼我說這一句,是因為這句話的意思就是執行緒是被切換的,是被動的。而事實上,執行緒是主動切換的。
  之前所有的學習,為了降低學習難度並防止出現個人難以理解的莫名其妙的情況,我們把虛擬機器調整成單核的。既然執行緒是執行的,而CPU正是執行程式碼的。多個CPU想想似乎還有執行緒被切換的可能性,但事實上我們單個也能夠執行的好好的,這就說明執行緒是不可能“被”切換的,因為CPU拿不出另一個手來幹活了。
  Windows通過SwapContext用來實現執行緒切換,我們用IDA來初步認識一下,先查詢一下它的引用:

程式執行緒篇——執行緒切換(下)

  然後我們看一下引用SwapContext的函式KiSwapContext的引用:

程式執行緒篇——執行緒切換(下)

  然後再看看裡面的唯一引用KiSwapThread的引用:

程式執行緒篇——執行緒切換(下)

  然後再看看裡面的一個函式KeWaitForSingleObject的引用:

程式執行緒篇——執行緒切換(下)

  可以發現,有大量的函式都會呼叫我們的SwapContext,這個僅僅是一個小小的縮影。由於篇幅限制就不再展示,自己有興趣可以看看這個函式的交叉引用的數量到底多麼大。Windows中絕大部分API都呼叫了SwapContext函式也就是說,當執行緒只要呼叫了API,就是導致執行緒切換。
  程式碼是在記憶體中的,如果執行緒不屬於一個程式,如果Cr3不切換的話,明顯是不行的。執行緒切換時會比較是否屬於同一個程式,如果不是,切換Cr3Cr3換了,程式也就切換了。
  如果不呼叫API,就可以一直佔用CPU嗎?

時鐘中斷

  絕大部分系統核心函式都會呼叫SwapContext
函式,來實現執行緒的切換,那麼這種切換是執行緒主動呼叫的。那如果當前的執行緒不去呼叫系統API,作業系統如何實現執行緒切換呢?
  我們可以通過中斷異常來實現中斷一個正在執行的程式。其中,時鐘中斷也是一種中斷,中斷號0x30Windows系列作業系統為10-20毫秒。如要獲取當前的時鐘間隔值,可使用GetSystemTimeAdjustment這個API進行獲取。如下示意圖就是對時鐘中斷執行時的流程示意圖以供瞭解:

graph TD KiStartUnexpectedRange --> KiEndUnexpectedRange --> KiUnexpectedInterruptTail --> HalBeginSystemInterrupt --> HalEndSystemInterrupt --> KiDispatchInterrupt --> SwapContext

  如果一個執行緒不呼叫API,在程式碼中遮蔽中斷(CLI指令),並且不會出現異常,那麼當前執行緒將永久佔有CPU

時間片管理

  時鐘中斷會導致執行緒進行切換,但並不是說只要有時鐘中斷就一定會切換執行緒,時鐘中斷時,如下兩種情況會導致執行緒切換:
  1、當前的執行緒CPU時間片到期
  2、有備用執行緒:KPCR.PrcbData.NextThread

時間片

  當一個新的執行緒開始執行時,初始化程式會在_KTHREAD.Quantum
賦初始值,該值的大小由_KPROCESS.ThreadQuantum決定。每次時鐘中斷會呼叫KeUpdateRunTime函式,該函式每次將當前執行緒Quantum減少3個單位,如果減到0,則將KPCR.PrcbData.QuantumEnd的值設定為非0KiDispatchInterrupt判斷時間片到期,呼叫KiQuantumEnd重新設定時間片、找到要執行的執行緒。

存在備用執行緒

  這個值被設定時,即使當前執行緒的CPU時間片沒有到期,仍然會被切換。

  綜上所述,本部分先做一個小總結來看看執行緒切換的三種情況:

  1. 當前執行緒主動呼叫API

    graph LR API函式 --> KiSwapThread --> KiSwapContext --> SwapContext
  2. 當前執行緒時間片到期:

    graph LR KiDispatchInterrupt --> KiQuantumEnd --> SwapContext
  3. 有備用執行緒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的目的是為了任務切換,在作業系統層面也就是執行緒切換,但WindowsLinux並沒有使用,而是採用堆疊來儲存執行緒的各種暫存器。但是一個CPU只有一個TSS,但是執行緒很多,如何用一個TSS來儲存所有執行緒的ESP0呢?本問題將會作為思考題,下一篇進行詳細講述。

FS

  FS:[0]暫存器在3環時指向TEB,進入0環後FS:[0]指向KPCR。系統中同時存在很多個執行緒,這就意味著FS:[0]在3環時指向的TEB要有多個,即每個執行緒一份。但在實際的使用中我們發現,當我們在3環檢視不同執行緒的FS暫存器時,FS的段選擇子都是相同的,那是如何實現通過一個FS暫存器指向多個TEB呢?這一切的一切都在SwapContext這個函式裡面,逆向此函式作為本篇思考題,下一篇繼續講解。

執行緒優先順序

  之前在上一篇,我們簡單介紹了執行緒的等待連結串列和排程連結串列。這部分我們談談執行緒優先順序的事情。
  之前講過有三種情況會導致執行緒切換,在KiSwapThreadKiQuantumEnd函式中都是通過KiFindReadyThread來找下一個要切換的執行緒,KiFindReadyThread是根據什麼條件來選擇下一個要執行的執行緒呢?
  排程連結串列有32個,每次都從頭開始查詢效率太低,所以Windows都過一個DWORD型別變數的變數來記錄,正好是32位,一個位代表一個連結串列,當向排程連結串列.中掛入或者摘除某個執行緒時,會判斷當前級別的連結串列是否為空,為空將.變數對應位置0,否則置1,這個變數就是_kiReadySummary。多CPU會隨機尋找KiDispatcherReadyListHead指向的陣列中的執行緒,執行緒可以繫結某個CPU,可以使用APISetThreadAffinityMask進行設定。
  如果沒有就緒執行緒怎麼辦?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得到ESP0SS0TSS中儲存的一定是當前執行緒的ESP0SS0嗎?如何做到的?
4️⃣ FS:[0]在3環時指向TEB但是執行緒有很多,FS:[0]指向的是哪個執行緒的TEB如何做到的?
5️⃣ 0環的ExceptionList在哪裡備份的?
6️⃣ IdleThread是什麼?什麼時候執行?如何找到這個函式?
7️⃣ 分析KiFindReadyThread,檢視是怎樣查詢就緒執行緒的。
8️⃣ 模擬執行緒切換與Windows的執行緒切換有哪些區別?
9️⃣ 走一遍時鐘中斷流程,分析KeUpdateRunTine函式。

下一篇

  程式執行緒篇——總結與提升

相關文章