进程线程004 Windows线程切换的三种方式

主动切换(调用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,导致线程切换

总结

  1. Windows中绝大部分的API都会调用SwapContext函数,也就是说,当线程调用了API就会导致线程切换

时钟中断切换

那么如果当前的线程不去调用系统API,操作系统是不是就无法实现线程切换了呢?实际上并不是这样?我们先要来分析一下如何中断一个正在执行的程序

如何中断一个正在执行的程序

  1. 异常 比如缺页或者INT N指令
  2. 中断 比如时钟中断

系统时钟

IDT表中断号IRQ说明
0x30IRQ0时钟中断

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。到这里,大致的流程也就分析完成了。这说明当时钟中断发生的时候,也会触发线程切换

时钟中断的执行流程:

  1. KiStartUnexpectedRange
  2. KiEndUnexpectedRange
  3. KiUnexpectedInterruptTail
  4. HalEndSystemInterrupt
  5. KiDispatchInterrupt
  6. SwapContext

总结

线程切换的几种情况

  1. 主动调用API函数
  2. 时钟中断
  3. 异常处理

如果一个线程不调用API,在代码中屏蔽中断,并且不会出现异常,那么当前线程将永久占有CPU。单核占有率100%,2核就是50%

时间片管理

我们已经知道时钟中断会导致线程进行切换,但并不是说只要有时钟中断就一定会切换线程,时钟中断时,两种情况会导致线程切换

  1. 当前线程的时间片到期
  2. 存在备用线程(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时间片总结
  1. 当一个新的线程开始执行时,初始化程序会在_KTHREAD.Quantum赋初始值,该值的大小由_KPROCESS.ThreadQuantum决定(观察ThreadQuantum大小)
  2. ​ 每次时钟中断会调用KeUpdateRunTime函数,该函数每次将当前线程Quantum减少3个单位,如果减到0,则将KPCR.PrcbData.QuantumEnd的值设置为非0
  3. KiDispatchInterrupt判断时间片到期后,调用KiQuantumEnd(重新设置时间片、找到要运行的线程)

2.存在备用线程

在这里插入图片描述

接着回到KiDispatchInterrupt函数,这里首先会判断CPU时间片是否到期,接着判断备用线程是否为0,如果在不为0有备用线程的前提下,继续往下执行

在这里插入图片描述

同样会调用SwapContext函数进行线程切换

在这里插入图片描述

如果以上两个条件都不满足,代码会进行跳转,函数直接retn返回,此时不会发生线程切换

总结

线程切换的三种情况

  1. 当前线程主动调用API:API函数–>KiSwapThread–>KiSwapContext–>SwapContext
  2. 当前线程的时间片到期:KiDispatchInterrrupt–>KiQuantumEnd–>KiSwapContext–>SwapContext
  3. 有备用线程:KiDispatchInterrrupt–>SwapContext
  4. 如果时钟中断的时候时间片没有到期且没有备用线程,那么函数会直接返回,不会发生线程切换
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鬼手56

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值