一、前言

通过之前的学习,我们知道了线程切换有两种触发情况,一种是API主动调用切换,另一种是时钟中断切换,时钟中断方式我们还没学。

当线程切换发生时,要调用 KiFindReadyThread 函数从调度链表里找到下一个就绪线程。这次课我们就来分析 KiFindReadyThread 函数是如何根据线程优先级找到就绪线程的。

这个函数用了二分查找和大量位运算,代码非常长,就算拿着源码看,都要花些功夫。

二、预备知识

逆向之前,介绍一下必不可少的预备知识。


1. 调度链表 KiDispatcherReadyListHead

我写了一篇博客介绍调度链表:

https://blog.csdn.net/Kwansy/article/details/109545949

其中介绍了一个全局变量 KiDispatcherReadyListHead ,这个地址存储了32个链表头,分别对应32个优先级的调度链表,地址越高,优先级越高。如果FLink 等于 BLink 等于地址,说明此时链表为空,比如现在我 dd 打印,这时操作系统挂起,所有线程都处于等待状态,全部调度链表都是空的:

kd> dd KiDispatcherReadyListHead
8055bc20  8055bc20 8055bc20 8055bc28 8055bc28
8055bc30  8055bc30 8055bc30 8055bc38 8055bc38
...

2. 全局变量 _KiReadySummary

KiFindReadyThread 函数找调度线程的时候,优先选择优先级高的链表。

有一个32位全局变量 _KiReadySummary ,它的每一位都对应一个优先级,如下图,30和28位置1,表示优先级30和28的调度链表里有线程:


3. 二分查找

关于二分查找,大家可以自行学习,我以前也写过一篇博客,可供参考:

二分查找自用模板

KiFindReadyThread 函数用了二分算法来查找某个32位整数左起第一个置1的位。


4. KiFindFirstSetLeft

KiFindFirstSetLeft 是一个全局的字节数组,大小是256字节,可以用kd> db KiFindFirstSetLeft l100 查看,里面的内容如下:

const CCHAR KiFindFirstSetLeft[256] = {0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7};

这个数组配合下面的宏,可以高效的找到32位里左起第一个置1位的位置:

// 一个比较关键的宏函数,作用是找到32位整型变量 Set 里左起第一个置1的位的下标,存储到 Member 里
// 算法分析:
// 把32位分成4字节,两轮二分,确定了左起第一个“有1”的字节的偏移,记录在 _Offset
// Set >> _Offset 是把第一个有1的字节移到低8位
// KiFindFirstSetLeft[Set >> _Offset] 得到的是8位里左起第1个置1位的位置,如 0000 0001 得到的是0,0011 0000 得到的是5
// KiFindFirstSetLeft[Set >> _Offset] + _Offset 得到的是在整个32位里,左起第一个置1的位的位置
#define KeFindFirstSetLeftMember(Set, Member) {                        \ULONG _Mask;                                                       \ULONG _Offset = 16;                                                \if ((_Mask = Set >> 16) == 0) {                                    \_Offset = 0;                                                   \_Mask = Set;                                                   \}                                                                  \if (_Mask >> 8) {                                                  \_Offset += 8;                                                  \}                                                                  \*(Member) = KiFindFirstSetLeft[Set >> _Offset] + _Offset;          \
}

5. KiFindReadyThread 的参数

观察IDA,我们导入了PDB文件,显示 KiFindReadyThread 有两个参数:

; __fastcall KiFindReadyThread(x, x)
@KiFindReadyThread@8 proc near

阅读XP的源码,发现 KiFindReadyThread 的声明是这样的:

PKTHREAD
FASTCALL
KiFindReadyThread (IN ULONG ProcessorNumber,IN KPRIORITY LowPriority);

ProcessorNumber 是 CPU 编号,从 KPCR 里获取 ,单核模式下这个参数是没有用的;

LowPriority 是最低优先级,KiSwapThread 里调用,传的是0。举例说明,如果这个参数是8,那么等价于 _KiReadySummary 的低8位置0,也就是忽略优先级0-7的线程。

