前面分析过了SwapContext函数,用来线程切换的;线程切换需要2个线程,一个是当前线程,一个是用来切换的目标线程,我们知道当前线程可以通过KPCR+0x124位置的CurrentThread获得,那么目标线程该如何获得呢?下面一块来研究一下
线程切换的方式
先回顾一下线程切换三种方式的过程:
当前线程主动调用API:
API函数 -> KiSwapThread -> KiSwapContext -> SwapContext
当前线程时间片到期:
KiDipatchInterrupt -> KiQuantumEnd -> SwapContext
KPCR中存有备用线程:
KiDispatchInterrupt -> SwapContext
在有备用线程的条件下,SwapContext的目标线程参数可以通过(KPCR.PrcbData.NextThread)直接取出。那么另外两种方式,是如何找到下一个要切换的线程呢?
- 先看主动调用API的方式,进入IDA分析一下KiSwapContext函数执行之前的流程
由图,我们可以看出,KiSwapThread函数内,会先执行KiFindReadyThread取出目标线程,之后再执行KiSwapContext函数的 - 再看时间片到期的方式,我们进入KiQuantumEnd进行分析
发现,KiQuantumEnd内部,也会先执行KiFindReadyThread函数,返回一个线程结构体
通过分析可以得知,线程切换的目标线程,与KiFindReadyThread有关,接下来就结合KiFindReadyThread函数来分析一下如何获取目标线程。
线程查找
在分析查找线程之前,我们先来回顾一下之前学习的调度链表的知识
调度链表共32个,如果说一个线程,它满足运行条件了,就会被扔到这个链表里面(根据优先级),也就是说,线程切换的时候,就是从这个调度链表里面找一个线程出来,而KiFindReadyThread函数就是干这事的。
KiFindReadyThread查找方式
这个函数的查找方式非常简单暴力,会按照优先级别进行查找:31..30..29..28
换句话说,在本次查找中,如果级别31的链表里面有线程,那么就不会查找级别为30的链表,直接从级别31的链表里取一个出来
如何高效查找
调度链表有32个,如果每次都从开始查找效率就太低了,因此Windows通过一个DWORD类型的变量来记录:
当向调度链表(32个)中挂入或者摘除某个线程时,会判断当前级别的链表是否为空,会判断当前级别的链表是否为空,为空则将DWORD变量(_KiReadySummary)对应位置0,否则置1。大致如下图:
多CPU下会随机寻找KiDispatcherReadyListHead指向的数组中的线程。线程可以绑定某个CPU(使用API:setThreadAffinityMask)
如果没有就绪线程怎么办
- 双向链表的值一样,且等于当前地址,说明该链表是空的
- 双向链表的值一样,但是不等于当前地址,说明该链表只有一个线程
- 双向链表的值不一样,说明链表中存在2个或者2个以上个线程
那么,如果32个调度链表都是空的怎么办?
我们来进入IDA看一下执行流程:
- 查看KiFindReadyThread执行后的代码
若eax值为空(即没有取到线程)那么会跳转到loc_8000EA85的位置执行 - 来看看loc_8000EA85处做了什么
实际上只做了一件事,就是给eax赋值,因为KiFindReadyThread函数没有找到就绪线程,因此eax值是空的,这里给eax赋的值,就是KPCR.PrcbData.IdleThread。也就是KPCR中存着的空闲线程
这下我们弄明白了,如果就绪链表中没有线程,那么发生线程切换时,会切换到一个Idle线程继续执行,从而保证CPU一直稳定的执行
参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?p=53
参考文章:
- https://blog.csdn.net/qq_38474570/article/details/104286223
- https://blog.csdn.net/qq_41988448/article/details/103435464
参考笔记:张嘉杰的笔记








