事件等待

c++

Posted by YiMiTuMi on September 16, 2021

原子操作

全局变量:

DWORD dwVal = 0;

线程中的代码:

dwVab++;

这行代码是不安全的,因为对应的汇编代码为(单核多核都不安全):

mov eax, [0x12345678]

add eax, 1

mov [0x12345678], eax

当代码在执行最后的mov前发生线程切换就会导致最后dwVab返回的值出现错误,并不安全,因为线程切换会发生在任何指令结束之后。

将3行汇编修改为一行(单核安全):

INC DWORD PTR DS:[0x12345678] //实现自加一操作

若当前系统为单核的那么这行指令安全,因为某一时刻只有一个线程会执行当前代码,但是若当前系统为多核的情况下当前代码仍然不安全,因为会出现多个cpu同时执行这一行代码,改成:

LOCK INC DWORD PTR DS:[0x12345678]    

此时代码安全,LOCK锁定的是使用指令的内存,即在同一时刻只有一个线程能够读取内存。

CPU在执行一条指令时,他会先把下一条指令地址读取到CPU中,即先读取下一条指令地址,后执行当前指令,当前指令结束后,再读取下一条指令地址,执行上一次读取的指令。所以在多核情况下会出现多个CPU同时读取一条指令的情况。

注:单核情况下,线程切换会发生在任何指令结束之后,一旦发生线程切换则会导致数据修改出现问题。

LOCK指令:

LOCK指令锁定的是一段内存即,CPU在执行一条指令时,他会先把下一条指令地址读取到CPU中,此时LOCK锁定的就是读取的指令的这段地址,当任意一个CPU读取这条指令时,将当前地址锁定,其他CPU无法读取这条指令。若:

0x40123456 LOCK INC DWORD PTR DS:[0x12345678]

这条指令的开始地址为 0x40123456,所以LOCK锁定的内存地址范围 = 0x40123456 + 指令长度,当这条指令执行结束,内存地址被放开。

原子操作API:

Interlockedlncrement           InterlockedExchangeAdd

InterlockedDecrement           InterlockedFlushSList

InterlockedExchange            InterlockedPopEntrySList

IntellockedCompareExchange     Inter

Interlockedlncrement函数解析:

可以看出Interlockedlncrement函数中使用 lock 指令锁定了内存。

 ; LONG __stdcall InterlockedIncrement(volatile LONG *lpAddend)
                 public InterlockedIncrement
 InterlockedIncrement proc near          ; CODE XREF: CreatePipe+57↓p
                                         ; sub_7C82CB9A+41↓p ...

 lpAddend        = dword ptr  4

                 mov     ecx, [esp+lpAddend]
                 mov     eax, 1

 loc_7C8097FF:                           ; DATA XREF: .data:off_7C88500C↓o
                 lock xadd [ecx], eax
                 inc     eax
                 retn    4
 InterlockedIncrement endp

xadd:

先交换后加,即 xadd [ecx], eax

mov edx, eax

mov eax, ecx

mov [ecx], edx

add [ecx], eax

若ecx = 3,eax = 1 执行后:ecx = 4,eax = 3

临界区

临界区:一次只允许一个线程进入直到离开。

实现简单临界区(效率低):

全局变量:Flag = 0

进入临界区:
Lad:
	mov eax, 1
	lock xadd [Flag], eax   //先交换后在加
	cmp eax, 0         //先改值后判断,防止出现多线程同时判断问题
	jz endLab
	dec [Flag]
	//线程等待sleep
endLab:
	ret

离开临界区:
	lock dec [Flag]

错误实现简单临界区:

全局变量:Flag = 0

进入临界区:
Lad:
	LOCK inc dword ptr Flag
	cmp Flag, 1               
	jz endLab
	lock dec [Flag]
	//线程等待sleep
endLab:
	ret

离开临界区:
	lock dec [Flag]

此代码若Flag变量自增(inc)完后发生线程切换(即LOCK inc dword ptr Flag语句执行结束后),会导致在比较Flag是否为1前,其他线程调用 LOCK inc dword ptr Flag,再次使Flag的值自增导致判断失败。(概率低,不代表没有,可能会是时钟切换)

当 lock dec [Flag] 指令和cmp同时执行时会出现Flag值不对的问题。

自旋锁

在多核环境下,ntoskrnl.exe中会有很多这种代码:

call   ds:__imp_@KeAcquireInStackQueuedSpinLockRaiseToSynch@8 ; KeAcquireInStackQueuedSpinLockRaiseToSynch(x,x)

这是在多核环境下windows加的自旋锁

KeAcquireSpinLockAtDpcLevel函数:

KeAcquireSpinLockAtDpcLevel函数接受一个全局变量的指针为参数。

KeAcquireSpinLockAtDpcLevel proc near

arg_0   =  dword ptr 4

				mov ecx, [esp + arg_0]

	loc_469A08:

				lock bts dword ptr [ecx], 0  //检测ecx的值,将ECX指向数据的第0位的值写道 CF中
											 //然后将ecx指向的变量的第0个位置值1
				 
				jb    short loc_469A12       //如果 CF = 1 跳转,说明已经有线程在跑当前函数了
				retn  4

	loc_469A12:  //循环等待ecx的值 为 0,即等待线程跑完

				test  dword ptr [ecx], 1
				jz    short loc_469A08	    //[ecx] = 0,则跳转
				pause                       //PAUSE指令提升了自旋等待循环(spin-wait loop)的性能。	
				jmp   short loc_469A12

KeAcquireSpinLockAtDpcLevel endp

关键代码:

lock bts dword ptr [ecx], 0 

Lock是锁前缀,保证这条指令在同一时刻只能有一个CPU访问。

BTS指令,设置并检查 将ECX指向数据的第0(x)位的值写道 CF中,后将第0(x)位置1,即执行完第0位一定是1。

即:

1)自旋锁只对多核有意义。

2)自旋锁与临界区、事件、互斥体一样,都是一种同步机制,都可以让当前线程处于等待状态,区别在于自旋锁不用切换线程。

线程等待与唤醒

临界区和Windows自旋锁,这两种同步方案在线程无法进入临界区时都会让当前线程进入等待状态,其中:

