Windows XP 多核同步(一)

ʕ •ᴥ•ʔ ɔ:

1 同步与互斥

  • 并发:单核或多核下,在操作系统中,同个处理机上有多个程序同时运行即并发。并发可分为同步和互斥。
    • 同步:并发执行的环境中各个线程必须严格按照规定的某种先后次序来运行。
    • 互斥:同一个资源同一时间只有一个访问者可以进行访问,其他访问者需要等前一个访问者访问结束才可以开始访问该资源。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
  • 并行:仅在多核下,两个及以上数量的线程同一时刻在执行。

多核并发情况下,存在并行的情况。多线程同时处理某些资源就会存在出错的情况,而本文就主要探讨一下问题和解决。

注意⚠️:如果线程中的代码只对局部变量进行访问,就不存在并发的问题;只有当访问全局变量的时候,才需要关注并发的问题。这是因为局部变量每次访问都是在本次调用的堆栈里操作,不会产生互相影响。

例1:

1
2
3
4
DWORD  dwVal = 0;	//全局变量

线程中的代码:
dwVal++; //只有一行,安全吗?

很明显是不安全的,这里涉及到多核同一时刻(并行)修改内存。实际这一行代码在汇编情况下处理涉及到读写操作(原子操作):

1
2
3
4
mov	eax,[0x12345678]
add eax,1 //当第一个线程执行到这里时,产生了线程切换的话
//第二个线程也执行到这里时,将会出现逻辑错误
mov [0x12345678],eax

这里需要注意:读数据并不会产生并发问题,写内存的时候才会!

改成下面的代码安全么?

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
2
3
4
ntoskrnl - 单处理器,不支持PAE
ntkrnlpa - 单处理器,支持PAE
ntkrnlmp - 多处理器,不支持PAE
ntkrpamp - 多处理器,支持PAE

原子操作API例如一些如下:

1
2
3
4
5
6
7
8
InterlockedIncrement
InterlockedExchangeAdd
InterlockedDecrement
InterlockedFlushSList
InterlockedExchange
InterlockedPopEntrySList
InterlockedCompareExchange
InterlockedPushEntrySList

以InterlockedIncrement(实现对某个变量加1操作)举例:

单核版本:

1
2
3
4
5
6
7
8
.text:0046FAB0 ; __fastcall __InterlockedIncrement(x)
.text:0046FAB0 public @__InterlockedIncrement@4
.text:0046FAB0 @__InterlockedIncrement@4 proc near
.text:0046FAB0 mov eax, 1
.text:0046FAB5 xadd [ecx], eax
.text:0046FAB9 inc eax
.text:0046FABA retn
.text:0046FABA @__InterlockedIncrement@4 endp

多核版本:

1
2
3
4
5
6
7
8
.text:0046FAB0 ; __fastcall __InterlockedIncrement(x)
.text:0046FAB0 public @__InterlockedIncrement@4
.text:0046FAB0 @__InterlockedIncrement@4 proc near
.text:0046FAB0 mov eax, 1
.text:0046FAB5 lock xadd [ecx], eax
.text:0046FAB9 inc eax
.text:0046FABA retn
.text:0046FABA @__InterlockedIncrement@4 endp

多核比单核多了使用lock,关于xadd reg/mem, reg汇编指令解释:

  • xadd [ecx], eax:
    1. 先交换值:xchg [ecx], eax;
    2. 再将二者相加给目的操作数:add [ecx], eax

1.2 临界区

临界区用来解决互斥问题。

如果是多行代码作为一个原子操作,如下:

1
2
3
4
5
6
7
8
9
关键代码A	//N行代码要求原子操作
关键代码B //单独加LOCK可以吗?
关键代码C
LOCK INC DWORD PTR DS:[0x12345678]
LOCK INC DWORD PTR DS:[0x12345678]
LOCK INC DWORD PTR DS:[0x12345678]
LOCK INC DWORD PTR DS:[0x12345678]
LOCK INC DWORD PTR DS:[0x12345678]
.......

这样虽然每一行都保证只能一个线程占有资源,但是会浪费资源,或者其它一些问题。

解决这一问题的方法就是引入临界区,然后进入临界区前加锁,出临界区是解锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
Lab:
mov eax,1
lock xadd [Flag],eax
cmp eax,0
jz endLab
dec [Flag]
//调用线程等待Sleep ……
jmp Lab;
endLab:
//临界区,处理业务
lock dec [Flag]
ret

如下代码为不使用xadd实现的临界区:

以下代码在单核情况下没问题,但是在多核情况下就会出问题了:

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
#include "stdafx.h"
#include <tchar.h>
#include <windows.h>

int g_value = 0;
int CriticalLock = 1; // 1表示可以进入临界区

DWORD WINAPI MyThread(LPVOID TID)
{
for (int i = 0; i < 10; i++)
{
CriStart:
Sleep(20); // 提高效率
__asm
{
dec [CriticalLock];
jz CriEnd;
inc [CriticalLock];
jmp CriStart;
}
CriEnd:
// 耗时业务
g_value+=3;
Sleep(20);
g_value-=2;
printf("%d\n", g_value);

__asm inc [CriticalLock];
}
return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
for (int i = 0; i < 10; i++)
{
CreateThread(0,0,MyThread,(LPVOID)i,0,0);
}

getchar();
printf("所有线程结束,g_value = %d\n", g_value);
getchar();
return 0;
}

正确的操作如下:

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
#include "stdafx.h"
#include <tchar.h>
#include <windows.h>

int g_value = 0;
int CriticalLock = 0; // 0表示可以进入临界区

DWORD WINAPI MyThread(LPVOID TID)
{
for (int i = 0; i < 10; i++)
{
CriStart:
__asm
{
mov eax, 1;
lock xchg [CriticalLock], eax;
cmp eax, 0;
jz CriEnd;
}
Sleep(20); // 提高效率
__asm jmp CriStart;
CriEnd:
// 耗时业务
g_value+=3;
Sleep(20);
g_value-=2;
printf("%d\n", g_value);

__asm lock dec [CriticalLock];
}
return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
for (int i = 0; i < 10; i++)
{
CreateThread(0,0,MyThread,(LPVOID)i,0,0);
}

getchar();
printf("所有线程结束,g_value = %d\n", g_value);
getchar();
return 0;
}

1.3 自旋锁

