首页
社区
课程
招聘
[原创]Windows内核学习笔记之线程(下)
2021-12-24 11:22 18991

[原创]Windows内核学习笔记之线程(下)

2021-12-24 11:22
18991

上篇:Windows内核学习笔记之线程(中)

六.Windows线程调度和切换

1.基本概念

一旦系统中存在多个线程,则这些线程将共享物理的处理器资源,它们实际上只得到了一部分处理器时间。如果线程的个数大于处理器的数量,则自然地,哪些线程先执行,哪些线程后执行,这需要由系统的线程调度器来决定。

 

Windows的线程调度器建立在线程的优先级的基础上,它允许一个高优先级的线程将CPU资源从低优先级的线程上抢过来,而线程的优先级并不是绝对一成不变,在系统执行过程中有些线程的优先级会动态地变化。另外,有些线程具有处理器亲和性,即只能在某些处理器上运行;而有些线程对特定的处理器更具有倾向性。

2.线程优先级

Windows内核将线程的优先级分为32级,其值从0到31。其中0级为最低,31级为最高,16级以上用于实时线程,16级以下则用于普通线程。因此,这32个值被分成了三种类别:

  • 实时类别:16~31

  • 动态类别:1~15

  • 系统类别:0

在源码中,由如下的宏定义说明了这3个类别

1
2
3
4
#define LOW_PRIORITY                0
#define LOW_REALTIME_PRIORITY       16
#define HIGH_PRIORITY               31
#define MAXIMUM_PRIORITY            32

进程对象KPROCESS结构的BasePriority域指定了一个进程的基本优先级,由于进程本身并不参与调度,所以这个值的意义在于该进程中的每个线程在初始化时都可以直接从该进程对象中获得基本的优先级。而线程对象KTHREAD结构的BasePriority和Priority域则分别定义了一个线程的静态和动态优先级。进程和线程的基本优先级是保持不变的,除非是通过调用函数KeSetPriorityAndQuantumProcess或KeSetBasePriorityThread来显式地改变。

 

运行在动态优先级类别中的线程,它们的实际优先级可能会根据特定的情形而作调整,所以,KTHREAD结构的Priority域记录了一个线程当前实际优先级。它往往是在BasePriority域的基础上提升一定的优先级增量。但是,无论怎么调整,Prioriry域值的范围都在1~15。

 

执行体层提供了6个优先级分类:

  • 实时:Realtime

  • 高:High

  • 普通之上:Above Normal

  • 普通:Normal

  • 普通之下:Below Normal

  • 低:Low

执行体运行按着6个类别来设置进程的优先级,这些选择项对应的优先级分别是24,13,10,8,6和4。执行体在创建进程时,调用PspComputeQuantumAndPriority来计算优先级,而PspComputerQuantumAndPriority函数根据进程EPROCESS对象中的PriorityClass域来查表以便获得内核层的优先级定义。此对照表定义在全局变量 PspPriorityTable中。以下是一些相关定义:

1
2
3
4
5
6
7
8
9
#define PROCESS_PRIORITY_CLASS_UNKNOWN      0
#define PROCESS_PRIORITY_CLASS_IDLE         1
#define PROCESS_PRIORITY_CLASS_NORMAL       2
#define PROCESS_PRIORITY_CLASS_HIGH         3
#define PROCESS_PRIORITY_CLASS_REALTIME     4
#define PROCESS_PRIORITY_CLASS_BELOW_NORMAL 5
#define PROCESS_PRIORITY_CLASS_ABOVE_NORMAL 6
 
KPRIORITY PspPriorityTable[PROCESS_PRIORITY_CLASS_REALTIME+1] = {8, 4, 8, 13, 24, 6, 10};

执行体层的优先级类别经过全局表PspPriorityTable变换后,就变成了内核层上的0~31优先级值。另外,在执行体层上,除了通过优先级类别的方式来设置一个进程的基本优先级以外,还可以通过NtSetInformationProcess函数对基本优先级进行微调,并非绝对由全局表PspPriorityTable决定的。

 

从这里对进程优先级的控制也可看到,执行体层的职能是提供管理和策略支持,并为上层应用程序提供服务,而内核层提供的是基本的线程调度机制。这正是策略与机制相分离的设计思想。

 

Windows提供的函数中,有不少带有"KPRIORITY Increment"参数,它代表了优先级提升量。优先级提升应该发生在该线程作出调度决定(即插入到对应优先级的调度链表中)以前。内核提供的带Increment参数的函数包括:

  • KeInsertQueueApc

  • KePulseEvent

  • KeSetEvent

  • KeReleaseMutant

  • KeReleaseSemaphore

  • KeSetProcess

  • KeBoostPriorityThread

  • KeTeriminateThread

下面这些是优先级提升的典型情形:

  • 当一个I/O操作完成时,正在等待此I/O线程有更多的机会被立即执行。IoCompleteRequest函数有一个参数是优先级提升值,至于具体的提升量则由负责I/O的驱动程序来完成

  • 一个线程在等待事件和信号量以后,其优先级得到提升

  • 前台线程从等待状态中醒来时,有一点小小的优先级提升

  • 在平衡集管理器系统线程中,扫描那些已进入就绪状态4s,但尚未被执行的线程,将它们的优先级提升至15。其用意在于避免优先级反转情形的长时间出现

  • 窗口线程因窗口的活动而醒来时,会得到一个额外的优先级提升。这是窗口管理系统在调用KeSetEvent函数时设置的