结合汇编,我分析出传参用到的两个寄存器:

参数:
ecx: ProcessorNumber CPU编号,从KPCR里取,单核版本不使用
edx: LowPriority 最低优先级,这里是0

三、KiFindReadyThread 单核版本源码

我把多核相关的预处理删除了,下面是我整理过的,添加了注释的 KiFindReadyThread 源码。

const CCHAR KiFindFirstSetLeft[256] = {0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7};// 一个比较关键的宏函数,作用是找到32位整型变量 Set 里左起第一个置1的位的下标,存储到 Member 里
// 算法分析:
// 把32位分成4字节,两轮二分,确定了左起第一个“有1”的字节的偏移,记录在 _Offset
// Set >> _Offset 是把第一个有1的字节移到低8位
// KiFindFirstSetLeft[Set >> _Offset] 得到的是8位里左起第1个置1位的位置,如 0000 0001 得到的是0,0011 0000 得到的是5
// KiFindFirstSetLeft[Set >> _Offset] + _Offset 得到的是在整个32位里,左起第一个置1的位的位置
#define KeFindFirstSetLeftMember(Set, Member) {                        \ULONG _Mask;                                                       \ULONG _Offset = 16;                                                \if ((_Mask = Set >> 16) == 0) {                                    \_Offset = 0;                                                   \_Mask = Set;                                                   \}                                                                  \if (_Mask >> 8) {                                                  \_Offset += 8;                                                  \}                                                                  \*(Member) = KiFindFirstSetLeft[Set >> _Offset] + _Offset;          \
}PKTHREAD
FASTCALL
KiFindReadyThread (IN ULONG ProcessorNumber,IN KPRIORITY LowPriority)
{ULONG HighPriority;PRLIST_ENTRY ListHead;PRLIST_ENTRY NextEntry;ULONG PrioritySet;KAFFINITY ProcessorSet;PKTHREAD Thread;PKTHREAD Thread1;PKTHREAD Thread2 = NULL;ULONG WaitLimit;CCHAR Processor;Processor = (CCHAR)ProcessorNumber;PrioritySet = (~((1 << LowPriority) - 1)) & KiReadySummary; // _KiReadySummary 将低 LowPriority 位清0的值KeFindFirstSetLeftMember(PrioritySet, &HighPriority); // HighPriority 等于左起第一个置1位的下标,表示该优先级有就绪线程ListHead = &KiDispatcherReadyListHead[HighPriority]; // 找到该优先级的调度链表头PrioritySet <<= (31 - HighPriority); // 此时最高位是左起第一个置1的位,如果值是0,说明没有就绪线程while (PrioritySet != 0) {//// If the next bit in the priority set is a one, then examine the// corresponding dispatcher ready queue.//// 如果最高位是1,则遍历这个优先级调度链表if ((LONG)PrioritySet < 0) {NextEntry = ListHead->Flink; // NextEntry 指向当前优先级调度链表里的第一个线程ASSERT(NextEntry != ListHead); // 当前优先级置1,链表里却没有值,是不可能的Thread = CONTAINING_RECORD(NextEntry, KTHREAD, WaitListEntry); // 计算 KTHREADRemoveEntryList(&Thread->WaitListEntry); // 从链表里删除该线程if (IsListEmpty(ListHead)) {ClearMember(HighPriority, KiReadySummary); // 如果该优先级的调度链表已经是空的,那么 KiReadySummary 相应的位清零}return Thread;}HighPriority -= 1;ListHead -= 1;PrioritySet <<= 1;};//// No thread could be found, return a null pointer.//return NULL;
}

四、逆向分析 KiFindReadyThread

有了源码,我们已经把 KiFindReadyThread 分析得非常清楚了,但是我在分析的过程中,并不是只看源码的,因为源码里有一些条件编译代码,不对照汇编看,是搞不清楚它到底有没有编译进去的。

下面我贴出我的汇编注释,仅供参考。

