要点回顾
在之前的文章中学习过,线程在进入临界区之前会调用WaitForSingleObject或者WaitForMultipleObjects,此时如果有信号,线程会从函数中退出并进入临界区,如果没有信号那么线程会将自己挂入等待链表,然后将自己挂入等待网,最后切换线程。
其它线程在适当的时候,调用方法修改被等待对象的SignalState为有信号(不同的等待对象,会调用不同的函数),并将等待该对象的其它线程从等待链表中摘掉(临时复活)。这样,当前线程便会在WaitForSingleObject或者WaitForMultipleObjects恢复执行(在哪切换在哪开始执行),如果符合唤醒条件,此时会修改SignalState的值,并将自己从等待网上摘下来,此时的线程才是真正的唤醒。
唤醒方式的差异
被等待对象的不同,在唤醒的过程中也会有所差异。再回顾这个循环。
不同的被等待对象,主要差异体现在两点:
- 循环开始时,判断是否符合激活条件的方式不同。
- 符合激活条件的情况下,修改SignalState的具体操作不同。
创建事件对象:信号
分析
本篇主要讨论事件这个可等待对象与线程唤醒机制的关联。首先是创建事件对象API:
1 | HANDLE CreateEvent( |
事件仅有一个_DISPATCHER_HEADER结构,作为可等待对象最重要的一个结构,把它和事件放到一起来看。
1 | _DISPATCHER_HEADER |
在创建事件对象时,CreateEvent的第二个和第三个参数分别决定了_DISPATCHER_HEADER的Type和SignalState的值。这里用一个实验验证对SignalState影响。
编写并运行如下代码(环境:Windows XP,IDE:VC++6.0)
c1
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
HANDLE g_hEvent;
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
//当事件变成已通知时
WaitForSingleObject(g_hEvent, INFINITE);
printf("Thread执行了!\n");
return 0;
}
int main()
{
//创建事件
//默认安全属性 对象类型 初始状态 名字
g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
//设置为有信号
//SetEvent(g_hEvent);
//创建线程
::CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
getchar();
return 0;
}观察运行结果
无任何输出,g_hEvent句柄处于无信号状态。
修改代码并重新执行
c1
g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL); //第三个参数由FALSE改为TRUE
再次观察结果
发现打印出了结果。
接着继续修改代码。
c1
2g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL); //第三个参数再次改回FALSE
SetEvent(g_hEvent); //设置信号量,将原先注释掉的语句添加回来观察修改后的结果
结论
- CreateEvent函数的第三个参数决定了事件对象一开始是否有信号。
- CreateEvent函数第三个参数为
TRUE
时,效果等同于在下一行调用了SetEvent()。
创建事件对象:类型
前面提到CreateEvent的第二个和第三个参数分别决定了_DISPATCHER_HEADER的Type和SignalState的值,这部分验证第二个参数对Type值的影响以及Event对象两种类型(0、1)的介绍。
分析
编写并运行如下代码(环境:Windows XP,IDE:VC++6.0)
c1
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
HANDLE g_hEvent;
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
WaitForSingleObject(g_hEvent, INFINITE);
printf("Thread1执行了!\n");
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
WaitForSingleObject(g_hEvent, INFINITE);
printf("Thread2执行了!\n");
return 0;
}
DWORD WINAPI ThreadProc3(LPVOID lpParameter)
{
WaitForSingleObject(g_hEvent, INFINITE);
printf("Thread3执行了!\n");
return 0;
}
int main()
{
//创建事件
//默认安全属性 对象类型 初始状态 名字
g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
HANDLE hThread[3];
//创建3个线程
hThread[0] = ::CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
hThread[1] = ::CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);
hThread[2] = ::CreateThread(NULL, 0, ThreadProc3, NULL, 0, NULL);
//设置事件为已通知
SetEvent(g_hEvent);
//等待线程结束 销毁内核对象
WaitForMultipleObjects(3, hThread, TRUE, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(hThread[2]);
CloseHandle(g_hEvent);
getchar();
return 0;
}观察运行结果
可以看出,在CreateEvent函数第二个参数值为FALSE时,Event的类型是1。此时经过SetEvent设置事件信号后,仅仅有一个线程执行了。
这种类型的Event,属于事件同步对象(Type=1),当其它线程调用SetEvent函数时,会根据WaitListHead找到第一个,且只需要一个等待对象符合条件就可以被激活的线程(即_KWAIT_BLOCK.WaitType=1),将其临时复活。接着被临时复活的线程若能成功退出循环,便将自己从等待网摘除,完成执行。
接下来修改代码,重新编译、执行
c1
g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); //第二个参数由FALSE改为TRUE
观察结果
此时,在CreateEvent函数第二个参数值为TRUE时,Event的类型是0(因线程均已复活,所以此时无法查看被等待对象)。此时经过SetEvent设置事件信号后,所有线程都执行了。
这种类型的Event,属于通知类型对象(Type=0),特点就是,其它线程调用SetEvent函数时,会根据Event对象的WaitListHead找到所有等待该对象的线程,将它们临时复活。
结论
SetEvent对应的内核函数:KeSetEvent
- 修改信号量SignalState为1
- 判断对象类型
- 如果类型为通知类型对象(Type=0),唤醒所有等待该状态的线程。
- 如果类型为事件同步对象(Type=1),从链表头找到第一个等待类型为WaitAny(_KWAIT_BLOCK.WaitType=1)的线程。
KeWaitForSingleObject分析
到这里还有一个问题没有解决,就是通知类型对象仅仅是被临时复活,为什么却能全部得到执行呢?要解开这个问题,需要分析KeWaitForSingleObject的关键循环。
分析
在对Event部分分析过后,这里对这个循环的逻辑进行梳理:
- 首先,KeWaitForSingleObject函数的参数包含了被等待对象的地址,就是Object参数,红色方框可以看到,被等待对象的首地址,也就是_DISPATCHER_HEADER的首地址,被传给了ebx。
- 往下看,橙色方框里会判断被等待对象的类型(_DISPATCHER_HEADER.Type)是否为2(互斥体),互斥体的情况比较特殊,会单独处理;其余情况则会跳转。
- 若不是互斥体,跳转到蓝色方框,这里会先判断被等待对象的SignalState的值是否为0,即是否有信号。若无信号,则跳走。
- 接下来到紫色方框部分,这里会判断Type的类型是否为1,若为1,说明是事件同步对象,此时会跳转到上方的代码块,将SignalState的值置0,之后退出循环。
- 再到绿色方框,这里会判断类型是否为5(信号量Semaphore的Type值为5),若为5,则会跳转到粉色方框,将SignalState的值减1,之后退出循环
- 若不为5,只有可能是通知类型对象,此时不作任何修改,退出循环。
通过分析,可以解决之前的疑惑,通知类型对象在符合激活条件的情况下,是不会对SignalState的值进行修改的,因此所有临时复活的线程都能完成执行。
结论
- 通知类型对象(Type=0),不修改SignalState
- 事件同步对象(Type=1),SignalState置0
- 信号量(Type=5),SignalState减1
参考资料
参考教程:
- 海哥逆向中级预习班
参考链接:
- https://blog.csdn.net/qq_41988448/article/details/104895544 (CSDN-lzyddf学习笔记)
- https://blog.csdn.net/weixin_42052102/article/details/83421697 (CSDN-My classmates学习笔记)
- https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-kewaitforsingleobject (KeWaitForSingleObject)