Windows XP 32bit内核版本区分:

1
2
3
4
ntoskrnl - 单处理器,不支持PAE
ntkrnlpa - 单处理器,支持PAE
ntkrnlmp - 多处理器,不支持PAE
ntkrpamp - 多处理器,支持PAE

Windows自旋锁:函数中如KiAcquireSpinLock只要包含SpinLock都是自旋锁函数。

分析一个看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
text:00469994 ; __stdcall KeAcquireSpinLockAtDpcLevel(x)
.text:00469994 public _KeAcquireSpinLockAtDpcLevel@4
.text:00469994 _KeAcquireSpinLockAtDpcLevel@4 proc near
.text:00469994
.text:00469994 arg_0 = dword ptr 4
.text:00469994
.text:00469994 mov ecx, [esp+arg_0]
.text:00469998
.text:00469998 loc_469998: ; CODE XREF: KeAcquireSpinLockAtDpcLevel(x)+14↓j
.text:00469998 lock bts dword ptr [ecx], 0 //CF = 第0位,第0位 = 1
.text:0046999D jb short loc_4699A2 //跳转:CF == 1
.text:0046999F retn 4 //返回:CF == 0,可以进入临界区。说明参数地址中的值只有两种状态,0/1
//0:没人在临界区,我可以进;1:有人在临界区,我旋转等待
.text:004699A2 ; ---------------------------------------------------------------------------
.text:004699A2
.text:004699A2 loc_4699A2: ; CODE XREF: KeAcquireSpinLockAtDpcLevel(x)+9↑j
.text:004699A2 ; KeAcquireSpinLockAtDpcLevel(x)+18↓j
.text:004699A2 test dword ptr [ecx], 1
.text:004699A8 jz short loc_469998
.text:004699AA pause //CPU降温指令,降低CPU功率
.text:004699AC jmp short loc_4699A2
.text:004699AC _KeAcquireSpinLockAtDpcLevel@4 endp

解释一下 bts 指令,它是把 [ecx] 的第 0 位的值放到 CF,然后给第0 位置1(说明我在用临界区了)。

jb 指令看 CF=1就跳转。也就是说,

  • 如果原来第0位是0,就retn,可以进入临界区了,说明该临界区还没有线程在用,那我用;

  • 如果原来第0位是1,那么 jb 跳转,开始自旋,直到 [ecx] 0位变成0,重新执行 KiAcquireSpinLock 。

自旋锁只对于多核才有意义,如果是单核反而会造成大量的性能损失。

关于X86的BT指令,经过本人测试,有以下结论。

BT系列指令:位测试和设置指令。

  1. 按照源操作指定的位号(最低位下标为0),测试目的操作数指定的位,并将被测试位拷贝复制到EFLAG得CF。
  2. 操作目的操作数指定的位。
指令 含义 实例测试
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.png

2.png

总结:

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

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
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
#include <ntifs.h>
#include <ntddk.h>

int HookAddr = 0;
unsigned int OldThread = 0;
char shellcode[8] = { 0xE9,0,0,0,0 ,0x9c,0x8b,0x0b };
char yshellcode[8] = { 0x26, 0xC6, 0x46, 0x2D,0x02,0x9c,0x8b,0x0b };

KDPC dpc = { 0 };
KTIMER timer = { 0 };
LARGE_INTEGER duringtime = { 0 };

VOID DPCRoutine(_In_ struct _KDPC* Dpc, _In_opt_ PVOID DeferredContext,
_In_opt_ PVOID SystemArgument1, _In_opt_ PVOID SystemArgument2)
{
if (OldThread)
DbgPrint("Report Per 2s : Calls Old Thread %x \n", OldThread);
OldThread = 0;
KeSetTimer(&timer, duringtime, &dpc);
}

void __declspec(naked) HookSwapContext()
{
__asm
{
mov byte ptr es : [esi + 2Dh] , 2;
mov [OldThread], edi;
mov eax, [HookAddr];
add eax, 5;
push eax;
ret;
}
}

unsigned int __declspec(naked) GetKernelBase()
{
__asm
{
mov eax, fs: [34h] ;
mov eax, [eax + 18h];
mov eax, [eax];
mov eax, [eax + 18h];
ret;
}
}

void __declspec(naked) HookProc()
{
_asm
{
pushad;
pushfd;
xor edx, edx;
lea esi, shellcode;
mov ebx, [esi];
mov ecx, [esi + 4];

mov edi, [HookAddr];
mov eax, [edi];
mov edx, [edi + 4];
lock cmpxchg8b qword ptr[edi];
popfd;
popad;
ret;
}
}

void __declspec(naked) UnHookProc()
{
_asm
{
pushad;
pushfd;
xor edx, edx;
lea esi, yshellcode;
mov ebx, [esi];
mov ecx, [esi + 4];

mov edi, [HookAddr];
mov eax, [edi];
mov edx, [edi + 4];
lock cmpxchg8b qword ptr[edi];
popfd;
popad;
ret;

}

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
UnHookProc();
while (1)
{
KeDelayExecutionThread(KernelMode, TRUE, & duringtime);
if (!OldThread)
{
break;
}
}

KeCancelTimer(&timer);
DbgPrint("Unloaded Successfully!");
return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = UnloadDriver;

unsigned int base = GetKernelBase();

HookAddr = base + 0x6A8E2;

//初始化 shellcode
unsigned int jmpdes = (int)HookSwapContext - HookAddr - 5;
RtlCopyMemory(&shellcode[1], &jmpdes, 4);


KeInitializeTimer(&timer);
KeInitializeDpc(&dpc, DPCRoutine, NULL);
duringtime.QuadPart = -20 * 1000 * 1000;

HookProc();

KeSetTimer(&timer, duringtime, &dpc);

DbgPrint("Loaded Successfully!");

return STATUS_SUCCESS;
}

2 线程等待与唤醒

临界区和自旋锁,这两种同步方案在线程无法进入临界区时都会让当前线程进入等待状态。
一种是通过Sleep函数实现的,一种是通过让当前的CPU“空转”实现的,但这两种等待方式都有局限性:

  1. 通过Sleep函数进行等待,等待时间该如何确定呢(怎么确认多长时间后自己可以进入临界区)?
  2. 通过“空转”的方式进行等待,只有等待时间很短的情况下才有意义,否则对CPU资源是种浪费,并且自旋锁只能在多核的环境下才有意义。