3.线程状态

KTHREAD的State域是一个跟线程调度有关的成员,反映了线程的当前调度状态,其类型是枚举类型KTHREAD_STATE,定义如下:

1
2
3
4
5
6
7
8
9
typedef enum _KTHREAD_STATE {
    Initialized,
    Ready,
    Running,
    Standby,
    Terminated,
    Waiting,
    Transition
} KTHREAD_STATE;
名称 含义
Initialized 已初始化:说明一个线程对象的内部状态已初始化,这是线程创建过程中的一个内部状态,此时线程尚未加入到进程的链表中,没有启动
Ready 就绪:代表该线程已经准备就绪,等待被调度执行。当线程调度器选择一个线程来执行时,它只考虑处于就绪状态的线程。此时,线程已被加入到某个处理器的就绪线程链表中
Running 运行:线程正在运行。该线程一直占有处理器,直到分到的时限结束,或者被一个更高优先级的线程抢占,或者线程终止,或者主动放弃处理器执行权,或者进入等待状态
Standby 备用:处于备用状态的线程已经被选中作为某个处理器上下一个要运行的线程。对于系统中的每个处理器,只能有一个线程可以处于备用状态。然而,一个处于备用状态的线程在真正被执行以前,有可能被被更高优先级的线程抢占
Terminated 已终止:表示线程已完成任务,正在进行资源回收。可以通过使用函数KeterminateThread来设置此状态
Waiting 等待:表示一个线程正在等待某个条件,比如等待一个分发器对象变成有信号状态,也可以等待多个对象。当等待的条件满足时,线程或者立即开始执行,或者回到就绪状态
Transition 转移:处于转移状态的线程已经准备好运行,但是它的内核栈不在内存中。一旦它的内核栈被换入内存,则该线程进入就绪状态
DeferredReady 延迟的就绪:处于延迟的就绪状态的线程也已经准备好可以运行了,但是,与就绪状态不同的是,它尚未确定在哪个处理器上运行。当有机会被调度时,或者直接转入备用状态,或者转到就绪状态。因此,此状态是为了多级处理器而引入的,对于单处理器系统没有意义
GateWait 门等待:线程正在等待一个门对象。此状态与等待状态类似,只不过它是专门针对门对象而设计
 

下图是这些状态的转移图:

 

avatar

 

在一个线程的生命周期中,它从完成初始化开始进入到线程调度器的视野中,之后,一直到完成所有预定的功能,最后终止并被销毁。

4.线程调度

在通过系统调用NtCreateThread创建线程的过程中,会调用KeReadyThread来改变线程的状态,让线程进入就绪状态,由该函数的反汇编结果可以得知,该函数的实现是通过调用KiReadyThread来实现的,在该函数中同时会完成线程的调度。由于KiReadyThread的函数声明是fastcall,所以在调用之前会将线程对象作为参数赋给ecx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.text:0041501A ; int __stdcall KeReadyThread(PETHREAD Thread)
.text:0041501A _KeReadyThread@4 proc near              ; CODE XREF: PspCreateThread(x,x,x,x,x,x,x,x,x,x,x)+3AD↓p
.text:0041501A                                         ; PspCreateThread(x,x,x,x,x,x,x,x,x,x,x)+8EFD9↓p ...
.text:0041501A
.text:0041501A Thread          = dword ptr  8
.text:0041501A
.text:0041501A                 mov     edi, edi
.text:0041501C                 push    ebp
.text:0041501D                 mov     ebp, esp
.text:0041501F                 push    ebx
.text:00415020                 xor     ecx, ecx
.text:00415022                 call    ds:__imp_@KeAcquireQueuedSpinLockRaiseToSynch@4 ; KeAcquireQueuedSpinLockRaiseToSynch(x)
.text:00415028                 mov     ecx, [ebp+Thread] ; 将线程对象赋给ecx
.text:0041502B                 mov     bl, al
.text:0041502D                 call    @KiReadyThread@4 ; KiReadyThread(x)
.text:00415032                 mov     cl, bl
.text:00415034                 call    @KiUnlockDispatcherDatabase@4 ; KiUnlockDispatcherDatabase(x)
.text:00415039                 pop     ebx
.text:0041503A                 pop     ebp
.text:0041503B                 retn    4
.text:0041503B _KeReadyThread@4 endp

KiReadyThread函数执行过程中会用到比较多结构体的成员

 

以下是用到的线程对象中的成员