临界区  -- 通过Sleep函数实现

Windows -- 通过让当前的CPU“空转”实现

其局限性:

1)通过Sleep函数进行等待,无法设置合适的等待时间

2)通过“空转”的方式进行等待,只有等待时间很短的情况下才有意义,否则对CPU资源时种浪费。而其自旋锁只能在多核的环境下才有意义。

等待与唤醒机制

在Windows中,一个线程可以通过等待一个或者多个可等待对象,从而进入等待状态,另一个线程可以在某些时刻唤醒等待这些对象的其他线程。

      线程A             →           可等待对象          ←      线程B

WaitForSingleObject                                         SetEvent

WaitForMultipleObject                                       ReleaseSemaphore

															ReleaseMutant

可等待对象

Windbg查看:

dt _KPROCESS       进程

dt _KTHREAD		   线程

dt _KTIMER         定时器

dt _KSEMAPHORE     信号量

dt _KEVENT         事件

dt _KMUTANT        互斥体

dt _FILE_OBJECT    文件

1)以上大部分结构体的第一个成员从都为 +0x000 Header : _DISPATCHER_HEADER 结构体,即可等待对象。

2)如文件等可等待对象,第一个参数不一定为 _DISPATCHER_HEADER,但结构体中一定会存在_DISPATCHER_HEADER结构体。

3)即Windows想使一个对象成为可等待对象时,就会在其中嵌入一个 _DISPATCHER_HEADER 结构体。

4)可等待对象都可以通过 WaitForSingleObject 等函数进行等待。

可等待对象的差异

上面说过可等待对象中都会存在一个 _DISPATCHER_HEADER 结构体,但有些 _DISPATCHER_HEADER 结构体不一定在可等待对象的开头位置,其中的差异主要在代码上:

WaitForSingleObject (3环)

↓

NtWaitForSingleObject (内核)

				   1)通过3环用户提供的句柄,找到等待对象的内核地址
			
↓	               2)如果是以 _DISPATCHER_HEADER 开头,直接使用
			
				   3)如果不是以 _DISPATCHER_HEADER 开头的对象,则找到在其中嵌入的 _DISPATCHER_HEADER 对象

KeWaitForSingleObject (内核)

	    主要实现

其差异主要在 NtWaitForSingleObject 函数对于 _DISPATCHER_HEADER结构体位置的查找上。

等待块(_KWAIT_BLOCK)

当一个线程进入等待状态时,在其 _KTHREAD 对象中会有一个 _KWAIT_BLOCK(等待块)结构体:

	kd> dt _KTHREAD
	ntdll!_KTHREAD
	+0x070 WaitBlockList    : Ptr32 _KWAIT_BLOCK :

		kd> dt _KWAIT_BLOCK
		ntdll!_KWAIT_BLOCK
		   +0x000 WaitListEntry    : _LIST_ENTRY
		   +0x008 Thread           : Ptr32 _KTHREAD       
		   +0x00c Object           : Ptr32 Void
		   +0x010 NextWaitBlock    : Ptr32 _KWAIT_BLOCK
		   +0x014 WaitKey          : Uint2B
		   +0x016 WaitType         : UChar
		   +0x017 BlockState       : UChar

+0x000 WaitListEntry : _LIST_ENTRY:

在_DISPATCHER_HEADER结构体中,存在一个保存所有等待该对象的所有线程的 WaitListEntry 值的链表。

+0x008 Thread : Ptr32 _KTHREAD:

指向当前线程结构体。

+0x00c Object : Ptr32 Void:

指向一个被等待对象的地址。(如果被等待兑现是个进程,哪就是进程的地址_KPROCESS,若是线程则是线程的地址_KTHREAD,其他可等待对象也这样)

+0x010 NextWaitBlock : Ptr32 _KWAIT_BLOCK:

单项链表,保存当前线程的等待块

+0x014 WaitKey : Uint2B:

当前等待块在 NextWaitBlock链表中的索引。

+0x016 WaitType : UChar:

当等待多个等待对象时,若其中一个满足则激活此时 WaitType = 1,若需要所有的等待对象都满足则 WaitType = 0

+0x017 BlockState : UChar:

指向当前等待块开始的地址

_KTIMER (定时器):

kd> dt _KTIMER
ntdll!_KTIMER
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 DueTime          : _ULARGE_INTEGER
   +0x018 TimerListEntry   : _LIST_ENTRY
   +0x020 Dpc              : Ptr32 _KDPC
   +0x024 Period           : Int4B

_DISPATCHER_HEADER:

kd> dt _DISPATCHER_HEADER
ntdll!_DISPATCHER_HEADER
   +0x000 Type             : UChar
   +0x001 Absolute         : UChar
   +0x002 Size             : UChar
   +0x003 Inserted         : UChar
   +0x004 SignalState      : Int4B
   +0x008 WaitListHead     : _LIST_ENTRY

+0x000 Type : UChar:

类型,每一种对象都有一个类型,如 互斥体为2,信号量为5。(可通过查看可等待对象的初始化代码查找)

+0x004 SignalState : Int4B:

是否有信号量,大于0则存在

+0x008 WaitListHead : _LIST_ENTRY:

双向链表头,圈着所有的等待块

PLARGE_INTEGER结构体

使用LARGE_INTEGER的数据结构来表示64位数据。

typedef union _LARGE_INTEGER {
    struct {
        ULONG LowPart;
        LONG HighPart;
    } DUMMYSTRUCTNAME;
    struct {
        ULONG LowPart;
        LONG HighPart;
    } u;
#endif //MIDL_PASS
    LONGLONG QuadPart;
} LARGE_INTEGER;

_MmUserProbeAddress:

kd> dd MmUserProbeAddress
83fad71c  7fff0000 80741000 0003ffff c03fff78
83fad72c  c0601ff8 00000000 00000001 00000002
83fad73c  00000001 00000000 00000002 00000000
83fad74c  00000001 00000002 00000001 00000000
83fad75c  00000002 00000000 00000000 00000000
83fad76c  80000000 00000000 00000000 00000000
83fad77c  00000000 00000800 80000000 00000200
83fad78c  80000000 00000800 00000000 00000200