.text:00429884 ; 参数:
.text:00429884 ; ecx: ProcessorNumber CPU编号,从KPCR里取,单核版本不使用
.text:00429884 ; edx: LowPriority 最低优先级,这里是0
.text:00429884
.text:00429884 ; __fastcall KiFindReadyThread(x, x)
.text:00429884 @KiFindReadyThread@8 proc near          ; CODE XREF: KiAdjustQuantumThread(x)+63↑p
.text:00429884                                         ; KeDelayExecutionThread(x,x,x)+12F↑p ...
.text:00429884                 xor     eax, eax
.text:00429886                 inc     eax
.text:00429887                 mov     ecx, edx        ; ecx = LowPriority
.text:00429889                 shl     eax, cl         ; eax = 1 << LowPriority
.text:0042988B                 push    10h
.text:0042988D                 pop     ecx             ; ecx = 16
.text:0042988D                                         ; ecx 在这里的作用是记录偏移,初始化为16,意思是假设高16位至少有一个置1的位
.text:0042988E                 dec     eax
.text:0042988F                 not     eax
.text:00429891                 and     eax, _KiReadySummary ; 全局变量 _KiReadySummary 有32位,对应32个就绪队列
.text:00429891                                         ; eax = (~((1 << LowPriority) - 1)) & _KiReadySummary
.text:00429891                                         ; 此时 eax 存的是 _KiReadySummary 将低 LowPriority 位清0的值
.text:00429897                 mov     edx, eax        ;
.text:00429897                                         ; 下面是利用二分+位图实现了查找左起第一个置1位
.text:00429899                 shr     edx, 10h        ; 右移16位
.text:00429899                                         ; 如果结果是0,说明高16位全是0
.text:00429899                                         ; 如果不是0,说明高16位至少有一个置1位
.text:0042989C                 jnz     short loc_4298A2
.text:0042989E                 xor     ecx, ecx        ; ecx 清零
.text:004298A0                 mov     edx, eax        ;
.text:004298A0                                         ;
.text:004298A0                                         ; 经过第一次二分查找,ecx 存储的偏移可能是0或16
.text:004298A0                                         ; 如果是0,表示高16位全是0,只关注低16位
.text:004298A0                                         ; 此时 edx 存储了 _KiReadySummary ,但只关注低16位
.text:004298A0                                         ;
.text:004298A0                                         ; 如果是16,说明高16位不全是0,只关注高16位
.text:004298A0                                         ; 此时 edx 存储了 _KiReadySummary 的高16位
.text:004298A2
.text:004298A2 loc_4298A2:                             ; CODE XREF: KiFindReadyThread(x,x)+18↑j
.text:004298A2                 test    edx, 0FFFFFF00h ; 低8位清零
.text:004298A8                 jz      short loc_4298AD ; 如果结果为0,说明第一个置1的位就在低8位里面
.text:004298AA                 add     ecx, 8          ; 否则偏移 +8
.text:004298AA                                         ;
.text:004298AA                                         ;
.text:004298AA                                         ; 此时 ecx 存储的是左起第一个置1位所在的字节内的偏移
.text:004298AA                                         ; 举例说明,假如 _KiReadySummary 是 0x30000000
.text:004298AA                                         ; 那么 ecx 就等于 5,因为 0x30 == 0011 0000,左起第一个1下标是5
.text:004298AD
.text:004298AD loc_4298AD:                             ; CODE XREF: KiFindReadyThread(x,x)+24↑j
.text:004298AD                 mov     edx, eax
.text:004298AF                 shr     edx, cl
.text:004298B1                 push    esi             ; 保存 esi
.text:004298B2                 push    1Fh
.text:004298B4                 movsx   edx, ds:_KiFindFirstSetLeft[edx]
.text:004298BB                 add     edx, ecx        ; edx = 左起第一个置1的位的下标
.text:004298BB                                         ; 例如 _KiReadySummary = 0x30000000,计算出来就是29
.text:004298BD                 pop     ecx
.text:004298BE                 sub     ecx, edx        ; ecx = 31 - edx
.text:004298BE                                         ; ecx 的值表示左起第 ecx 位是1
.text:004298C0                 shl     eax, cl         ; _KiReadySummary 左移 cl 位
.text:004298C0                                         ; 如果结果是0,表示没有就绪线程
.text:004298C2                 lea     esi, _KiDispatcherReadyListHead[edx*8] ; 根据刚才计算得到的优先级,找对应的就绪链表
.text:004298C9                 test    eax, eax
.text:004298CB                 jz      short loc_4298D9 ; 没有就绪线程,返回NULL
.text:004298CD
.text:004298CD loc_4298CD:                             ; CODE XREF: KiFindReadyThread(x,x)+53↓j
.text:004298CD                 test    eax, eax
.text:004298CF                 jl      short loc_4298DD
.text:004298D1                 dec     edx
.text:004298D2                 sub     esi, 8
.text:004298D5                 shl     eax, 1
.text:004298D7                 jnz     short loc_4298CD
.text:004298D9
.text:004298D9 loc_4298D9:                             ; CODE XREF: KiFindReadyThread(x,x)+47↑j
.text:004298D9                 xor     eax, eax
.text:004298DB
.text:004298DB loc_4298DB:                             ; CODE XREF: KiFindReadyThread(x,x)+6C↓j
.text:004298DB                 pop     esi
.text:004298DC                 retn
.text:004298DD ; ---------------------------------------------------------------------------
.text:004298DD
.text:004298DD loc_4298DD:                             ; CODE XREF: KiFindReadyThread(x,x)+4B↑j
.text:004298DD                 mov     eax, [esi]      ; eax: 链表头.FLink
.text:004298DD                                         ; eax 指向第一个线程的 +0x60 处的 WaitListEntry
.text:004298DF                 mov     ecx, [eax]      ; ecx: 指向下一个线程的 WaitLinkEntry.FLink
.text:004298E1                 sub     eax, 60h        ; eax: 指向 KTHREAD,作为返回值
.text:004298E4                 push    edi
.text:004298E5                 mov     edi, [eax+_KTHREAD.___u24.WaitListEntry.Blink] ; edi: 指向上一个线程的 WaitListEntry.FLink
.text:004298E8                 mov     [edi], ecx      ; 上一个线程的FLink指向了下一个线程
.text:004298EA                 mov     [ecx+4], edi    ; 下一个线程的BLink指向了上一个线程的FLink
.text:004298EA                                         ; 这两步是将当前线程从链表中删除
.text:004298ED                 cmp     [esi], esi      ; 当前链表是否为空?
.text:004298EF                 pop     edi
.text:004298F0                 jnz     short loc_4298DB
.text:004298F2                 xor     esi, esi
.text:004298F4                 inc     esi
.text:004298F5                 mov     ecx, edx
.text:004298F7                 shl     esi, cl
.text:004298F9                 not     esi
.text:004298FB                 and     _KiReadySummary, esi ; 如果当前优先级调度链表为空,则修改 _KiReadySummary 相应的位
.text:00429901                 pop     esi
.text:00429902                 retn
.text:00429902 @KiFindReadyThread@8 endp