有没有更加合理的等待方式呢?只有在条件成熟的时候才将当前线程唤醒?Windows在Ntdll!RtlEnterCriticalSection函数中其实就给出了一种折中的方式(当前使用临界区的线程不是CurrentThreadId的话就会调用RtlpWaitForCriticalSection.ZwWaitForSingleObject挂起当前线程)。其他情况就会自旋(可参考base/ntdll/ia64/critsect.s)。可参考:深入理解CRITICAL_SECTION。

2.1 可等待对象

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

1
2
3
4
线程A:					线程B:
WaitForSingleObject --> 可等待对象 <-- 唤醒可等待对象 SetEvent
WaitForMultipleObjects --> 可等待对象 <-- 唤醒可等待对象 ReleaseSemaphore
<-- 唤醒可等待对象 ReleaseMutant

常见的可等待对象有如下这些:

1
2
_KPROCESS(进程)	_KTHREAD(线程)	_KTIMER(定时器)	_KSEMAPHORE(信号量)	_KEVENT(事件)	_KMUTANT(互斥体)
_FILE_OBJECT (文件)

需要特别说明的是文件对_FILE_OBJECT,其他对象都是以_DISPATCHER_HEADER开头,而_FILE_OBJECT如下:

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
kd> dt _FILE_OBJECT
ntdll!_FILE_OBJECT
+0x000 Type : Int2B
+0x002 Size : Int2B
+0x004 DeviceObject : Ptr32 _DEVICE_OBJECT
+0x008 Vpb : Ptr32 _VPB
+0x00c FsContext : Ptr32 Void
+0x010 FsContext2 : Ptr32 Void
+0x014 SectionObjectPointer : Ptr32 _SECTION_OBJECT_POINTERS
+0x018 PrivateCacheMap : Ptr32 Void
+0x01c FinalStatus : Int4B
+0x020 RelatedFileObject : Ptr32 _FILE_OBJECT
+0x024 LockOperation : UChar
+0x025 DeletePending : UChar
+0x026 ReadAccess : UChar
+0x027 WriteAccess : UChar
+0x028 DeleteAccess : UChar
+0x029 SharedRead : UChar
+0x02a SharedWrite : UChar
+0x02b SharedDelete : UChar
+0x02c Flags : Uint4B
+0x030 FileName : _UNICODE_STRING
+0x038 CurrentByteOffset : _LARGE_INTEGER
+0x040 Waiters : Uint4B
+0x044 Busy : Uint4B
+0x048 LastLock : Ptr32 Void
+0x04c Lock : _KEVENT
+0x05c Event : _KEVENT
+0x06c CompletionContext : Ptr32 _IO_COMPLETION_CONTEXT

虽然该对象不是以_DISPATCHER_HEADER开头,但是在偏移+0x04c和+0x05c的成员是可等待对象_KEVENT,所以该对象也是可等待对象。

可参考:Windows内核情景分析—内核对象。

2.2 线程等待网

一、一个线程下的所有被等待对象

每个线程KTHREAD偏移0x5c的成员WaitBlockList(_KWAIT_BLOCK结构)为该线程的对象等待块,记录了该线程正在等待的那些对象。每个等待块对应一个被等待对象。该线程所有被等待的对象都会被双向循环等待块链串起来,结构如下。

1
2
3
4
5
6
7
8
9
10
11
12
kd> dt _KWAIT_BLOCK
ntdll!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY //该线程下所有被等待对象链,双向循环链表
//指向被等待对象的 _DISPATCHER_HEADER + 0x8
+0x008 Thread : Ptr32 _KTHREAD //当前等待块所属的线程对象(Body)
+0x00c Object : Ptr32 Void //当前等待块对应的等待对象(线程所等待对象Body)
+0x010 NextWaitBlock : Ptr32 _KWAIT_BLOCK //指向下一个等待块(单向循环链表)
+0x014 WaitKey : Uint2B //当前等待块的索引,0表示第一个
+0x016 WaitType : Uint2B //0:只有所有等待对象都被激活才行,1:只要有一个等待对象被激活即可
//0:该线程要等所有的对象,1:该线程等待一个对象就走
//0:WaitForMultipleObjects(nCount,lpHandles,TRUE,dwMilliseconds)
//1:WaitForMultipleObjects(nCount,lpHandles,FALSE,dwMilliseconds)

二、一个被等待对象被哪些线程等待

每个可被等待的对象都有成员_DISPATCHER_HEADER,该成员的结构如下。

1
2
3
4
5
6
7
8
kd> dt _DISPATCHER_HEADER
ntdll!_DISPATCHER_HEADER
+0x000 Type : UChar //对象类型
+0x001 Absolute : UChar
+0x002 Size : UChar
+0x003 Inserted : UChar
+0x004 SignalState : Int4B //是否有信号,> 0 表示有信号,即被激活
+0x008 WaitListHead : _LIST_ENTRY //串着等待线程的_KWAIT_BLOCK

其中成员WaitListHead是一个双向的循环链表,串着当前被等待对象被哪些线程等待,串的是那些线程的WaitBlockList(_KWAIT_BLOCK).WaitListEntry。

有如下关系:

3.png

三、_KWAIT_BLOCK.WaitListEntry与_DISPATCHER_HEADER.WaitListHead的关系

  1. 有如下代码。

    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
    #include "stdafx.h"
    #include <windows.h>

    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]对象。

  2. 查看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
    kd> !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
      8
      kd> 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又指回去第一个对象。

  3. 分析_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
    21
    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
    ...
    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为内核对象首成员时管用):

    1. _KWAIT_BLOCK.WaitListEntry.FLINK == _KWAIT_BLOCK。
    2. _DISPATCHER_HEADER.WaitListHead.FLINK == _KWAIT_BLOCK。
    3. &_DISPATCHER_HEADER.WaitListHead.FLINK == _KWAIT_BLOCK.WaitListEntry.BLINK 。

四、源代码中的判空

源代码常有以下语句来判断链表是否为空。在函数KeSetEvent有如下代码:

1
2
3
4
5
6
if (IsListEmpty(&Event->Header.WaitListHead) == TRUE) 
{
Event->Header.SignalState = 1;
}

#define IsListEmpty(ListHead) ((ListHead)->Flink == (ListHead))