MmUserProbeAddress(xp)= 7fff0000

一个进程4g内存中前64K和后64K没有映射物理内存的,所以无法使用,7fff0000 – 80000000为后64K,所以:

MmUserProbeAddress标识的就是高2G内存最大所能访问地址的边界。

_KWAIT_BLOCK 和 WaitBlock: [4] _KWAIT_BLOCK

_KWAIT_BLOCK:

kd> dt _KWAIT_BLOCK
ntdll!_KWAIT_BLOCK
   +0x000 WaitListEntry    : _LIST_ENTRY
   +0x008 Thread           : Ptr32 _KTHREAD
   +0x00c Object           : Ptr32 Void
   +0x010 NextWaitBlock    : Ptr32 _KWAIT_BLOCK
   +0x014 WaitKey          : Uint2B
   +0x016 WaitType         : UChar
   +0x017 BlockState       : UChar

_KWAIT_BLOCK 位于 KTHREAD线程结构体中,指向当前线程真正使用的那个等待块的位置,如果只有一个等待块则指向的就是 WaitBlock 第一个等待块的位置。

WaitBlock: [4] _KWAIT_BLOCK:

在等待对象超过3个,最后一个留给定时器。之前不会额外分配内存,保存等待对象。

若超时时间不为零,则会存在2个等待对象,WaitBlock的第一个对象为当前等待的对象,第四个对象是固定给定时器使用的。即:当等待时间为零时,NextWaitBlock指向的是自身。当等待时间不为零时,NextWaitBlock指向是第四个等待块即定时器。

线程等待对象

							 线程(\_KTHREAD)
						
						 WaitBlockList   →   → 
						
											 ↓
						
   										     →	  →	 等待块(\_KWAIT\_BLOCK)					                        
  对象(\_DISPATCHER\_HEADER)    ←        ←        ←   Object            
	   waitListHead		 ←        ←        ←		   WaitListEntry   
						                               Thread												
													   NextWaitBlock   ↓  
													   WaitKey         ↓
													   WaitType        ↓
													   BlockState      ↓
  																		   ↓
																	   ↓
   										             等待块(\_KWAIT\_BLOCK)					                        
  对象(\_DISPATCHER\_HEADER)    ←        ←        ←   Object            
	   waitListHead		 ←        ←        ←		   WaitListEntry   
						                               Thread												
													   NextWaitBlock   
													   WaitKey
													   WaitType
													   BlockState

即:

1)等待中的线程,一定在等待链表中(KIWaitListHead),同时也一定在等待网(由所有等待线程和等待对象组成的关系网)上。即若线程处于等待状态则 WaitBlockList位置处不为空,否则为空。

2)线程通过调用 WaitForSingleObject/WaitForMultipleObject/Sleep 函数将自己挂到等待网中。

3)线程什么时候会再次执行取决于其他线程何时调用相关函数,等待对象不同调用的函数也不同。

WaitForSingleObject/WaitForMultipleObject 分析

无论可等待对象是何种类型,线程都是通过:

WaitForSingleObject

WaitForMultipleObject

进入等待状态的,这两个函数是理解线程等待与唤醒机制的核心。

NtWaitForSingleObject

NtWaitForSingleObject:

作用:

1)调用 ObReferenceObjectByHandle 函数,通过对象句柄找到等待对象结构体地址

2)调用 KeWaitForSingleObject 函数,进入关键循环。

参数:

1)HANDLE Handle  用户层传递的等待对象的句柄

2)BOOLEAN Alertable 对应KTHREAD结构体的Alertable属性 如果为1在插入用户APC时,该线程将会被唤醒,否则不会。

3)PLARGE_INTEGER Timeout 超时时间 (地址)

