等待对象

Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html 

 等待对象

1. 临界区

  1)全局变量潜在问题

    全局变量在面临全局切换时会出现安全隐患,这是众所周知的,但下面代码安全么?

      INC DOWRD PTR DS:[0x12345678]

    在单核下是安全的,但是在多核下是不安全的,因为多核可能会存在多个CPU执行一行代码的情况。

    像下面这样写才是安全的:

      INC DOWRD PTR DS:[0x12345678]

  2)临界区的设计

    全局变量 dword Flag = 0;

    进入临界区                       离开临界区

    Lab:           lock dec [Flag]

      mov eax,1;

      lock xadd[Flag],eax;

      cmp eax,0;

      jz endLab

      dec [Flag]

      // 线程等待Sleep

    endLab:

      ret

    

2.自旋锁

  Windows在多核下才会存在自旋锁。

 

3. 线程的等待与唤醒

  1)等待与唤醒机制

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

    其操作关系如下图:

    

  2)可等待对象

    其_OBJECT中以_DISPATCHER_HEADER开头的,就成为可等待对象。

    如果没_DISPATCHER_HEADER,其会嵌入一个_DISPATCHER_HEADER的结构体(比如FILE_OBJECT +0x5c _KEVENT),使其变为可等待对象。

  3)可等待对象的差异

    其差异如下,在NtWaitForSingleObject如果不是可等待对象,其会插入一个_DISPATCH_HEADER结构体,使其变为可等待对象。

     

4. _KWAIT_BLOCK解析

  struct _KWAIT_BLOCK {

  struct _LIST_ENTRY WaitListEntry; //0x0    同一可等待对象的下一个等待块

  struct _KTHREAD* Thread; //0x8         等待线程 

  VOID* Object; //0xc              可等待对象(进程、线程、事件)

  struct _KWAIT_BLOCK* NextWaitBlock; //0x10  等待线程的下一个等待块

  USHORT WaitKey; //0x14   等待块索引,第一个为0,第二个为1,.....

  USHORT WaitType; //0x16   若只有一个可等待对象符合条件就激活线程则为1,全部符合则为0.

  };

 

5. _DISPATCH_HEADER解析

  struct _DISPATCHER_HEADER
  {
      UCHAR Type;         类型(Event,Thread,互斥体....)   
      UCHAR Absolute;  
      UCHAR Size;     
      UCHAR Inserted;        
      LONG SignalState;       是否有信号,>0表示有信号
      struct _LIST_ENTRY WaitListHead;   
  }; 

 

6.等待网解读

  一个线程与一个等待对象生成唯一一个等待块。

  如下图,其中线程1中存在B等待对象,线程2中同时存在A、B两个等待对象。

  该模型很好展示出其对应的三者之间的关系。

  其中线程2如果想查看其存在的等待对象,从_KTHREAD+0x5c处找到WaitListEntry,然后直接NextWaitBlock往下遍历,从等待块的Object可以找到对应的对象。

  如果等待对象B想查看其对应的等待线程,从WaitListHeader出发双向链表然后找WaitBlock.Thread。

  从这种角度来讲,其很容易理解WaitBlock相当于线程与可等待对象的一个中转站(十字路口),这样整幅图相当于一个地图,很好理解。

    

7.WaitForSingleObject解读

  我们上面那幅图的形成,是如何挂上去的呢?答案是调用WaitForSingleObject。

  又何时被摘下来的呢?答案也是WaitForSingleObject。

  是不是感觉很神奇?我们下面来分析一下。

  1)WaitForSingleObject调用顺序

    WaitForSingleObject(三环)->NtWaitForSingleObject(零环)->KeWaitForSingleObject(零环)。

    其中在NtWaitForSingleObject中将传入的句柄通过查找句柄表找到内核对象Object,然后传入KeWaitForSingleObject。

  2)KeWaitForSingleObject 解读

    KeWaitForSingleObject 是一个非常复杂的函数,

 

8.KeWaitForSingleObject 解读

1)前半部分:将等待块挂入线程

  如下代码,当WaitForSingleObject设置超时时间时,除了可等待对象,还为定时器分配一个等待块,然后挂入链表。

  _KTHREAD中预先存在WaitBlock[4](+0x70),先使用四个等待块,其pThread->WaitBlockList(+0x5c)指向WaitBlock[0]。

  

   该代码执行之后,其形成的数据结构如下:

   