(61)分析 KiFindReadyThread 函数 —— 线程优先级相关推荐

  1. 【Linux 内核】进程优先级与调度策略 ③ ( 设置、获取线程优先级的核心函数 | 修改线程调度策略函数 )

    文章目录 一.设置.获取线程优先级的核心函数 二.修改线程调度策略函数 一.设置.获取线程优先级的核心函数 设置.获取 线程 优先级的 核心 函数 : ① 设置 " 创建线程 " ...

  2. linux线程调度函数,Linux调度策略及线程优先级设置

    Linux内核的三种调度策略: 1,SCHED_OTHER 分时调度策略, 2,SCHED_FIFO实时调度策略,先到先服务.一旦占用cpu则一直运行.一直运行直到有更高优先级任务到达或自己放弃 3, ...

  3. Windows进程与线程学习笔记(九)—— 线程优先级/进程挂靠/跨进程读写

    Windows进程与线程学习笔记(九)-- 线程优先级/进程挂靠/跨进程读写 要点回顾 线程优先级 调度链表 分析 KiFindReadyThread 分析 KiSwapThread 总结 进程挂靠 ...

  4. 进程线程006 Windows线程切换-线程优先级

    文章目录 内容回顾 调度链表 如何高效查找 如果没有就绪线程怎么办 内容回顾 之前我们已经了解过,有三种情况会导致线程切换: 当前线程主动调用API:API函数–>KiSwapThread–&g ...

  5. 线程优先级抢占实验【RT-Thread学习笔记 3】

    同时处于就绪状态的线程,优先级高的先执行. 高优先级就绪时,低优先级任务让出CPU,让高优先级任务先执行. 创建两个任务函数: //线程优先级抢占 void thread1_entry(void *p ...

  6. 了解Java线程优先级,更要知道对应操作系统的优先级,不然会踩坑

    Java 多线程系列第 6 篇. 这篇我们来看看 Java 线程的优先级. Java 线程优先级 Thread 类中,使用如下属性来代表优先级. private int priority; 我们可以通 ...

  7. [改善Java代码]线程优先级只使用三个等级

    线程的优先级(priority)决定了线程获得CPU运行的机会,优先级越高获得的运行机会越大,优先级越低获得的机会越小.Java的线程有10个级别(准确的说是11个级别,级别为0的线程是JVM,应用程 ...

  8. 【Linux 内核】线程调度示例一 ③ ( 获取线程优先级 | 设置线程调度策略 | 代码示例 )

    文章目录 一.获取线程优先级 1.pthread_attr_setschedparam 和 pthread_attr_getschedparam 函数 2.获取线程优先级代码示例 二.设置线程调度策略 ...

  9. 【Android 逆向】arm 汇编 ( 使用 IDA 解析 arm 架构的动态库文件 | 分析 malloc 函数的 arm 汇编语言 )

    文章目录 一.分析 malloc 函数的 arm 汇编语言 一.分析 malloc 函数的 arm 汇编语言 在上一篇博客 [Android 逆向]arm 汇编 ( 使用 IDA 解析 arm 架构的 ...

最新文章

  1. JVM 与 Linux 的内存关系详解
  2. globalmem设备代码分析
  3. 使用CAShapeLayer与UIBezierPath画出想要的图形
  4. Security+ 学习笔记49 事件调查
  5. UML各种图画法总结
  6. PHP验证时有用的几段代码
  7. 阿里云 oss 图片在 img 中访问失败,浏览器中正常访问
  8. 当第一资本、高盛、摩根士丹利等巨头纷纷启用CDO时,您想到了什么?
  9. 基于FPGA的电子计算器设计(中)
  10. Android P 禁用OTG U盘使用
  11. 红蜘蛛不受控制解决方案
  12. Android系统镜像编译、烧录及调试
  13. 推荐几款好用的UI框架 和 后台管理系统(开源免费)
  14. 向量图 正弦交流电路_第五节 正弦交流电路的相量(图)法求解.ppt
  15. 阿米洛键盘失灵_阿米洛 海韵评测:可爱的键帽,强大的轴型,少女心十足!...
  16. 阿里云总裁王文彬谈阿里云未来三个定位
  17. 猫和路由器是完全两码事!!!!
  18. 【机房收费系统】之结账
  19. JavaScript 代码混淆实战(七)|逗号表达式的混淆
  20. 虹科分享 | IOTA网络性能监控 | 如何有效分析VoIP问题

热门文章

  1. 成功解决pycharm 没有菜单栏
  2. 成功解决absl.flags._exceptions.UnrecognizedFlagError: Unknown command line flag 'data_format'
  3. PyQt:成功解决PyQt4升级到PyQt5改变的函数或方法
  4. PyTorch 深度学习: 60 分钟极速入门
  5. 已解决:虚拟机无法获取所有权
  6. 平凡的世界和你我 (武惠良与杜丽丽)
  7. BrowserLog——使用Chrome控制台作为Log查看器
  8. Win32汇编环境搭建教程(MASM32 SDK)
  9. [BZOJ1015] [JSOI2008] 星球大战starwar (并查集)
  10. Java 位图法排序