• 等待对象


    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);},将会出现死锁。

  • 相关阅读:
    curl库使用文件传输
    linux 命令
    第三方库交叉编译
    指针越界
    GetWindowRect GetClientRect
    libevent
    C#关闭窗体
    C# log日志窗口
    C++同一时刻仅允许一个实例,包含多用户的场景。
    C# 引用类型
  • 原文地址:https://www.cnblogs.com/onetrainee/p/12606946.html
Copyright © 2020-2023  润新知