文章目录
- 主动切换(调用API)
- KiSwapContext函数分析
- 哪些API调用了SwapContext函数
- 总结
- 时钟中断切换
- 如何中断一个正在执行的程序
- 系统时钟
- 时钟中断的执行流程
- 总结
- 时间片管理
- 1.时间片到期
- 什么是时间片?
- 时间片什么时候发生改变?
- CPU时间片到期了如何处理?
- CPU时间片总结
- 2.存在备用线程
- 总结
主动切换(调用API)
之前我们已经学习了模拟Windows线程切换的代码,里面用于线程切换的函数就是SwitchContext。只要调用这个函数就会导致线程切换,Windows也有类似的函数:KiSwapContext
KiSwapContext函数分析
用IDA打开ntkrlpa.exe,找到KiSwapContext函数
.text:004699B4 sub esp, 10h
.text:004699B7 mov [esp+10h+var_4], ebx ; ------------------------
.text:004699BB mov [esp+10h+var_8], esi
.text:004699BF mov [esp+10h+var_C], edi ; 保存当前线程的寄存器现场
.text:004699C3 mov [esp+10h+var_10], ebp ; ---------------------------
首先,KiSwapContext保存当前线程的寄存器现场
.text:004699C6 mov ebx, ds:0FFDFF01Ch ; 取出KPCR存到ebx
接着取出KPCR存到ebx
.text:004699CC mov esi, ecx ; ecx是上一层调用的函数传进来的 是要切换线程的KTHREAD
这个ecx来自上一层函数的传参
Ctrl+X找到上一层调用
ecx来自于eax,而eax是KiFindReadyThread
函数的返回值,该函数会返回一个就绪线程的KTHREAD
.text:004699C6 mov ebx, ds:0FFDFF01Ch ; 取出KPCR存到ebx
.text:004699CC mov esi, ecx ; 要切换线程的KTHREAD
.text:004699CE mov edi, [ebx+124h] ; KPCR+0x124是当前线程的KTHREAD结构体
回到KiSwapContext函数,此时esi存储的是要切换线程的KTHREAD,edi是当前线程的KTHREAD。
.text:004699D4 mov [ebx+124h], esi ; 修改KPCR里的当前线程为目标线程
接着修改KPCR里的当前线程为目标线程
.text:004699DA mov cl, [edi+58h]
.text:004699DD call SwapContext ; 进行线程切换
接着调用SwapContext函数进行线程切换,跟进SwapContext函数。这个函数的代码比较复杂,先来看几个关键代码
这行代码将当前的ESP存储到KernelStack里,继续往下找到另外一行关键代码
这行代码将目标线程的KernelStack存到ESP里。真正的线程切换从这里开始,从这行代码往后已经不再是当前线程了,而是目标线程的堆栈。
哪些API调用了SwapContext函数
现在我们知道了只要调用了SwapContext就会导致线程切换,那么现在我们可以看一下到底有多少个API调用了这个函数
先找到KiSwapContext的函数KiSwapThread
打开交叉引用,可以看到有7个函数都调用了KiSwapThread。那就意味着只要我们调用了这里面的任何一个函数都会导致线程切换。
再来查看一下其中一个父函数KeWaitForSingleObject,看看这个函数被多少个函数调用
总共270个函数调用了父函数KeWaitForSingleObject,还有6个父函数我们没有查看。这270个函数如果再被其他函数调用也会导致线程切换
这样我们可以得出一个结论,绝大多数的内核函数都会调用SwapContext,导致线程切换
总结
- Windows中绝大部分的API都会调用SwapContext函数,也就是说,当线程调用了API就会导致线程切换
时钟中断切换
那么如果当前的线程不去调用系统API,操作系统是不是就无法实现线程切换了呢?实际上并不是这样?我们先要来分析一下如何中断一个正在执行的程序
如何中断一个正在执行的程序
- 异常 比如缺页或者INT N指令
- 中断 比如时钟中断
系统时钟
IDT表中断号 | IRQ | 说明 |
0x30 | IRQ0 | 时钟中断 |
Windows系列的操作系统每隔10-20毫秒会触发一次时钟中断。如果要获取当前系统的时钟中断间隔,可使用W32 API:GetSystemTimeAdjustment
时钟中断的执行流程
接下来分析时钟中断的执行流程
用IDA打开ntkrnlpa.exe,搜索_IDT
找到中断号为0x30的中断处理函数,只要分析这个函数,就能知道系统的执行流程
这里调用了一个非当前模块的函数
在导入表中我们可以看到这个函数来自于HAL
然后又调用了一个HAL模块中的函数。
继续跟进,用IDA打开hal.dll,找到HalBeginSystemInterrupt函数。这个函数并没有回到ntkrnlpa.exe
再往下找到HalEnableSystemInterrupt函数
这个函数在内部又调用了KiDispatchInterrupt
接着搜索导入表,可以看到这个函数来自于ntoskrl内核文件
继续在ntoskrl跟进KiDispatchInterrupt函数
这个函数里面也调用了SwapContext。到这里,大致的流程也就分析完成了。这说明当时钟中断发生的时候,也会触发线程切换
时钟中断的执行流程:
- KiStartUnexpectedRange
- KiEndUnexpectedRange
- KiUnexpectedInterruptTail
- HalEndSystemInterrupt
- KiDispatchInterrupt
- SwapContext
总结
线程切换的几种情况
- 主动调用API函数
- 时钟中断
- 异常处理
如果一个线程不调用API,在代码中屏蔽中断,并且不会出现异常,那么当前线程将永久占有CPU。单核占有率100%,2核就是50%
时间片管理
我们已经知道时钟中断会导致线程进行切换,但并不是说只要有时钟中断就一定会切换线程,时钟中断时,两种情况会导致线程切换
- 当前线程的时间片到期
- 存在备用线程(KPCR.PrcbData.NextThread)
下面分别解释这两种情况
1.时间片到期
什么是时间片?
当一个新的线程开始执行的时候,初始化程序会在_KTHREAD.Quantum
赋初始值,该值的大小由_KPROCESS.ThreadQuantum
决定
我们在winbdg中随便查看一个进程结构体
kd> dt _KPROCESS 889e0288
ntdll!_KPROCESS
+0x000 Header : _DISPATCHER_HEADER
+0x010 ProfileListHead : _LIST_ENTRY [ 0x889e0298 - 0x889e0298 ]
+0x018 DirectoryTableBase : 0x7ea1d520
+0x01c LdtDescriptor : _KGDTENTRY
+0x024 Int21Descriptor : _KIDTENTRY
+0x02c ThreadListHead : _LIST_ENTRY [ 0x889e1960 - 0x88846e58 ]
+0x034 ProcessLock : 0
+0x038 Affinity : _KAFFINITY_EX
+0x044 ReadyListHead : _LIST_ENTRY [ 0x889e02cc - 0x889e02cc ]
+0x04c SwapListEntry : _SINGLE_LIST_ENTRY
+0x050 ActiveProcessors : _KAFFINITY_EX
+0x05c AutoAlignment : 0y0
+0x05c DisableBoost : 0y0
+0x05c DisableQuantum : 0y0
+0x05c ActiveGroupsMask : 0y1
+0x05c ReservedFlags : 0y0000000000000000000000000000 (0)
+0x05c ProcessFlags : 0n8
+0x060 BasePriority : 8 ''
+0x061 QuantumReset : 6 ''
其中0x061这个位置的QuantumReset值为6。这就意味着当进程里面的线程开始执行的时候,初始化的程序就会将QuantumReset的值拿出来存到当前线程结构体的Quantum里。这个值就是当前线程时间片的大小
时间片什么时候发生改变?
每次时钟中断会调用KeUpdateRunTime函数,该函数每次将当前线程Quantum减少3个单位,如果减到0,则将KPCR.PrcbData.QuantumEnd
的值设置为非0
在IDA中找到KeUpdateRunTime函数
每一次时钟中断,都会把当前线程的CPU时间片减少3,
接着会判断这个值是否为0,如果为零,就会把QuantumEnd的值设置为非0,这个值是一个标志,标志着当前CPU的时间片有没有用完。
没有用完的时候这个值为0,如果用完了,会存储一个非0的值。
CPU时间片到期了如何处理?
KiDispatchInterrupt会判断时间片是否到期。
这个函数是每一次系统时钟中断最后要执行的函数
这行代码会判断当前的CPU时间片是否到期,当系统时间片到期后会发生跳转
如果时间片到期会将QuantumEnd修改为0,然后调用KiQuantumEnd函数,跟进这个函数
这个函数主要做的事情就是将CPU的时间片重新设置为ThreadQuantum,也就是最开始看的6
设置完成之后会调用KiFindReadyThread,通过这个函数找到下一个要运行的线程。找到以后函数返回。
也就是说KiQuantumEnd函数的作用是重设CPU时间片 找到下一个要运行的线程,接着跳转
跳转以后,先调用KiReadyThread将当前线程挂到就绪链表里,然后调用SwapContext切换线程
CPU时间片总结
- 当一个新的线程开始执行时,初始化程序会在
_KTHREAD.Quantum
赋初始值,该值的大小由_KPROCESS.ThreadQuantum
决定(观察ThreadQuantum大小) - 每次时钟中断会调用KeUpdateRunTime函数,该函数每次将当前线程Quantum减少3个单位,如果减到0,则将
KPCR.PrcbData.QuantumEnd
的值设置为非0 - KiDispatchInterrupt判断时间片到期后,调用KiQuantumEnd(重新设置时间片、找到要运行的线程)
2.存在备用线程
接着回到KiDispatchInterrupt函数,这里首先会判断CPU时间片是否到期,接着判断备用线程是否为0,如果在不为0有备用线程的前提下,继续往下执行
同样会调用SwapContext函数进行线程切换
如果以上两个条件都不满足,代码会进行跳转,函数直接retn返回,此时不会发生线程切换
总结
线程切换的三种情况
- 当前线程主动调用API:API函数–>KiSwapThread–>KiSwapContext–>SwapContext
- 当前线程的时间片到期:KiDispatchInterrrupt–>KiQuantumEnd–>KiSwapContext–>SwapContext
- 有备用线程:KiDispatchInterrrupt–>SwapContext
- 如果时钟中断的时候时间片没有到期且没有备用线程,那么函数会直接返回,不会发生线程切换