Windows XP 多核同步(一)
ʕ •ᴥ•ʔ ɔ:
1 同步与互斥
- 并发:单核或多核下,在操作系统中,同个处理机上有多个程序同时运行即并发。并发可分为同步和互斥。
- 同步:并发执行的环境中各个线程必须严格按照规定的某种先后次序来运行。
- 互斥:同一个资源同一时间只有一个访问者可以进行访问,其他访问者需要等前一个访问者访问结束才可以开始访问该资源。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
- 并行:仅在多核下,两个及以上数量的线程同一时刻在执行。
多核并发情况下,存在并行的情况。多线程同时处理某些资源就会存在出错的情况,而本文就主要探讨一下问题和解决。
注意⚠️:如果线程中的代码只对局部变量进行访问,就不存在并发的问题;只有当访问全局变量的时候,才需要关注并发的问题。这是因为局部变量每次访问都是在本次调用的堆栈里操作,不会产生互相影响。
例1:
1 | DWORD dwVal = 0; //全局变量 |
很明显是不安全的,这里涉及到多核同一时刻(并行)修改内存。实际这一行代码在汇编情况下处理涉及到读写操作(原子操作):
1 | mov eax,[0x12345678] |
这里需要注意:读数据并不会产生并发问题,写内存的时候才会!
改成下面的代码安全么?
1 | INC DWORD PTR DS:[0x12345678] //一行汇编代码,安全吗? |
答案是:单核情况下是安全的,单核下CPU会保证一条汇编指令的原子性。多核下是不安全的,同样是因为存在多线程对内存的写操作。如果两个CPU同时执行该指令,[0x12345678] 的初始值是0,那么两个CPU执行后,本应该 [0x12345678] 是2,结果却有可能是1。
为了解决这个问题,可以使用 LOCK 对某个内存地址“加锁”,将指令修改成如下:
1 | 0x40123456 LOCK INC DWORD PTR DS:[0x12345678] |
⚠️ LOCK:不是把其他CPU锁住,而是将当前代码所在的内存0x40123456锁住,而不是0x12345678。也就是同时只允许一个CPU来读0x40123456这块内存。
单核模式下,单条汇编指令一定是满足原子性的,所以根本用不到 LOCK ,我们可以验证一下这个说法,首先了解一下 WINDOWS 提供的原子性操作API。
1.1 原子操作API
本文涉及的内容都是基于多核情况下的。
Windows XP 32bit内核版本区分:
1 | ntoskrnl - 单处理器,不支持PAE |
原子操作API例如一些如下:
1 | InterlockedIncrement |
以InterlockedIncrement(实现对某个变量加1操作)举例:
单核版本:
1 | .text:0046FAB0 ; __fastcall __InterlockedIncrement(x) |
多核版本:
1 | .text:0046FAB0 ; __fastcall __InterlockedIncrement(x) |
多核比单核多了使用lock,关于xadd reg/mem, reg汇编指令解释:
xadd [ecx], eax:- 先交换值:
xchg [ecx], eax; - 再将二者相加给目的操作数:
add [ecx], eax
- 先交换值:
1.2 临界区
临界区用来解决互斥问题。
如果是多行代码作为一个原子操作,如下:
1 | 关键代码A //N行代码要求原子操作 |
这样虽然每一行都保证只能一个线程占有资源,但是会浪费资源,或者其它一些问题。
解决这一问题的方法就是引入临界区,然后进入临界区前加锁,出临界区是解锁。
1 | Lab: |
如下代码为不使用xadd实现的临界区:
以下代码在单核情况下没问题,但是在多核情况下就会出问题了:
1 |
|
正确的操作如下:
1 |
|
1.3 自旋锁
Windows XP 32bit内核版本区分:
1 | ntoskrnl - 单处理器,不支持PAE |
Windows自旋锁:函数中如KiAcquireSpinLock只要包含SpinLock都是自旋锁函数。
分析一个看看:
1 | text:00469994 ; __stdcall KeAcquireSpinLockAtDpcLevel(x) |
解释一下 bts 指令,它是把 [ecx] 的第 0 位的值放到 CF,然后给第0 位置1(说明我在用临界区了)。
jb 指令看 CF=1就跳转。也就是说,
如果原来第0位是0,就retn,可以进入临界区了,说明该临界区还没有线程在用,那我用;
如果原来第0位是1,那么 jb 跳转,开始自旋,直到 [ecx] 0位变成0,重新执行 KiAcquireSpinLock 。
自旋锁只对于多核才有意义,如果是单核反而会造成大量的性能损失。
关于X86的BT指令,经过本人测试,有以下结论。
BT系列指令:位测试和设置指令。
- 按照源操作指定的位号(最低位下标为0),测试目的操作数指定的位,并将被测试位拷贝复制到EFLAG得CF。
- 操作目的操作数指定的位。
| 指令 | 含义 | 实例测试 |
|---|---|---|
| BT DEST,SRC | 将 SRC 指定的 DEST 中一位的数值复制到 CF。 | mov ax, 1 0000 0001b;bt ax,0x7;BT执行前:EAX = 0x101,EFLAG = 0x206,CF = 0。 BT执行前:EAX = 0x101,EFLAG = 0x206,CF = 0。 mov ax, 1 1000 0001b;bt ax,0x7;BT执行前:EAX = 0x181,EFLAG = 0x206,CF = 0。 BT执行后:EAX = 0x101,EFLAG = 0x207,CF = 1。 |
| BTS DEST,SRC | BTS 将 SRC 指定的 DEST 中一位的数值复制到 CF,且将 DEST 中该位置位。 | mov ax, 1 0000 0001b;bts ax,0x7;BTS执行前:EAX = 0x101,EFLAG = 0x206,CF = 0。 BTS执行后:EAX = 0x181,EFLAG = 0x206,CF = 0。 mov ax, 1 1000 0001b;bts ax,0x7;BTS执行前:EAX = 0x181,EFLAG = 0x206,CF = 0。 BTS执行后:EAX = 0x181,EFLAG = 0x207,CF = 1。 |
| BTC DEST,SRC | BTC 将 SRC 指定的 DEST 中一位的数值复制到 CF,且将 DEST 中该位取反。 | mov ax, 1 0000 0001b;btc ax,0x7;BTC执行前:EAX = 0x101,EFLAG = 0x206,CF = 0。 BTC执行后:EAX = 0x181,EFLAG = 0x206,CF = 0。 mov ax, 1 1000 0001b;btc ax,0x7;BTC执行前:EAX = 0x181,EFLAG = 0x206,CF = 0。 BTC执行后:EAX = 0x101,EFLAG = 0x207,CF = 1。 |
| BTR DEST,SRC | BTR将 SRC 指定的 DEST 中一位的数值复制到 CF,且将 DEST中该位复位。 | mov ax, 1 0000 0001b;btr ax,0x7;BTR执行前:EAX = 0x101,EFLAG = 0x206,CF = 0。 BTR执行后:EAX = 0x101,EFLAG = 0x206,CF = 0。 mov ax, 1 1000 0001b;btr ax,0x7;BTR执行前:EAX = 0x181,EFLAG = 0x206,CF = 0。 BTR执行后:EAX = 0x101,EFLAG = 0x207,CF = 1。 |
EFLAG寄存器(Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 1:Basic Architecture 3.4.3),具体可参看该章节和文章x86—EFLAGS寄存器详解。