函数:

 ; NTSTATUS __stdcall NtWaitForSingleObject(HANDLE Object, BOOLEAN Alertable, PLARGE_INTEGER Timeout)
                 public _NtWaitForSingleObject@12
 _NtWaitForSingleObject@12 proc near     ; CODE XREF: NTFastDOSIO(x,x)+21B↓p
                                         ; LsaRegisterLogonProcess(x,x,x)+A3↓p
                                         ; DATA XREF: ...

 var_3C          = dword ptr -3Ch
 var_38          = dword ptr -38h
 var_34          = dword ptr -34h
 var_30          = dword ptr -30h
 var_2C          = dword ptr -2Ch
 var_28          = dword ptr -28h
 var_24          = dword ptr -24h
 var_20          = dword ptr -20h
 AccessMode      = byte ptr -1Ch
 ms_exc          = CPPEH_RECORD ptr -18h
 Object          = dword ptr  8
 Alertable       = byte ptr  0Ch
 Timeout         = dword ptr  10h

 ; FUNCTION CHUNK AT PAGE:0051E42E SIZE 0000000D BYTES
 ; FUNCTION CHUNK AT PAGE:0051E440 SIZE 0000000E BYTES
 ; FUNCTION CHUNK AT PAGE:0051E453 SIZE 0000000F BYTES
 ; FUNCTION CHUNK AT PAGE:0051E467 SIZE 0000000E BYTES
 ; FUNCTION CHUNK AT PAGE:0051E47A SIZE 0000000B BYTES

 ; __unwind { // __SEH_prolog
                 push    2Ch
                 push    offset stru_40D9F8
                 call    __SEH_prolog

                 mov     eax, large fs:124h     //获取当前线程结构体
												//(fs = KPCR)fs:124 = _KPCR + 120 + 4 = _KPRCB + 4 = CurrentThread : Ptr32 _KTHREAD
				 
                 mov     al, [eax+140h]         //获取先前模式 _KTHREAD + 140 = _KTHREAD.PreviousMode
                 mov     [ebp+AccessMode], al   //保存到临时变量

                 mov     esi, [ebp+Timeout]     //获取超时时间

                 xor     ebx, ebx               //清零 ebx
                 cmp     esi, ebx				//判断超时时间是否为 0
                 jz      short loc_48F1CF       //超时时间 = 0, 跳转

                 cmp     al, bl				    //判断先前是否运行在0环
                 jz      short loc_48F1CF       //运行在0环,跳转
				 

				 //若设置了超时时间,并且之前运行在用户层
                 mov     [ebp+ms_exc.registration.TryLevel], ebx
                 mov     eax, _MmUserProbeAddress   //_MmUserProbeAddress:_MmUserProbeAddress(xp)= 7fff0000 
													//高2G内存最大所能访问地址的边界
                 cmp     esi, eax            //判断超时时间是否等于MmUserProbeAddress
                 jnb     loc_51E42E          //不相等则跳转

                 mov     ecx, [esi]          //获取 LARGE_INTEGER.LowPart的值,低32位
                 mov     [ebp+var_3C], ecx   //保存到临时变量
                 mov     eax, [esi+4]        //获取 LARGE_INTEGER.HighPart的值,高32位

 loc_48F1BC:                             ; CODE XREF: NtWaitForSingleObject(x,x,x)+8F2BA↓j
                 mov     [ebp+var_38], eax
                 mov     [ebp+var_34], ecx
                 mov     [ebp+var_30], eax
                 lea     esi, [ebp+var_34]
                 mov     [ebp+Timeout], esi
                 or      [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh


 loc_48F1CF:                             ; CODE XREF: NtWaitForSingleObject(x,x,x)+22↑j
                                         ; NtWaitForSingleObject(x,x,x)+26↑j
                 push    ebx             ; HandleInformation
                 lea     eax, [ebp+var_20]          //对象访问类型,即标识返回的是什么类型对象如线程、进程等
                 push    eax             ; Object
                 push    dword ptr [ebp+AccessMode] ; AccessMode
                 push    ebx             ; ObjectType
                 push    100000h         ; DesiredAccess
                 push    [ebp+Object]    ; Handle
                 call    _ObReferenceObjectByHandle@24 ; ObReferenceObjectByHandle(x,x,x,x,x,x)  //根据提供的 Handle 值得到 Object

                 mov     edi, eax              //eax返回对应的对象结构体
                 cmp     edi, ebx              //判断edi 是否为空,ebx = 0
                 jl      short loc_48F221      //小于等于则跳转

                 mov     ecx, [ebp+var_20]     //对象访问类型,即标识返回的是什么类型对象如线程、进程等
                 mov     eax, [ecx-10h]        //eax = ebp - 30h
                 mov     eax, [eax+48h]        //eax = ebp + 18h

                 cmp     eax, ebx
                 jl      short loc_48F1FA
                 add     eax, ecx

 loc_48F1FA:                             ; CODE XREF: NtWaitForSingleObject(x,x,x)+7A↑j
				 //调用 KeWaitForSingleObject
                 mov     [ebp+ms_exc.registration.TryLevel], 1
                 push    esi             ; Timeout      
                 push    dword ptr [ebp+Alertable] ; Alertable
                 push    dword ptr [ebp+AccessMode] ; WaitMode
                 push    6               ; WaitReason
                 push    eax             ; Object    //对象结构体
                 call    _KeWaitForSingleObject@20 ; KeWaitForSingleObject(x,x,x,x,x)
                 mov     edi, eax
                 mov     [ebp+var_2C], edi

 loc_48F215:                             ; CODE XREF: NtWaitForSingleObject(x,x,x)+8F304↓j
                 or      [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
                 mov     ecx, [ebp+var_20] ; Object
                 call    @ObfDereferenceObject@4 ; ObfDereferenceObject(x)

 loc_48F221:                             ; CODE XREF: NtWaitForSingleObject(x,x,x)+6D↑j
                 mov     eax, edi        //将返回的对象结构体保存到 eax

 loc_48F223:                             ; CODE XREF: NtWaitForSingleObject↓j
                 call    __SEH_epilog
                 retn    0Ch             //结束,清空堆栈
 ; } // starts at 48F17C
 _NtWaitForSingleObject@12 endp


 loc_51E42E:                             ; CODE XREF: NtWaitForSingleObject(x,x,x)+32↑j
 ; __unwind { // __SEH_prolog
                 mov     ecx, [eax]
                 mov     [ebp+var_3C], ecx
                 mov     eax, [eax+4]
                 jmp     loc_48F1BC
 ; } // starts at 51E42E

KeWaitForSingleObject

KeWaitForSingleObject–上半部分(构造等待块):

1)向 \_KTHREAD(+70)位置的等待块赋值

2)如果超时时间不为0,KTHREAD(+70)第四个等待块与第一个等待块关联起来,即:第一个等待块指向第四个等待块,第四个等待块指向第一个等待块。(关联定时器)

3)KTHREAD(+5C)指向第一个\_KWAIT\_BLOCK。

4)进入关键循环