⚠️注意:这里判断链表为空,并不是只有一个节点的意思,如下:

  1. 对象KEVENT == 0x86237618 ,仅被一个线程等待,如下。

    1
    2
    3
    4
    5
    6
    7
    8
    kd> 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中。

  2. 查看&Event->Header.WaitListHead。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    kd> 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逆向分析有这样一个结论:

如果一个对象被多个线程等待,该对象的等待块链有以下关系:

  1. 当前对象Event->Header.WaitListHead == Event->Header.WaitListHead.Flink,Event + 0x8 == &Event->Header.WaitListHead。
  2. 第一个&Event->Header.WaitListHead == _KWAIT_BLOCK.WaitListEntry.Blink。

举例说明:

1
2
3
4
5
6
7
8
9
10
PLIST_ENTRY WaitEntry;
PLIST_ENTRY ListHead;

Event = (PKEVENT)Object;
ListHead = &Event->Header.WaitListHead;
WaitEntry = ListHead->Flink;
while ((WaitEntry != WaitList) && (Object->SignalState > 0)) {
...
WaitEntry = WaitEntry->Flink;
}

这里有这样的关系:&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]有以下两种状态:

  1. 如使用WaitSingleObject(HANDLE x, -1),则WaitBlock[3].NextWaitBlock = NULL;
  2. 如使用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 练习:查看线程的等待块

查看线程的被等待对象有哪些?

  1. 在3环有如下代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include "stdafx.h"
    #include <windows.h>

    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;
    }
  2. 查看通过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
    73
    kd> !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。

  3. 查看线程的等待块WaitBlockList(+0x5c)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    kd> 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
    ...
  4. 查看_KWAIT_BLOCK如下。

    1
    2
    3
    4
    5
    6
    7
    8
    kd> 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
    39
    kd> 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逆向分析有这样一个结论:

如果一个对象被多个线程等待,该对象的等待块链有以下关系:

  1. 当前对象Event->Header.WaitListHead == Event->Header.WaitListHead.Flink,Event + 0x8 == &Event->Header.WaitListHead。
  2. 第一个&Event->Header.WaitListHead == _KWAIT_BLOCK.WaitListEntry.Blink。

举例说明:

1
2
3
4
5
6
7
8
9
10
PLIST_ENTRY WaitEntry;
PLIST_ENTRY WaitList;

Event = (PKEVENT)Object;
ListHead = &Event->Header.WaitListHead;
WaitEntry = ListHead->Flink;
while ((WaitEntry != WaitList) && (Object->SignalState > 0)) {
...
WaitEntry = ListHead->Flink;
}

这里有这样的关系:&Event->Header.WaitListHead == _KWAIT_BLOCK.WaitListEntry.Blink,所以循环while中会用来判断是否已经完成循环。

有如下代码:

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
#include "stdafx.h"
#include <windows.h>

HANDLE hEvent[4] = {NULL};

WORD WINAPI ThreadProc(LPVOID lpParamter)
{
WaitForMultipleObjects(2,hEvent,FALSE,-1);
printf("Hello World!\n");
return 0;
}
WORD WINAPI ThreadProc_2(LPVOID lpParamter)
{
WaitForSingleObject(hEvent[0],-1);
printf("Hello World!\n");
return 0;
}
WORD WINAPI ThreadProc_3(LPVOID lpParamter)
{
WaitForSingleObject(hEvent[0],-1);
printf("Hello World!\n");
return 0;
}
WORD WINAPI ThreadProc_4(LPVOID lpParamter)
{
WaitForSingleObject(hEvent[0],-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);
CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadProc_3,NULL,0,NULL);
CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadProc_4,NULL,0,NULL);

getchar();

return 0;
}

如上代码,对象句柄hEvent[0]被四个线程等待,这里简单随便拿一个除了主线程之外的四个线程中的一个进行分析。

  1. 在Windbg中找到线程ThreadProc进行分析。

    1
    2
    3
    4
    5
    kd> !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
    139
    kd> !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。

  2. 分析线程ThreadProc。

    1
    2
    3
    4
    5
    6
    kd> dt _KTHREAD 0x85e51968
    nt!_KTHREAD
    +0x000 Header : _DISPATCHER_HEADER
    ...
    +0x05c WaitBlockList : 0x85e519d8 _KWAIT_BLOCK
    ...
  3. 找到该线程的等待块上第一个等待对象hEvent[0]。

    1
    2
    3
    4
    5
    6
    7
    8
    kd> 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。

    1. 要先得到&Event->Header.WaitListHead的值,KEVENT第一个成员是_DISPATCHER_HEADER。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      kd> 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)也就可以用了。

    2. 保险起见,确定一下_KWAIT_BLOCK.WaitListEntry.Blink == 0x8608c288里面存的是不是第一个_KWAIT_BLOCK 0x85e519d8。

      1
      2
      3
      kd> dd 0x8608c288
      8608c288 85e519d8 866bb540 004f0006 e5726854
      8608c298 86121bd0 8607fd90 8608c3d8 00000000
  4. 所以WaitListEntry和WaitListHead是你存我我存你,但是WaitListHead存的永远是_KWAIT_BLOCK,而dd WaitListEntry.Flink里面的值才是真正_KWAIT_BLOCK。

下面开始查看一下等待hEvent[0]的所有线程:

  1. 第 1 个等待hEvent[0]的线程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    kd> 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就终止。

  2. 往下看,当某个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
    53
    kd> 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kernel32.dll