偏移 名称 含义
0x128 Preempted 布尔值,说明这个线程是否被高优先级的线程抢占了,只有当一个线程正在运行或者正在等待运行而被高优先级线程抢占的时候,此值才会是TRUE
0x033 Priority 指明了线程的优先级
0x02D State 反映了当前线程的状态
0x129 ProcessReadyQueue 布尔值,说明线程是否在所属进程的KPROCESS对象的ReadyListEntry链表中,TRUE表示在此链表中,FALSE表示不在此链表中
0x60 WaitListEntry 双向链表节点,当一个线程正在等待被执行时,该域作为一个线程节点加入到某个链表中
0x60 SwapListEntry 单链表节点,当线程的内核栈需要被换入时,插入到以全局变量KiStackInSwapListHead为链表头的单链表中
0x12A KernelStackResident 布尔值,说明该线程的内核栈是否驻留在内存中,当内核栈被换出内存时,此值将被设置成FALSE;当换入内存时,在设置成TRUE
0x124 Affinity 指定了线程的处理器亲和性,此值初始继承自进程对象的Affinity值
0x1BA IdealProcessor 指明了在多处理器上该线程的理想处理器
0x12B NextProcessor 指明了该线程开始运行时的处理器
 

以下是用到的进程对象中的成员

偏移 名称 作用
0x65 State 说明了进程是否在内存中,共有六种可能的状态:ProcessInMemory, ProcessOutOfMemory, ProcessInTransition, ProcessOutTransition, ProcessInSwap, ProcessOutSwap
0x40 ReadyListHead 是一个双向链表头,记录了进程中处于就绪状态但尚未被加入全局就绪链表的线程,这个域的意义在于,当一个进程被换出内存后,它所属的线程一旦就绪,则被挂到此链表中,并要求换入该进程。此后,当该进程被换入内存时,ReadyListHead中的所有线程被加入到系统全局的就绪线程链表中
0x48 SwapListEntry 单链表项,当一个进程要被换出内存时,它通过此域加入到KiProcessOutSwapListHead为链头的单链表中;当一个进程被换入内存时,它通过此域加入到以KiProcessInSwapListHead为链头的单链表中
0x60 StackCount 记录当前由多少线程栈位于内存中
 

每个CPU都有一个KPCR结构体,那也就有KPCB结构体,在一个多核环境下就会有多个KPCB结构体,而全局变量KiProcessorBlock保存的就是不同的核对应的KPCB结构体地址。全局变量kiReadySummary则保存了这些处理器的使用情况,当相应处理器没有被使用到的时候全局变量KiReadySummary中相应的位就会是0,否则是1

 

完成变量的赋值以后,判断进程状态是否为ProcessInMemory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.text:00405667 ; int __fastcall KiReadyThread(PKTHREAD Thread)
.text:00405667 @KiReadyThread@4 proc near              ; CODE XREF: KiInsertQueue(x,x,x)+7B↑p
.text:00405667                                         ; KiUnwaitThread(x,x,x,x)-7671↑p ...
.text:00405667
.text:00405667 var_8           = dword ptr -8
.text:00405667 var_Preempted   = byte ptr -1
.text:00405667 Thread          = dword ptr  8
.text:00405667
.text:00405667                 mov     edi, edi
.text:00405669                 push    ebp
.text:0040566A                 mov     ebp, esp
.text:0040566C                 push    ecx
.text:0040566D                 push    ecx
.text:0040566E                 push    ebx
.text:0040566F                 push    esi
.text:00405670                 mov     esi, ecx        ; 将线程对象赋给esi
.text:00405672                 lea     ecx, [esi+_KTHREAD.Preempted] ; 将Preempted地址赋给ecx
.text:00405678                 mov     al, [ecx]       ; 将Preempted地址中的内容赋给al
.text:0040567A                 mov     byte ptr [ecx], 0 ; 将Preempted赋值为0
.text:0040567D                 mov     ecx, ds:_KeTickCount.LowPart ; 将时间赋给ecx
.text:00405683                 push    edi
.text:00405684                 mov     edi, [esi+_KTHREAD.ApcState.Process]
.text:00405687                 mov     [ebp+var_Preempted], al ; 将al赋给局部变量
.text:0040568A                 movsx   eax, [esi+_KTHREAD.Priority] ; 将线程优先级赋给eax
.text:0040568E                 mov     [esi+_KTHREAD.WaitTime], ecx ; 为WaitTime赋值
.text:00405691
.text:00405691 loc_405691:                             ; CODE XREF: KiReadyThread(x)+9817↓j
.text:00405691                 cmp     [edi+_KPROCESS.State], 0 ; 判断进程状态是否为ProcessInMemory
.text:00405695                 jnz     loc_432827

如果不是,修改线程状态为Ready,ProcessReadyQueue为TRUE,将线程对象的WaitListEntry域链接到所属进程对象的WaitListEntry域中

1
2
3
4
5
6
7
8
9
10
.text:00432827 loc_432827:                             ; CODE XREF: KiReadyThread(x)+2E↑j
.text:00432827                 mov     [esi+_KTHREAD.State], 1 ; 将线程State赋值为Ready
.text:0043282B                 mov     [esi+_KTHREAD.ProcessReadyQueue], 1 ; 将线程ProcessReadyQueue赋值为TRUE
.text:00432832                 lea     ecx, [edi+_KPROCESS.ReadyListHead] ; 将进程ReadListHead地址赋给ecx
.text:00432835                 mov     edx, [ecx+LIST_ENTRY.Blink]
.text:00432838                 lea     eax, [esi+60h]  ; 取出WaitListEntry地址赋给eax
.text:0043283B                 mov     [eax+LIST_ENTRY.Flink], ecx
.text:0043283D                 mov     [eax+LIST_ENTRY.Blink], edx
.text:00432840                 mov     [edx+LIST_ENTRY.Flink], eax
.text:00432842                 mov     [ecx+LIST_ENTRY.Blink], eax

判断进程对象是否为ProcessOutOfMemory,如果不是则退出函数

1
2
.text:00432845                 cmp     [edi+_KPROCESS.State], 1 ; 判断进程状态是否为ProcessOutOfMemory
.text:00432849                 jnz     loc_405745

如果是则将进程的SwapListEntry域加入到链表KiProcessInSwapListHead中

1
2
3
4
5
6
7
8
9
10
11
12
.text:00432853                 mov     eax, ds:_KiProcessInSwapListHead
.text:00432858                 lea     ecx, [edi+_KPROCESS.SwapListEntry]
.text:0043285B
.text:0043285B loc_43285B:                             ; CODE XREF: KiReadyThread(x)+2D20B↓j
.text:0043285B                 mov     [ecx], eax
.text:0043285D                 mov     edx, eax
.text:0043285F                 mov     esi, ecx
.text:00432861                 mov     edi, offset _KiProcessInSwapListHead
.text:00432866                 lock cmpxchg [edi], esi
.text:0043286A                 cmp     eax, edx
.text:0043286C                 jz      loc_41475D
.text:00432872                 jmp     short loc_43285B

调用KiSetSwapEvent通知交换线程执行换入操作然后退出函数

1
2
3
4
.text:0041475D loc_41475D:                             ; CODE XREF: KiReadyThread(x)+2D205↓j
.text:0041475D                                         ; .text:0044B716↓j
.text:0041475D                 call    _KiSetSwapEvent@0 ; KiSetSwapEvent()
.text:00414762                 jmp     loc_405745

如果进程状态是ProcessInMemory,判断KernelStackResident是否为0

1
2
.text:0040569B                 cmp     [esi+_KTHREAD.KernelStackResident], 0
.text:004056A2                 jz      loc_41473A

如果为0,修改进程与线程对象中的成员,将线程对象的SwapListEntry域加入到链表KiStackInSwapListHead中,调用KiSetSwapEvent后退出函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:0041473A loc_41473A:                             ; CODE XREF: KiReadyThread(x)+3B↑j
.text:0041473A                 inc     [edi+_KPROCESS.StackCount] ; StackCount加1
.text:0041473E                 mov     [esi+_KTHREAD.State], 6 ; 线程状态赋值为Transition
.text:00414742                 mov     eax, ds:_KiStackInSwapListHead
.text:00414747                 lea     ecx, [esi+60h]  ; 将线程对象的SwapListEntry地址赋给ecx
.text:0041474A
.text:0041474A loc_41474A:                             ; CODE XREF: KiReadyThread(x)+F0F4↓j
.text:0041474A                 mov     [ecx], eax
.text:0041474C                 mov     edx, eax
.text:0041474E                 mov     esi, ecx
.text:00414750                 mov     edi, offset _KiStackInSwapListHead
.text:00414755                 lock cmpxchg [edi], esi
.text:00414759                 cmp     eax, edx
.text:0041475B                 jnz     short loc_41474A
.text:0041475D
.text:0041475D loc_41475D:                             ; CODE XREF: KiReadyThread(x)+2D205↓j
.text:0041475D                                         ; .text:0044B716↓j
.text:0041475D                 call    _KiSetSwapEvent@0 ; KiSetSwapEvent()
.text:00414762                 jmp     loc_405745

如果KernelStackResident不为0,接下来会将线程状态该位Standby,线程中的Affinity与线程中的SoftAffinity与的结果如果不为0,就会将结果与全局变量KiIdelSummary进行与操作,如果为0就会将Affinity与全局变量KiIdleSummary进行与操作,结果保留在edx中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:004056A8                 movzx   ecx, [esi+_KTHREAD.IdealProcessor] ; 将IdealProcessor赋给ecx
.text:004056AF                 mov     edi, [esi+_KTHREAD.Affinity] ; 将Affinity赋给edi
.text:004056B5                 mov     [esi+_KTHREAD.State], 3 ; 将线程状态改为Standby
.text:004056B9                 mov     edx, ds:_KiProcessorBlock[ecx*4]
.text:004056C0                 mov     ebx, [edx+4D0h] ; 将MultiThreadSetMaster赋给ebx
.text:004056C6                 mov     edx, [esi+_KTHREAD.SoftAffinity]
.text:004056CC                 and     edx, edi
.text:004056CE                 jz      short loc_4056D2
.text:004056D0                 mov     edi, edx
.text:004056D2
.text:004056D2 loc_4056D2:                             ; CODE XREF: KiReadyThread(x)+67↑j
.text:004056D2                 mov     edx, ds:_KiIdleSummary
.text:004056D8                 and     edx, edi
.text:004056DA                 jnz     loc_4106D5

如果满足条件,接下来就会从KiProcessorBlock中获取相应的KPCB,此时ecx保存的是线程的IdealProcessor

1
2
3
4
5
6
7
8
9
10
11
.text:004056E0                 xor     edx, edx
.text:004056E2                 inc     edx             ; edx赋值为1
.text:004056E3                 mov     ebx, edx        ; 将edx赋给ebx
.text:004056E5                 shl     ebx, cl
.text:004056E7                 test    edi, ebx
.text:004056E9                 jz      loc_441903
.text:004056EF
.text:004056EF loc_4056EF:                             ; CODE XREF: KiReadyThread(x)+3C2A9↓j
.text:004056EF                                         ; KiReadyThread(x)+3C2D6↓j
.text:004056EF                 mov     [esi+_KTHREAD.NextProcessor], cl
.text:004056F5                 mov     ebx, ds:_KiProcessorBlock[ecx*4] ; 取出KPCB地址赋给ebx

判断获取的KPCB是否有下一线程

1
2
3
.text:004056FC                 mov     edi, [ebx+8]    ; 将下一线程对象地址赋给edi
.text:004056FF                 test    edi, edi
.text:00405701                 jnz     loc_40EE20

如果有下一线程,就会判断下一线程的优先级是否小于要设置状态的线程的优先级,此时的eax保存的是调度的线程的优先级

1
2
3
4
.text:0040EE20 loc_40EE20:                             ; CODE XREF: KiReadyThread(x)+9A↑j
.text:0040EE20                 movsx   ecx, [edi+_KTHREAD.Priority]
.text:0040EE24                 cmp     eax, ecx
.text:0040EE26                 jle     loc_405716

如果小于,就会将调度线程赋值到KPCB的下一线程中

1
2
3
4
5
6
7
8
9
10
.text:0041232E loc_41232E:                             ; CODE XREF: KiReadyThread(x)+97E7↑j
.text:0041232E                 call    @KeReleaseQueuedSpinLockFromDpcLevel@4 ; KeReleaseQueuedSpinLockFromDpcLevel(x)
.text:00412333                 mov     [ebx+8], esi    ; 将调度线程赋值给下一线程
.text:00412336                 mov     eax, large fs:20h
.text:0041233C                 mov     cl, [esi+_KTHREAD.NextProcessor]
.text:00412342                 cmp     [eax+10h], cl   ; 将cl赋值给Number
.text:00412345                 jz      loc_405745
.text:0041234B                 xor     eax, eax
.text:0041234D                 inc     eax             ; eax=1
.text:0041234E                 jmp     loc_40EB31

如果获取的KPCB不存在下一线程,就会判断调度线程的优先级是否大于获取的KPCB的正在运行的线程的优先级

1
2
3
4
.text:00405707                 mov     ecx, [ebx+4]    ; 将当前线程对象地址赋给ecx
.text:0040570A                 movsx   edi, [ecx+_KTHREAD.Priority] ; 将线程优先级赋给edi
.text:0040570E                 cmp     eax, edi
.text:00405710                 jg      loc_40EB10

如果大于,就会将调度线程赋值给下一线程

1
2
3
4
5
6
7
8
9
10
11
12
.text:0040EB10 loc_40EB10:                             ; CODE XREF: KiReadyThread(x)+A9↑j
.text:0040EB10                 mov     [ecx+_KTHREAD.Preempted], 1
.text:0040EB17                 mov     [ebx+8], esi    ; 将调度进程赋给下一线程
.text:0040EB1A                 mov     eax, large fs:20h
.text:0040EB20                 mov     cl, [esi+_KTHREAD.NextProcessor]
.text:0040EB26                 cmp     [eax+10h], cl
.text:0040EB29                 jz      loc_405745
.text:0040EB2F                 mov     eax, edx
.text:0040EB31
.text:0040EB31 loc_40EB31:                             ; CODE XREF: KiReadyThread(x)+CCE7↓j
.text:0040EB31                 shl     eax, cl
.text:0040EB33                 jmp     loc_40E9BC

如果调度线程优先级小于当前线程的优先级,就会将线程状态设置为Ready,将esi指向线程对象的WaitListEntry地址,从全局链表KiDispatcherReadyListHead中根据优先级获取调度链表,此时的eax代表了调度线程的优先级,判断Preempted是否为0

1
2
3
4
5
6
.text:00405716 loc_405716:                             ; CODE XREF: KiReadyThread(x)+97BF↓j
.text:00405716                 mov     [esi+_KTHREAD.State], 1 ; 将线程状态赋值为Ready
.text:0040571A                 add     esi, 60h        ; esi指向WaitListEntry地址
.text:0040571D                 cmp     [ebp+var_Preempted], 0
.text:00405721                 lea     ecx, _KiDispatcherReadyListHead[eax*8]
.text:00405728                 jnz     loc_40565105651

如果不为0,就会将调度线程加到调度链表头部

1
2
3
4
5
6
7
.text:00405651 loc_405651:                             ; CODE XREF: KiReadyThread(x)+C1↓j
.text:00405651                 mov     edi, [ecx+LIST_ENTRY.Flink]
.text:00405653                 mov     [esi+LIST_ENTRY.Flink], edi
.text:00405655                 mov     [esi+LIST_ENTRY.Blink], ecx
.text:00405658                 mov     [edi+LIST_ENTRY.Blink], esi
.text:0040565B                 mov     [ecx+LIST_ENTRY.Flink], esi
.text:0040565D                 jmp     loc_40573B

如果为0,则将调度线程加到调度链表尾部

1
2
3
4
5
.text:0040572E                 mov     edi, [ecx+LIST_ENTRY.Blink]
.text:00405731                 mov     [esi+LIST_ENTRY.Flink], ecx
.text:00405733                 mov     [esi+LIST_ENTRY.Blink], edi
.text:00405736                 mov     [edi+LIST_ENTRY.Flink], esi
.text:00405738                 mov     [ecx+LIST_ENTRY.Blink], esi

根据调度线程优先级,将KiReadySummary相应的位置1,这里的edx等于1,eax为调度线程的优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:0040573B loc_40573B:                             ; CODE XREF: KiReadyThread(x)-A↑j
.text:0040573B                 mov     ecx, eax
.text:0040573D                 shl     edx, cl
.text:0040573F                 or      ds:_KiReadySummary, edx
.text:00405745
.text:00405745 loc_405745:                             ; CODE XREF: KiReadyThread(x)+935F↓j
.text:00405745                                         ; KiReadyThread(x)+94C2↓j ...
.text:00405745                 pop     edi
.text:00405746                 pop     esi
.text:00405747                 pop     ebx
.text:00405748                 leave
.text:00405749                 retn
.text:00405749 @KiReadyThread@4 endp

由此可以知道,在KiDispatcherReadyListHead保存了需要被调度的不同优先级的线程的链表,不同的下标代表了不同的优先级。

5.线程切换

有两种情况会发生线程切换,分为主动切换和被动切换。主动切换是通过调用一些系统提供的API,在这些API中会调用KiSwapThread,而该函数通过调用SwapContext来实现线程切换。被动切换则发生在线程分配的时限用完或者被抢占的时候,此时KiDispatchInter会处理这两种情况,而无论哪种情况,最后都会通过调用函数SwapContext来切换线程,具体情况如下图所示:

 

avatar

 

在KiSwapThread中首先会判断下一线程是否为NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:0040AB0A ; _DWORD __cdecl KiSwapThread()
.text:0040AB0A @KiSwapThread@0 proc near               ; CODE XREF: KeDelayExecutionThread(x,x,x):loc_40A56D↑p
.text:0040AB0A                                         ; KeWaitForMultipleObjects(x,x,x,x,x,x,x,x):loc_40AAEE↑p ...
.text:0040AB0A
.text:0040AB0A                 mov     edi, edi
.text:0040AB0C                 push    esi
.text:0040AB0D                 push    edi
.text:0040AB0E                 mov     eax, large fs:20h ; 将当前线程KPCB赋给eax
.text:0040AB14                 mov     esi, eax        ; 将eax赋给esi
.text:0040AB16                 mov     eax, [esi+8]    ; 将下一线程赋给eax
.text:0040AB19                 test    eax, eax        ; 下一线程是否为NULL
.text:0040AB1B                 mov     edi, [esi+4]    ; 将当前线程赋给edi
.text:0040AB1E                 jnz     loc_410939

如果不为NULL,则将其清空,然后跳转到loc_40AB3B处执行

1
2
3
.text:00410939 loc_410939:                             ; CODE XREF: KiSwapThread()+14↑j
.text:00410939                 and     dword ptr [esi+8], 0 ; 将下一线程赋值为0
.text:0041093D                 jmp     loc_40AB3B

如果没有下一线程,就会调用KiFindReadyThread来获取要切换的线程,接下来和上面一样开始运行loc_40AB3B的代码

1
2
3
4
5
6
7
8
9
10
11
.text:0040AB24                 push    ebx
.text:0040AB25                 movsx   ebx, byte ptr [esi+10h] ; 将Number赋给ebx
.text:0040AB29                 xor     edx, edx        ; 清空edx
.text:0040AB2B                 mov     ecx, ebx        ; 将Number赋给ecx
.text:0040AB2D                 call    @KiFindReadyThread@8 ; KiFindReadyThread(x,x)
.text:0040AB32                 test    eax, eax
.text:0040AB34                 jz      loc_41073E
.text:0040AB3A
.text:0040AB3A loc_40AB3A:                             ; CODE XREF: KiSwapThread()+5C54↓j
.text:0040AB3A                                         ; KiSwapThread()+5C78↓j ...
.text:0040AB3A                 pop     ebx

loc_40AB3B的代码就是通过调用KiSwapContext来完成线程切换,此时eax保存的是要切换的线程对象,调用前会将其赋给ecx,判断返回值是否为0

1
2
3
4
5
6
7
8
.text:0040AB3B loc_40AB3B:                             ; CODE XREF: KiSwapThread()+5E33↓j
.text:0040AB3B                 mov     ecx, eax
.text:0040AB3D                 call    @KiSwapContext@4 ; KiSwapContext(x)
.text:0040AB42                 test    al, al
.text:0040AB44                 mov     cl, [edi+_KTHREAD.WaitIrql] ; NewIrql
.text:0040AB47                 mov     edi, [edi+_ETHREAD.Tcb.WaitStatus]
.text:0040AB4A                 mov     esi, ds:__imp_@KfLowerIrql@4 ; KfLowerIrql(x)
.text:0040AB50                 jnz     loc_41BC76

如果不为0,则调用KiDeliverApc来交付APC队列

1
2
3
4
5
6
7
8
9
10
.text:0041BC76 loc_41BC76:                             ; CODE XREF: KiSwapThread()+46↑j
.text:0041BC76                 mov     cl, 1           ; NewIrql
.text:0041BC78                 call    esi ; KfLowerIrql(x) ; KfLowerIrql(x)
.text:0041BC7A                 xor     eax, eax
.text:0041BC7C                 push    eax             ; int
.text:0041BC7D                 push    eax             ; int
.text:0041BC7E                 push    eax             ; PreviousMode
.text:0041BC7F                 call    _KiDeliverApc@12 ; KiDeliverApc(x,x,x)
.text:0041BC84                 xor     cl, cl
.text:0041BC86                 jmp     loc_40AB56

如果为0,退出函数执行

1
2
3
4
5
6
7
.text:0040AB56 loc_40AB56:                             ; CODE XREF: KiSwapThread()+1117C↓j
.text:0040AB56                 call    esi ; KfLowerIrql(x) ; KfLowerIrql(x)
.text:0040AB58                 mov     eax, edi
.text:0040AB5A                 pop     edi
.text:0040AB5B                 pop     esi
.text:0040AB5C                 retn
.text:0040AB5C @KiSwapThread@0 endp

KiSwapContext函数则将要切换的线程对象赋值到当前KPCR的当前线程中,通过调用函数SwapContext来完成切换

 

在SwapContext中,首先就将目标线程的状态改为Running

1
2
3
4
5
6
7
.text:00405942 SwapContext     proc near               ; CODE XREF: KiUnlockDispatcherDatabase(x)+99↑p
.text:00405942                                         ; KiSwapContext(x)+2A↑p ...
.text:00405942
.text:00405942 ; FUNCTION CHUNK AT .text:00405AC9 SIZE 00000033 BYTES
.text:00405942
.text:00405942                 or      cl, cl
.text:00405944                 mov     es:[esi+_KTHREAD.State], 2 ; 将当前线程状态赋值为Running

判断NpxState是否为0

1
2
3
4
5
6
.text:0040597D loc_40597D:                             ; CODE XREF: SwapContext+18F↓j
.text:0040597D                                         ; SwapContext+1A0↓j ...
.text:0040597D                 mov     ebp, cr0
.text:00405980                 mov     edx, ebp        ; edx赋值为cr0
.text:00405982                 cmp     [edi+_KTHREAD.NpxState], 0
.text:00405986                 jz      loc_405AA4

如果为0,就会修改Cr0

1
2
3
4
5
6
7
8
.text:00405AA4 loc_405AA4:                             ; CODE XREF: SwapContext+44↑j
.text:00405AA4                 and     edx, 0FFFFFFF1h
.text:00405AA7                 mov     ecx, [ebx+4]
.text:00405AAA                 cmp     ebp, edx
.text:00405AAC                 jz      short _ScPatchFxb
.text:00405AAE                 mov     cr0, edx        ; 修改cr0
.text:00405AB1                 mov     ebp, edx
.text:00405AB1 SwapContext

将当前栈顶地址赋给当前线程的KernelStack,将目标线程的栈底和栈边界分别赋值到KPCR相应的位置中,将栈底赋给eax后再提升0x210

1
2
3
4
5
6
7
8
9
10
.text:0040598C loc_40598C:                             ; CODE XREF: _ScPatchFxe+E↓j
.text:0040598C                 mov     cl, [esi+_ETHREAD.Tcb.DebugActive]
.text:0040598F                 mov     [ebx+KPCR.SpareUnused], cl
.text:00405992                 cli
.text:00405993                 mov     [edi+_KTHREAD.KernelStack], esp ; 将当前栈顶保存到当前线程的KernelStack
.text:00405996                 mov     eax, [esi+_KTHREAD.InitialStack] ; 取出目标线程的栈底赋给eax
.text:00405999                 mov     ecx, [esi+_KTHREAD.StackLimit] ; 将目标线程的栈顶赋给ecx
.text:0040599C                 sub     eax, 210h       ; 栈地址提升0x210
.text:004059A1                 mov     [ebx+KPCR.anonymous_0.NtTib.StackLimit], ecx
.text:004059A4                 mov     [ebx+KPCR.anonymous_0.NtTib.StackBase], eax

再次将eax提升0x10以后赋值给TSS中的esp0供线程进行模式切换使用

1
2
3
4
5
.text:004059CA                 sub     eax, 10h        ; 栈地址提升0x10
.text:004059CD
.text:004059CD loc_4059CD:                             ; CODE XREF: SwapContext+86↑j
.text:004059CD                 mov     ecx, [ebx+KPCR.TSS]
.text:004059D0                 mov     [ecx+4], eax    ; 将栈地址赋给TSS的esp0

