前一篇介绍了海哥写的一份Windows线程切换代码,通过对代码的分析和学习,我们知道了线程切换的本质就是堆栈的切换,其中有一个非常关键的函数:SwitchContext,当调用这个函数时,就会导致线程切换。同样,Windows也有一个用于线程切换的函数:KiSwapContext
KiSwapContext分析
我们先从这个函数开始说起,当然,相比海哥写的代码,Windows中切换线程的代码更为复杂,但本质还是一样的,这里不作详细分析,分析关键函数,找到KiSwapContext的核心实现。
- 首先定位到KiSwapContext 根据这几步,我们发现,外层函数传来了一个未知的参数ecx
- 我们跟进调用KiSwapContext的KiSwapThread
- 分析调用KiSwapContext的代码 可以发现,ecx的值,来源于KiFindReadyThread的返回值,顾名思义,这是一个在就绪队列中查找线程的函数,因此返回值应为一个KTHREAD
- 有了上面几步的分析,再回来看,就好理解了 这几步的含义是,先把当前运行的线程取出到edi中,然后将刚刚从就绪队列中取出来的线程,放到KPCR中。我们可以看到,目前esi,edi,分别存放了切换后将执行的线程和正在执行的线程,但这里没有实现,需要进一步跟进SwapContext函数。
- 进入SwapContext函数后,忽略细节,我们可以很快找到线程切换最精髓的两条语句 堆栈切换,回忆一下,上一篇海哥写的程序里,线程切换最关键的两条语句也是这样的原理,将esp保存到原线程的KernelStack中,并将新线程的KernelStack的值赋给esp,从而实现堆栈的切换,这也就是线程切换的本质。
主动切换
函数调用过程
在分析完KiSwapContext函数后,我们可以总结出这样一个调用过程:
1 | KiSwapThread -> KiSwapContext -> SwapContext(内部实现线程切换) |
虽然,真正的切换是SwapContext函数实现的,但是经过分析,从KiSwapThread到KiSwapContext再到SwapContext是一个顺序执行的过程。所以我们可以认为,凡是调用了KiSwapThread函数,就一定会触发线程切换。
- 在IDA中查看KiSwapThread的交叉引用表 我们可以看到,一共有7个函数调用了KiSwapThread函数,说明执行这些函数时,都会发生线程切换
- 随机选取其中一个调用KiSwapThread的函数:KeWaitForSingleObject,查看KeWaitForSingleObject的交叉引用表 我们可以看到有很多函数都调用了KeWaitForSingleObject,这也意味着这些函数在执行时,都会发生线程切换,因为它们最终都会调用SwapContext函数
小结
我们可以看到,Windows中绝大部分API都会直接或间接调用SwapContext这个函数,也就是说,只要调用这些API函数,就会发生线程切换,这种通过调用API函数导致的线程切换叫做主动切换。
时钟中断切换
上面介绍了主动切换,需要依赖对系统API函数的调用才能触发。那么,如果不去主动调用系统API函数,该如何触发线程切换呢?这里介绍另一个导致线程切换的方式,通过时钟中断。
为何要采用时钟中断的方式呢?实际上我们在切换线程时,必须先让当前执行的线程停下来,保存了线程当前的环境后,再去切换线程。线程的暂停也意味着程序的暂停。那么,如何中断一个正在执行的程序呢?
- 异常:例如缺页异常或者INT N指令
- 中断:例如时钟中断
系统时钟
(IDT表)中断号 | IRQ | 说明 |
---|---|---|
0x30 | IRQ0 | 时钟中断 |
- 在Windows操作系统中,每10~20毫秒便会触发一次时钟中断
- 想要获取当前版本Windows时钟间隔值,可使用Win32API:GetSystemTimeAdjustment
时钟中断的执行流程
进入IDA,我们一起来分析一下时钟中断的执行流程
- Alt+T 搜索_IDT,找到IDT表
- 之前中断门进0环学习过,int 2e执行的是KiSystemService,而时钟中断是int 30,所以我们可以很快定位它的中断例程是KiStartUnexpectedRange()
- 进入KiStartUnexpectedRange 发现里面跳转到了KiEndUnexpectedRange函数
- 继续跟进KiEndUnexpectedRange 内部跳转到函数KiUnexpectedInterruptTail
- 进入KiUnexpectedInterruptTail内部 在这个函数结束前,我们可以看到,它调用了一个外部函数HalEndSystemInterrupt,在导入表中可以看到,这个外部函数位于HAL.dll
- 用IDA打开hal.dll,找到HalEndSystemInterrupt继续分析,这个函数不大,一眼看完就可以发现,它又调用了一个外部函数KiDispatchInterrupt 我们再次进入导入表查看 巧了嘛!这个函数是ntoskrnl的,那我们又调回去了。。。
- 我们进入KiDispatchInterrupt康康 哦吼,我们发现了什么?这不是就是SwapContext嘛!就是线程切换函数!
- 经过这么多步,终于找到了关键的函数,这里简单梳理一下流程
小结
分析完时钟中断的执行流程可以发现,时钟中断最终会执行SwapContext函数,同样会发生线程切换。
异常处理
还有一种导致线程切换的就是异常处理了。当程序发生异常时,会根据中断号,跳转到相应中断处理例程进行处理,也会导致线程的切换,这里不作详细分析了。具体的可以参考任务段这篇通过TSS模拟实现进程切换。本质同样是堆栈的切换。
关于进程切换
本质上,进程的切换就是线程的切换,所以并不存在真正意义上进程的切换,与普通线程的切换相比,进程的切换仅仅是,两个线程不属于同一进程。因此在线程切换的过程中,Cr3换了,从而进程也就换了。
总结
- 如果一个线程不调用API,并且在代码中屏蔽中断(通过CLI指令),并且程序不会出现异常,那么当前线程将永久占有CPU(单核CPU占用率100%,2核CPU占用率50%)
- Windows并且是“抢占式”操作系统,所谓的“抢“必须是当前线程允许其它线程“抢”,否则是“抢”不到的
参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?p=48
参考文章: