avatar

Catalog
事件

要点回顾

在之前的文章中学习过,线程在进入临界区之前会调用WaitForSingleObject或者WaitForMultipleObjects,此时如果有信号,线程会从函数中退出并进入临界区,如果没有信号那么线程会将自己挂入等待链表,然后将自己挂入等待网,最后切换线程。

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

唤醒方式的差异

被等待对象的不同,在唤醒的过程中也会有所差异。再回顾这个循环。

不同的被等待对象,主要差异体现在两点:

  1. 循环开始时,判断是否符合激活条件的方式不同
  2. 符合激活条件的情况下,修改SignalState的具体操作不同

创建事件对象:信号

分析

本篇主要讨论事件这个可等待对象与线程唤醒机制的关联。首先是创建事件对象API:

c
1
2
3
4
5
6
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset, //决定_DISPATCHER_HEADER.Type
BOOL bInitialState, //决定_DISPATCHER_HEADER.SignalState
LPCTSTR lpName
);

事件仅有一个_DISPATCHER_HEADER结构,作为可等待对象最重要的一个结构,把它和事件放到一起来看。

c
1
2
3
4
5
6
7
8
_DISPATCHER_HEADER
+0x000 Type //CreateEvent的第二个参数决定了当前事件对象的类型
//TRUE:通知类型对象 FALSE:事件同步对象
+0x001 Absolute
+0x002 Size
+0x003 Inserted
+0x004 SignalState //CreateEvent的第三个参数决定了这里是否有值
+0x008 WaitListHead

在创建事件对象时,CreateEvent的第二个和第三个参数分别决定了_DISPATCHER_HEADER的Type和SignalState的值。这里用一个实验验证对SignalState影响。

  1. 编写并运行如下代码(环境:Windows XP,IDE:VC++6.0)

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

    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;
    }
  2. 观察运行结果

    无任何输出,g_hEvent句柄处于无信号状态。

  3. 修改代码并重新执行

    c
    1
    g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);	//第三个参数由FALSE改为TRUE
  4. 再次观察结果

    发现打印出了结果。

  5. 接着继续修改代码。

    c
    1
    2
    g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);	//第三个参数再次改回FALSE
    SetEvent(g_hEvent); //设置信号量,将原先注释掉的语句添加回来
  6. 观察修改后的结果

结论

  • CreateEvent函数的第三个参数决定了事件对象一开始是否有信号。
  • CreateEvent函数第三个参数TRUE时,效果等同于在下一行调用了SetEvent()

创建事件对象:类型

前面提到CreateEvent的第二个和第三个参数分别决定了_DISPATCHER_HEADER的Type和SignalState的值,这部分验证第二个参数对Type值的影响以及Event对象两种类型(0、1)的介绍。

分析

  1. 编写并运行如下代码(环境:Windows XP,IDE:VC++6.0)

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

    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;
    }
  2. 观察运行结果

    可以看出,在CreateEvent函数第二个参数值为FALSE时,Event的类型是1。此时经过SetEvent设置事件信号后,仅仅有一个线程执行了。

    这种类型的Event,属于事件同步对象(Type=1),当其它线程调用SetEvent函数时,会根据WaitListHead找到第一个且只需要一个等待对象符合条件就可以被激活的线程(即_KWAIT_BLOCK.WaitType=1),将其临时复活。接着被临时复活的线程若能成功退出循环,便将自己从等待网摘除,完成执行。

  3. 接下来修改代码,重新编译、执行

    c
    1
    g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);		//第二个参数由FALSE改为TRUE
  4. 观察结果

    此时,在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部分分析过后,这里对这个循环的逻辑进行梳理:

  1. 首先,KeWaitForSingleObject函数的参数包含了被等待对象的地址,就是Object参数,红色方框可以看到,被等待对象的首地址,也就是_DISPATCHER_HEADER的首地址,被传给了ebx。
  2. 往下看,橙色方框里会判断被等待对象的类型(_DISPATCHER_HEADER.Type)是否为2(互斥体),互斥体的情况比较特殊,会单独处理;其余情况则会跳转。
  3. 若不是互斥体,跳转到蓝色方框,这里会先判断被等待对象的SignalState的值是否为0,即是否有信号。若无信号,则跳走。
  4. 接下来到紫色方框部分,这里会判断Type的类型是否为1,若为1,说明是事件同步对象,此时会跳转到上方的代码块,将SignalState的值置0,之后退出循环
  5. 再到绿色方框,这里会判断类型是否为5(信号量Semaphore的Type值为5),若为5,则会跳转到粉色方框,将SignalState的值减1,之后退出循环
  6. 若不为5,只有可能是通知类型对象,此时不作任何修改,退出循环

通过分析,可以解决之前的疑惑,通知类型对象在符合激活条件的情况下,是不会对SignalState的值进行修改的,因此所有临时复活的线程都能完成执行。

结论

  • 通知类型对象(Type=0),不修改SignalState
  • 事件同步对象(Type=1),SignalState置0
  • 信号量(Type=5),SignalState减1

参考资料

参考教程:

  • 海哥逆向中级预习班

参考链接:

  1. https://blog.csdn.net/qq_41988448/article/details/104895544 (CSDN-lzyddf学习笔记)
  2. https://blog.csdn.net/weixin_42052102/article/details/83421697 (CSDN-My classmates学习笔记)
  3. https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-kewaitforsingleobject (KeWaitForSingleObject)
Author: cataLoc
Link: http://cata1oc.github.io/2020/08/14/%E4%BA%8B%E4%BB%B6/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