将栈顶地址修改为目标线程的KernelStack,将目标线程的Teb

1
2
3
4
.text:004059D3                 mov     esp, [esi+_KTHREAD.KernelStack] ; 将目标线程的内核栈顶地址赋给esp
.text:004059D6                 mov     eax, [esi+_KTHREAD.Teb]
.text:004059D9                 mov     [ebx+KPCR.anonymous_0.NtTib.Self], eax ; 为KPCR的地址赋值
.text:004059DC                 sti

判断当前线程和目标线程的所属进程是否相同

1
2
3
4
.text:004059DD                 mov     eax, [edi+_KTHREAD.ApcState.Process] ; 将当前线程的Process赋给eax
.text:004059E0                 cmp     eax, [esi+_KTHREAD.ApcState.Process] ; 判断目标线程与当前线程的Process是否相等
.text:004059E3                 mov     [edi+_KTHREAD.IdleSwapBlock], 0
.text:004059E7                 jz      short loc_405A29

如果不同会清空gs寄存器,修改cr3寄存器的内容为目标进程的页表基地址

1
2
3
4
5
6
7
8
9
.text:004059E9                 mov     edi, [esi+_KTHREAD.ApcState.Process] ; 将目标线程的Process赋给edi
.text:00405A0D                 xor     eax, eax
.text:00405A0F                 mov     gs, eax         ; GS寄存器清0
.text:00405A11                 assume gs:nothing
.text:00405A11                 mov     eax, [edi+_KPROCESS.DirectoryTableBase] ; 取出当前线程的Cr3
.text:00405A14                 mov     ebp, [ebx+KPCR.TSS]
.text:00405A17                 mov     ecx, dword ptr [edi+_KPROCESS.IopmOffset]
.text:00405A1A                 mov     [ebp+1Ch], eax
.text:00405A1D                 mov     cr3, eax        ; 修改cr3寄存器为当前线程的cr3

为了让fs:[0]指向的地址是正确的KPCR地址,需要将当前KPCR地址赋给fs寄存器段选择子对应的段描述符中,此时GDT表基址偏移0x38的地址对应的段描述符就是fs寄存器的段选择子对应的段描述符

1
2
3
4
5
6
7
.text:00405A34 loc_405A34:                             ; CODE XREF: SwapContext+E2↑j
.text:00405A34                 mov     eax, [ebx+KPCR.anonymous_0.NtTib.Self] ; 将KPCR地址赋给eax
.text:00405A37                 mov     ecx, [ebx+KPCR.GDT] ; 将GDT表地址赋给ecx
.text:00405A3A                 mov     [ecx+3Ah], ax   ; 修改低16位地址
.text:00405A3E                 shr     eax, 10h        ; 右移0x10得到高16位地址
.text:00405A41                 mov     [ecx+3Ch], al   ; 修改高16位的低8
.text:00405A44                 mov     [ecx+3Fh], ah   ; 修改高16位的高8

最后退出函数

1
2
3
4
5
6
7
8
9
.text:00405A47                 inc     [esi+_KTHREAD.ContextSwitches] ; 增加ContextSwitchs
.text:00405A4A                 inc     dword ptr [ebx+61Ch]
.text:00405A50                 pop     ecx
.text:00405A51                 mov     [ebx], ecx
.text:00405A53                 cmp     [esi+_KTHREAD.ApcState.KernelApcPending], 0 ; 判断是否有内核APC要执行
.text:00405A57                 jnz     short loc_405A5D
.text:00405A59                 popf
.text:00405A5A                 xor     eax, eax
.text:00405A5C                 retn

七.参考资料

  • 《Windows内核原理与实现》

  • 《Windows内核源码情景分析》(上册)


阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!

最后于 2021-12-24 11:26 被1900编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (4)
雪    币: 17
活跃值: (458)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
半斤而不八两 2022-1-1 19:37
2
0
楼主你好,我今天逆xp sp3的内核,KPRCB并没有延迟链表跟就绪链表,windows现在不用延迟链表了嘛?
雪    币: 22399
活跃值: (25302)
能力值: ( LV15,RANK:910 )
在线值:
发帖
回帖
粉丝
1900 6 2022-1-2 11:13
3
1
半斤而不八两 楼主你好,我今天逆xp sp3的内核,KPRCB并没有延迟链表跟就绪链表,windows现在不用延迟链表了嘛?

就绪链表是用全局变量保存的,延迟链表是和DPC机制有关

最后于 2022-1-9 17:23 被1900编辑 ,原因:
雪    币: 184
活跃值: (6572)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
Golden_Boy 2022-4-19 17:08
4
0
大佬推荐点学习资料呗
雪    币: 22399
活跃值: (25302)
能力值: ( LV15,RANK:910 )
在线值:
发帖
回帖
粉丝
1900 6 2022-4-19 17:20
5
0
风清涯 大佬推荐点学习资料呗
后面的参考资料不是给出了嘛。。。
游客
登录 | 注册 方可回帖
返回