DWORD __stdcall WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds)
↓ ↓ //hHandle:被等待对象,dwMilliseconds:超时时间
DWORD __stdcall WaitForSingleObjectEx(HANDLE hHandle, DWORD dwMilliseconds, BOOL bAlertable)
↓ ↓ //bAlertable:对应KTHREAD结构体的Alertable属性
↓ ↓ //如果为1 在插入用户APC时,该线程将被吵醒
ntdll.dll
DWORD __stdcall NtWaitForSingleObject(HANDLE hHandle, BOOL bAlertable, DWORD dwMilliseconds)
3环 ↓ ↓
-----------------------------------------------------------------------------------------------------
0环
ntkrpamp.exe
NTSTATUS NtWaitForSingleObject(IN HANDLE Handle, IN BOOLEAN Alertable, IN PLARGE_INTEGER Timeout OPTIONAL)
↓ ↓
NTSTATUS KeWaitForSingleObject(IN PVOID Object, IN KWAIT_REASON WaitReason, IN KPROCESSOR_MODE WaitMode,
IN BOOLEAN Alertable, IN PLARGE_INTEGER Timeout OPTIONAL)
//WaitReason:指定等待的原因。驱动:Executive,3环程序:UserRequest
//WaitMode:指定调用者是在KernelMode还是UserMode中等待
  1. NtWaitForSingleObject (base/ntos/ob/obwait.c)
    1)调用ObReferenceObjectByHandle函数,通过对象句柄找到等待对象结构体地址。
    2)调用KeWaitForSingleObject函数,进入关键循环。

  2. 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中,函数大致功能如下。

  1. 先检查if(Timeout > 0 && PreviousMode != KernelMode),更新Timeout。
  2. 使用参数一Handle调用ObReferenceObjectByHandle获得被等待对象Object。
  3. _OBJECT_HEADER.Type.DefaultObject = [[Object - 0x10] + 0x48],得到DefaultObject,并对其进行判断,判断方法看下一节。
  4. 调用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
2
3
4
5
6
7
ObjectHeader = OBJECT_TO_OBJECT_HEADER( Object );	//获取_OBJECT_HEADER
WaitObject = ObjectHeader->Type->DefaultObject;

if ((LONG_PTR)WaitObject >= 0) {

WaitObject = (PVOID)((PCHAR)Object + (ULONG_PTR)WaitObject);//加上偏移得到直接可等待对象的地址
}

在《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
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
①if (Thread->WaitNext == FALSE)  goto WaitStart;
Thread->WaitNext = FALSE; //使线程可以调度
InitializeWaitSingle(); //初始化等待块并进行填充

do
{
if (Thread->ApcState.KernelApcPending && (Thread->WaitIrql < APC_LEVEL))
②执行掉所有Pending中的内核APC
else //SignalState > 0 情况处理
{
③处理互斥体对象
if(互斥体)
{
if(SignalState > 0 || 互斥体的OwnerThread == 当前线程)
{
if(SignalState == 0x80000000) //SignalState等于最大信号量上限
异常处理;
else {SignalState -= 1; goto NoWait}//KiWaitSatisfyMutant()
}
}
④处理非互斥体对象
if (SignalState > 0)
{
if(同步事件) SignalState =0;
if(信号量) SignalState -= 1;
goto NoWai;
}

//通知事件不改变SignalState
⑤若所等等待的对象没有信号,就检查当前状态是否可被强制唤醒
如果当前线程可以被吵醒,就break当前循环,退出等待
TestForAlertPending(Alertable);

⑥if(Timeout) //Timeout > 0 情况处理
{
if(超时) goto NoWait;
}

⑦把已经准备好的KWAIT_BLOCK数据结构插入目标对象的_DISPATCHER_HEADER.WaitListHead队列。
准备线程切换
KiSwapThread();
}//End else

WaitStart:
Thread->WaitIrql = KeRaiseIrqlToDpcLevel(); //提升irql至DISPATCH_LEVEL
InitializeWaitSingle(); //初始化等待块并进行填充
KiLockDispatcherDatabaseAtSynchLevel(); //锁定分发器dispatcher database,锁定线程调度
}
while(true)

NoWait:
KiAdjustQuantumThread(Thread);
KiUnlockDispatcherDatabase(Thread->WaitIrql);
return WaitStatus;

源代码如下(base/ntos/ke/wait.c):

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
NTSTATUS
KeWaitForSingleObject (
IN PVOID Object,
IN KWAIT_REASON WaitReason,
IN KPROCESSOR_MODE WaitMode,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Timeout OPTIONAL
)
{

LARGE_INTEGER DueTime;
LARGE_INTEGER NewTime;
PKMUTANT Objectx;
PLARGE_INTEGER OriginalTime;
PRKQUEUE Queue;
LOGICAL StackSwappable;
PRKTHREAD Thread;
PRKTIMER Timer;
PKWAIT_BLOCK WaitBlock;
NTSTATUS WaitStatus;
PKWAIT_BLOCK WaitTimer;

#if defined(_COLLECT_WAIT_SINGLE_CALLDATA_)
RECORD_CALL_DATA(&KiWaitSingleCallData);
#endif

ASSERT((PsGetCurrentThread()->StartAddress != (PVOID)KeBalanceSetManager) || (ARGUMENT_PRESENT(Timeout)));
Thread = KeGetCurrentThread();
Objectx = (PKMUTANT)Object;
OriginalTime = Timeout;
Timer = &Thread->Timer;
WaitBlock = &Thread->WaitBlock[0];
WaitTimer = &Thread->WaitBlock[TIMER_WAIT_BLOCK];

if (Thread->WaitNext == FALSE) {
goto WaitStart;
}
Thread->WaitNext = FALSE; //使线程可以调度
InitializeWaitSingle(); //初始化等待块并进行填充

do {
if (Thread->ApcState.KernelApcPending && (Thread->WaitIrql < APC_LEVEL)) {
KiUnlockDispatcherDatabase(Thread->WaitIrql); //执行掉所有Pending中的内核APC
} else {
ASSERT(Objectx->Header.Type != QueueObject);

if (Objectx->Header.Type == MutantObject) {
if ((Objectx->Header.SignalState > 0) || (Thread == Objectx->OwnerThread)) {
if (Objectx->Header.SignalState != MINLONG) { //MINLONG = 0x80000000 = -2147483648
KiWaitSatisfyMutant(Objectx, Thread); //把目标对象上已经接收到的“信号”消耗掉,SignalState--
WaitStatus = (NTSTATUS)(Thread->WaitStatus);
goto NoWait;

} else {
KiUnlockDispatcherDatabase(Thread->WaitIrql);
ExRaiseStatus(STATUS_MUTANT_LIMIT_EXCEEDED);
}
}
} else if (Objectx->Header.SignalState > 0) {
KiWaitSatisfyOther(Objectx);
WaitStatus = (NTSTATUS)(0);
goto NoWait;
}

//若所等等待的对象没有信号,就检查当前状态是否可被强制唤醒
//如果当前线程可以被吵醒,就break当前循环,退出等待
TestForAlertPending(Alertable);
if (ARGUMENT_PRESENT(Timeout)) { //用户提供了一个超时值,就检测超时情况
if (!(Timeout->LowPart | Timeout->HighPart)) {
WaitStatus = (NTSTATUS)(STATUS_TIMEOUT);
goto NoWait;
}
if (KiInsertTreeTimer(Timer, *Timeout) == FALSE) {
WaitStatus = (NTSTATUS)STATUS_TIMEOUT;
goto NoWait;
}
DueTime.QuadPart = Timer->DueTime.QuadPart;
}
//把已经准备好的KWAIT_BLOCK数据结构插入目标对象的_DISPATCHER_HEADER.WaitListHead队列。
InsertTailList(&Objectx->Header.WaitListHead, &WaitBlock->WaitListEntry);
Queue = Thread->Queue;
if (Queue != NULL) {
KiActivateWaiterQueue(Queue);
}

Thread->State = Waiting;
if (StackSwappable != FALSE) {
InsertTailList(&KiWaitListHead, &Thread->WaitListEntry); //把线程加入等待队列
}
ASSERT(Thread->WaitIrql <= DISPATCH_LEVEL);
WaitStatus = (NTSTATUS)KiSwapThread();
if (WaitStatus != STATUS_KERNEL_APC) {
return WaitStatus;
}

if (ARGUMENT_PRESENT(Timeout)) {
Timeout = KiComputeWaitInterval(OriginalTime,
&DueTime,
&NewTime);
}
}

WaitStart:
#if defined(NT_UP)
Thread->WaitIrql = KeRaiseIrqlToDpcLevel(); //提升irql至DISPATCH_LEVEL
#else
Thread->WaitIrql = KeRaiseIrqlToSynchLevel(); //Is higher than any DEVICE_IRQL
#endif
InitializeWaitSingle(); //初始化等待块并进行填充
KiLockDispatcherDatabaseAtSynchLevel(); //锁定分发器dispatcher database,锁定线程调度

} while (TRUE);

KiUnlockDispatcherDatabase(Thread->WaitIrql);
return WaitStatus;

NoWait:
KiAdjustQuantumThread(Thread);
KiUnlockDispatcherDatabase(Thread->WaitIrql);
return WaitStatus;
}

在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。
  • 互斥体(MutantObject):SignalState -= 1。
  • 信号量(SemaphoreObject):SignalState -= 1。

4 KEVENT事件对象

事件对象分为:通知类型对象、事件同步对象。

事件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
kd> dt _KEVENT
nt!_KEVENT
+0x000 Header : _DISPATCHER_HEADER

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

_KEVENT的_DISPATCHER_HEADER.Type相应就有两种。

参考源代码base\ntos\inc\ke.h中_KOBJECTS的定义,_DISPATCHER_HEADER.Type == 0,1。

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
#define DISPATCHER_OBJECT_TYPE_MASK 0x7
typedef enum _KOBJECTS {
EventNotificationObject = 0, //通知类型对象
EventSynchronizationObject = 1, //事件同步对象
MutantObject = 2,
ProcessObject = 3,
QueueObject = 4,
SemaphoreObject = 5,
ThreadObject = 6,
Spare1Object = 7,
TimerNotificationObject = 8,
TimerSynchronizationObject = 9,
Spare2Object = 10,
Spare3Object = 11,
Spare4Object = 12,
Spare5Object = 13,
Spare6Object = 14,
Spare7Object = 15,
Spare8Object = 16,
Spare9Object = 17,
ApcObject,
DpcObject,
DeviceQueueObject,
EventPairObject,
InterruptObject,
ProfileObject
} KOBJECTS;

4.1 创建事件对象

在3环使用函数CreateEvent创建事件对象。

1
2
3
4
5
6
HANDLE CreateEventA(
[in, optional] LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性,是否可继承,默认为NULL不可继承
[in] BOOL bManualReset, //事件复位方式,下面做解释
[in] BOOL bInitialState, //事件创建后:TRUE已通知状态,FALSE未通知状态
[in, optional] LPCSTR lpName //事件对象的名称
);

关于参数bManualReset:

  • TRUE:Wait到事件对象后(SignalState > 0)会一直保持已通知状态,需要调用ResetEvent()API设置为无信号。对应通知类型对象(EventNotificationObject)。
  • FALSE:参数二:Wait到事件对象后将事件置为未通知状态(SignalState == 0),不会修改为已通知状态。对应事件同步对象(EventSynchronizationObject)。

4.2 SetEvent函数

SetEvent函数将指定的事件对象设置为信号状态。

1
2
3
BOOL __stdcall SetEvent(
[in] HANDLE hEvent //事件对象的句柄
);

调用路径如下:

1
2
3
4
5
6
7
8
9
10
kernel32.dll
BOOL __stdcall SetEvent(HANDLE hEvent)
↓ ↓
ntdll.dll
NTSTATUS __stdcall NtSetEvent(HANDLE EventHandle, PLONG PreviousState)
----------------------------------------------------------------------------------------
ntkrpamp.exe
NTSTATUS __stdcall NtSetEvent(HANDLE EventHandle, PLONG PreviousState)
↓ ↓
LONG __stdcall KeSetEvent(PRKEVENT Event, KPRIORITY Increment, BOOLEAN Wait)

KeSetEvent调用框架如下(base\ntos\inc\eventobj.c):

1
2
3
4
5
6
7
8
9
10
①if(线程没有等待对象) SignalState = 1;
else //等待Event线程有多个
{
②if(通知事件 || WaitAll)
{
③if(SignalState == 0) {SignalState = 1;④KiWaitTest(Event,Increment);}
}else{
⑤KiUnwaitThread();
}
}

在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
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
KeSetEvent
①if(线程没有等待对象) SignalState = 1;
else //等待Event线程有多个
{
②if(通知事件 || WaitAll)
{
③if(SignalState == 0) {SignalState = 1;④KiWaitTest(Event,Increment);}
}else{ //同步对象+WaitAny
⑤KiUnwaitThread();
}
}
//从②开始就有些不解,为什么不把同步和通知对象分开处理
//同步对象+WaitAll时,在等待该对象的某个线程上还有可能等待其他对象
//这里应该参考下面的WRK的KeSetEvent函数
//所以下面就不继续分析了,我觉得有问题