总结:
- 自旋锁只对多核有意义。
- 自旋锁与临界区、事件、互斥体一样,都是一种同步机制,都可以让当前线程处于等待状态,区别在于自旋锁不用切换线程。
1.4 高并发HOOK
一、
这个问题的关键是,hook 后一般是 e8 / e9 后跟4字节,总共5字节,但没办法一次性改5个字节,可能改了第一个字节,正要改后4个字节时,别的线程进来访问这块内存了,就会出错。
这里介绍三种办法:
- 短跳中转,短跳中转(2字节)是比较常用的,修改前2字节跳到某个方便HOOK的地方(长跳),不多做介绍。
- 中断门,中断门也是只用改两个字节,需要先构造中断门,也不介绍。
- 找一条一次性修改8字节的指令,这个指令就是
cmpxchg8b。
cmpxchg8b mem64指令的工作如下:
比较 mem64 和 EDX:EAX
如果相等,那么把 ECX:EBX 存储到 mem64
如果不相等,那么把 mem64 存储到 EDX:EAX
我们要一次性改8字节内存,用的是相等的情况,先把要写入的内容放到 ECX:EBX ,然后调用 cmpxchg8b 指令即可。
以下参考同步篇——事件等待与唤醒代码,有时间再细品:
1 |
|
2 线程等待与唤醒
临界区和自旋锁,这两种同步方案在线程无法进入临界区时都会让当前线程进入等待状态。
一种是通过Sleep函数实现的,一种是通过让当前的CPU“空转”实现的,但这两种等待方式都有局限性:
- 通过Sleep函数进行等待,等待时间该如何确定呢(怎么确认多长时间后自己可以进入临界区)?
- 通过“空转”的方式进行等待,只有等待时间很短的情况下才有意义,否则对CPU资源是种浪费,并且自旋锁只能在多核的环境下才有意义。
有没有更加合理的等待方式呢?只有在条件成熟的时候才将当前线程唤醒?Windows在Ntdll!RtlEnterCriticalSection函数中其实就给出了一种折中的方式(当前使用临界区的线程不是CurrentThreadId的话就会调用RtlpWaitForCriticalSection.ZwWaitForSingleObject挂起当前线程)。其他情况就会自旋(可参考base/ntdll/ia64/critsect.s)。可参考:深入理解CRITICAL_SECTION。
2.1 可等待对象
在Windows中,一个线程可以通过等待一个或者多个可等待对象,从而进入等待状态,另一个线程可以在某些时刻唤醒等待这些对象的其他线程。
1 | 线程A: 线程B: |
常见的可等待对象有如下这些:
1 | _KPROCESS(进程) _KTHREAD(线程) _KTIMER(定时器) _KSEMAPHORE(信号量) _KEVENT(事件) _KMUTANT(互斥体) |
需要特别说明的是文件对_FILE_OBJECT,其他对象都是以_DISPATCHER_HEADER开头,而_FILE_OBJECT如下:
1 | kd> dt _FILE_OBJECT |
虽然该对象不是以_DISPATCHER_HEADER开头,但是在偏移+0x04c和+0x05c的成员是可等待对象_KEVENT,所以该对象也是可等待对象。
可参考:Windows内核情景分析—内核对象。
2.2 线程等待网
一、一个线程下的所有被等待对象
每个线程KTHREAD偏移0x5c的成员WaitBlockList(_KWAIT_BLOCK结构)为该线程的对象等待块,记录了该线程正在等待的那些对象。每个等待块对应一个被等待对象。该线程所有被等待的对象都会被双向循环等待块链串起来,结构如下。
1 | kd> dt _KWAIT_BLOCK |
二、一个被等待对象被哪些线程等待
每个可被等待的对象都有成员_DISPATCHER_HEADER,该成员的结构如下。
1 | kd> dt _DISPATCHER_HEADER |
其中成员WaitListHead是一个双向的循环链表,串着当前被等待对象被哪些线程等待,串的是那些线程的WaitBlockList(_KWAIT_BLOCK).WaitListEntry。
有如下关系:

