转载自:http://blog.csdn.net/ithzhang/article/details/8708299
浅谈windows句柄表
windows定义了很多内核对象:进程对象、线程对象、互斥量对象、信号量对象、事件对象、文件对象等等。在调用相应的函数创建这些对象后,我们都可以通过HANDLE类型的句柄来引用它们。或许你在一些书上看到过说句柄相当于指针,它指向具体的对象。在某种程度上来说这是不错的,但是进一步深入探究时就会发现这样的说法很不准确。说到句柄就不能不提句柄表,句柄必须通过句柄表才能找到所引用的内核对象,但是很多书中对句柄表却是一带而过,不加深究。这是因为Microsoft并未就句柄表发布过任何官方的文档,通过逆向工程获得结论又很难让人信服。
相信没有什么能比windows公布的源代码更有说服力了,接下来我会通过windows公布的WRK(windows research kernel)源代码,来为大家深入解析一下windows句柄表。虽然是基于WRK的,不能保证其他版本的windows系统也采用相同的机制,但是我想它们之间或许都是大同小异的。
windows使用句柄对进程中的各种对象进行引用。实际上windows句柄就是一个索引,它存储了关联对象在句柄表的索引值,每个索引对应句柄表中的一个表项。通过句柄存储的索引,就可以很容易获得该句柄项对应的对象的指针。
句柄表中存储了很多了句柄表项,类似下面的结构:
索引 | 该索引对应对象指针 |
1 | 0xfffffddf |
2 | 0xkdkkdh |
3 | 0xkdkhkd |
4 | 0x3jdkkg |
上图仅仅为了演示用,并不代表实际意义。
句柄表是针对于进程而言的,在一个进程使用的句柄,直接在另一个进程中使用是毫无意义的。换句话说,句柄仅在一个进程内有效。当一个进程的句柄传递给另一个句柄后,句柄就不再有效。举例来说:在进程A中索引值为8的句柄,用于引用a对象。在进程B的句柄表中,虽然索引为8的句柄项可能不为空,但是有可能是引用的b对象。此对象非彼对象。
在windows Server 2003(与WRK有相同的内核)中,句柄表是一个多层次的结构。它的类型为:HANDLE_TABLE。定义如下:
- <span style="font-size:18px;"> typedef struct _HANDLE_TABLE {
- ULONG_PTR TableCode;//指针指向句柄表的存储结构。
- struct _EPROCESS *QuotaProcess; // 所属进程的指针
- HANDLE UniqueProcessId; // 这个进程的 ProcessID
- EX_PUSH_LOCK HandleTableLock[HANDLE_TABLE_LOCKS];//句柄表所,仅在句柄表扩展时使用。
- LIST_ENTRY HandleTableList; // 所有的 HandleTAble 在内核中形成一个 List ,这是 Entry
- EX_PUSH_LOCK hangleConventionEvent//若在访问句柄表时发生了竞争则在此锁上等待。
- PHANDLE_TRACE_DEBUG_INFO DebugInfo;
- LONG extrainfoPag;
- ULONG FirstFree; // 空闲链表表头句柄索引。
- ULONG LastFree; // 最近被释放的句柄索引。
- ULONG NextHandleNeedingPool;//下一次句柄表扩展的起始句柄索引。
- LONG HandleCount;//正在使用的句柄表项数量。
- union{
- ULONG Flags;//标志域
- BOOLEAN StrictFIFO:1;//是否使用FIFO风格的重用。
- };
- } HANDLE_TABLE, *PHANDLE_TABLE;</span>
我们看到该结构包含很多的成员,但此处我们仅讨论对我们有帮助的几个。
我们看到HANDLE_TABLE的TableCode成员是一个指针,实际上它的高30位指向存储句柄表的存储结构,该存储结构初始时为4KB大小的一个页面。低2位代表当前句柄表的层数。最多为三层,但是初始时只有一层,以后随着句柄数量的不断增加会不断扩展。
每个句柄表项大小为8Byte,其结构为HANGLE_TABLE_ENTRY。
,由于windows在为句柄表分配内存时是按页面大小4KB来申请内存的。因此每次为句柄表申请一个新的页面时,句柄表就增加了512项。
前面我们曾提过,windows句柄表是层次结构的。下图就是当句柄表为三层时的结构图:
如果低2位为0,说明句柄表只有一层。TableCode指向的页面直接存储的句柄,由于每个表项占8Byte,因此4KB页面最多可以存储512个表项。
下图为当句柄表为一层时的结构图:
如果低2的值为1,此时句柄表有两层,TableCode指向最高层页面。而最高层页面用于存储指向下一层页面的指针。由于32位系统下指针占4Byte,因此最高层页面可以存储1024个指针。每个指针同样指向4KB的存储表项的页面。可以存储512个句柄表项。此时整个windows句柄表可以存储1024*512个表项。如下图所示:
如果低2位值为2,此时句柄表有三层,如下图所示:
最高层和次高层都是存储的指针,最低层页面存储句柄项,因此此时可以整个句柄表可以存储1024*1024*512个句柄表项。
但是window限定了每个进程句柄表存储的句柄表项不得超过:2^24=16777216。
实际上,在每个最低层页面的第一个表项都有特殊用途,所以每个最低层页面真正供进程使用的表项为511个。
在进程创建时,系统会给新进程分配一个单层的句柄表。随着进程中句柄数量的不断增加,句柄表会由单层扩展为二层,最后被扩展为三层。
在HANDLE_TABLE结构中,FirstFree域记录了当前句柄表中的空闲句柄单链表。说其是单链表,但是每个元素之间不是通过指针而是通过句柄索引值来连接的。句柄值按HANDLE_VALUE_INC宏定义逐个递增,windows定义该宏的值为4,为什么是4?我们可以通过windbg的!handle命令我们可以查看一下windows自带的计算器程序的句柄表的情况:
可以看到第一个表项对应的句柄值为4,而且后面的句柄值都是4的倍数。这是为什么呢?这因为microsoft将句柄的低两位用来存储该索引对应的句柄表的层次号。在前面曾介绍过,TableCode成员的低两位用以控制句柄表的层次。而此处句柄表的低两位是用于指明该索引所处的层次,有助于快速的定位索引。因此所有的句柄必须右移两位,也就是除以4才能得到它的实际在句柄表中的索引。这也就是句柄值都是4的倍数的原因。
FirstFree成员存储链表头句柄的索引值。在索引项HANDLE_TABLE_ENTRY结构(马上会介绍)中,我们看到有一个名为NextFreeTableEntry的成员。该成员存储下一个空闲句柄索引值。当进程需要创建新的句柄,该句柄会被加入到句柄。这时就可以从FirstFree取得第一个空闲句柄索引,假设该索引指向x表项,并将该x表项的NextFreeTableEntry成员赋值给FirstFree。此时,原来链表头的NextFreeTableEntry就变成了现在的FirstFree,成为链表头。
伪代码如下:
1:从FirstFree取出索引,该索引指向的句柄表项x。
2:FirstFree=x.NextFreeTableEntry;
在释放x句柄项时,将x句柄索引赋值给FirstFree,并将赋值前的FirstFree的值赋值给x.NextFreeTableEntry。
伪代码如下:
1:Index temp=FirstFree;
2:将x表项的索引赋值给FirstFree
3:x.NextFreeTableEntry=temp;
NextHandleNeedingPool成员记录了下一次对句柄表进行扩展时,扩展页面的第一个索引。也就是说当句柄表所有已分配的页面都满了之后,下一个页面的其实索引值。因此,windows句柄表只是在确实不够用的时候才进行简单的线性增长。并不会一次分配多个页面或扩展多层。
前面我们曾多次提到句柄表项,其实它是HANDLE_TABLE_ENTRY结构,定义如下:
- typedef struct _HANDLE_TABLE_ENTRY
- {
- union {
- PVOID Object;//内核对象指针。
- ULONG ObAttributes;//内核对象属性。
- PHANDLE_TABLE_ENTRY_INFO InfoTable;//
- ULONG_PTR Value;
- };
- union {
- union {
- ACCESS_MASK GrantedAccess;
- struct{
- USHORT GrantedAccessIndex;
- USHORT CreatorBackTraceIndex;
- };
- };
- LONG NextFreeTableEntry;
- };
- } HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;
该结构由两个大的union组成,占8字节。句柄表项存储了该表项对应的对象的地址,以及其他的一些属性信息。
在第一个union中有一个Object指针,它指向了句柄所代表的内核对象。第二个union中,如果句柄表项指向了一个有效的对象,那么GrantedAccess成员记录了该句柄的访问掩码。当句柄为空闲时,NextFreeTableEntry成员存储下一空闲节点的索引值,用以连接句柄表的空闲链表。
window提供的API:GetCurrentThread和GetCurrentProcess,可以返回当前线程和进程的句柄。其实在句柄表中是没有存储当前进程的句柄和当前线程句柄。GetCurrentThread返回句柄的值是-2,当我们以此句柄作为参数传递给windows其他API函数时,在函数内部如果检测到该句柄值为-2,不会查找句柄表,立即返回到线程对象地址。类似的调用GetCurrentProcess时,其实返回的是-1,当使用此进程句柄时,同样不需要查找句柄表,会立即返回指向当前进程对象的指针。
本文使用比较通俗的语言,对windows句柄表的结构做了简单的介绍。目的是让对句柄表存在疑问的童鞋对句柄表有个清晰的认识。有说的不对的地方,欢迎指正!!
2013、3、22于浙江杭州