KiWaitTest
{
ListHead = &Event->Header.WaitListHead;
WaitEntry = ListHead->Flink;

//通知事件+WaitAll、通知事件+WaitAny、同步对象+WaitAll,SignalState == 1
//同步对象+WaitAny,SignalState值未知
//KiUnwaitThread();
while(SignalState > 0 && WaitEntry != ListHead)
{
WaitBlock = CONTAINING_RECORD(WaitEntry, KWAIT_BLOCK, WaitListEntry);

do {
_EX_Entry = &WaitBlock->WaitListEntry;
_EX_Entry->Blink->Flink = _EX_Entry->Flink;
_EX_Entry->Flink->Blink = _EX_Entry->Blink;

WaitBlock = WaitBlock->NextWaitBlock;
} while (WaitBlock != Thread->WaitBlockList);

if (Thread->WaitListEntry.Flink != NULL) {
_EX_Entry = &Thread->WaitListEntry;
_EX_Entry->Blink->Flink = _EX_Entry->Flink;
_EX_Entry->Flink->Blink = _EX_Entry->Blink;
}
WaitEntry = ListHead->Flink;
}

while (!IsListEmpty(&ReadyList)) {
ThreadEntry = RemoveHeadList(&ReadyList);
Thread = CONTAINING_RECORD(ThreadEntry, KTHREAD, WaitListEntry);
KiReadyThread(Thread);
}

return;
}

WRK版本:

KiWaitTestWithoutSideEffects和KiWaitTestSynchronizationObject都是将在一个等待对象上所有的等待线程全部从等待网上摘出,并且清空将线程从等待链表中摘出。

前者和后者的区别:前者摘出链时SignalState不改变(为1),后者的WaitAny类型将SignalState置0。

以上函数中涉及到两个循环:

  1. 大循环:循环地将在该等待对象上的所有线程从等待网上摘掉,并且从线程等待链上摘掉。
  2. 小循环(KiUnwaitThread):把一个线程上所有的被等待对象从自己的链上摘出,但是所有的等待块还在这个线程上挂着。

WaitEntry = ListHead->Flink;并没有错误,因为KiUnwaitThread摘链后,链表ListHead->Flink已经更新。

按我自己的理解KeSetEvent就应该根据被等待对象在不同线程上的等待方式WaitAny/WaitAny来设置if-else,而不是根据通知还是同步来设置:

  • Any:
    • 通知:将所有线程下等待网并唤醒
    • 同步:将等待该对象的第一个线程下等待网并唤醒(Set一次只能被用一次)
  • All:
    • 通知/同步:循环判断等待该对象的线程的其他被等待对象是否有信号,都有信号才将这个线程下网

源码中还有一个细节:在KeSetEvent中信号量设置为1后,调用KiWaitTest去将线程的等待块全部摘了,然后回到WaitForSingleObject中若判断SigleState不满足唤醒条件时,又回将等待块挂上去。但是还是不理解为什么要更改SigleState。

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
KeSetEvent
{
if (EventNotificationObject && SignalState == 1 && Wait == FALSE)
return 1;

OldState = SignalState;
SignalState = 1;

if ((OldState == 0) && ListHead->Flink != ListHead)
{
if (EventNotificationObject)
{
KiWaitTestWithoutSideEffects(Event, Increment);
} else
{
KiWaitTestSynchronizationObject(Event, Increment);
}
}

return OldState;
}

KiWaitTestWithoutSideEffects
{
ListHead = &Event->Header.WaitListHead;
WaitEntry = ListHead->Flink;
do {
WaitBlock = CONTAINING_RECORD(WaitEntry, KWAIT_BLOCK, WaitListEntry);

if (WaitBlock->WaitType == WaitAny) {
KiUnwaitThread(Thread, (NTSTATUS)WaitBlock->WaitKey, Increment);
} else {
KiUnwaitThread(Thread, STATUS_KERNEL_APC, Increment);//实际效果此行应该注释
}
WaitEntry = ListHead->Flink;
} while (WaitEntry != ListHead);

return;
}

//我认为该函数注释掉一下两行才正确
//若是同步对象,找到第一个WaitAny的线程下网下线程等待链即可
//WaitAll的同步对象就先不用管,让对象自身SignalState为1即可
KiWaitTestSynchronizationObject
{
ListHead = &Event->Header.WaitListHead;
WaitEntry = ListHead->Flink;
do {
WaitBlock = CONTAINING_RECORD(WaitEntry, KWAIT_BLOCK, WaitListEntry);
if (WaitBlock->WaitType == WaitAny) {
//Event->Header.SignalState = 0;
KiUnwaitThread(Thread, (NTSTATUS)WaitBlock->WaitKey, Increment);
break; //找到第一个后直接退出
}
//KiUnwaitThread(Thread, STATUS_KERNEL_APC, Increment);
WaitEntry = ListHead->Flink;
} while (WaitEntry != ListHead);

return;
}

KiUnwaitThread
{
do {
_EX_Entry = &WaitBlock->WaitListEntry;
_EX_Entry->Blink->Flink = _EX_Entry->Flink;
_EX_Entry->Flink->Blink = _EX_Entry->Blink;

WaitBlock = WaitBlock->NextWaitBlock;
} while (WaitBlock != Thread->WaitBlockList);

if (Thread->WaitListEntry.Flink != NULL) {
_EX_Entry = &Thread->WaitListEntry;
_EX_Entry->Blink->Flink = _EX_Entry->Flink;
_EX_Entry->Flink->Blink = _EX_Entry->Blink;
}

while (!IsListEmpty(&ReadyList)) {
ThreadEntry = RemoveHeadList(&ReadyList);
Thread = CONTAINING_RECORD(ThreadEntry, KTHREAD, WaitListEntry);
KiReadyThread(Thread);
}

return;
}

KeSetEvent:

  1. 如果等待队列是空的,没有线程在睡眠等待,就把Event->Header.SignalState设置成1。这样,如果此后有线程对此事件对象执行KetWaitForSingleObject()操作,就因此而不必睡眠等待。这对于通知型和同步型的事件对象都是一样。
  2. 已经有线程在这个对象上睡眠等待,那就要从中唤醒一个或所有线程。这时候的处理取决于事件对象的类型以及等待的方式:
    • 对于通知型的事件对象,或者等待者的等待方式是WaitAll,而且此前SignalState为0,就将SignalState置1,并通过KiWaitTest()唤醒这个线程,以及等待队列中所有符合条件的线程。可是,要是SignalState本来就已经是1,则没有任何影响。
    • 否则,对于同步型的事件对象,并且等待者的等待方式是WaitAny,就通过KiUnwaitThread()唤醒等待队列中的第一个线程。此时并不改变SignalState的值。

