进程线程篇——线程切换(下)
写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看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
函数。
下一篇
本文来自博客园,作者:寂静的羽夏 ,一个热爱计算机技术的菜鸟
转载请注明原文链接:https://www.cnblogs.com/wingsummer/p/15787751.html