進程線程篇——線程切換(下)


寫在前面

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

你如果是從中間插過來看的,請仔細閱讀 羽夏看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

本節練習

本節的答案將會在下一節進行講解,務必把本節練習做完后看下一個講解內容。不要偷懶,實驗是學習本教程的捷徑。

  俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教程了,越到后面,不做練習的話容易夾生了,開始還明白,后來就真的一點都不明白了。本節練習較多,請保質保量的完成。本篇答案將會在文章正文講解。

0️⃣ 進程線程篇——線程切換(上) 模擬的線程切換模擬了什么切換,它們是什么?試分析。並實現模擬線程的掛起和恢復函數。
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函數。

下一篇

  進程線程篇——總結與提升


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM