windows进程与线程学习——调度相关结构学习
来学习一下调度相关的结构
很早就听说过断链隐藏的操作,因为 Windows 都是使用链表去管理进程,线程等结构的,所以断链可以达到隐藏自身的目的。那么这里就引申出来一个问题,为什么断链可以隐身且不破坏大部分的功能呢,下面的线程调度会给出答案。
线程调度
操作系统的一些理论,线程有三种状态:就绪(ready)、等待(wait)、运行(running)。
至于为什么进程/线程断链可以达到隐藏且继续执行的目的,这里先给出答案:
- 因为 CPU 调度/执行时基于线程的,所以进程断链只会影响获取进程的 API 获取的结果而不会影响 CPU 调度。
- 因为 CPU 调度使用的和线程断链的链表不是同一个链表,因此线程断链也不会影响线程本身继续被 CPU 调度。
等待链表
在上篇文章中讲到了线程的结构,其中有一个对象:
1 | +0x074 WaitListEntry : _LIST_ENTRY |
因为它们在同一位置,所以同一时刻一个线程只能属于 WaitListEntry
中或者 SwapListEntry
中,其中等待链表是双链表结构。线程调用了Sleep
或者WaitForSingleObject
等函数时,就挂到一个链表之中,它是等待链表。
似乎 Windows7 版本开始删除了该全局变量,挂在了 KPCR 结构体下,我们可以通过以下方式找到:
1 | kd> dg 0x30 |
先从 0x30 指示的段描述符中取得 KPCR 的结构体地址,然后输出它的 data 字段,可以看到 WaitListHead
链表和 DispatcherReadyListHead
的 32 个链表。
这里验证一下,如果线程挂在 WaitListHead 中,那么线程状态应该是 waiting
状态的,观察 KTHREAD
字段说明,可以得到。
值 | 状态 | 描述 |
---|---|---|
0x00 | Initialized | 线程已初始化,但尚未开始运行。 |
0x01 | Ready | 线程处于就绪状态,可以被调度器分配给处理器执行。 |
0x02 | Running | 线程正在处理器上运行。 |
0x03 | Standby | 线程已被选择为下一个执行的线程,等待处理器空闲。 |
0x04 | Terminated | 线程已终止,正在清理资源。 |
0x05 | Waiting | 线程正在等待某个事件或资源(如 I/O、同步对象)。 |
0x06 | Transition | 线程处于等待状态,但缺少必要的资源(例如尚未加载到内存的线程堆栈)。 |
0x07 | DeferredReady | 线程曾处于等待状态,现在已准备好执行,但调度尚未发生。 |
0x08 | GateWaitObsolete | 该状态已过时,仅用于向后兼容旧版 Windows。 |
那么理论上来说,上面的线程 State
字段取值应该为 5。
取得 Flink 上的值 0x884ded7c
,因为该字段在 KTHREAD+0x74
中,而指针一般都指向对应的链表字段,所以需要将地址 -0x74,下面给出输出的部分数据
1 | kd> dt _KTHREAD 884ded7c-0x74 |
可以看到对应上了基本,线程优先级 12,线程状态 5(Waiting)。
调度链表
调度链表有 32 个圈,就是优先级是 0-31
,0为最低优先级,31 为最高,默认优先级一般是 8。改变优先级就是从一个圈里面卸下来挂到另外一个圈上,这 32 个圈是正在调度中的线程,包括准备运行的线程(Ready)。比如:只有一个 CPU
但有10 个线程在运行,那么某一时刻,正在运行的线程在 KPCR
的 data
中,其他 9 个在这 32 个圈中。
调度链表不包括正在运行的线程这一点是可以肯定的,可以做如下实验:
- 找到 KPCR 的 CurrentThread,查看对应的优先级
- 从根据优先级找到对应的调度链表,发现对应优先级的链表为空。
然后查看对应的调度链表
发现为空,可以说明,正在运行的线程不会出现在调度链表中,而是直接挂在 KPCR 的 CurrentThread
字段。
通过学习这两个结构,也可以得出一个结论了:
线程调度是基于线程,也依赖等待链表和调度链表的,不管如何断链隐藏,遍历这两个链表一定能遍历得到真实的所有线程。如果尝试把线程从这两个链表断开,那么这个线程就永远不会被调度,也就永远跑不起来了,这背离了我们隐藏线程的初衷。