KeWaitForSingleObject函数:

 ; NTSTATUS __stdcall KeWaitForSingleObject(PVOID Object, KWAIT_REASON WaitReason, KPROCESSOR_MODE WaitMode, BOOLEAN Alertable, PLARGE_INTEGER Timeout)
                 public _KeWaitForSingleObject@20
 _KeWaitForSingleObject@20 proc near     ; CODE XREF: ExAcquireFastMutexUnsafe(x)+14↑p
                                         ; ExpWaitForResource(x,x)+2A↓p ...

 var_14          = dword ptr -14h
 var_10          = dword ptr -10h
 var_C           = byte ptr -0Ch
 var_4           = dword ptr -4
 Object          = dword ptr  8
 WaitReason      = dword ptr  0Ch
 WaitMode        = byte ptr  10h
 Alertable       = byte ptr  14h
 Timeout         = dword ptr  18h

 ; FUNCTION CHUNK AT .text:00401AE2 SIZE 0000002F BYTES
 ; FUNCTION CHUNK AT .text:00405112 SIZE 0000007E BYTES
 ; FUNCTION CHUNK AT .text:0040CB8F SIZE 00000008 BYTES
 ; FUNCTION CHUNK AT .text:0040D364 SIZE 00000041 BYTES
 ; FUNCTION CHUNK AT .text:0040DE4F SIZE 0000001E BYTES
 ; FUNCTION CHUNK AT .text:004117B2 SIZE 0000000A BYTES
 ; FUNCTION CHUNK AT .text:0042BB51 SIZE 0000002F BYTES
 ; FUNCTION CHUNK AT .text:0042BED3 SIZE 00000010 BYTES
 ; FUNCTION CHUNK AT .text:0044023A SIZE 00000074 BYTES
 ; FUNCTION CHUNK AT .text:00440534 SIZE 0000000A BYTES
 ; FUNCTION CHUNK AT .text:00440E96 SIZE 00000009 BYTES
 ; FUNCTION CHUNK AT .text:004461F7 SIZE 00000028 BYTES
 ; FUNCTION CHUNK AT .text:00446227 SIZE 00000019 BYTES

                 mov     edi, edi        ; KeWaitForMutexObject
                 push    ebp
                 mov     ebp, esp
                 sub     esp, 14h
                 push    ebx
                 push    esi
                 push    edi                 //保存堆栈

                 mov     eax, large fs:124h  //获取当前线程结构体 
											 //(fs = KPCR)fs:124 = _KPCR + 120 + 4 = _KPRCB + 4 = CurrentThread : Ptr32 _KTHREAD

                 mov     edx, [ebp+Timeout]  //获取参数 Timeout
                 mov     ebx, [ebp+Object]   //获取参数 对象结构体
                 mov     esi, eax            //esi = _KTHREAD

                 cmp     byte ptr [esi+5Ah], 0  //esi+5Ah = _KTHREAD.WaitNext,判断是否存在下一个等待对象
                 mov     [ebp+var_4], edx       //ebp - 4 = 函数返回地址 = Timeout
                 lea     edi, [esi+70h]         //获取esi+70h = _KTHREAD.WaitBlock地址
                 lea     eax, [esi+0B8h]        //获取 esi+ 0B8h = _KTHREAD.WaitBlock[0].NextWaitBlock.NextWaitBlock([1]).NextWaitBlock([2]) = WaitBlock[3] 的地址 
                 jnz     loc_44023A             //_KTHREAD.WaitNext != 0 存在则跳转

 loc_4051CF:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)-4E↑j
                                         ; KeWaitForSingleObject(x,x,x,x,x)-3A↑j ...
                 call    ds:__imp__KeRaiseIrqlToDpcLevel@0 ; KeRaiseIrqlToDpcLevel() //KeRaiseIrqlToDpcLevel例程将硬件优先级提高到IRQL = DISPATCH_LEVEL,
																					 //从而屏蔽当前处理器上等效或较低IRQL的中断。  
																					 //返回调用发生时的IRQL。 eax = 调用发生时的IRQL
                 xor     ecx, ecx                  //清零ecx
                 cmp     [ebp+Timeout], ecx        //判断超时是否为空
                 mov     [esi+58h], al             //获取等待中断请求级别 esi+58h = _KTHREAD.WaitIrql = eax = 调用发生时的IRQL,用完后要恢复IRQL
                 mov     [esi+5Ch], edi            //esi+5Ch = _KTHREAD.WaitBlockList, 不存在下一个等待对象,将当前进入等待时的_KTHREAD.WaitBlock地址放到链表
                 mov     [edi+0Ch], ebx            //_KTHREAD.WaitBlock.Object = 对象结构体
                 mov     [edi+14h], cx             //_KTHREAD.WaitBlock.WaitKey(索引) = cx = 0, 不存在下一个等待对象所以当前等待块为第一个
                 mov     word ptr [edi+16h], 1     //_KTHREAD.WaitBlock.WaitType = 1, 等待一个对象,默认:当等待多个等待对象时,若其中一个满足则激活此时 WaitType = 1
                 mov     [esi+54h], ecx            //_KTHREAD.WaitStatus(等待状态) = 0
                 jz      loc_40CB8F                //超时为空则跳转

                 lea     eax, [esi+0B8h]           //获取 esi+ 0B8h = _KTHREAD.WaitBlock[0].NextWaitBlock.NextWaitBlock([1]).NextWaitBlock([2]) = WaitBlock[3] 的地址 
                 mov     [edi+10h], eax            //edi+10h = _KTHREAD.WaitBlock[0].NextWaitBlock = WaitBlock[3],将WaitBlock数组的第一个的NEXT指向第4个WaitBlock
                 mov     [eax+10h], edi            //eax+10h = _KTHREAD.WaitBlock[3].NextWaitBlock = WaitBlock[0],将WaitBlock数组的最后一个的NEXT指向第1个WaitBlock


				 //WaitListHead(环形链表) 中插入 _KTIMER._DISPATCHER_HEADER 结构体,因为只有自己一个结构体,所以使下一个也指向自己
                 mov     [esi+0F8h], eax    //esi+0F8h = esi+0F0h + 8h = _KTHREAD._KTIMER + 8 = _KTHREAD._KTIMER._DISPATCHER_HEADER + 8 = 
											//_KTHREAD._KTIMER._DISPATCHER_HEADER.WaitListHead.Flink

                 mov     [esi+0FCh], eax   //esi+0FCh = esi+0F0h + Ch = _KTHREAD._KTIMER + Ch = _KTHREAD._KTIMER._DISPATCHER_HEADER.WaitListHead.Blink 


 loc_40520E:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)+79F2↓j
                 mov     al, [ebp+Alertable]
                 mov     dl, byte ptr [ebp+WaitReason]
                 mov     [esi+164h], al
                 mov     al, [ebp+WaitMode]
                 test    al, al
                 mov     [esi+59h], al
                 mov     [esi+5Bh], dl
                 mov     [esi+60h], ecx
                 jz      loc_4052C3
                 cmp     byte ptr [esi+141h], 0
                 jz      loc_4052C3
                 cmp     byte ptr [esi+33h], 19h
                 mov     [ebp+Object], 1
                 jge     short loc_4052C3

 loc_405248:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)+126↓j
                 mov     edx, [ebp+Timeout]   //关键循环

 loc_40524B:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)+3B0EC↓j
                                         ; KeWaitForSingleObject(x,x,x,x,x)+3B0F5↓j
                 cmp     byte ptr [esi+49h], 0
                 mov     eax, ds:_KeTickCount.LowPart
                 mov     [esi+68h], eax
                 jnz     loc_4461F7

 loc_40525D:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)+4105D↓j
                 cmp     byte ptr [ebx], 2    //判断等待对象是否是互斥体类型
                 jnz     loc_405168           //不是互斥体 跳转
                 mov     eax, [ebx+4]
                 test    eax, eax             //判断 SignalState 是否为0
                 jg      loc_40D364           //SignalState == 0则跳转
                 cmp     esi, [ebx+18h]       //判断当前线程是否正在使用这个互斥体
                 jz      loc_40D364           //若使用当前互斥体的线程就是当前线程,则跳转
											  //实现重入


 loc_40527A:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)-34↑j
                                         ; .text:00446222↓j
                 cmp     [ebp+Alertable], 0
                 jnz     loc_42BB51
                 cmp     [ebp+WaitMode], 0
                 jz      short loc_405294
                 cmp     byte ptr [esi+4Ah], 0
                 jnz     loc_440534

 loc_405294:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)+E8↑j
                                         ; KeWaitForSingleObject(x,x,x,x,x)+269D5↓j
                 test    edx, edx
                 jz      loc_405112
                 mov     eax, [edx+4]
                 or      eax, [edx]
                 jnz     loc_401AE2

 loc_4052A7:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)-36AC↑j
                 mov     edi, 102h

 loc_4052AC:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)-15↑j
                                         ; KeWaitForSingleObject(x,x,x,x,x)+8200↓j
                 push    esi
                 call    _KiAdjustQuantumThread@4 ; KiAdjustQuantumThread(x)

 loc_4052B2:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)+3B399↓j
                                         ; KeWaitForSingleObject(x,x,x,x,x)+41090↓j
                 mov     cl, [esi+58h]
                 call    @KiUnlockDispatcherDatabase@4 ; KiUnlockDispatcherDatabase(x)
                 mov     eax, edi

 loc_4052BC:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)-58↑j
                 pop     edi
                 pop     esi
                 pop     ebx
                 leave
                 retn    14h
 ; ---------------------------------------------------------------------------

 loc_4052C3:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)+88↑j
                                         ; KeWaitForSingleObject(x,x,x,x,x)+95↑j ...
                 mov     [ebp+Object], ecx
                 jmp     short loc_405248
 _KeWaitForSingleObject@20 endp

