(61)分析 KiFindReadyThread 函数 —— 线程优先级
一、前言
通過(guò)之前的學(xué)習(xí),我們知道了線程切換有兩種觸發(fā)情況,一種是API主動(dòng)調(diào)用切換,另一種是時(shí)鐘中斷切換,時(shí)鐘中斷方式我們還沒(méi)學(xué)。
當(dāng)線程切換發(fā)生時(shí),要調(diào)用 KiFindReadyThread 函數(shù)從調(diào)度鏈表里找到下一個(gè)就緒線程。這次課我們就來(lái)分析 KiFindReadyThread 函數(shù)是如何根據(jù)線程優(yōu)先級(jí)找到就緒線程的。
這個(gè)函數(shù)用了二分查找和大量位運(yùn)算,代碼非常長(zhǎng),就算拿著源碼看,都要花些功夫。
二、預(yù)備知識(shí)
逆向之前,介紹一下必不可少的預(yù)備知識(shí)。
1. 調(diào)度鏈表 KiDispatcherReadyListHead
我寫(xiě)了一篇博客介紹調(diào)度鏈表:
https://blog.csdn.net/Kwansy/article/details/109545949
其中介紹了一個(gè)全局變量 KiDispatcherReadyListHead ,這個(gè)地址存儲(chǔ)了32個(gè)鏈表頭,分別對(duì)應(yīng)32個(gè)優(yōu)先級(jí)的調(diào)度鏈表,地址越高,優(yōu)先級(jí)越高。如果FLink 等于 BLink 等于地址,說(shuō)明此時(shí)鏈表為空,比如現(xiàn)在我 dd 打印,這時(shí)操作系統(tǒng)掛起,所有線程都處于等待狀態(tài),全部調(diào)度鏈表都是空的:
kd> dd KiDispatcherReadyListHead 8055bc20 8055bc20 8055bc20 8055bc28 8055bc28 8055bc30 8055bc30 8055bc30 8055bc38 8055bc38 ...2. 全局變量 _KiReadySummary
KiFindReadyThread 函數(shù)找調(diào)度線程的時(shí)候,優(yōu)先選擇優(yōu)先級(jí)高的鏈表。
有一個(gè)32位全局變量 _KiReadySummary ,它的每一位都對(duì)應(yīng)一個(gè)優(yōu)先級(jí),如下圖,30和28位置1,表示優(yōu)先級(jí)30和28的調(diào)度鏈表里有線程:
3. 二分查找
關(guān)于二分查找,大家可以自行學(xué)習(xí),我以前也寫(xiě)過(guò)一篇博客,可供參考:
二分查找自用模板
KiFindReadyThread 函數(shù)用了二分算法來(lái)查找某個(gè)32位整數(shù)左起第一個(gè)置1的位。
4. KiFindFirstSetLeft
KiFindFirstSetLeft 是一個(gè)全局的字節(jié)數(shù)組,大小是256字節(jié),可以用kd> db KiFindFirstSetLeft l100 查看,里面的內(nèi)容如下:
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};這個(gè)數(shù)組配合下面的宏,可以高效的找到32位里左起第一個(gè)置1位的位置:
// 一個(gè)比較關(guān)鍵的宏函數(shù),作用是找到32位整型變量 Set 里左起第一個(gè)置1的位的下標(biāo),存儲(chǔ)到 Member 里 // 算法分析: // 把32位分成4字節(jié),兩輪二分,確定了左起第一個(gè)“有1”的字節(jié)的偏移,記錄在 _Offset // Set >> _Offset 是把第一個(gè)有1的字節(jié)移到低8位 // KiFindFirstSetLeft[Set >> _Offset] 得到的是8位里左起第1個(gè)置1位的位置,如 0000 0001 得到的是0,0011 0000 得到的是5 // KiFindFirstSetLeft[Set >> _Offset] + _Offset 得到的是在整個(gè)32位里,左起第一個(gè)置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 的參數(shù)
觀察IDA,我們導(dǎo)入了PDB文件,顯示 KiFindReadyThread 有兩個(gè)參數(shù):
; __fastcall KiFindReadyThread(x, x) @KiFindReadyThread@8 proc near閱讀XP的源碼,發(fā)現(xiàn) KiFindReadyThread 的聲明是這樣的:
PKTHREAD FASTCALL KiFindReadyThread (IN ULONG ProcessorNumber,IN KPRIORITY LowPriority);ProcessorNumber 是 CPU 編號(hào),從 KPCR 里獲取 ,單核模式下這個(gè)參數(shù)是沒(méi)有用的;
LowPriority 是最低優(yōu)先級(jí),KiSwapThread 里調(diào)用,傳的是0。舉例說(shuō)明,如果這個(gè)參數(shù)是8,那么等價(jià)于 _KiReadySummary 的低8位置0,也就是忽略優(yōu)先級(jí)0-7的線程。
結(jié)合匯編,我分析出傳參用到的兩個(gè)寄存器:
參數(shù): ecx: ProcessorNumber CPU編號(hào),從KPCR里取,單核版本不使用 edx: LowPriority 最低優(yōu)先級(jí),這里是0三、KiFindReadyThread 單核版本源碼
我把多核相關(guān)的預(yù)處理刪除了,下面是我整理過(guò)的,添加了注釋的 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};// 一個(gè)比較關(guān)鍵的宏函數(shù),作用是找到32位整型變量 Set 里左起第一個(gè)置1的位的下標(biāo),存儲(chǔ)到 Member 里 // 算法分析: // 把32位分成4字節(jié),兩輪二分,確定了左起第一個(gè)“有1”的字節(jié)的偏移,記錄在 _Offset // Set >> _Offset 是把第一個(gè)有1的字節(jié)移到低8位 // KiFindFirstSetLeft[Set >> _Offset] 得到的是8位里左起第1個(gè)置1位的位置,如 0000 0001 得到的是0,0011 0000 得到的是5 // KiFindFirstSetLeft[Set >> _Offset] + _Offset 得到的是在整個(gè)32位里,左起第一個(gè)置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 等于左起第一個(gè)置1位的下標(biāo),表示該優(yōu)先級(jí)有就緒線程ListHead = &KiDispatcherReadyListHead[HighPriority]; // 找到該優(yōu)先級(jí)的調(diào)度鏈表頭PrioritySet <<= (31 - HighPriority); // 此時(shí)最高位是左起第一個(gè)置1的位,如果值是0,說(shuō)明沒(méi)有就緒線程while (PrioritySet != 0) {//// If the next bit in the priority set is a one, then examine the// corresponding dispatcher ready queue.//// 如果最高位是1,則遍歷這個(gè)優(yōu)先級(jí)調(diào)度鏈表if ((LONG)PrioritySet < 0) {NextEntry = ListHead->Flink; // NextEntry 指向當(dāng)前優(yōu)先級(jí)調(diào)度鏈表里的第一個(gè)線程ASSERT(NextEntry != ListHead); // 當(dāng)前優(yōu)先級(jí)置1,鏈表里卻沒(méi)有值,是不可能的Thread = CONTAINING_RECORD(NextEntry, KTHREAD, WaitListEntry); // 計(jì)算 KTHREADRemoveEntryList(&Thread->WaitListEntry); // 從鏈表里刪除該線程if (IsListEmpty(ListHead)) {ClearMember(HighPriority, KiReadySummary); // 如果該優(yōu)先級(jí)的調(diào)度鏈表已經(jīng)是空的,那么 KiReadySummary 相應(yīng)的位清零}return Thread;}HighPriority -= 1;ListHead -= 1;PrioritySet <<= 1;};//// No thread could be found, return a null pointer.//return NULL; }四、逆向分析 KiFindReadyThread
有了源碼,我們已經(jīng)把 KiFindReadyThread 分析得非常清楚了,但是我在分析的過(guò)程中,并不是只看源碼的,因?yàn)樵创a里有一些條件編譯代碼,不對(duì)照匯編看,是搞不清楚它到底有沒(méi)有編譯進(jìn)去的。
下面我貼出我的匯編注釋,僅供參考。
.text:00429884 ; 參數(shù): .text:00429884 ; ecx: ProcessorNumber CPU編號(hào),從KPCR里取,單核版本不使用 .text:00429884 ; edx: LowPriority 最低優(yōu)先級(jí),這里是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,意思是假設(shè)高16位至少有一個(gè)置1的位 .text:0042988E dec eax .text:0042988F not eax .text:00429891 and eax, _KiReadySummary ; 全局變量 _KiReadySummary 有32位,對(duì)應(yīng)32個(gè)就緒隊(duì)列 .text:00429891 ; eax = (~((1 << LowPriority) - 1)) & _KiReadySummary .text:00429891 ; 此時(shí) eax 存的是 _KiReadySummary 將低 LowPriority 位清0的值 .text:00429897 mov edx, eax ; .text:00429897 ; 下面是利用二分+位圖實(shí)現(xiàn)了查找左起第一個(gè)置1位 .text:00429899 shr edx, 10h ; 右移16位 .text:00429899 ; 如果結(jié)果是0,說(shuō)明高16位全是0 .text:00429899 ; 如果不是0,說(shuō)明高16位至少有一個(gè)置1位 .text:0042989C jnz short loc_4298A2 .text:0042989E xor ecx, ecx ; ecx 清零 .text:004298A0 mov edx, eax ; .text:004298A0 ; .text:004298A0 ; 經(jīng)過(guò)第一次二分查找,ecx 存儲(chǔ)的偏移可能是0或16 .text:004298A0 ; 如果是0,表示高16位全是0,只關(guān)注低16位 .text:004298A0 ; 此時(shí) edx 存儲(chǔ)了 _KiReadySummary ,但只關(guān)注低16位 .text:004298A0 ; .text:004298A0 ; 如果是16,說(shuō)明高16位不全是0,只關(guān)注高16位 .text:004298A0 ; 此時(shí) edx 存儲(chǔ)了 _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 ; 如果結(jié)果為0,說(shuō)明第一個(gè)置1的位就在低8位里面 .text:004298AA add ecx, 8 ; 否則偏移 +8 .text:004298AA ; .text:004298AA ; .text:004298AA ; 此時(shí) ecx 存儲(chǔ)的是左起第一個(gè)置1位所在的字節(jié)內(nèi)的偏移 .text:004298AA ; 舉例說(shuō)明,假如 _KiReadySummary 是 0x30000000 .text:004298AA ; 那么 ecx 就等于 5,因?yàn)?0x30 == 0011 0000,左起第一個(gè)1下標(biāo)是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 = 左起第一個(gè)置1的位的下標(biāo) .text:004298BB ; 例如 _KiReadySummary = 0x30000000,計(jì)算出來(lái)就是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 ; 如果結(jié)果是0,表示沒(méi)有就緒線程 .text:004298C2 lea esi, _KiDispatcherReadyListHead[edx*8] ; 根據(jù)剛才計(jì)算得到的優(yōu)先級(jí),找對(duì)應(yīng)的就緒鏈表 .text:004298C9 test eax, eax .text:004298CB jz short loc_4298D9 ; 沒(méi)有就緒線程,返回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 指向第一個(gè)線程的 +0x60 處的 WaitListEntry .text:004298DF mov ecx, [eax] ; ecx: 指向下一個(gè)線程的 WaitLinkEntry.FLink .text:004298E1 sub eax, 60h ; eax: 指向 KTHREAD,作為返回值 .text:004298E4 push edi .text:004298E5 mov edi, [eax+_KTHREAD.___u24.WaitListEntry.Blink] ; edi: 指向上一個(gè)線程的 WaitListEntry.FLink .text:004298E8 mov [edi], ecx ; 上一個(gè)線程的FLink指向了下一個(gè)線程 .text:004298EA mov [ecx+4], edi ; 下一個(gè)線程的BLink指向了上一個(gè)線程的FLink .text:004298EA ; 這兩步是將當(dāng)前線程從鏈表中刪除 .text:004298ED cmp [esi], esi ; 當(dāng)前鏈表是否為空? .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 ; 如果當(dāng)前優(yōu)先級(jí)調(diào)度鏈表為空,則修改 _KiReadySummary 相應(yīng)的位 .text:00429901 pop esi .text:00429902 retn .text:00429902 @KiFindReadyThread@8 endp總結(jié)
以上是生活随笔為你收集整理的(61)分析 KiFindReadyThread 函数 —— 线程优先级的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: (60)逆向分析 KiSwapThrea
- 下一篇: (62)时钟中断切换线程,时间片管理,