WRK版本复合海哥说的:
SetEvent对应的内核函数:KeSetEvent
1)修改信号值SignalState为1
2)判断对象类型
3)如果类型为通知类型对象(0) ,唤醒所有等待该状态的线程
4)如果类型为事件同步对象(1) ,从链表头找到第一个 (WaitType == WaitAny)

ReactOS版本:(也有一定小问题)

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
KeSetEvent
①if(线程没有等待对象) SignalState = 1;
else //等待Event线程有多个
{
②if(通知事件 || WaitAll)
{
③if(SignalState == 0) {SignalState = 1;④KiWaitTest(Event,Increment);}
}else{ //同步对象+WaitAny
⑤KiUnwaitThread();
}
}

KiWaitTest
while(SignalState > 0 && WaitEntry != ListHead)
{
if(WaitAny)//通知事件+WaitAny
{
SignalState == 1;
}else //通知事件+WaitAll、同步对象+WaitAll,SignalState == 1
{
while(NextWaitBlock)
{
if(NextWaitBlock.SignalState == 0) goto Next;
WaitEntry = WaitEntry->Blink;
KiSatisifyMultipleObjectWaits(CurrentWaitBlock);//Satisfiying a Wait All
}
}
KiUnwaitThread();
Next:
WaitEntry = WaitEntry->Flink;
}


KiWaitTest
while(SignalState > 0 && WaitEntry != ListHead)
{
if(WaitAny) 处理SignalState == 1; //通知事件+WaitAny

//通知事件+WaitAll、通知事件+WaitAny、同步对象+WaitAll,SignalState == 1
//同步对象+WaitAny,SignalState值未知
//KiUnwaitThread();
do {
_EX_Entry = &WaitBlock->WaitListEntry;
_EX_Entry->Blink->Flink = _EX_Entry->Flink;
_EX_Entry->Flink->Blink = _EX_Entry->Blink;

WaitBlock = WaitBlock->NextWaitBlock;
} while (WaitBlock != Thread->WaitBlockList);

if (Thread->WaitListEntry.Flink != NULL) {
_EX_Entry = &Thread->WaitListEntry;
_EX_Entry->Blink->Flink = _EX_Entry->Flink;
_EX_Entry->Flink->Blink = _EX_Entry->Blink;
}

WaitEntry = ListHead->Flink;
}


KiUnwaitThread
{
KiUnlinkThread(Thread, WaitStatus);
处理线程优先级
}

KiUnlinkThread
{
Thread->WaitStatus |= WaitStatus;
WaitBlock = Thread->WaitBlockList;
do {
RemoveEntryList(&WaitBlock->WaitListEntry);
WaitBlock = WaitBlock->NextWaitBlock;
} while (WaitBlock != Thread->WaitBlockList);

if (Thread->WaitListEntry.Flink != NULL) {
RemoveEntryList(&Thread->WaitListEntry);
}
}

#define RemoveEntryList(Entry) {
PLIST_ENTRY _EX_Entry;
_EX_Entry = (Entry);
_EX_Entry->Blink->Flink = _EX_Entry->Flink;
_EX_Entry->Flink->Blink = _EX_Entry->Blink;
}

5 _KOBJECTS

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
#define DISPATCHER_OBJECT_TYPE_MASK 0x7
typedef enum _KOBJECTS {
EventNotificationObject = 0, //通知类型对象
EventSynchronizationObject = 1, //事件同步对象
MutantObject = 2,
ProcessObject = 3,
QueueObject = 4,
SemaphoreObject = 5,
ThreadObject = 6,
Spare1Object = 7,
TimerNotificationObject = 8,
TimerSynchronizationObject = 9,
Spare2Object = 10,
Spare3Object = 11,
Spare4Object = 12,
Spare5Object = 13,
Spare6Object = 14,
Spare7Object = 15,
Spare8Object = 16,
Spare9Object = 17,
ApcObject, //18
DpcObject, //19
DeviceQueueObject, //20
EventPairObject, //21
InterruptObject, //22
ProfileObject //23
} KOBJECTS;

//
// Enumerated kernel types
//
// Kernel object types.
//
// N.B. There are really two types of event objects; NotificationEvent and
// SynchronizationEvent. The type value for a notification event is 0,
// and that for a synchronization event 1.
//
// N.B. There are two types of new timer objects; NotificationTimer and
// SynchronizationTimer. The type value for a notification timer is
// 8, and that for a synchronization timer is 9. These values are
// very carefully chosen so that the dispatcher object type AND'ed
// with 0x7 yields 0 or 1 for event objects and the timer objects.

在Windows中,每一种对象都需要有一个对应的类型对象(即OBJECT_TYPE对象),其定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define OBJECT_LOCK_COUNT 4

typedef struct _OBJECT_TYPE {
ERESOURCE Mutex;
LIST_ENTRY TypeList;
UNICODE_STRING Name; // Copy from object header for convenience
PVOID DefaultObject;
ULONG Index;
ULONG TotalNumberOfObjects;
ULONG TotalNumberOfHandles;
ULONG HighWaterNumberOfObjects;
ULONG HighWaterNumberOfHandles;
OBJECT_TYPE_INITIALIZER TypeInfo;
#ifdef POOL_TAGGING
ULONG Key;
#endif //POOL_TAGGING
ERESOURCE ObjectLocks[ OBJECT_LOCK_COUNT ];
} OBJECT_TYPE, *POBJECT_TYPE;

系统定义的对象种类是有限的,WRK 支持 31 种对象,各有一个全局的 POBJECT_TYPE 变量指向其类型对象,表 2.4 列出了这 31 种对象,读者可以在 WRK 源代码中找到它们的定义和用法。(Windows内核原理与实现)

5.png

如互斥体:

1
2
3
4
5
6
7
8
9
10
11
POBJECT_TYPE ExMutantObjectType;
...
Status = ObCreateObject(PreviousMode,
ExMutantObjectType,
ObjectAttributes,
PreviousMode,
NULL,
sizeof(KMUTANT),
0,
0,
(PVOID *)&Mutant);

在函数NtCreateMutant等都会用到。