//存在下一个等待对象		
 loc_44023A:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)+29↑j
                 xor     ecx, ecx
                 mov     byte ptr [esi+5Ah], 0
                 mov     [esi+5Ch], edi
                 and     word ptr [edi+14h], 0
                 inc     ecx
                 mov     [edi+0Ch], ebx
                 mov     [edi+16h], cx
                 and     dword ptr [esi+54h], 0
                 test    edx, edx
                 jnz     short loc_44029A
                 mov     [edi+10h], edi
				 //会走 loc_44025B

 loc_44025B:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)+3B10C↓j
                 mov     al, [ebp+Alertable]
                 and     dword ptr [esi+60h], 0
                 cmp     [ebp+WaitMode], 0
                 mov     [esi+164h], al
                 mov     al, [ebp+WaitMode]
                 mov     [esi+59h], al
                 mov     al, byte ptr [ebp+WaitReason]
                 mov     [esi+5Bh], al
                 jz      short loc_440291
                 cmp     byte ptr [esi+141h], 0
                 jz      short loc_440291
                 cmp     byte ptr [esi+33h], 19h
                 jge     short loc_440291
                 mov     [ebp+Object], ecx
                 jmp     loc_40524B	
 
 //互斥体实现重入的关键函数,互斥体重入次数为 80000000h
 loc_40D364:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)+CB↑j
                                        ; KeWaitForSingleObject(x,x,x,x,x)+D4↑j
                 cmp     eax, 80000000h
                 jz      loc_44620D
                 dec     dword ptr [ebx+4]
                 jnz     short loc_40D39D
                 movzx   eax, byte ptr [ebx+1Dh]    //判断是否是所属进程
                 sub     [esi+0D4h], eax            //是否禁用了内核APC
                 cmp     byte ptr [ebx+1Ch], 1      //比较石佛被放弃使用了
                 mov     [ebx+18h], esi             //设置互斥所属线程
                 jz      loc_42BED3                 //被弃用 跳转

KeWaitForSingleObject关键循环

while (TRUE)  //每次线程被其他线程唤醒,都要进入这个循环
{
	if (符合激活条件)  1. 超时 2.等待对象 SignalState > 0(由其他线程的修改SignalState函数修改)3.插入APC,强制唤醒
	{
		//1) 修改 SignalState (不同类型的等待对象处理方式不同)
		//2) 退出循环
	}
	else
	{
		if (第一次执行)
		{
			将当前线程的等待块(即刚分配好的)挂到等待对象的链表(WaitListHead)中。
		}

		//将自己挂入等待队列(KiWaitListHead)
		
		//切换线程,再次获得CPU时,从这里开始执行(切换线程意味着开始等待了,当再次获得CPU从上次进入等待的位置继续执行)
	}
}

// 1) 线程将自己 WaitBlock 位置清零
// 2) 释放 _KWAIT_BLOCK 所占的内存

总结:

不同的等待对象,用不同的方法来修改 _DISPATCHER_HEADER(SignalState)。比如:可等待对象是EVENT,则其他线程通常使用 SetEvent 来设置 SignalState = 1并且,将正在等待该对象的其他线程唤醒,也就是从等待链表(KiWaitListHead)中摘出来。但是,SetEvent函数并不会将线程从等待网上摘下来,是否要摘下来,由当前线程自己来决定。

即,其他线程在适当的时候,调用方法修改被等待对象的 SignalState 为有信号(不同的等待对象,会调用同的函数),并将等待该对象的其他线程从等待链表中摘除。这样,当前线程便会在WaitForSingleObject或者WaitFoeMultipleObjects恢复执行(在哪切换的线程,在哪开始执行),如果符合唤醒条件,此时会修改 SignalState 的值,并将自己从等待网上摘下来,此时的线程才是真正的唤醒。

无论是什么等待对象,只有在设置 SignalState, 和修改 SignalState 时不同。

强制唤醒

