windows内核研究(进程与线程-等待链表和调度链表和线程切换)
进程和线程
等待链表和调度链表
之前的章节有讲到线程的链表,当我们手动去断链时,在调试器中确实看不到相关线程了,但是并不影响操作系统的正常执行,那就说明操作系统并不是通过这个链表来访问各个线程的
等待链表
当线程调用了Sleep()或者WaitForSingleObject()等函数时,就挂到这个链表
在WinDbg中查看
dd KiWaitListHead
在我win10x86上并不是这个变量名
33个链表
线程有3种状态:就绪、等待、运行
正在运行中的线程存储在KPCR中,就绪和等待的线程全在另外的33个链表中(一个等待链表,32个就绪链表)
查看调度链表:
dd KiDispatcherReadyListHead L70
同样在我win10x86上并不是这个变量名
总结:
- 正在运行的线程在KPCR中
- 准备运行的线程在32个调度链表中(0-31级)
- 等待状态的线程存储在等待链表中
- 这些链表挂在_KTHREAD一个相同的相同的位置(我当前操作系统是9c)
线程切换-主动切换
在windows中有一个函数用来做线程切换KiSwapContext
时钟中断切换
如果要获取当前的时钟间隔,可以使用Win32API:GetSystemTimeAdjustment
当发生时钟中断时,系统的执行流程:
_IDT表中对应的函数
总结:
- 主动调用API函数
- 时针中断
- 异常处理
如果一个线程不调用API,在代码中屏蔽中断(CLI指令),并且不会出现异常,那么当前线程将永久占有CPU,单核就100%,双核就是50%
时间片管理
时钟中断的前置条件:
- 当前的线程CPU时间片到期
- 有备用线程(KPCR.PrcbData.NextThread)
当一个新的线程开始执行时,初始化程序会在_KTHREAD.QuantumReset
赋初始值,该值的大小由_KPROCESS.ThreadQuantum
决定
我们查看一下这个WmiApSrv.exe进程的信息
dt _KPROCESS afbf2040 // 查看afbf2040这个地址的_KPROCESS结构
注:内核中不同的操作系统结构体中的属性名称和值可能会不一样
每次时钟中断会调用KeUpdateRunTime
函数,该函数每次将当前线程QuantumReset
减少3个单位,如果减到0,则将KPCR.PrcbData.QuantumEnd
的值设置为非0
这个函数目前我在win10_x86环境下已经搜索不到了
调用KiDispatchInterrupt
函数来判断时间片到期,调用KiQuantumEnd(重新设置时间片,找到要运行的线程)
线程切换的三种情况总结:
- 当前线程主动调用API
- .API函数->KiSwapThread->KiSwapContext->SwapContext
- 当前线程时间片到期
- KiDispatchInterrupt->KiQuantumEnd->SwapContext
- 有备用线程(KPCR.PrcbData.NextThread)
- KiDispatchInterrupt->SwapContext
线程切换与TSS的关系
线程的内核堆栈
调用API进0环
- 普通调用:通过TSS.ESP0得到0环堆栈
- 快速调用:从MSR得到一个临时0环堆栈,代码执行后仍然通过TSS.ESP0得到当前线程0环堆栈
线程切换与FS的关系
FS:[0]寄存器在3环时指向TEB,进入0环后FS:[0]指向KPCR
在系统中同时存在多个线程,这意味着在3环FS:[0]指向多个TEB,但在实际的使用中发现,在3环查看不同线程的FS寄存器时,FS的段选择子都是相同的,那是如何实现通过一个FS寄存器指向多个TEB呢?
这其中的细节任然在SwapContext代码中实现
直接说结论(懒得翻代码了):虽然在3环中FS:[0]的段选择子没有发生变化 ,但是在其中的基址发生了变化
线程的优先级
前端有讲过,切换线程有三种情况:1、主动切换,2、当前线程时间片到期,3、有备用线程,在线程切换时,都是通过KiFindReadyThread
函数来找下一个要切换的线程
KiFindReadyThread是根据什么来选择下一个要执行的线程呢?
按照优先级进行查找:在本次查找中,如果当前高级别的链表里面有线程,那么就不会查找下一级的链表了
但是这样每次查找都要从最优先级的链表开始查效率会非常低,所以微软通过一个DWORD变量来记录,当向调度链表(32个)中挂入或者移除某个线程时, 会判断当前级别的链表是否为空,如为空就将DWORD对应的位置为0,否则置为1
这个变量是:_KiReadySummary
没有就绪线程的情况
有一种情况,就是当我_KiReadySummary变量中的所有位都为0时,没有就绪等待执行的线程了,CPU会怎么办呢?
PrcbData结构体:
- CurrentThread // 当前准备执行的线程
- NextThread // 下一个要执行的线程
IdleThread
// 空闲线程
答案是如果_KiReadySummary对应的位都为0时,那么CPU就会去把每个进程中的空闲进程拿过去来执行
进程挂靠
进程与线程的关系:
- 一个进程可以包含多个线程
- 一个进程至少要有一个线程
进程为线程提供资源,也就是提供CR3的值,CR3中存储的是页目录表的基址,CR3确定了,线程能访问的内存也就确定了
当一个线程执行如下代码时CPU要如何解析呢?
mov eax,dword ptr ds:[0x12345678]
- 当CPU解析一个线性地址时,要通过页目录表来查找到对应的物理地址,页目录表基址存储在CR3寄存器当中
- 当前CR3的值来源于当前进程(
_KPROCESS.DirectoryTableBae
) - 在
_ETHREAD
结构体当中,就有一个成员ThreadsProcess
指向当前的进程(在_KTHREAD结构体中还有一个成员中的子成员也指向当前进程:ApcState->Process
)【那在线程切换时,由谁提供指向的进程值呢?】 - 在线程切换的时候,会比较
_KTHREAD
结构体ApcState->Process
处指定的EPROCESS
是否为同一个,如果不是同一个,会将ApcState->Process
处指定的EPROCESS
的DirectoryTableBase
的值取出,赋值给CR3
参考SwapContext函数
修改CR3的值
在得知正常情况下CR3的值是由_KTHREAD中ApcState->Process成员来提供的,那么CR3中的值可以随便改吗?
mov cr3,A.DirecctoryTableBase
mov eax,dword ptr ds:[0x12345678] // 获取到的是A进程的0x12345678内存
mov cr3,B.DirecctoryTableBase
mov eax,dword ptr ds:[0x12345678] // 获取到的是B进程的0x12345678内存
mov cr3,C.DirecctoryTableBase
mov eax,dword ptr ds:[0x12345678] // 获取到的是C进程的0x12345678内存
将当前CR3的值改为其它进程称之为进程挂靠
这样就可以在当前进程中访问其它进程的内存空间(ReadProcessMemory函数)
分析NtReadVirtualMemory函数执行流程
- NtReadVirtualMemory
- KiAttachProcess
- 修改_KTHREAD结构体ApcState->Process中的值为要读取的进程的值
- 修改CR3
如果我们自己来实现代码,在切换CR3后关闭中断,并且不会调用导致线程切换的API,就可以不用去修改_KTHREAD结构体ApcState->Process中的值
跨进程读写内存
跨进程操作
进程A中的线程代码:
mov cr3,B.DirctoryTableBase // 将CR3切换为B进程
mov eax,dword ptr ds:[0x12345678] // 将B进程中的0x12345678中的值保存到eax
mov dword ptr ds:[0x00412345],eax // 再将eax中的值保存到进程B中0x00412345的地址
mov cr3,A.DirctoryTableBase // 将CR3切回A进程
思考:如何将数据传递给A进程呢?