三、_KWAIT_BLOCK.WaitListEntry与_DISPATCHER_HEADER.WaitListHead的关系
有如下代码。
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
HANDLE hEvent[4] = {NULL};
DWORD WINAPI ThreadProc(LPVOID lpParamter)
{
WaitForMultipleObjects(2,hEvent,FALSE,-1);
printf("Hello World!\n");
return 0;
}
WORD WINAPI ThreadProc_2(LPVOID lpParamter)
{
WaitForSingleObject(hEvent[1],-1);
printf("Hello World!\n");
return 0;
}
int main(int argc, char* argv[])
{
hEvent[0] = CreateEvent(NULL,TRUE,FALSE,NULL);
hEvent[1] = CreateEvent(NULL,TRUE,FALSE,NULL);
CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadProc,NULL,0,NULL);
CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadProc_2,NULL,0,NULL);
getchar();
return 0;
}线程
ThreadProc共等待两个Event对象,两个线程都在等待hEvent[1]对象。查看
ThreadProc线程的被等待对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS 866b9020 SessionId: 0 Cid: 0a94 Peb: 7ffde000 ParentCid: 0a74
DirBase: 06e403e0 ObjectTable: e275ae68 HandleCount: 16.
Image: 0430_Dispatcher
...
kd> !process 0x866b9020
...
THREAD 86288020 Cid 0a94.0934 Teb: 7ffdc000 Win32Thread: 00000000 WAIT: (UserRequest) UserMode Non-Alertable
...
kd> dt _KTHREAD 0x86288020
ntdll!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
...
+0x05c WaitBlockList : 0x86288090 _KWAIT_BLOCK
...
kd> dt _KWAIT_BLOCK 0x86288090
ntdll!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x86706700 - 0x86706700 ]
+0x008 Thread : 0x86288020 _KTHREAD
+0x00c Object : 0x867066f8 Void
+0x010 NextWaitBlock : 0x862880a8 _KWAIT_BLOCK
+0x014 WaitKey : 0
+0x016 WaitType : 1可以看到:
WaitListEntry:链上只有一个节点,说明该被等待对象只有一个线程在等待。只有被等待的对象能看到,不被等待的对象创建了也不在线程的等待块上。NextWaitBlock:单向循环链表,该线程在等待两个Event对象,所以后面串着的肯定是另一个对象,如下。1
2
3
4
5
6
7
8kd> dt _KWAIT_BLOCK 0x862880a8
ntdll!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x860ef460 - 0x86454508 ]
+0x008 Thread : 0x86288020 _KTHREAD
+0x00c Object : 0x86454500 Void
+0x010 NextWaitBlock : 0x86288090 _KWAIT_BLOCK
+0x014 WaitKey : 1
+0x016 WaitType : 1可以看到,这里的
NextWaitBlock又指回去第一个对象。
分析
_DISPATCHER_HEADER.WaitListHead与_KWAIT_BLOCK.WaitListEntry关系。这里被等待的对象是
Event,查看一下第一个被等待对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21kd> dt _KWAIT_BLOCK 0x86288090
ntdll!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x86706700 - 0x86706700 ]
+0x008 Thread : 0x86288020 _KTHREAD
+0x00c Object : 0x867066f8 Void
+0x010 NextWaitBlock : 0x862880a8 _KWAIT_BLOCK
+0x014 WaitKey : 0
+0x016 WaitType : 1
...
kd> dt _KEVENT 0x867066f8
ntdll!_KEVENT
+0x000 Header : _DISPATCHER_HEADER
...
kd> dt _DISPATCHER_HEADER 0x867066f8
ntdll!_DISPATCHER_HEADER
+0x000 Type : 0 ''
+0x001 Absolute : 0x40 '@'
+0x002 Size : 0x4 ''
+0x003 Inserted : 0x86 ''
+0x004 SignalState : 0n0
+0x008 WaitListHead : _LIST_ENTRY [ 0x86288090 - 0x86288090 ]可以看到,
WaitListHead上挂着一个_KWAIT_BLOCK,即为第一个被等待对象。同时,有如下关系(仅对_DISPATCHER_HEADER为内核对象首成员时管用):
_KWAIT_BLOCK.WaitListEntry.FLINK == _KWAIT_BLOCK。_DISPATCHER_HEADER.WaitListHead.FLINK == _KWAIT_BLOCK。&_DISPATCHER_HEADER.WaitListHead.FLINK == _KWAIT_BLOCK.WaitListEntry.BLINK。
四、源代码中的判空
源代码常有以下语句来判断链表是否为空。在函数KeSetEvent有如下代码:
1 | if (IsListEmpty(&Event->Header.WaitListHead) == TRUE) |
⚠️注意:这里判断链表为空,并不是只有一个节点的意思,如下:
对象
KEVENT == 0x86237618,仅被一个线程等待,如下。1
2
3
4
5
6
7
8kd> dt 0x8610e0a8 _KWAIT_BLOCK
nt!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x86237620 - 0x86237620 ]
+0x008 Thread : 0x8610e020 _KTHREAD
+0x00c Object : 0x86237618 Void
+0x010 NextWaitBlock : 0x8610e0a8 _KWAIT_BLOCK
+0x014 WaitKey : 0
+0x016 WaitType : 1先要获取一下
ListHead(即WaitListEntry.Flink的地址),在_DISPATCHER_HEADER中。查看
&Event->Header.WaitListHead。1
2
3
4
5
6
7
8
9
10
11kd> dt _KEVENT 0x86237618
nt!_KEVENT
+0x000 Header : _DISPATCHER_HEADER
kd> dt _DISPATCHER_HEADER 0x86237618
nt!_DISPATCHER_HEADER
+0x000 Type : 0 ''
+0x001 Absolute : 0 ''
+0x002 Size : 0x4 ''
+0x003 Inserted : 0 ''
+0x004 SignalState : 0n0
+0x008 WaitListHead : _LIST_ENTRY [ 0x8610e0a8 - 0x8610e0a8 ]得到:
&Event->Header.WaitListHead == 0x86237618+0x8 == 0x86237620。然后
ListHead->Flink == 0x8610e0a8,可得到ListHead->Flink == 0x8610e0a8 != ListHead == 0x86237620。
五、源代码中使用_KWAIT_BLOCK.WaitListEntry与_DISPATCHER_HEADER.WaitListHead
通过Windbg+IDA Pro逆向分析有这样一个结论:
如果一个对象被多个线程等待,该对象的等待块链有以下关系:
- 当前对象
Event->Header.WaitListHead == Event->Header.WaitListHead.Flink,Event + 0x8 == &Event->Header.WaitListHead。 - 第一个
&Event->Header.WaitListHead == _KWAIT_BLOCK.WaitListEntry.Blink。
举例说明:
1 | PLIST_ENTRY WaitEntry; |
这里有这样的关系:&Event->Header.WaitListHead == _KWAIT_BLOCK.WaitListEntry.Blink,所以循环while中会用来判断是否已经完成循环。具体分析见章节2.4。
六、线程如何存储等待块?
KTHREAD +0x070 WaitBlock : [4] _KWAIT_BLOCK是一个数组,当一个线程所等待的对象小于等于3个时并不会额外去申请内存存放等待块,会将被等待的对象的_KWAIT_BLOCK存到这个位置,其中第四个成员WaitBlock[3]只能用于存储计时器对象的等待块,其索引WaitKey == 0x102。
当线程所等待的对象小于等于3个时,WaitBlock[3]有以下两种状态:
- 如使用
WaitSingleObject(HANDLE x, -1),则WaitBlock[3].NextWaitBlock = NULL; - 如使用
WaitSingleObject(HANDLE x, y),y > 0,则WaitBlock[3].NextWaitBlock 指向 WaitBlock[0],然后WaitBlock[0].NextWaitBlock 指向 WaitBlock[3]。
当线程所等待的对象大于3个时,将会分配新的空间来存储等待块,并不会使用+0x070 WaitBlock来进行存储。
七、总结:
1、等待中的线程,一定在等待链表中(KiWaitListHead),同时也一定在这张网上(KTHREAD +5C的位置不为空)。
2、线程通过调用WaitForSingleObject/WaitForMultipleObjects函数将自己挂到这张网上。
3、线程什么时候会再次执行取决于其他线程何时调用相关函数,等待对象不同调用的函数也不同。
2.3 练习:查看线程的等待块
查看线程的被等待对象有哪些?
在3环有如下代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
HANDLE hEvent = NULL;
DWORD WINAPI ThreadProc(LPVOID lpParamter)
{
WaitForSingleObject(hEvent,-1);
printf("Hello World!\n");
return 0;
}
int main(int argc, char* argv[])
{
hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadProc,NULL,0,NULL);
getchar();
return 0;
}查看通过
CreateThread创建的线程。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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS 860a5370 SessionId: 0 Cid: 0b90 Peb: 7ffde000 ParentCid: 0a74
DirBase: 06e40380 ObjectTable: e10b85a8 HandleCount: 14.
Image: 0430_Dispatcher
kd> !process 0x860a5370
PROCESS 860a5370 SessionId: 0 Cid: 0b90 Peb: 7ffde000 ParentCid: 0a74
DirBase: 06e40380 ObjectTable: e10b85a8 HandleCount: 14.
Image: 0430_Dispatcher
VadRoot 863c1a88 Vads 23 Clone 0 Private 40. Modified 0. Locked 0.
DeviceMap e1535598
Token e270cd48
ElapsedTime 00:00:06.937
UserTime 00:00:00.015
KernelTime 00:00:00.000
QuotaPoolUsage[PagedPool] 12436
QuotaPoolUsage[NonPagedPool] 920
Working Set Sizes (now,min,max) (188, 50, 345) (752KB, 200KB, 1380KB)
PeakWorkingSetSize 188
VirtualSize 7 Mb
PeakVirtualSize 7 Mb
PageFaultCount 181
MemoryPriority BACKGROUND
BasePriority 8
CommitCharge 48
DebugPort 86374710
THREAD 8638ac98 Cid 0b90.0588 Teb: 7ffdd000 Win32Thread: 00000000 WAIT: (WrLpcReply) UserMode Non-Alertable
8638ae8c Semaphore Limit 0x1
Waiting for reply to LPC MessageId 00009b74:
Current LPC port e272b938
Not impersonating
DeviceMap e1535598
Owning Process 0 Image: <Unknown>
Attached Process 860a5370 Image: 0430_Dispatcher
Wait Start TickCount 106630 Ticks: 375 (0:00:00:05.859)
Context Switch Count 46
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address 0x00401640
Start Address 0x7c8106f5
Stack Init aaa3e000 Current aaa3dc50 Base aaa3e000 Limit aaa3b000 Call 0
Priority 12 BasePriority 8 PriorityDecrement 2 DecrementCount 16
ChildEBP RetAddr
aaa3dc68 80504836 nt!KiSwapContext+0x2f (FPO: [Uses EBP] [0,0,4])
aaa3dc74 804fc068 nt!KiSwapThread+0x8a (FPO: [0,0,0])
aaa3dc9c 805a4397 nt!KeWaitForSingleObject+0x1c2 (FPO: [Non-Fpo])
aaa3dd50 8054261c nt!NtRequestWaitReplyPort+0x63d (FPO: [Non-Fpo])
aaa3dd50 7c92e4f4 nt!KiFastCallEntry+0xfc (FPO: [0,0] TrapFrame @ aaa3dd64)
0012fce4 00000000 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
THREAD 86372020 Cid 0b90.086c Teb: 7ffdc000 Win32Thread: 00000000 WAIT: (UserRequest) UserMode Non-Alertable
86293380 NotificationEvent
Not impersonating
DeviceMap e1535598
Owning Process 0 Image: <Unknown>
Attached Process 860a5370 Image: 0430_Dispatcher
Wait Start TickCount 106630 Ticks: 375 (0:00:00:05.859)
Context Switch Count 5
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address 0x00401014
Start Address 0x7c8106e9
Stack Init aad78000 Current aad77ca0 Base aad78000 Limit aad75000 Call 0
Priority 12 BasePriority 8 PriorityDecrement 2 DecrementCount 16
ChildEBP RetAddr
aad77cb8 80504836 nt!KiSwapContext+0x2f (FPO: [Uses EBP] [0,0,4])
aad77cc4 804fc068 nt!KiSwapThread+0x8a (FPO: [0,0,0])
aad77cec 805c1750 nt!KeWaitForSingleObject+0x1c2 (FPO: [Non-Fpo])
aad77d50 8054261c nt!NtWaitForSingleObject+0x9a (FPO: [Non-Fpo])
aad77d50 7c92e4f4 nt!KiFastCallEntry+0xfc (FPO: [0,0] TrapFrame @ aad77d64)
0052ff44 00000000 ntdll!KiFastSystemCallRet (FPO: [0,0,0])如上第一个线程是主线程,通过
CreateThread创建的线程应该是THREAD 86372020。查看线程的等待块
WaitBlockList(+0x5c)。1
2
3
4
5
6
7
8
9
10kd> dt _KTHREAD 86372020
ntdll!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
...
+0x05c WaitBlockList : 0x86372090 _KWAIT_BLOCK
+0x060 WaitListEntry : _LIST_ENTRY [ 0x8638acf8 - 0x8628b460 ]
+0x060 SwapListEntry : _SINGLE_LIST_ENTRY
...
+0x070 WaitBlock : [4] _KWAIT_BLOCK
...查看
_KWAIT_BLOCK如下。1
2
3
4
5
6
7
8kd> dt _KWAIT_BLOCK 0x86372090
ntdll!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x86293388 - 0x86293388 ]
+0x008 Thread : 0x86372020 _KTHREAD //当前等待块所属的线程
+0x00c Object : 0x86293380 Void //当前等待块对应的等待对象(线程所等待对象Body)
+0x010 NextWaitBlock : 0x86372090 _KWAIT_BLOCK //指向下一个等待块(单向循环链表)
+0x014 WaitKey : 0 //当前等待块的索引,0表示第一个
+0x016 WaitType : 1 //0:只有所有等待对象都被激活才行,1:只要有一个等待对象被激活即可可以看到
NextWaitBlock指向本身,而不是指向定时器。查看一下定时器(WaitBlock[3]):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
30
31
32
33
34
35
36
37
38
39kd> dt _KWAIT_BLOCK 0x86372020+0x70
ntdll!_KWAIT_BLOCK
//WaitBlock[0]
+0x000 WaitListEntry : _LIST_ENTRY [ 0x86293388 - 0x86293388 ]
+0x008 Thread : 0x86372020 _KTHREAD
+0x00c Object : 0x86293380 Void
+0x010 NextWaitBlock : 0x86372090 _KWAIT_BLOCK
+0x014 WaitKey : 0
+0x016 WaitType : 1
//WaitBlock[1]
kd> dt _KWAIT_BLOCK 0x86372020+0x70+0x18
ntdll!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x008 Thread : 0x86372020 _KTHREAD
+0x00c Object : (null)
+0x010 NextWaitBlock : (null)
+0x014 WaitKey : 0
+0x016 WaitType : 0
//WaitBlock[2]
kd> dt _KWAIT_BLOCK 0x86372020+0x70+0x18*2
ntdll!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x008 Thread : 0x86372020 _KTHREAD
+0x00c Object : (null)
+0x010 NextWaitBlock : (null)
+0x014 WaitKey : 0
+0x016 WaitType : 0
//WaitBlock[3]
kd> dt _KWAIT_BLOCK 0x86372020+0x70+0x18*3
ntdll!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x86372118 - 0x86372118 ]
+0x008 Thread : 0x86372020 _KTHREAD
+0x00c Object : 0x86372110 Void
+0x010 NextWaitBlock : (null)
+0x014 WaitKey : 0x102
+0x016 WaitType : 1
2.4 练习:查看一个对象被多个线程等待
通过Windbg+IDA Pro逆向分析有这样一个结论:
如果一个对象被多个线程等待,该对象的等待块链有以下关系:
- 当前对象
Event->Header.WaitListHead == Event->Header.WaitListHead.Flink,Event + 0x8 == &Event->Header.WaitListHead。 - 第一个
&Event->Header.WaitListHead == _KWAIT_BLOCK.WaitListEntry.Blink。
举例说明:
1 | PLIST_ENTRY WaitEntry; |
这里有这样的关系:&Event->Header.WaitListHead == _KWAIT_BLOCK.WaitListEntry.Blink,所以循环while中会用来判断是否已经完成循环。
有如下代码:
1 |
|
如上代码,对象句柄hEvent[0]被四个线程等待,这里简单随便拿一个除了主线程之外的四个线程中的一个进行分析。
在Windbg中找到线程
ThreadProc进行分析。1
2
3
4
5kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS 8607f140 SessionId: 0 Cid: 0c24 Peb: 7ffd5000 ParentCid: 0a74
DirBase: 06e403e0 ObjectTable: e27b2990 HandleCount: 18.
Image: 0430_Dispatcher找到线程
ThreadProc。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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139kd> !process 0x8607f140
PROCESS 8607f140 SessionId: 0 Cid: 0c24 Peb: 7ffd5000 ParentCid: 0a74
DirBase: 06e403e0 ObjectTable: e27b2990 HandleCount: 18.
Image: 0430_Dispatcher
VadRoot 866bbdc0 Vads 29 Clone 0 Private 47. Modified 0. Locked 0.
DeviceMap e1535598
Token e2ef2540
ElapsedTime 00:00:05.015
UserTime 00:00:00.015
KernelTime 00:00:00.000
QuotaPoolUsage[PagedPool] 12484
QuotaPoolUsage[NonPagedPool] 1160
Working Set Sizes (now,min,max) (199, 50, 345) (796KB, 200KB, 1380KB)
PeakWorkingSetSize 199
VirtualSize 10 Mb
PeakVirtualSize 10 Mb
PageFaultCount 191
MemoryPriority BACKGROUND
BasePriority 8
CommitCharge 58
DebugPort 863272b0
THREAD 85e53208 Cid 0c24.0cc8 Teb: 7ffdf000 Win32Thread: 00000000 WAIT: (WrLpcReply) UserMode Non-Alertable
85e533fc Semaphore Limit 0x1
Waiting for reply to LPC MessageId 00015804:
Current LPC port e27a2580
Not impersonating
DeviceMap e1535598
Owning Process 0 Image: <Unknown>
Attached Process 8607f140 Image: 0430_Dispatcher
Wait Start TickCount 423771 Ticks: 241 (0:00:00:03.765)
Context Switch Count 50
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address 0x00401780
Start Address 0x7c8106f5
Stack Init aa04b000 Current aa04ac50 Base aa04b000 Limit aa048000 Call 0
Priority 10 BasePriority 8 PriorityDecrement 2 DecrementCount 16
ChildEBP RetAddr
aa04ac68 80504836 nt!KiSwapContext+0x2f (FPO: [Uses EBP] [0,0,4])
aa04ac74 804fc068 nt!KiSwapThread+0x8a (FPO: [0,0,0])
aa04ac9c 805a4397 nt!KeWaitForSingleObject+0x1c2 (FPO: [Non-Fpo])
aa04ad50 8054261c nt!NtRequestWaitReplyPort+0x63d (FPO: [Non-Fpo])
aa04ad50 7c92e4f4 nt!KiFastCallEntry+0xfc (FPO: [0,0] TrapFrame @ aa04ad64)
WARNING: Frame IP not in any known module. Following frames may be wrong.
0012fce4 00000000 0x7c92e4f4
THREAD 85e51968 Cid 0c24.0e78 Teb: 7ffde000 Win32Thread: 00000000 WAIT: (UserRequest) UserMode Non-Alertable
8608c280 NotificationEvent
862c1528 NotificationEvent
Not impersonating
DeviceMap e1535598
Owning Process 0 Image: <Unknown>
Attached Process 8607f140 Image: 0430_Dispatcher
Wait Start TickCount 423771 Ticks: 241 (0:00:00:03.765)
Context Switch Count 11
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address 0x00401005
Start Address 0x7c8106e9
Stack Init aa64c000 Current aa64b960 Base aa64c000 Limit aa649000 Call 0
Priority 12 BasePriority 8 PriorityDecrement 2 DecrementCount 16
ChildEBP RetAddr
aa64b978 80504836 nt!KiSwapContext+0x2f (FPO: [Uses EBP] [0,0,4])
aa64b984 804fbd88 nt!KiSwapThread+0x8a (FPO: [0,0,0])
aa64b9bc 805c1a37 nt!KeWaitForMultipleObjects+0x284 (FPO: [Non-Fpo])
aa64bd48 8054261c nt!NtWaitForMultipleObjects+0x297 (FPO: [Non-Fpo])
aa64bd48 7c92e4f4 nt!KiFastCallEntry+0xfc (FPO: [0,0] TrapFrame @ aa64bd64)
WARNING: Frame IP not in any known module. Following frames may be wrong.
0052ff34 00000000 0x7c92e4f4
THREAD 8607e960 Cid 0c24.0248 Teb: 7ffdd000 Win32Thread: 00000000 WAIT: (UserRequest) UserMode Non-Alertable
8608c280 NotificationEvent
Not impersonating
DeviceMap e1535598
Owning Process 0 Image: <Unknown>
Attached Process 8607f140 Image: 0430_Dispatcher
Wait Start TickCount 423771 Ticks: 241 (0:00:00:03.765)
Context Switch Count 10
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address 0x0040100a
Start Address 0x7c8106e9
Stack Init aac0e000 Current aac0dca0 Base aac0e000 Limit aac0b000 Call 0
Priority 10 BasePriority 8 PriorityDecrement 2 DecrementCount 16
ChildEBP RetAddr
aac0dcb8 80504836 nt!KiSwapContext+0x2f (FPO: [Uses EBP] [0,0,4])
aac0dcc4 804fc068 nt!KiSwapThread+0x8a (FPO: [0,0,0])
aac0dcec 805c1750 nt!KeWaitForSingleObject+0x1c2 (FPO: [Non-Fpo])
aac0dd50 8054261c nt!NtWaitForSingleObject+0x9a (FPO: [Non-Fpo])
aac0dd50 7c92e4f4 nt!KiFastCallEntry+0xfc (FPO: [0,0] TrapFrame @ aac0dd64)
WARNING: Frame IP not in any known module. Following frames may be wrong.
0062ff44 00000000 0x7c92e4f4
THREAD 866bb4d0 Cid 0c24.08b4 Teb: 7ffdc000 Win32Thread: 00000000 WAIT: (UserRequest) UserMode Non-Alertable
8608c280 NotificationEvent
Not impersonating
DeviceMap e1535598
Owning Process 0 Image: <Unknown>
Attached Process 8607f140 Image: 0430_Dispatcher
Wait Start TickCount 423771 Ticks: 241 (0:00:00:03.765)
Context Switch Count 9
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address 0x00401014
Start Address 0x7c8106e9
Stack Init aab0a000 Current aab09ca0 Base aab0a000 Limit aab07000 Call 0
Priority 10 BasePriority 8 PriorityDecrement 2 DecrementCount 16
ChildEBP RetAddr
aab09cb8 80504836 nt!KiSwapContext+0x2f (FPO: [Uses EBP] [0,0,4])
aab09cc4 804fc068 nt!KiSwapThread+0x8a (FPO: [0,0,0])
aab09cec 805c1750 nt!KeWaitForSingleObject+0x1c2 (FPO: [Non-Fpo])
aab09d50 8054261c nt!NtWaitForSingleObject+0x9a (FPO: [Non-Fpo])
aab09d50 7c92e4f4 nt!KiFastCallEntry+0xfc (FPO: [0,0] TrapFrame @ aab09d64)
WARNING: Frame IP not in any known module. Following frames may be wrong.
0072ff44 00000000 0x7c92e4f4
THREAD 8623bda8 Cid 0c24.0ca4 Teb: 7ffdb000 Win32Thread: 00000000 WAIT: (UserRequest) UserMode Non-Alertable
8608c280 NotificationEvent
Not impersonating
DeviceMap e1535598
Owning Process 0 Image: <Unknown>
Attached Process 8607f140 Image: 0430_Dispatcher
Wait Start TickCount 423771 Ticks: 241 (0:00:00:03.765)
Context Switch Count 8
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address 0x0040100f
Start Address 0x7c8106e9
Stack Init aad68000 Current aad67ca0 Base aad68000 Limit aad65000 Call 0
Priority 10 BasePriority 8 PriorityDecrement 2 DecrementCount 16
ChildEBP RetAddr
aad67cb8 80504836 nt!KiSwapContext+0x2f (FPO: [Uses EBP] [0,0,4])
aad67cc4 804fc068 nt!KiSwapThread+0x8a (FPO: [0,0,0])
aad67cec 805c1750 nt!KeWaitForSingleObject+0x1c2 (FPO: [Non-Fpo])
aad67d50 8054261c nt!NtWaitForSingleObject+0x9a (FPO: [Non-Fpo])
aad67d50 7c92e4f4 nt!KiFastCallEntry+0xfc (FPO: [0,0] TrapFrame @ aad67d64)
WARNING: Frame IP not in any known module. Following frames may be wrong.
0082ff44 00000000 0x7c92e4f4如上,线程
ThreadProc对应的信息为THREAD 85e51968。分析线程
ThreadProc。1
2
3
4
5
6kd> dt _KTHREAD 0x85e51968
nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
...
+0x05c WaitBlockList : 0x85e519d8 _KWAIT_BLOCK
...找到该线程的等待块上第一个等待对象
hEvent[0]。1
2
3
4
5
6
7
8kd> dt _KWAIT_BLOCK 0x85e519d8
nt!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x8623be18 - 0x8608c288 ]
+0x008 Thread : 0x85e51968 _KTHREAD
+0x00c Object : 0x8608c280 Void
+0x010 NextWaitBlock : 0x85e519f0 _KWAIT_BLOCK
+0x014 WaitKey : 0
+0x016 WaitType : 1验证一下
&Event->Header.WaitListHead == _KWAIT_BLOCK.WaitListEntry.Blink。要先得到
&Event->Header.WaitListHead的值,KEVENT第一个成员是_DISPATCHER_HEADER。1
2
3
4
5
6
7
8
9
10
11
12kd> dt _KEVENT 0x8608c280
nt!_KEVENT
+0x000 Header : _DISPATCHER_HEADER
kd> dt _DISPATCHER_HEADER 0x8608c280
nt!_DISPATCHER_HEADER
+0x000 Type : 0 ''
+0x001 Absolute : 0xac ''
+0x002 Size : 0x4 ''
+0x003 Inserted : 0x86 ''
+0x004 SignalState : 0n0
+0x008 WaitListHead : _LIST_ENTRY [ 0x85e519d8 - 0x866bb540 ]因为
Event->Header.WaitListHead == Event->Header.WaitListHead.Flink == 0x85e519d8,所以&Event->Header.WaitListHead == 0x8608c280+0x8 == 0x8608c288。然后
_KWAIT_BLOCK.WaitListEntry.Blink == 0x8608c288,所以得到验证,while (WaitEntry != WaitList)也就可以用了。保险起见,确定一下
_KWAIT_BLOCK.WaitListEntry.Blink == 0x8608c288里面存的是不是第一个_KWAIT_BLOCK 0x85e519d8。1
2
3kd> dd 0x8608c288
8608c288 85e519d8 866bb540 004f0006 e5726854
8608c298 86121bd0 8607fd90 8608c3d8 00000000
所以
WaitListEntry和WaitListHead是你存我我存你,但是WaitListHead存的永远是_KWAIT_BLOCK,而dd WaitListEntry.Flink里面的值才是真正_KWAIT_BLOCK。
下面开始查看一下等待hEvent[0]的所有线程:
第 1 个等待
hEvent[0]的线程。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18kd> dt _KWAIT_BLOCK 0x85e519d8
nt!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x8623be18 - 0x8608c288 ]
+0x008 Thread : 0x85e51968 _KTHREAD
+0x00c Object : 0x8608c280 Void
+0x010 NextWaitBlock : 0x85e519f0 _KWAIT_BLOCK
+0x014 WaitKey : 0
+0x016 WaitType : 1
kd> dt _DISPATCHER_HEADER 0x8608c280
nt!_DISPATCHER_HEADER
+0x000 Type : 0 ''
+0x001 Absolute : 0xac ''
+0x002 Size : 0x4 ''
+0x003 Inserted : 0x86 ''
+0x004 SignalState : 0n0
+0x008 WaitListHead : _LIST_ENTRY [ 0x85e519d8 - 0x866bb540 ]
//&WaitListHead = 0x8608c280 + 0x8 == 0x8608c288需要从
WaitEntry = ListHead->Flink;方向开始遍历链表,顺着ListHead->Flink的方向,终止条件是:当某个ListHead->Flink==&Event->Header.WaitListHead == 0x8608c288就终止。往下看,当某个
ListHead->Flink == 0x8608c288就终止。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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53kd> dt _KWAIT_BLOCK 0x85e519d8
nt!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x8623be18 - 0x8608c288 ]
+0x008 Thread : 0x85e51968 _KTHREAD
+0x00c Object : 0x8608c280 Void
+0x010 NextWaitBlock : 0x85e519f0 _KWAIT_BLOCK
+0x014 WaitKey : 0
+0x016 WaitType : 1
kd> dt _DISPATCHER_HEADER 0x8608c280
nt!_DISPATCHER_HEADER
+0x000 Type : 0 ''
+0x001 Absolute : 0xac ''
+0x002 Size : 0x4 ''
+0x003 Inserted : 0x86 ''
+0x004 SignalState : 0n0
+0x008 WaitListHead : _LIST_ENTRY [ 0x85e519d8 - 0x866bb540 ]
//Object == 0x8608c280
//ListHead == &Event->Header.WaitListHead == 0x8608c280+0x8 == 0x8608c288
//ListHead.Flink == 0x85e519d8 != 0x8608c288,继续往下遍历
//下一个ListHead.Flink == dd 0x85e519d8 == 0x8623be18
kd> dt _KWAIT_BLOCK 0x8623be18
nt!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x8607e9d0 - 0x85e519d8 ]
+0x008 Thread : 0x8623bda8 _KTHREAD
+0x00c Object : 0x8608c280 Void
+0x010 NextWaitBlock : 0x8623be18 _KWAIT_BLOCK
+0x014 WaitKey : 0
+0x016 WaitType : 1
//Object == 0x8608c280
//ListHead.Flink == 0x8607e9d0 != 0x8608c288,继续往下遍历
kd> dt _KWAIT_BLOCK 0x8607e9d0
nt!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x866bb540 - 0x8623be18 ]
+0x008 Thread : 0x8607e960 _KTHREAD
+0x00c Object : 0x8608c280 Void
+0x010 NextWaitBlock : 0x8607e9d0 _KWAIT_BLOCK
+0x014 WaitKey : 0
+0x016 WaitType : 1
//Object == 0x8608c280
//ListHead.Flink == 0x866bb540 != 0x8608c288,继续往下遍历
kd> dt _KWAIT_BLOCK 0x866bb540
nt!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x8608c288 - 0x8607e9d0 ]
+0x008 Thread : 0x866bb4d0 _KTHREAD
+0x00c Object : 0x8608c280 Void
+0x010 NextWaitBlock : 0x866bb540 _KWAIT_BLOCK
+0x014 WaitKey : 0
+0x016 WaitType : 1
//Object == 0x8608c280
//ListHead.Flink == 0x8608c288 == 0x8608c288,终止遍历综上,在同一个
Object下有4个Thread,说明有4个线程在等待当前对象。
3 WaitForSingleObject分析
3.1 WaitForSingleObject
先分析一下WaitForSingleObject调用过程:
1 | kernel32.dll |
NtWaitForSingleObject (
base/ntos/ob/obwait.c)
1)调用ObReferenceObjectByHandle函数,通过对象句柄找到等待对象结构体地址。
2)调用KeWaitForSingleObject函数,进入关键循环。KeWaitForSingleObject:上半部分
1) 向_KTHREAD(+70)位置的等待块赋值。
2) 如果超时时间不为0,KTHREAD(+70)第四个等待块与第一个等待块关联起来:第一个等待块指向第四个等待块,第四个等待块指向第一个等待块。(_KWAIT_BLOCK第四个成员是给计时器用的)。
3)KTHREAD(+5C)指向第一个_KWAIT_BLOCK。
4) 进入关键循环。
总结:
不同的等待对象,用不同的方法来修改_DISPATCHER_HEADER(SignalState)。
比如:如果可等待对象是EVENT,其他线程通常使用SetEvent来设置SignalState = 1。
并且,将正在等待该对象的其他线程唤醒,也就是从等待链表(KiWaitListHead)中摘出来。但是,SetEvent函数并不会将线程从等待网上摘下来,是否要下来,由当前线程自己来决定。
3.2 NtWaitForSingleObject
1 | NTSTATUS NtWaitForSingleObject(IN HANDLE Handle, IN BOOLEAN Alertable, IN PLARGE_INTEGER Timeout OPTIONAL ) |
该函数源码在base/ntos/ob/obwait.c中,函数大致功能如下。
- 先检查
if(Timeout > 0 && PreviousMode != KernelMode),更新Timeout。 - 使用参数一
Handle调用ObReferenceObjectByHandle获得被等待对象Object。 _OBJECT_HEADER.Type.DefaultObject = [[Object - 0x10] + 0x48],得到DefaultObject,并对其进行判断,判断方法看下一节。- 调用
KeWaitForSingleObject。
⚠️需要注意参数二:参数Alertable是个布尔量,表示是否允许本次等待因用户空间APC而中断,或者说被“吵醒(Alert)”。
- 吵醒与唤醒是不同的,唤醒是因为所等待的条件得到了满足(仓库到货),而吵醒是因为别的原因而醒来(与仓库无关)。
- 举例,唤醒:等待时间到,吵醒:插入APC而去执行这个APC。
《Windows内核情景分析6.3 P473》
3.2.1 DefaultObject
可等待对象分为:直接可等待对象间接可等待对象。
- 直接可等待对象:
_DISPATCHER_HEADER结构为内核对象的第一个成员。 - 间接可等待对象:内核对象第一个成员不是
_DISPATCHER_HEADER结构,而是往该内核对象内嵌入一个直接可等待对象,所以实际可等待的是嵌入于其内部的事件对象。
_OBJECT_HEADER.Type.DefaultObject可以为指针或位移量(偏移)。
指针:指向
ObpDefaultObject,当前的对象并不是用来进行操作的直接等待对象,它指向一个默认的等待对象,然后传给KeWaitForSingleObject用,这种情况是特殊的,地址 > 0x80000000。(其实有点类似_FILE_OBJECT中并不是使用对象本身,而是使用加上偏移后的Event)。位移量:两种情况。
- DefaultObject == 0,直接可等待对象就是对象本身。
- DefaultObject > 0,直接可等待对象嵌入当前对象当中,如_FILE_OBJECT中嵌入Event。
如何判断是地址还是偏移?
ReactOS中用到的是#define IsPointerOffset(Ptr)((LONG_PTR)(Ptr)>=0),而XP源码如下:
1 | ObjectHeader = OBJECT_TO_OBJECT_HEADER( Object ); //获取_OBJECT_HEADER |
在《Windows内核情景分析》6.3中是这样说的:
LONG_PTR是个特殊的数据类型,它既可被用做指针,又可被用做整数。宏操作名IsPointerOffset()的意思是判断一个(形式上的)指针实际上是否是一个位移量。怎么判断呢?如果参数Ptr大于等于0,即其最高位为0,则就是一个位移量;而若小于0,即其最高位为1,则就是个指针。(实际上这里就是利用有符号数来进行判断,正数的最高位为1,同时内核地址肯定是大于0x80000000的,一举两得的方法)我们知道,在Windows中,系统空间与用户空间的分界是0x80000000,所以凡是指向内核中任何一个数据结构的指针,其最高位必定是1。所以,只要DefaultObject的最高位为0,就是位移量,而位移量为0就表示目标对象本身就是可等待对象。
为什么WaitObject要加上偏移?
NtWaitForSingleObject可以等待直接的、间接的可等待对象,然而KeWaitForSingleObject只能等待真正的可直接等待对象,所以,必须将间接可等待对象转换为直接可等待对象,而每个对象的可直接等待对象记录在其对象类型的DefaultObject字段中。
3.3 KeWaitForSingleObject
线程等待与唤醒主要就是函数KeWaitForSingleObject。
基本调用框架如下:
1 | ①if (Thread->WaitNext == FALSE) goto WaitStart; |
源代码如下(base/ntos/ke/wait.c):
1 | NTSTATUS |
在KeWaitForSingleObject刚开始进入函数的时候会初始化第一个等待对象WaitBlock[3]的等待块链,并根据等待时间不同判断其是否与计时器WaitBlock[3]进行关联。
当SignalState == 1或者超时满足条件时,直接跳转到NoWait就返回了,在该函数中并没有看到在哪里将等待该对象的线程从线程等待链、对象等待网上断链,也没有将_KWAIT_BLOCK所占内存释放,那这些操作是在哪执行的呢?
根据我自己的逆向分析,上述对应的清理工作,不同的对象在不同的地方进行,如事件对象的清理工作是在KeSetEvent中进行,具体看4.2 中的KeSetEvent分析。
也就是说,KeWaitForSingleObject使
3.4 不同对象下修改SignalState
在分析 函数KeWaitForSingleObject时,当处理SignalState > 0情况时,针对互斥体_KMUTANT对象、事件_KEVENT对象、信号_KSEMAPHORE对象时,对SignalState的处理方式不同:
- 事件:
- 通知事件(EventNotificationObject):
不改变SignalState,SignalState > 0。 - 同步事件(EventSynchronizationObject):
SignalState = 0。
- 通知事件(EventNotificationObject):
- 互斥体(MutantObject):
SignalState -= 1。 - 信号量(SemaphoreObject):
SignalState -= 1。
4 KEVENT事件对象
事件对象分为:通知类型对象、事件同步对象。
事件结构如下:
1 | kd> dt _KEVENT |
_KEVENT的_DISPATCHER_HEADER.Type相应就有两种。
参考源代码base\ntos\inc\ke.h中_KOBJECTS的定义,_DISPATCHER_HEADER.Type == 0,1。
1 |
|
4.1 创建事件对象
在3环使用函数CreateEvent创建事件对象。
1 | HANDLE CreateEventA( |
关于参数bManualReset:
- TRUE:Wait到事件对象后(SignalState > 0)会一直保持已通知状态,需要调用
ResetEvent()API设置为无信号。对应通知类型对象(EventNotificationObject)。 - FALSE:参数二:Wait到事件对象后将事件置为未通知状态(SignalState == 0),不会修改为已通知状态。对应事件同步对象(EventSynchronizationObject)。
4.2 SetEvent函数
SetEvent函数将指定的事件对象设置为信号状态。
1 | BOOL __stdcall SetEvent( |
调用路径如下:
1 | kernel32.dll |
KeSetEvent调用框架如下(base\ntos\inc\eventobj.c):
1 | ①if(线程没有等待对象) SignalState = 1; |
在KeWaitForSingleObject刚开始进入函数的时候会初始化第一个等待对象WaitBlock[3]的等待块链,并根据等待时间不同判断其是否与计时器WaitBlock[3]进行关联。
当SignalState == 1或者超时满足条件时,直接跳转到NoWait就返回了,在该函数中并没有看到在哪里将等待该对象的线程从线程等待链、对象等待网上断链,也没有将_KWAIT_BLOCK所占内存释放,那这些操作是在哪执行的呢?
根据我自己的逆向分析,上述对应的清理工作(没有将_KWAIT_BLOCK所占内存释放),不同的对象在不同的地方进行,如事件对象的清理工作是在KeSetEvent中进行,具体看4.2 中的KeSetEvent分析。
源码、WRK、ReactOS的
KeWaitForSingleObject相同。
MD😠,分析了一周源码版本和ReactOS版本的KeSetEvent,怎么分析都不对,WRK版本的KiWaitTestSynchronizationObject有两行代码也有小瑕疵,注释掉就好理解了。下面直接查看WRK版本即可,好理解一些。
源码版本:(我觉得有问题,直接看WRK版本即可)
1 | KeSetEvent |
WRK版本:
KiWaitTestWithoutSideEffects和KiWaitTestSynchronizationObject都是将在一个等待对象上所有的等待线程全部从等待网上摘出,并且清空将线程从等待链表中摘出。
前者和后者的区别:前者摘出链时SignalState不改变(为1),后者的WaitAny类型将SignalState置0。
以上函数中涉及到两个循环:
- 大循环:循环地将在该等待对象上的所有线程从等待网上摘掉,并且从线程等待链上摘掉。
- 小循环(KiUnwaitThread):把一个线程上所有的被等待对象从自己的链上摘出,但是所有的等待块还在这个线程上挂着。
WaitEntry = ListHead->Flink;并没有错误,因为KiUnwaitThread摘链后,链表ListHead->Flink已经更新。
按我自己的理解KeSetEvent就应该根据被等待对象在不同线程上的等待方式WaitAny/WaitAny来设置if-else,而不是根据通知还是同步来设置:
- Any:
- 通知:将所有线程下等待网并唤醒
- 同步:将等待该对象的第一个线程下等待网并唤醒(Set一次只能被用一次)
- All:
- 通知/同步:循环判断等待该对象的线程的其他被等待对象是否有信号,都有信号才将这个线程下网
源码中还有一个细节:在KeSetEvent中信号量设置为1后,调用KiWaitTest去将线程的等待块全部摘了,然后回到WaitForSingleObject中若判断SigleState不满足唤醒条件时,又回将等待块挂上去。但是还是不理解为什么要更改SigleState。
1 | KeSetEvent |
KeSetEvent:
- 如果等待队列是空的,没有线程在睡眠等待,就把
Event->Header.SignalState设置成1。这样,如果此后有线程对此事件对象执行KetWaitForSingleObject()操作,就因此而不必睡眠等待。这对于通知型和同步型的事件对象都是一样。 - 已经有线程在这个对象上睡眠等待,那就要从中唤醒一个或所有线程。这时候的处理取决于事件对象的类型以及等待的方式:
- 对于通知型的事件对象,或者等待者的等待方式是WaitAll,而且此前SignalState为0,就将SignalState置1,并通过
KiWaitTest()唤醒这个线程,以及等待队列中所有符合条件的线程。可是,要是SignalState本来就已经是1,则没有任何影响。 - 否则,对于同步型的事件对象,并且等待者的等待方式是WaitAny,就通过
KiUnwaitThread()唤醒等待队列中的第一个线程。此时并不改变SignalState的值。
- 对于通知型的事件对象,或者等待者的等待方式是WaitAll,而且此前SignalState为0,就将SignalState置1,并通过
WRK版本复合海哥说的:
SetEvent对应的内核函数:KeSetEvent
1)修改信号值SignalState为1
2)判断对象类型
3)如果类型为通知类型对象(0) ,唤醒所有等待该状态的线程
4)如果类型为事件同步对象(1) ,从链表头找到第一个 (WaitType == WaitAny)
ReactOS版本:(也有一定小问题)
1 | KeSetEvent |
5 _KOBJECTS
1 |
|
在Windows中,每一种对象都需要有一个对应的类型对象(即OBJECT_TYPE对象),其定义为:
1 |
|
系统定义的对象种类是有限的,WRK 支持 31 种对象,各有一个全局的 POBJECT_TYPE 变量指向其类型对象,表 2.4 列出了这 31 种对象,读者可以在 WRK 源代码中找到它们的定义和用法。(Windows内核原理与实现)

如互斥体:
1 | POBJECT_TYPE ExMutantObjectType; |
在函数NtCreateMutant等都会用到。