(61)分析 KiFindReadyThread 函数 —— 线程优先级
一、前言
通过之前的学习,我们知道了线程切换有两种触发情况,一种是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 函数 —— 线程优先级相关推荐
- 【Linux 内核】进程优先级与调度策略 ③ ( 设置、获取线程优先级的核心函数 | 修改线程调度策略函数 )
文章目录 一.设置.获取线程优先级的核心函数 二.修改线程调度策略函数 一.设置.获取线程优先级的核心函数 设置.获取 线程 优先级的 核心 函数 : ① 设置 " 创建线程 " ...
- linux线程调度函数,Linux调度策略及线程优先级设置
Linux内核的三种调度策略: 1,SCHED_OTHER 分时调度策略, 2,SCHED_FIFO实时调度策略,先到先服务.一旦占用cpu则一直运行.一直运行直到有更高优先级任务到达或自己放弃 3, ...
- Windows进程与线程学习笔记(九)—— 线程优先级/进程挂靠/跨进程读写
Windows进程与线程学习笔记(九)-- 线程优先级/进程挂靠/跨进程读写 要点回顾 线程优先级 调度链表 分析 KiFindReadyThread 分析 KiSwapThread 总结 进程挂靠 ...
- 进程线程006 Windows线程切换-线程优先级
文章目录 内容回顾 调度链表 如何高效查找 如果没有就绪线程怎么办 内容回顾 之前我们已经了解过,有三种情况会导致线程切换: 当前线程主动调用API:API函数–>KiSwapThread–&g ...
- 线程优先级抢占实验【RT-Thread学习笔记 3】
同时处于就绪状态的线程,优先级高的先执行. 高优先级就绪时,低优先级任务让出CPU,让高优先级任务先执行. 创建两个任务函数: //线程优先级抢占 void thread1_entry(void *p ...
- 了解Java线程优先级,更要知道对应操作系统的优先级,不然会踩坑
Java 多线程系列第 6 篇. 这篇我们来看看 Java 线程的优先级. Java 线程优先级 Thread 类中,使用如下属性来代表优先级. private int priority; 我们可以通 ...
- [改善Java代码]线程优先级只使用三个等级
线程的优先级(priority)决定了线程获得CPU运行的机会,优先级越高获得的运行机会越大,优先级越低获得的机会越小.Java的线程有10个级别(准确的说是11个级别,级别为0的线程是JVM,应用程 ...
- 【Linux 内核】线程调度示例一 ③ ( 获取线程优先级 | 设置线程调度策略 | 代码示例 )
文章目录 一.获取线程优先级 1.pthread_attr_setschedparam 和 pthread_attr_getschedparam 函数 2.获取线程优先级代码示例 二.设置线程调度策略 ...
- 【Android 逆向】arm 汇编 ( 使用 IDA 解析 arm 架构的动态库文件 | 分析 malloc 函数的 arm 汇编语言 )
文章目录 一.分析 malloc 函数的 arm 汇编语言 一.分析 malloc 函数的 arm 汇编语言 在上一篇博客 [Android 逆向]arm 汇编 ( 使用 IDA 解析 arm 架构的 ...
最新文章
- JVM 与 Linux 的内存关系详解
- globalmem设备代码分析
- 使用CAShapeLayer与UIBezierPath画出想要的图形
- Security+ 学习笔记49 事件调查
- UML各种图画法总结
- PHP验证时有用的几段代码
- 阿里云 oss 图片在 img 中访问失败,浏览器中正常访问
- 当第一资本、高盛、摩根士丹利等巨头纷纷启用CDO时,您想到了什么?
- 基于FPGA的电子计算器设计(中)
- Android P 禁用OTG U盘使用
- 红蜘蛛不受控制解决方案
- Android系统镜像编译、烧录及调试
- 推荐几款好用的UI框架 和 后台管理系统(开源免费)
- 向量图 正弦交流电路_第五节 正弦交流电路的相量(图)法求解.ppt
- 阿米洛键盘失灵_阿米洛 海韵评测:可爱的键帽,强大的轴型,少女心十足!...
- 阿里云总裁王文彬谈阿里云未来三个定位
- 猫和路由器是完全两码事!!!!
- 【机房收费系统】之结账
- JavaScript 代码混淆实战(七)|逗号表达式的混淆
- 虹科分享 | IOTA网络性能监控 | 如何有效分析VoIP问题
热门文章
- 成功解决pycharm 没有菜单栏
- 成功解决absl.flags._exceptions.UnrecognizedFlagError: Unknown command line flag 'data_format'
- PyQt:成功解决PyQt4升级到PyQt5改变的函数或方法
- PyTorch 深度学习: 60 分钟极速入门
- 已解决:虚拟机无法获取所有权
- 平凡的世界和你我 (武惠良与杜丽丽)
- BrowserLog——使用Chrome控制台作为Log查看器
- Win32汇编环境搭建教程(MASM32 SDK)
- [BZOJ1015] [JSOI2008] 星球大战starwar (并查集)
- Java 位图法排序