第七章 Windows 2000的对象管理
翻译:Kendiv( [email protected] )
更新:Sunday, May 22, 2005
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
OBJECT_TYPE结构
在前面讨论对象表头字段时,总是涉及到“类型对象”或OBJECT_TYPE结构,那么现在就让我们开始讨论它们。正式的来讲,一个类型对象是一种特殊类型的对象,它可以是一个事件(event)、设备(device)或者进程(process),也可是一个OBJECT_HEADER或其它可选的对象表头子结构。和普通对象的唯一的不同是:类型对象使用一种特殊的方式与其他对象发生关联。一个类型对象属于“主要对象(master object)”类别,此类对象的作用是:定义同类对象的公共属性,并可以将这些对象的子对象保存到一个双向链表中。因此,类型对象也常被称为“对象类型”,以强调它们与普通对象的区别。
类型对象的对象体由一个OBJECT_TYPE结构体和一个内嵌的OBJECT_TYPE_INITIALIZER结构组成,列表7-9给出了这两个结构体的定义。ObCreateObject()在创建对象的过程中,使用OBJECT_TYPE_INITIALIZER结构来构建适当的对象头。例如,ObpAllocateObject()(由ntoskrnl.exe导出)分别使用MaintainHandleCount和MaintainTypeList来决定新创建的对象表头中是否需要包含OBJECT_HANDLE_DB和OBJECT_CREATOR_INFO结构。如果设置了MaintainTypeList标志,那么此类型的对象将会构成一个双向链表,OBJECT_TYPE结构中的ObjectListHead成员是该链表的表头和表尾(这意味着该双向链表是一个环形结构)。OBJECT_TYPE_INITIALIZER结构中的DefaultPagedPoolCharge和DefaultNonPagedPoolCharge成员提供了默认的限额和开销大小(在前面讨论OBJECT_QUOTA_CHARGES结构时曾讨论过限额问题)。
typedef struct _OBJECT_TYPE_INITIALIZER
{
/*000*/ WORD Length; //0x 004C
/*002*/ BOOLEAN UseDefaultObject;//OBJECT_TYPE.DefaultObject
/*003*/ BOOLEAN Reserved1;
/*004*/ DWORD InvalidAttributes;
/*008*/ GENERIC_MAPPING GenericMapping;
/*018*/ ACCESS_MASK ValidAccessMask;
/* 01C */ BOOLEAN SecurityRequired;
/*01D*/ BOOLEAN MaintainHandleCount; // OBJECT_HANDLE_DB
/*01E*/ BOOLEAN MaintainTypeList; // OBJECT_CREATOR_INFO
/* 01F */ BYTE Reserved2;
/*020*/ BOOL PagedPool;
/*024*/ DWORD DefaultPagedPoolCharge;
/*028*/ DWORD DefaultNonPagedPoolCharge;
/* 02C */ NTPROC DumpProcedure;
/*030*/ NTPROC OpenProcedure;
/*034*/ NTPROC CloseProcedure;
/*038*/ NTPROC DeleteProcedure;
/* 03C */ NTPROC_VOID ParseProcedure;
/*040*/ NTPROC_VOID SecurityProcedure; // SeDefaultObjectMethod
/*044*/ NTPROC_VOID QueryNameProcedure;
/*048*/ NTPROC_BOOLEAN OkayToCloseProcedure;
/* 04C */ }
OBJECT_TYPE_INITIALIZER,
* POBJECT_TYPE_INITIALIZER,
**PPOBJECT_TYPE_INITIALIZER;
#define OBJECT_TYPE_INITIALIZER_ /
sizeof (OBJECT_TYPE_INITIALIZER)
// -----------------------------------------------------------------
typedef struct _OBJECT_TYPE
{
/*000*/ ERESOURCE Lock;
/*038*/ LIST_ENTRY ObjectListHead; // OBJECT_CREATOR_INFO
/*040*/ UNICODE_STRING ObjectTypeName; // see above
/*048*/ union
{
/*048*/ PVOID DefaultObject; // ObpDefaultObject
/*048*/ DWORD Code; // File: 5C , WaitablePort: A0
};
/* 04C */ DWORD ObjectTypeIndex; // OB_TYPE_INDEX_*
/*050*/ DWORD ObjectCount;
/*054*/ DWORD HandleCount;
/*058*/ DWORD PeakObjectCount;
/* 05C */ DWORD PeakHandleCount;
/*060*/ OBJECT_TYPE_INITIALIZER ObjectTypeInitializer;
/* 0AC */ DWORD ObjectTypeTag; // OB_TYPE_TAG_*
/*0B0*/ }
OBJECT_TYPE,
* POBJECT_TYPE,
**PPOBJECT_TYPE;
#define OBJECT_TYPE_ /
sizeof (OBJECT_TYPE)
列表7-9. OBJECT_TYPE和OBJECT_TYPE_INITIALIZER结构
由于类型对象(也称对象类型)是Windows 2000对象世界的最基本的构建单位,ntoskrnl.exe在命名变量中保存它们,将其和对象的ObjectType(位于对象的OBJECT_HEADER中)进行比较就可以很容易的验证对象的类型。类型对象是唯一的---系统只针对每类对象创建一个类型对象。表7-4给出了Windows 2000涉及到的所有类型对象。该表各列的含义如下:
表7-4. 有效的对象类型
索引 |
标志 |
名称 |
C结构 |
是否公开 |
符号 |
1 |
“ObjT” |
“Type” |
OBJECT_TYPE |
No |
ObpTypeObjectType |
2 |
“Dire” |
“Directory” |
OBJECT_DIRECTORY |
No |
ObpDirectoryObjectType |
3 |
“Symb” |
“SymbolicLink” |
|
No |
ObpSymbolicLinkObjectType |
4 |
“Toke” |
“Token” |
TOKEN |
No |
SepTokenObjectType |
5 |
“Proc” |
“Process” |
EPROCESS |
Yes |
PsProcessType |
6 |
“Thre” |
“Thread” |
ETHREAD |
Yes |
PsThreadType |
7 |
“Job” |
“Job” |
|
Yes |
PsJobType |
8 |
“Even” |
“Event” |
KEVENT |
Yes |
ExEventObjectType |
9 |
“Even” |
“EventPair” |
KEVENT_PAIR |
No |
ExEventPairObjectType |
10 |
“Muta” |
“Mutant” |
KMUTANT |
No |
ExMutantObjectType |
11 |
“Call” |
“Callback” |
CALLBACK_OBJECT |
No |
ExCallbackObjectType |
12 |
“Sema” |
“Semaphore” |
KSEMAPHORE |
Yes |
ExSemaphoreObjectType |
13 |
“Time” |
“Timer” |
ETIMER |
No |
ExTimerObjectType |
14 |
“Prof” |
“Profile” |
KPROFILE |
No |
ExProfileObjectType |
15 |
“Wind” |
“WindowStation” |
|
Yes |
ExWindowStationObjectType |
16 |
“Desk” |
“Desktop” |
|
Yes |
ExDesktopObjectType |
17 |
“Sect” |
“Section” |
|
Yes |
MmSectionObjectType |
18 |
“Key” |
“Key” |
|
No |
CmpKeyObjectType |
19 |
“Port” |
“Port” |
|
Yes |
LpcPortObjectType |
20 |
“Wait” |
“WaitablePort” |
|
No |
LpcWaitablePortObjectType |
21 |
“Adap” |
“Adapter” |
ADAPTER_OBJECT |
Yes |
IoAdapterObjectType |
22 |
“Cont” |
“Controller” |
CONTROLLER_OBJECT |
No |
IoControllerObjectType |
23 |
“Devi” |
“Device” |
DEVICE_OBJECT |
Yes |
IoDeviceObjectType |
24 |
“Driv” |
“Driver” |
DRIVER_OBJECT |
Yes |
IoDriverObjectType |
25 |
“IoCo” |
“IoCompletion” |
IO_COMMPLETION |
No |
IoCompletionObjectType |
26 |
“File” |
“File” |
FILE_OBJECT |
Yes |
IoFileObjectType |
27 |
“WmiG” |
“WmiGuid” |
GUID |
No |
WmipGuidObjectType |
l “索引”列对应OBJECT_TYPE结构中的ObjectTypeIndex成员
l “标志”列是一个32位的标识符,它保存在OBJECT_TYPE结构中的ObjectTypeTag成员中。Windows 2000的标识符是由四个ANSI字符表示的二进值。在调试时,这些字符在16进制Dump中可很容易识别出来。通过检查ObjectTypeTag可以很容易的确定给定的对象是否是我们所期望的类型。在分配对象所占用的内存时,Windows 2000将ObjectTypeTag与0x80000000进行逻辑或运算后的值作为新内存块的标志。
l “名称”列给出了对象的名称,该列对应于类型对象的OBJECT_NAME结构。很明显类型标志是由对象名称的前四个字符构成的。如果对象名称不足四个,则用空格代替。
l “C结构”列是与对象类型相关的对象体的名字。这些C结构有的可以在DDK中找到,其他的可在w2k_def.h中找到。如果没有名字,那么意味着这个结构目前还是未知的。
l “符号”列给出了指向类型对象的指针的名字。如果对应的“是否公开”一列是“Yes”,则表示该指针变量是导出的,可以被内核模式驱动程序或应用程序访问(通过第六章给出的w2k_call.dll)。
“索引”列还需要进一步的解释。这里给出的索引值均来自对应的OBJECT_TYPE结构的ObjectTypeIndex成员。这里的索引值并不是预定义常量。这和Dispatcher和I/O对象使用的DISP_TYPE_*和IO_TYPE_*常量不同(参见表7-1和表7-2)。这里的索引值仅仅反映了系统创建这些类型对象的顺序。因此,你绝不应该使用ObjectTypeIndex来标识一个对象的类型。如果需要,则应该使用ObjectTypeTag,在以后OS中,ObjectTypeTag有很的稳定性。
对象句柄
尽管内核模式驱动程序可通过查询指向对象体的指针来联系该对象,但用户模式的程序却无法做到这一点。对于用户模式下的程序,当它调用相应的API打开一个对象时,它收到的是该对象的一个句柄,在随后的操作中只能使用该句柄来访问对象。尽管Windows 2000在很多地方都使用“句柄”这一术语,句柄的本质是一个进程相关的16位数字(它通常是四的倍数),用来作为索引访问进程的句柄表,内核为每个进程维护一个独立的句柄表。列表7-10中给出了HANDLE_TABLE结构。该表指向一个HANDLE_LAYER1结构,HANDLE_LAYER1结构由一个指向HANDLE_LAYER2结构的指针构成,而HANDLE_LAYER2结构又包含一个指向HANDLE_LAYER3结构。最后,在HANDLE_LAYER3结构中才包含指向实际的句柄表项的指针。句柄表项实际上就是一个HANDLE_ENTRY结构。
// HANDLE BIT-FIELDS
// -----------------
// 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
// F E D C B A 9 8 7 6 5 4 3 2 1 0 F E D C B A 9 8 7 6 5 4 3 2 1 0
// _________________________________________________________________
// |x|x|x|x|x|x|a|a|a|a|a|a|a|a|b|b|b|b|b|b|b|b|c|c|c|c|c|c|c|c|y|y|
// | not used | HANDLE_LAYER1 | HANDLE_LAYER2 | HANDLE_LAYER3 |tag|
#define HANDLE_LAYER_SIZE 0x00000100
// -----------------------------------------------------------------
#define HANDLE_ATTRIBUTE_INHERIT 0x00000002
#define HANDLE_ATTRIBUTE_MASK 0x00000007
#define HANDLE_OBJECT_MASK 0xFFFFFFF8
typedef struct _HANDLE_ENTRY // cf. OBJECT_HANDLE_INFORMATION
{
/*000*/ union
{
/*000*/ DWORD HandleAttributes;// HANDLE_ATTRIBUTE_MASK
/*000*/ POBJECT_HEADER ObjectHeader; // HANDLE_OBJECT_MASK
/*004*/ };
/*004*/ union
{
/*004*/ ACCESS_MASK GrantedAccess; // if used entry
/*004*/ DWORD NextEntry; // if free entry
/*008*/ };
/*008*/ }
HANDLE_ENTRY,
* PHANDLE_ENTRY,
**PPHANDLE_ENTRY;
#define HANDLE_ENTRY_ /
sizeof (HANDLE_ENTRY)
// -----------------------------------------------------------------
typedef struct _HANDLE_LAYER3
{
/*000*/ HANDLE_ENTRY Entries [HANDLE_LAYER_SIZE]; // bits 2 to 9
/*800*/ }
HANDLE_LAYER3,
* PHANDLE_LAYER3,
**PPHANDLE_LAYER3;
#define HANDLE_LAYER3_ /
sizeof (HANDLE_LAYER3)
// -----------------------------------------------------------------
typedef struct _HANDLE_LAYER2
{
/*000*/ PHANDLE_LAYER3 Layer3 [HANDLE_LAYER_SIZE]; // bits 10 to 17
/*400*/ }
HANDLE_LAYER2,
* PHANDLE_LAYER2,
**PPHANDLE_LAYER2;
#define HANDLE_LAYER2_ /
sizeof (HANDLE_LAYER2)
// -----------------------------------------------------------------
typedef struct _HANDLE_LAYER1
{
/*000*/ PHANDLE_LAYER2 Layer2 [HANDLE_LAYER_SIZE]; // bits 18 to 25
/*400*/ }
HANDLE_LAYER1,
* PHANDLE_LAYER1,
**PPHANDLE_LAYER1;
#define HANDLE_LAYER1_ /
sizeof (HANDLE_LAYER1)
// -----------------------------------------------------------------
typedef struct _HANDLE_TABLE
{
/*000*/ DWORD Reserved;
/*004*/ DWORD HandleCount;
/*008*/ PHANDLE_LAYER1 Layer1;
/* 00C */ struct _EPROCESS *Process; // passed to PsChargePoolQuota ()
/*010*/ HANDLE UniqueProcessId;
/*014*/ DWORD NextEntry;
/*018*/ DWORD TotalEntries;
/* 01C */ ERESOURCE HandleTableLock;
/*054*/ LIST_ENTRY HandleTableList;
/* 05C */ KEVENT Event;
/* 06C */ }
HANDLE_TABLE,
* PHANDLE_TABLE,
**PPHANDLE_TABLE;
#define HANDLE_TABLE_ /
sizeof (HANDLE_TABLE)
列表7-10. 句柄表、句柄表层和表项
这种三层寻址机制是一种非常巧妙的设计,它允许动态增加或减少句柄表项所需的存储空间,并且仅需很少的操作,而且其内存使用率也非常高。因为每个句柄表层最多可容纳256个指针,所以一个进程理论上可打开256*256*256(即16,777,216)个句柄。其中每个句柄表项占用8个字节,因此其最多占用128MB存储空间。不过,由于一个进程很少需要如此多的句柄,因此进程如果一开始就分配整个句柄表,那么将造成存储空间的浪费。Windows 2000使用的三层结构在开始时仅为每一层分配一个最小空间。如果不将HANDLE_TABLE自身计算在内的话,那么在开始时仅需256*4+256*4+256+89(即4,096)字节。可以看出最初的句柄结构正好可以放入一个物理内存页中(在32位平台上,一个物理页占用4KB字节)。
为了查找句柄的HANDLE_ENTRY,系统将句柄的32位值划分为3个8位分段, 0位、1位和高6位不使用。使用这三个8位分段,句柄解析机制以如下方式进行:
1. 句柄的18到25位作为HANDLE_LAYER1结构中的Layer2数组的索引,HANDLE_LAYER1结构由HANDLE_TABLE结构中的Layer1引用。
2. 句柄的10到17位作为第一步中所选择的HANDLE_LAYER2结构中的Layer3数组的索引。
3. 句柄的2到9位作为第二步中所选择的HANDLE_LAYER3结构中的Entries数组的索引。
4. 从上一步的Entries数组中取出的HANDLE_ENTRY结构包含一个指向OBJECT_HANDER(该对象头就是与给定Handle相关的对象头)的指针。
这听起来很容易让人糊涂,图7-2或许可以清楚地表示出到底发生了什么,图7-2和第四章的图4-3非常类似,图4-3描述了i386 CPU的线性地址与物理地址之间的转换机制。这两个算法都是将输入值分割为3个分段,其中的两个用于选择层次结构中的两个间接层,最后一个分段用于从目标层中选出一个具体表项。注意:层次化的句柄表模型是Windows 2000引入的,Windows NT 4.0仅提供了单层的句柄表。
图7-2. 从HANDLE到OBJECT_HANDER的解析过程
typedef struct _LIST_ENTRY
{
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;
补充:LIST_ENTRY结构
由于每个进程都拥有自己的句柄表,故内核必须以某种方式跟踪当前已分配的句柄表。因此,ntoskrnl.exe维护一个名为HandleTableListHead的LIST_ENTRY类型的变量,该变量指向由HANDLE_TABLE结构组成的双向链表的表头,内核使用HANDLE_TABLE结构中的HandleTableList将它们串联在一起。如果你使用Flink或Blink指针来访问HANDLE_TABLE时,你必须从指针地址中减去HandleTableList成员的偏移量0x54才能获取相应的HANDLE_TABLE的基地址。通过UniqueProcessId可很容易的确定拥有该句柄表的进程。通常情况下,链表中的第一个HANDLE_TABLE的所有者是系统进程(ID=8),其后的句柄表则是属于系统空闲进程(ID=0)的。系统空闲进程的句柄表可通过内部变量ObpKernelHandleTable来访问。
在访问句柄表时,系统将使用一对同步对象来保证数据的完整性(尤其是在多线程环境下)。全局的HandleTableListLock(由ntoskrnl.exe导出)将锁住整个句柄表链表,这里的HandleTableListLock实际上是一个ERESOURCE结构。这种类型的同步对象可作为一个排他锁也可用作一个共享锁。使用ExAcquireResourceExclusiveLite()函数将得到一个排他锁,而使用ExAcquireResourceSharedLite()函数得到的将是一个共享锁。对于这两种锁均可使用ExReleaseResourceLite()函数来释放获得的锁。在使用排他锁锁住句柄表链表后,在你释放锁之前,系统不会更改此链表的任何内容。链表中的每个HANDLE_TABLE可以拥有自己的ERESOURCE锁,HANDLE_TABLE结构中的HandleTableLock(参见列表7-10)提供了该锁。可使用ntoskrnl.exe提供的ExLockHandleTableExclusive()和ExLockHandleTableShared()来获取这个ERESOURCE锁,ExUnlockHandleTableShared()则用于释放这两种ERESOURCE锁。这几个函数只是ExAcquireResourceExclusiveLite()、ExAcquireResourceSharedLite()和ExReleaseResourceLite()函数的外包函数。
很不幸,所有这些由内核句柄管理器使用的基本函数和全局变量都没有文档化,而且无法访问,因为ntoskrnl.exe并没有导出它们。尽管根据对象句柄,可使用第六章提供的内核调用接口来找到它们对应的对象,但我不推荐这样做。原因之一是:这样的代码将无法在Windows NT 4.0下运行,因为Windows NT 4.0的句柄表与Windows 2000并不相同。另一个原因是:内核提供了一个“豪华”的函数来返回当前活动进程的句柄表的所有内容。这个函数就是NtQuerySystemInformation(),用来获取句柄信息的类别是:SystemHandleInformation(16)。请参考Schreiber(1999)或Nebbett(2000)的文章来了解如何调用此函数。SystemHandleInformation数据来自内部函数ExpGetHandleInformation(),这一函数又依赖于ObGetHandleInformation()函数。ObGetHandleInformation()函数将循环调用ExSnapShotHandleTables()函数,该函数将完成实际的枚举句柄表链表的工作。ExSnapShotHandleTables()函数需要一个Callback函数指针,它会针对对象涉及到的每个HANDLE_ENTRY调用该函数。ObGetHandleInformation()函数使用内部的Callback函数ObpCaptureHandleInformation()来填充调用者提供的缓冲区。这里用来填充的数据是一个结构体数组,该数组包含当前系统维护的每个句柄的相关信息。