avatar

Catalog
分析SwapContext

SwapContext这个函数是Windows线程切换的核心,无论是主动切换还是系统时钟导致的线程切换,最终都会调用这个函数。在这个函数中,除了切换堆栈以外,还做了一些其它事情,了解这些细节对我们学习操作系统至关重要。

遗留问题

在分析SwapContext函数之前,来回顾两个之前的文章中并没有交代的问题:

  • 我们知道,在程序从3环进入0环时,会发生权限的切换,这就意味着堆栈发生了切换,也必然,线程发生了切换。之前学习过,3环进入0环,有两种方式,分别是中断门进0环以及快速调用。这里我们来简单的回顾一下:

    • 通过中断门进0环时,会从TSS中获取到esp0的值。
    • 快速调用进入0环时,则是从MSR寄存器中获取esp0的值,但是实际情况是,在分析快速调用进0环使用的KiFastCallEntry函数时,我们发现,快速调用进入0环时也是通过TSS来获取esp0的值的,所以MSR寄存器给的值,实际上只是作为中间过渡用

    那么问题来了,TSS寄存器里面的这个esp0,到底是哪来的?如何保证每次切换线程后,TSS中的esp0对应的仍然是当前线程的esp0呢?分析SwapContext函数时便会找到答案。

  • 另一个问题呢,是关于FS的;我们知道FS:[0]寄存器在3环时指向TEB进入0环后FS:[0]指向KPCR;系统中同时存在很多个线程,那该如何保证FS:[0]在3环时一定是指向的当前正在运行的线程呢?同样,想知道这个答案,我们也需要通过分析SwapContext函数来解开。

SwapContext

SwapContext函数比较长,就分为5个部分来进行分析,当然,这5个部分是连续的。另外,由于我已经在IDA中分析好了,这里就不贴上源码,直接通过图片来分析了。

Part1

  1. 来看看这部分做了些啥事,首先将目前线程(即将切换的线程)的线程状态置为2。这一部分有几个外部通过寄存器传进来的参数的含义,具体可以看图

  2. 第二步将Eflags入栈,在线程切换时,会有很多判断操作,势必会影响到标志寄存器的值,这里需要保存一下

  3. 接下来的4行,放在一起看。这里有两个操作:

    1)将ExceptionList入栈,由于将发生线程切换,需要保存当前线程的异常链表。ebx指向的KPCR,所以[ebx]的值刚好是KPCR的第一个成员NtTib内的第一个成员,也就是ExceptionList

    2)KPCR+0x994的位置是DPCRoutineActive,DPC是延迟过程调用,和APC相对,这里不再扩展,需要注意一点,这个会有个判断,如果DPCRoutineActive的值不为0,那就执行蓝屏程序

  4. 第四步,这个_PPerfGlobalGroupMask,仅仅在Windows Server2003中,5.2版本出现的一个字段,位于NtTib+0x08的位置,主要与日志,调式相关的。

  5. 到这就差不多了,接下来从mov ebp, cr0这条指令开始,开始第二部分的分析

Part2

  1. 来看第二部分,先让edx获取当前线程的Cr0寄存器的值。这里仅作暂存,具体后面会用到

  2. KPCR中需要保存当前线程的相关信息,所以接下来,获取到目标线程的DebugActive写入到KPCR的DebugActive位上

  3. 这一步,比较好理解。毕竟一会要进行线程切换,总不能切换到一半去执行别的任务吧。因此就把中断屏蔽了

  4. 保存当前线程的esp到KernelStack字段中,这是我们熟知的经典线程切换操作的第一步。为什么没有紧接着进行第二步的操作呢?因为还有一些细节需要处理。接着往下看

  5. 第五步,主要做一些准备工作,这里能有两个操作,分别来看看

    1)将目标线程的StackLimit保存到KPCR的StackLimit位置上

    2)将目标线程的InitialStack处的值减去0x210后,赋到StackBase上。为什么要减去0x210呢?这里涉及到了内核堆栈的结构 每个线程的内核堆栈,栈底开始共有0x210个字节用于存储浮点寄存器相关的内容。因此KPCR中记录的栈基址需要减去0x210个字节

  6. 第六步,仍然是与浮点寄存器相关,在KTHREAD+0x031的位置,有一个字段叫做NpxState。这里主要是判断NpxState有没有浮点支持,以及上一个线程和当前线程对于浮点的支持是否相同,来决定是否需要重新修改Cr0寄存器的值。

  7. 下一部分,从loc_80004983开始