在APC专题中讲过,当我们插入一个用户APC时(Alertable = 1),当前线程是可以被唤醒的。因为,如果当前的线程在等待网上,执行完用户APC后,仍然要进入等待状态。即在执行完APC后再次进入等待状态,所以线程只是临时被唤醒,线程依旧在等待网上,并没有被摘除。真正唤醒线程必须有2个条件:

1)当前线程自己把自己从 KiWaitListHead 中摘除。

2)当前线程将自己从等待网络摘除,即清零 WaitBlock

cmpxchg8b 8字节比较交换指令

当对多并发的内核程序进行HOOK时,我们一般通过添加跳转指令来进行HOOK,因为条状指令需要E8 + 4个字节的地址一共5个指令,再使用memCopy函数是一个字节一个字节进行Copy的,若在此时发生线程切换,会导致代码出错,临界区、自旋锁只能加在自己的线程中无法对Hook的程序进行加锁,所以失效。解决办法就是避免在使用memCopy函数时发生线程切换,即一条指令Copy复制多个字节:

cmpxchg8b 指令

说明:

CMPXCHG主要为实现原子操作提供支持。

比较 EDX:EAX 中的 64 位值与操作数(目标操作数)。如果这两个值相等,则将 ECX:EBX 中的 64 位值存储到目标操作数。否则,将目标操作数的值加载到 EDX:EAX。目标操作数是 8 字节内存位置。对于一对 EDX:EAX 与 ECX:EBX 寄存器,EDX 与 ECX 包含 64 位值的 32 个高位,EAX 与 EBX 包含 32 个低位。

此指令可以配合 LOCK 前缀使用,此时指令将以原子方式执行。为了简化处理器的总线接口,目标操作数可以不考虑比较结果而接收一个写入周期。如果比较失败,则写回目标操作数;否则,将源操作数写入目标。(处理器永远不会只产生锁定读取而不产生锁定写入)。

事件(EVENT)

只允许一个线程进入临界区。

创建事件对象:信号

HANDLE CreateEventA(
  LPSECURITY_ATTRIBUTES lpEventAttributes,
  BOOL                  bManualReset,
  BOOL                  bInitialState,
  LPCSTR                lpName
);

CreateEvent 的 bInitialState 参数:

主要就是修改 _DISPATCHER_HEADER.SignalState 的。

初始化 _DISPATCHER_HEADER.Type 的值:

CreateEvent 的第 bManualReset 参数:

bManualReset = TRUE  //通知类型对象 -- 只是通知

bManualReset = FALSE  //事件同步对象 -- 控制线程同步

修改 _DISPATCHER_HEADER.Type = TRUE : 0 or FALSE : 1

bManualReset = TRUE,  _DISPATCHER_HEADER.Type = 0

bManualReset = FALSE, _DISPATCHER_HEADER.Type = 1

SetEvent函数分析

SerEvent 对应的内核函数: KeSetEvent

1)修改信号值 SignalState 为 1

2)判断对象类型

3)如果类型为通知类型对象 Type = 0,唤醒所有等待该状态的线程

4)如果类型为事件同步对象 Type = 1,从链表头找到第一个,等待类型为 waitAny的

释放事件 ResetEvent函数

ResetEvent函数主要是将 SignalState 修改为 0

信号量

可以允许多个线程同时进入临界区。

线程在进入临界区之前会通过调用 WaitForSingleObject 或者 WaitForMultipleObjects 来判断当前的事件对象是否有信号(SignalState > 0),只有当事件对象有信号时,才可以进入临界区(只允许一个线程进入直到退出的一段代码,不单指用 EnterCriticalSevtion() 和 LeaveCriticalSection() 而形成的临界区)。

创建信号量

HANDLE CreateSemaphore(
  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
  LONG                  lInitialCount,
  LONG                  lMaximumCount,
  LPCSTR                lpName
);

信号量内核结构体:

kd> dt _KSEMAPHORE
ntdll!_KSEMAPHORE
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 Limit            : Int4B          //lMaximumCount

初始化 _DISPATCHER_HEADER.Type 的值:

信号量的_DISPATCHER_HEADER.Type = 5,只有这一个值:

_DISPATCHER_HEADER.Type = 5

CreateSemaphore的第二个参数 lInitialCount:

设置同时进入临界区的线程数。

主要修改 _DISPATCHER_HEADER.SignalState 的值,信号量之所以可以允许多个线程同时进入临界区,是因为信号量的 _DISPATCHER_HEADER.SignalState 的值可以设置为一个数量。

CreateSemaphore的第三个参数 lMaximumCount:

信号量设置的允许同时进入临界区的线程数的最大数量,并修改_KSEMAPHORE.Limit的值。

释放信号量 ReleaseSemaphore函数

函数内核调用:

ReleaseSemaphore
	   ↓
NTReleaseSemaphore
	   ↓
KeReleaseSemaphore

1)设置 SignalState = SignalState + N(参数)

2)通过 WaitListHead 找到所有线程,并从等待链表中摘掉。是否从等待网上摘除要取决于WaitForSingleObject或者WaitFoeMultipleObjects。

互斥体

互斥体结构:

kd> dt _KMUTANT
nt!_KMUTANT
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 MutantListEntry  : _LIST_ENTRY
   +0x018 OwnerThread      : Ptr32 _KTHREAD
   +0x01c Abandoned        : UChar
   +0x01d ApcDisable       : UChar

+0x010 MutantListEntry : _LIST_ENTRY:

拥有互斥体线程(_KTHREAD.MutantListHead)是个链表头圈着所有互斥体。

+0x018 OwnerThread : Ptr32 _KTHREAD:

正在拥有互斥体的线程

+0x01c Abandoned : UChar:

是否已经被放弃不用,在KeReleaseMutant函数中会被修改,但仅仅是一个标识

+0x01d ApcDisable : UChar:

是否禁用内核APC

三环函数 CreateMutex 创建互斥体: Mutant  对应内核函数  NtCreateMutant  ApcDisable = 0(APC可以使用)

0环函数 CreateMutex 创建互斥体: Mutex  对应内核函数  NtCreateMutex  ApcDisable = 0(APC被禁止)

在KeWaitForSingleObject函数中,会将该值赋值给 _KTHREAD.KeraelAPCDisAble

