等待对象
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,.....
U
SHORT 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);},将会出现死锁。