Part3

  1. 这部分内容较多,慢慢来看,第一步eax-0x10,结合Part2的分析可以知道,eax刚刚提升了0x210个字节,用于存储浮点寄存器相关内容,这里又提升0x10个字节的目的,同样可以根据上图可知,_Trap_Frame结构的开始部分,有0x10字节存储的内容是用于虚拟8086模式下的值,因此这里再次提升0x10字节的堆栈

  2. 第二步是最为关键的一步,这里实现了两个关键的操作:

    1)将eax存的值赋值给TSS.esp0的位置,之前分析3环进0环时,有提到过,进入0环后的esp的位置,这里回顾一下: 而此时,eax所存的值,刚好位于快速调用进0环后esp所处的位置(InitialStack-0x210-0x10)。所以这个值,就是3环进0环后esp0的值,此处将这个值赋值给了TSS.esp0,自然也就解释了为什么TSS中存的esp0总是指向当前线程的0环堆栈,原因就是,每次堆栈切换发生时,SwapContext函数内,都会将切换后,线程堆栈栈顶存储到TSS.esp0的位置

    2)第二个操作,哎,是我们非常熟悉的线程切换的经典步骤第二步,切换堆栈。这里就不多解释了,总之,至此,堆栈切换完成了,但是还是有一些善后工作需要处理。相比海哥的ThreadSwitch模拟切换函数来说,SwapContext还是略微复杂些的。

  3. 第三步,很容易看懂,设置KPCR.NtTib.Self指向Teb。这步有啥用呢?到Part4就能明白啦

  4. 第四步,就做了一个事,判断线程切换前后的2个线程,是不是属于同一个进程,方法也很简单,分别取两个线程KTHREAD+0x44位置指向的值(这里要注意下,在KTHREAD+0x34的偏移处,有一个ApcState结构体,其中+0x10位置存着指向当前线程所属进程的指针 然后比较一下,若值不相同的话,那就将新的线程所属进程结构体的指针保存到edi中

  5. 第五步,紧接着第四步继续,如果俩线程的所属进程不同,就会走到这一步。这一步也有两个操作:

    1)因为进程切换了,因此Cr3的值也要跟着变,因此这里从新的进程中获取Cr3,并保存到TSS中

    2)同理,另一个需要更新的值,IO位图,也就是TSS最后一个元素,当然,这个值不重要,详情见图

  6. 下一部分,从loc_800049D7开始

Part4

  1. 这一部分,也就做一些收尾工作了,毕竟线程切换已经完了嘛。这里的第一步,最为关键。Part3的第三步,让KPCR.NtTib.Self指向了Teb。这里就用上了。我们有了这个Teb的地址后,就通过移位,将这个地址分3个部分(根据段描述符的结构),写入到GDT表中,下标为7的这个段描述符中。这个段描述符对应的段选择子是0x3B,也就是3环FS寄存器存着的段选择子。这就解释了文章开头提到的第二个问题,为什么3环FS:[0]指向的一定是当前线程的Teb,原因就在这里,因为每次线程切换时,都会给3环FS:[0]对应的段描述符赋上当前线程Teb的地址
  2. 第二步,主要做了一些统计相关的操作,例如,CPU发生了多少次线程切换,以及这个线程被切换了多少次
  3. 第三步,主要做了一些恢复现场的工作,具体看图中注释。

总结

至此,SwapContext函数已分析完毕,我们进一步了解了线程切换的细节,以及线程切换时,对TSS,FS的影响

参考教程:

  1. https://www.bilibili.com/video/BV1NJ411M7aE?p=51
  2. https://www.bilibili.com/video/BV1NJ411M7aE?p=52

参考文章:https://blog.csdn.net/weixin_42052102/article/details/83217867

参考笔记:张嘉杰的笔记,Joney的笔记

Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/04/03/%E5%88%86%E6%9E%90SwapContext/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