互斥体解析

互斥体(MUTANT)与事件(EVENT)和信号量(SEMAPHORE)一样,都可以用来进行线程的同步控制。

但需要指出的是,这几个对象都是内核对象,这就意味着,通过这些对象可以进行跨进程的线程同步控制,如:

A进程中的X线程
           
                  等待对象Z

B进程中的Y线程

极端情况:

如果B进程的Y线程还没来得及调用修改 SignalState 的函数(如SetEvent)就因意外情况而终止,那么等待对象Z将被遗弃,这也就意味着X线程将永远等待下去。即所谓的等待对象被遗弃

如果被等待对象是事件(EVENT)和信号量(SEMAPHORE)无法解决,但互斥体可以。

CreateMutex函数(创建互斥体)

初始化互斥体:

HANDLE CreateMutex(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,  //指向安全属性的指针
  BOOL                  bInitialOwner,      //初始化互斥对象的所有者
  LPCSTR                lpName              //指向互斥对象名的指针
);

CreateMutex函数调用流程:

CreateMutex → NtCreateMutant(内核函数) → KeInitializeMutant(内核函数)

初始化MUTANT结构体:

MUTANT.Header.Type = 2;

MUTANT.Header.SignalState = bInitialOwner? 0 : 1

MUTANT.OwnerThread = 当前线程 or NULL; (由bInitialOwner决定)

MUTANT.Abandoned = 0;

MUTANT.ApcDisable = 0;

bInitialOwner == TRUE 将当前互斥体挂入到当前线程的互斥体链表:_KTHREAD.MutantListHead, MUTANT.Header.SignalState = 0

ReleaseMutex 函数(释放)

释放互斥体:

BOOL ReleaseMutex(
  HANDLE hMutex
);

ReleaseMutex函数调用流程:

ReleaseMutex → NtReleaseMutant → KeReleseMutant(内核函数)

正常调用时:

MUTANT.Header.SignalState ++;(在WaitForSingleObject或者WaitFoeMultipleObjects中,每进入一次减一)

如果 SignalState = 1 说明其他进程可用,将该互斥体从线程链表中移除。

互斥体解决等待对象被遗弃问题

KeReleaseMutant函数在线程或进程突然死掉的情况下,操作系统也会去调用。

KeReleaseMutant通过我们自己正常调用和操作系统去调用唯一不同的是第三个参数 Abandon 的值:

正常调用时 : Abandon = FLASE

操作系统调用 : Abandon = TRUE

MmUnloadSystemImage → KeReleaseMutant(X, Y, Abandon, Z)  //是否被丢弃

KeReleaseMutant函数判断:

if (Abandon == FALSE)
{
	MUTANT.Header.SignalState ++;
}
else
{
	//将其置为符合其他线程激活条件的状态
	MUTANT.Hrader.SignalState = 1;
	MUTANT.OwnerThread = NULL;
}

if (MUTANT.Header.SignalState == 1)
{
	MUTANT.OwnerThread = NULL;
	//从当前线程互斥体链表中将当前互斥体移除
}

互斥体可重入

可重入是指,若当前线程通过互斥体A进入临界区后,但是当前临界区中存在继续等待互斥体A时,可再次进入。

互斥体在判断时,若没有信号,但是是所属进程依旧可以进入临界区。

_KMUTANT.OwnerThread 保存拥有当前正在拥有互斥体的线程,当程序在调用WaitForSingleObject或者WaitFoeMultipleObjects时回去判断互斥体的 _KMUTANT.OwnerThread 值是否是当前进程,若是当前进程则进入临界区。

 loc_40525D:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)+4105D↓j
                 cmp     byte ptr [ebx], 2    //判断等待对象是否是互斥体类型
                 jnz     loc_405168           //不是互斥体 跳转
                 mov     eax, [ebx+4]
                 test    eax, eax             //判断 SignalState 是否为0
                 jg      loc_40D364           //SignalState == 0则跳转
                 cmp     esi, [ebx+18h]       //判断当前线程是否正在使用这个互斥体
                 jz      loc_40D364           //若使用当前互斥体的线程就是当前线程,则跳转
											  //实现重入

 //互斥体实现重入的关键函数,互斥体重入次数为 80000000h
 loc_40D364:                             ; CODE XREF: KeWaitForSingleObject(x,x,x,x,x)+CB↑j
                                        ; KeWaitForSingleObject(x,x,x,x,x)+D4↑j
                 cmp     eax, 80000000h
                 jz      loc_44620D
                 dec     dword ptr [ebx+4]
                 jnz     short loc_40D39D
                 movzx   eax, byte ptr [ebx+1Dh]    //判断是否是所属进程
                 sub     [esi+0D4h], eax            //是否禁用了内核APC
                 cmp     byte ptr [ebx+1Ch], 1      //比较石佛被放弃使用了
                 mov     [ebx+18h], esi             //设置互斥所属线程
                 jz      loc_42BED3                 //被弃用 跳转

总结

并发是指多个线程在同时(并不真实)执行:

单核:分时执行,不是真正的同时

多核:在某一个时刻,会同时有多个线程在执行

通常提到的并发是指多个线程同时修改共有资源,多个线程同时执行也算是并发。

同步则是保证在并发执行的环境中各个线程可以有序的执行,保证并发执行是安全的。

线程访问局部变量不会产生并发问题,因为每个线程都有自己的堆栈,线程访问全局变量时才会出现并发问题,因为所有线程都共享同一个进程下的内存。

不同版本的内核文件:

单核:

ntkrnlpa.exe      2-9-9-12分页

ntoskrnl.exe      10-10-12分页

多核:

ntkrnlpa.exe      2-9-9-12分页

ntoskrnl.exe      10-10-12分页

当临界区、事件、互斥体得不到某种资源时就会发生线程切换,但自旋锁不会。

临界区、事件、互斥体、自旋锁等实现的线程安全、同步是通过线程切换实现的,即只要一个线程执行将其他线程挂起。(性能低)

创建信号量实际上就是创建一个 _DISPATCHER_HEADER 结构体,并给它赋值。

蓝色妖姬 – 相守