2)后半部分:将线程挂入等待链表、切换线程、从等待链表中摘除。

  后半部分是一个双向循环,简单简化为C语言如下:

  while(True){

    if(符合激活条件){

      // 1)修改SingalState

      // 2)退出循环      

    }else{

      if(第一次执行)

        将当前线程等待块挂到等待对象的链表(WaitListHead)中。

      // 将自己挂入等待队列(KiWaitListHead)

      // 切换线程...再次获取CPU时从这里执行

    }

  }

  // 1)释放将自己+5C位置清0

  // 2)释放_KWAIT_BLOCK所占内存

  1> 总结:

    不同的等待对象,用不同的方法来修改_DISPATCHER_HEADER(SignalState)。

    比如:如果可等待对象时EVENT,其他线程通常使用SetEvent来设置SiganlState = 1。并且,将正在等待对象的其他线程唤醒,也就是从等待链表(KiWaitListHead)中摘下来。

    但是,SetEvent函数并不会将线程从等待网上摘下来,是否要下来,由当前线程自己来决定。

  2> 举例:

    比如如上面等待网所示,可等待对象B上挂着线程1与线程2。此时线程c使用SetEvent函数设置B,其会把B的SignalState设置为1,之后B沿着其等待块找到线程1和线程2并将其在等待链表中摘除。

    当线程2被摘除之后,其会从线程切换(②)点处继续运行,此时,该线程需要自行判断是否可以恢复,线程2同时等待可等待对象AB,此时B恢复A不恢复,线程2仍然不能运行,故循环时再次把自己挂在等待链表中。

    只有当真正的判断可以恢复时,才会将其从等待网中摘除,恢复线程运行。

    

 

9. _EVNET 类型

  1)基本API函数

    ① CreateEvent 创建一个事件。

    ② SetEvent 将事件置为1。

    ③ ResetEvent 将事件置为0。

  2)Event类型

    Event._DISPATCHER_HEADER.type :0- 通知类型对象 ; 1 - 事件同步对象。

    注意,CreateEvent第二个参数bManualReset,其表示是否手动复原,如果设置为True,必须手工Reset复原,因此其会全部通知到,故Type为0.

    KeSetEvent函数行为如下,简单来说如果是通知

    

    ① 通知类型时(Type == 0)

      其关键代码如下,我们应该做的是不修改SignalState,下次循环还应该满足激活条件,如下图

      

    ②同步事件类型(Type==1)

      

  3)事件的本质

    事件的本质是两个线程之间构造一个临界区(当Type==1)时。

    我们应该理解临界区本质,一次只有一个线程可以访问,临界区可以并不只是一块相同的代码,可以是很多块映射,一次只能访问映射的一种。

    如下面图,事件的本质是临界区的两块映射,在两个线程的不同代码部分,当A线程进入临界区时,其调用WaitForSingleObject,此时其等待对象为1,

      可以直接调用,并将其临界区置为0,当下一个线程再调用WaitForSingleObject,其会将自身挂起,当A线程执行完调用SetEvent,此时唤醒B线程执行。

    

10.  信号量类型 SEMAPHORE

  按照前面事件的理解,事件可以同时让一个线程进入临界区,而信号量可以同时让多个线程进入临界区。

  

  1)应用场景

    在生产者消费者模型时,如果只有一个生产者,则可以使用事件来模拟;但是如果此时存在多个生产者,再用事件无法模拟。

    此时就需要使用信号量,将信号量的个数对应的生产者个数,即SignalState个数,可以通过SignalState>0来判断。

    当释放x时,SiganlState+x;获取n时,SignalState+n,这样整套流程就很好理解。

  2)Sempahore数据类型

    信号量的_DISPATCHER_HEADER.Type为5

    //0x14 bytes (sizeof)

    struct _KSEMAPHORE {

      struct _DISPATCHER_HEADER Header; //0x0

      LONG Limit; //0x10 最大个数

    };

 

11.  互斥体 Mutant

  1)不同间的进程通讯所存在的极端情况(A进程中X线程与B进程中Y线程共用等待对象Z):

    如果B进程的Y线程还没有来得及调用SignalState的函数(如SetEvent),那么等待对象Z将遗弃,则会将X线程将永久运行下去。

  2)允许重入

    死锁,Wait(A){Wait(A,B,C);},将会出现死锁。

posted @ 2020-03-31 18:00  OneTrainee  阅读(842)  评论(0编辑  收藏  举报