进程线程005 SwapContext函数分析

线程切换与TSS

SwapContext这个函数是Windows线程切换的核心,无论是主动切换还是系统时钟导致的线程切换,最终都会调用这个函数

在这个函数除了切换堆栈以外,还做了很多其他事情,下面就来学习一下线程切换与TSS的关系

内核堆栈

每一个线程都有一个内核堆栈,当API调用进零环的时候,必然要切换堆栈这个堆栈就是当前线程的零环堆栈。那这个线程零环的堆栈去哪里找呢?

在这里插入图片描述

KTHREAD结构体中有三个成员:InitialStack是当前堆栈的栈底,KernelStack是当前堆栈的栈顶,StackLimit是堆栈的边界。也就是说如果我们找到了这三个成员也就找到了内核堆栈。

内核堆栈结构

在这里插入图片描述

内核堆栈从结构上大体分成两部分,第一部分从InitialStack开始的0x210个字节存储的是当前线程用到的浮点寄存器的值。

从0x210再往后就是Trap_Frame结构体。完整结构如图:

在这里插入图片描述

调用API进零环

普通调用:通过TSS.ESP0得到零环堆栈

快速调用:从MSR得到一个临时的0环栈,代码执行后仍然通过TSS.ESP0得到当前线程的0环栈

在这里插入图片描述

我们找到KiFastCallEntry的代码,0FFDFF000的位置是KPCR,首先找到KPCR偏移为0x40的位置TSS,然后再找到TSS偏移4的位置ESP0,把这个值赋给了当前的esp。然后才开始往堆栈里压入值。

那么问题来了,**TSS中的ESP0来自于哪?**答案在SwapContext的代码里。

SwapContext代码分析

Intel设计TSS的目的是为了任务切换(线程切换),但Windows与Linux并没有使用,而是采用堆栈来保存线程的各种寄存器。

那么这里就有一个问题,**一个CPU只有一个TSS,但是线程很多,如何用一个TSS来保存所有线程的ESP0呢?**答案都在SwapContext的代码里
在这里插入图片描述

找到SwapContext中与TSS相关的代码,ebx就是当前CPU对应的结构体KPCR,通过KPCR找到TSS存到ecx里,

TSS偏移4的位置是ESP0,接着将eax存储到ESP0,继续往上找一下eax的值。

在这里插入图片描述

首先将目标线程的栈底存储到eax

在这里插入图片描述

此时eax指向上图的的InitStack

在这里插入图片描述

接着减去0x210

在这里插入图片描述

此时eax指向Trap_Frame结构

在这里插入图片描述

接着再减去0x10,也就是4个成员

在这里插入图片描述

Trap_Frame最底下的四个成员是给虚拟8086模式用的。通过刚才的计算得出,当前的eax指向的是0x078的位置SS

.text:00469B1C                 mov     ecx, [ebx+40h]  ; 通过KPCR取出TSS
.text:00469B1F                 mov     [ecx+4], eax    ; 将Trap_Frame.ESP0存到TSS.ESP0

这就是SwapContext函数对TSS的使用,它会将Trap_Frame.ESP0存到TSS.ESP0。

到这里,解决了之前提出的两个问题

**TSS中的ESP0来自于哪?**来自于0环的Tram_Frame结构体

**一个CPU只有一个TSS,但是线程很多,如何用一个TSS来保存所有线程的ESP0呢?**在发生线程切换的时候,SwapContext会将目标线程的ESP0存到TSS中,然后开始切换线程。就是说TSS永远存储的是当前线程的ESP0

在这里插入图片描述

TSS中除了ESP0之外还用到了一个值就是CR3,SwapContext会将当前TSS中的CR3修改为目标进程的CR3,然后切换CR3。

下面一行代码将当前线程的IO权限位图存到了TSS里。这个成员在Windows2000以后不用了

**结论:**Intel提供的TSS在Windows里只有三个成员是有意义的:ESP0 CR3和IO权限位图

线程切换与FS

FS:[0]寄存器在3环的时候指向TEB,进入0环后FS:[0]指向KPCR。

系统中同时存在很多个线程,这就意味着FS:[0]在3环的时候指向的TEB要有多个,有一个线程就要有一个TEB,但是在实际使用中我们发现在3环查看不同线程的FS寄存器时,FS的段选择子都是相同的,那么是如何实现通过一个FS寄存器指向多个TEB呢?

答案依然在SwapContext函数的代码里。

SwapContext代码分析

找到SwapContext与TEB相关的代码

在这里插入图片描述

首先取出目标线程的TEB 放到eax里

.text:00469B67                 mov     ecx, [ebx+3Ch]  ; 通过KPCR找到GDT表

接着通过KPCR找到GDT表,存到ecx

.text:00469B6A                 mov     [ecx+3Ah], ax

这里的ax是TEB的低16位,

在这里插入图片描述

而ecx+3A就是段描述符低4个字节的16-31位Base Address,也就是将TEB的低16位写到段描述符的16-31位。

.text:00469B6E                 shr     eax, 10h

将eax右移16位,这样就能得到TEB地址的高16位。因为低16位已经写到段描述符里了。

.text:00469B71                 mov     [ecx+3Ch], al   ; 将低8位写到段描述符0-7的位置
.text:00469B74                 mov     [ecx+3Fh], ah   ; 将高8位写到段描述符31-24的位置

高16位又分成两部分写到段描述符中,先将低8位写到段描述符0-7的位置,再将高8位写到段描述符31-24的位置。

通过这几行代码就将新的线程的TEB的地址写到了当前GDT表的段描述符的基址中。

这就回答了刚才的问题:如何实现通过一个FS寄存器指向多个TEB?

因为FS段选择子虽然没有发生变化,但是在线程切换的时候,会修改段选择子所指向的段描述符的基址为新的线程的TEB的地址。

SwapContext的其他问题

SwapContext有几个参数 怎么判断出来的?

四个参数,但真正有用的只有三个,分别是:

  • esi:当前线程结构体ETHREAD指针
  • edi:要切换的线程结构体ETHREAD指针
  • ebx:KPCR

在这里插入图片描述

首先找到SwapContext的父函数KiSwapContext

在这里插入图片描述

0FFDFF01Ch存储的就是KPCR,所以参数ebx就是KPCR

.text:004699CE                 mov     edi, [ebx+124h] ; KPCR+0x124是当前线程的KTHREAD结构体

ebx是KPCR,KPCR+0x124的位置就是当前线程的KTHREAD结构体

.text:004699CC                 mov     esi, ecx        ; ecx是上一层调用的函数传进来的 是要切换线程的KTHREAD

而esi来自于ecx,ecx是上一层调用的函数传进来的 是要切换线程的KTHREAD

在这里插入图片描述

找到上一层的函数,ecx来自于eax,是KiFindReadyThread的返回值,这个函数会查找一个就绪线程返回KTHREAD结构体

SwapContext在哪里实现了线程切换?

线程切换的本质就是切换堆栈

在这里插入图片描述

这行代码将目标线程的KernelStack存到ESP里,这行代码以后另一个线程复活

0环的ExceptionList是在哪里备份的

在这里插入图片描述

这行代码会将ExceptionList存储到ecx,然后将ExceptionList保存到堆栈

发布了99 篇原创文章 · 获赞 89 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_38474570/article/details/104273858