Windows线程切换
本来是想直接研究Win10 X64的内核,但是资料还是XP的最多,所以还是以Xp的为基础吧,毕竟win10仍然是NT架构
Vmware高版本(16)对XP的支持已经不行了(装不上VmwareTools),同时微软关闭了Xp的符号下载,windbg也没办法带符号调试,搭环境实在过于不理想,所以这里仅是对一些大佬的书面资料梳理一下
ntoskrnl.exe
ntoskrnl是windows的核心内核文件,在这里可以下载到所有Windows版本的内核文件
Download Ntoskrnl.exe and Fix Runtime Errors (exefiles.com)
线程切换
线程发生切换一般有两种情况:主动切换和被动切换,主动切换是调调用一些系统API时,里面调用的KiSwapThread导致的切换,被动切换发生于线程时间片用完,或者被抢占的时候,但是两种路径最后都会调用SwapContext来完成线程切换
会用相当多的系统调用调用到KiSwapThread,所以线程切换还是很频繁的
从KiSwapThread开始分析
1 | .text:004050BF ; _DWORD __cdecl KiSwapThread() |
如果有下一个函数,就直接调用把NextThread作为参数,调用KiSwapContext进行切换
1 | .text:004109AF ; START OF FUNCTION CHUNK FOR KiSwapThread() |
如果没有NextThread,就调用KiFindReadyThread寻找要切换的线程
1 | .text:004050D9 push ebx |
还找不到线程就是用IdleThread
1 | .text:0040EA85 ; START OF FUNCTION CHUNK FOR KiSwapThread() |
最终都是到004050F0调用KiSwapContext进一步操作,KiSwapContext就是调用SwapContext
1 | .text:00404828 ; __fastcall KiSwapContext(x) |
分析SwapContext,首先修改要切换到的线程的状态为Running
1 | .text:00404924 SwapContext proc near |
保存下异常链表
1 | .text:0040492C |
最重要的保存当前线程的esp到KernelStack
1 | .text:00404949 |
把esp恢复成新线程的堆栈
1 | .text:0040498F loc_40498F: |
把gs寄存器清零,确保fs:[0]指向KPCR
1 | .text:004049B8 loc_4049B8: |
1 | .text:004049D7 |
总结:
- 线程的切换本质上就是堆栈的切换
- 在切换线程时会清空GS寄存器
- 本质上不会进行进程切换,只不过如果切换的线程不在同一个进程,就顺带切换CR3
被动切换 时钟中断
触发流程,时钟中断注册的中断处理函数是HalpHpetClockInterrupt,其最终会调用到KiDispatchInterrupt来实施SwapContext的调用
1 | hal!HalpHpetClockInterrupt |
KeUpdateRunTime,把线程的Quantum-3,如果小于0了,且当前线程不是IdleThread(IdleThread当然随便运行),就给QuantumEnd赋值一个非0值标记时间片已经用完了
KiDispatchInterrupt和其派发中断的名称不是很相符(派发中断),这个函数首先就会判断时间片有没有用完
1 | .text:004048A2 |
如果时间片已经用完,重新分配时间片
1 | .text:00404902 |
然后下面会切换线程
1 | .text:004048B5 mov eax, [ebx+_KPCR.PrcbData.NextThread] ; 有下一个线程,就将其赋值给eax |
常用结构体
KPCR
KPCR是描述CPU信息的结构体,一个CPU有自己的一个KPCR
1 | //0x3748 bytes (sizeof) |
SelfPcr; //0x1c 指向自身的指针
Prcb; //0x20 指向KPRCB的指针
IDT TSS GDT 是这个CPU的三个表
KPRCB
KPRCB是紧跟在KPCR后面的,作为拓展结构体
1 | //0x3628 bytes (sizeof) |
CurrentThread; //0x4 当前线程
NextThread; //0x8 下个线程
IdleThread; //0xc 这个CPU的空闲线程
EPROCESS
进程结构体,进程的本体
1 | //0x2c0 bytes (sizeof) |
struct _KPROCESS Pcb; //0x0 EPROCESS里面嵌入的KPROCESS结构体
UniqueProcessId; //0xb4 进程的PID,
DebugPort; //0xec 调试的关键
ThreadListHead; //0x188 链上了当前进程内的所有线程
ActiveThreads; //0x198 活动线程的总数量
_PEB* Peb; //0x1a8 三环下的PEB
_SE_AUDIT_PROCESS_CREATION_INFO SeAuditProcessCreationInfo; //0x1ec 当前进程的完整路径
Flags; //0x270 进程的一些属性
ProcessExiting:进程退出标志位。置1后表明该进程已退出,但实际还在运行。可以达到反调试的效果。同时进程无法使用任务管理器结束。
ProcessDelete:进程退出标志位。置1后表明该进程已退出,但实际还在运行。可以达到反调试的效果。同时进程无法使用任务管理器结束。
BreakOnTermination:该位置1后,任务管理器结束进程时将提示“是否结束系统进程XXX”。结束后windbg将会断下。
VmTopDown:该位置1时,VirtualAlloc一类的申请内存函数将会从大地址开始申请。
ProcessInserted:该位置0后,OD附加进程列表找不到该进程。任务管理器结束不掉该进程。CE打不开该进程,无图标。
KPROCESS
1 | //0x98 bytes (sizeof) |
ETHREAD
1 | //0x2b8 bytes (sizeof) |
_KTHREAD Tcb; //0x0 跟EPROCESS一样,第一个结构体是嵌入的KTHREAD
StartAddress; //0x218 线程函数的起始地址
ThreadListEntry; //0x268 就是在这里链到进程的双向链表
KTHREAD
1 | //0x200 bytes (sizeof) |
Header; //0x0 可等待对象的标准头部
InitialStack; //0x28 当前线程的栈底
StackLimit; //0x2c 当前线程的最大栈顶
KernelStack; //0x30 线程切换时,会保存当前的ESP,等到再次切换回来时,从这里恢复ESP
_LIST_ENTRY ThreadListEntry; //0x1e0 这里仍然是链在进程的双向链表中