文章目录
线程切换与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保存到堆栈