Saturday, February 05, 2005

 

Some notes on Handle

1.
发信人: soycola (可乐◆难得相逢在人间), 信区: MSDN
标 题: 对象、id和handle
发信站: BBS 水木清华站 (Fri Jun 25 23:56:37 2004), 站内

这两天读源代码的笔记,稍微整理了一下,和往常一样,欢迎拍转。

不管是进程、线程还是event、mutex,从oo的观点看都是对象。每个对象都隶属于一个或者几个管理机构,这些管理机构负责对象的吃喝拉撒,包括:创建、销毁、按名存取以及其它和对象类型相关的特定任务等。例如,KTHREAD/KPROCESS等K字头的对象是kernel维护的,EPROCESS/PTHREAD等是由executive维护的。

客户程序可以用两种协议操纵关心的对象,其一是委托给管理该对象的机构按照客户的请求处理该对象,或者对象的管理机构可以干脆将该对象出租给客户让客户自己去折腾。在nt的应用程序看来,所有的内核对象都是由执行体保管的,例如当调用SetEvent触发一个事件的时候,客户程序并不知道内存中和该event对应的什么字段被从0修改成1(也许是相反),这就是上述第一种(托管型的)管理协议;而在nt的驱动程序看来,event、mutex、file等对象都是可见的,驱动程序可以向nt的对象管理器出示一个凭证(下面要讲的handle),就可以从对象管理器获得这个对象的内存地址,有了内存地址当然驱动程序就可以为所欲为了,当然驱动程序使用完对象之后必须把它归还给对象管理器(这个操作编程中称为Dereference),否则就会出现内存泄漏。

显然os在把这些对象创建出来的时候都得取个名字,这样别的代码才好使用它们。

名字提供了一种寻址协议,任何一个操纵这些对象的客户程序都必须提供这样一个名字,这样负责管理这些对象的机构才能在自己的部门内部查找到该对象。所以名字首要的任务是识别对象,也就是说得有唯一性,名字和实际的对象之间必须一一对应,一一对应至少应当做到在任何一个时刻系统的同一个部门内部不能有两个对象具有同样的名字。例如进程、线程、event等同属于句柄表这个部门,那么在任何时候进程的句柄表中不能有两个event具有同样的handle,也不能有一个thread和一个event具有相同的handle。(可惜这部分API不公开,我们还得用UUID自己写着玩)

在计算机里,对象有个天然的名字就是实现该对象的c++ object或者struct的内存地址,用这个作为名字最大的好处是速度快,客户拿到这个名字不用经过任何管理机构的介入就可以获得对该对象的完全的操纵权,前提是客户程序了解该对象的内存布局。实际上nt的kernel管理的一些简单对象(KEVENT/ KMUTEX)就是采用这种命名协议,当然实际上客户程序还是调用kernel的帮助函数来操纵这些对象的。这种名字不需要解析,所以单从这个角度看,维护这种对象几乎不需要开销,管理机构不需要类似花名册这样的东西,创建这种对象也非常的简单,直接声明一个结构体变量即可,可以在数据段,也可以在堆栈上,当然栈上的对象一般会在使用上受某些限制,栈上创建的KEVENT在等待的时候只能用kmode。但是,这并不是说这些对象就是完全孤立的,那样的话对象上也不能发挥什么
作用,任何一个对象都是处于和其它对象的关系中,系统运行的每个操作都是由若干个对象通力合作完成的。例如,为了支持抢占式线程调度,所有kthread对象都必须宣誓加入一些反动组织,比如就绪列表、等待列表等,这些反动组织的头目就是kernel中的线程调度程序。

用对象的地址作为对象的名字虽然方便,但是却在对象和其客户之间建立了过强的耦合,至少客户程序在编译期间必须包含定义了该对象内存布局的头文件,因为客户程序可以直接访问对象的内部成员,所以在对象的内存布局改变的时候,客户程序必须重新编译才能正常运行,这样就限制了这种策略只能用在对象比较简单、稳定的场合;另外运行期间让恶意或者有bug的客户程序拿到关键的对象指针也是一个安全隐患。(按:又想欺负C,C++等兄弟,哼)

为了减小对象和其客户程序之间的耦合关系,需要增强对象的封装,想办法将对象的内部结构隐藏起来。在语言这个层次上,有个比较简单的办法是在公用的头文件里头只声明对象的结构体,但不声明该结构体的内部结构,客户程序只能得到结构体的指针,由于不了解对象内部的结构,所以客户程序就只能依赖若干定义好的方式来访问对象了。

下面是ntddk.h里的一些例子:
//
// Define types that are not exported.
//

typedef struct _ACCESS_STATE *PACCESS_STATE;
typedef struct _CALLBACK_OBJECT *PCALLBACK_OBJECT;
typedef struct _EPROCESS *PEPROCESS;
typedef struct _ETHREAD *PETHREAD;
typedef struct _IO_TIMER *PIO_TIMER;
typedef struct _KINTERRUPT *PKINTERRUPT;
typedef struct _KTHREAD *PKTHREAD, *PRKTHREAD;
typedef struct _OBJECT_TYPE *POBJECT_TYPE;

在语言层次上,另外一个同样目的的方法是把对象的内存地址变换一下提交给客户,最简单的变换方法,比如加上、减去或者xor上一个已知值。可以达到向对象隐藏实际的内存地址的目的。当客户程序需要访问对象的时候,它向对象的管理机构提交这个变换后的指针,管理机构可以根据变化算法求逆得到原先的对象指针,然后就可以使用该对象了。windows
98管理进程和线程对象的时候,全程采用这种命名方法,用户态拿到的process id和thread id,虽然貌似非法指针(通常都是0xff...),但都可以通过简单的办法(和kernel32.dll的模块句柄xor)得到真实的PDB和TDB地址。(按:这个相关文章不少)

有的时候,对象本身比较复杂,常常包含了若干个子对象,出现这种现象的一个比较常见的原因是对象是在多个模块中实现的,每个模块负责对象一部分的行为,加起来就构成了一个完整的对象。例如thread对象,在kernel部分有KTHREAD子对象,负责实现线程调度相关的功能,在executive部分有ETHREAD子对象,负责实现和io、mm(内存管理)、lpc(本地进程通讯)、cm(配置管理)、fs(文件系统)、sm(安全引用监视)等模块
交互的功能,从nt4.0开始,win32子系统(win32k.sys)中相应的有W32THREAD对象,负责实现线程的user、gdi部分的行为,最后用户模式还有TEB对象,为像opengl、ole、winsock等一干乌合之众提供支持。所以当我们笼统的说‘线程’的时候,其实并不是一个内存地址,而是一堆内存地址,也就是说是很含糊的,这种含糊对计算机程序来说显然是不可接受的,所以必须发明一种新的命名机制来明确的指定线程,这个名字就代表了那一堆内存指针,并且到现在为止,可以很合理的提出一个新的需求:这个名字还要能在进程、线程之间交换。这个新的需求显然排除了将内存作为名字的可能性,因为每个win32进程都有独立的内存空间,一个进程内的地址到另外一个进程内变的毫无意义。这样一来就产生了id的概念,id就是实现了上面两个需求的对象命名机制。进程id、线程id都是标准的id,可以在进程间交互而不影响其含义,窗口句柄也具有这样的特点,所以也应当作为id,而不是句柄。

采用id的命名机制需要比刚才那个xor算法更复杂的维护机制,id的生成算法、id到对象指针的互相转换算法都有一些很好玩的地方,不过都不是很难理解。id的生成通常都保证id不但在固定的时刻不会重复,即系统任何时候没有两个thread具有同样的id,也保证了在相距不长的一个时间段内不会重名。对于hwnd,id的分配算法在源代码刚流出来的时候flier
分析过,比较好玩儿。从id到对象指针的转换必须得借助于某种查找表,这种设计任务是个八仙过海、各显神通的活儿。结果,在nt的设计中,我们看到pid、tid到peprocess、pethread的转换实际上和handle到对象的转换算法共享了设计思路,虽然代码并不完全一样。

参考代码:
PsLookupProcessThreadByCid
ObReferenceObjectByHandle

如果对象的名字仅仅是用来找到对应的对象,那么上面的策略完全够用了,但有些时候名字并不仅仅是寻址的作用,还实现了其它功能,这种复杂的情况要求名字本身就必须作为一个对象实现了,句柄(handle)就是这样一种特殊的对象。不过在nt的设计里,handle对象只在寻址功能外边附加了一个功能:权限管理。即一个handle不仅可以用来找到某个对象,还限制了拥有这个handle的客户程序能在该对象上执行的操作集合。所以为什么我认为hwnd不应当是handle,而是id,因为它只具有寻址的功能而没有其它附加任务。handle是per进程的,不能跨进程使用,就像人民币不能直接拿到美国去花一样,不过有个帮助函数DuplicateHandle可以将handle在进程之间兑换。

写不动了,先到这儿吧,好累啊....
--
一个热爱生活的人,应该经常修改昵称和签名档


※ 来源:·BBS 水木清华站 smth.org·[FROM: 61.49.151.*]

(按:比较八卦一点的读书笔记。也当是休息一下)

2.
By Flier
From http://www.blogcn.com/user8/flier_lu/blog/1191692.html

HWND 句柄分配算法浅析

在使用Win32 API进行程序开发的时候,句柄的概念处处可见。但是此句柄非彼句柄,各种不同的句柄实现方法很容易让人搞混。下面将源代码级粗略的分析一下HWND这种典型句柄的分配及使用方法,让我们对此类句柄有一个更透彻的了解。

HWND一般是通过User32.dll的CreateWindowEx函数建立窗口时返回的。此函数依照如下的调用路径完成实际功能。

ntos\w32\ntuser\client
CreateWindowEx (cltxt.h:38)
_CreateWindowEx (ntstub.c:1197)
ntos\w32\ntuser\kernel
NtUserCreateWindowEx (ntstub.c:8087)
xxxCreateWindowEx (createw.c:33)
HMAllocObject (handtabl.c:828)

xxxCreateWindowEx函数中调用HMAllocObject(handtabl.c:828)完成实际的句柄的分配

以下为引用:

/*
* Allocate memory for regular windows.
*/
pwnd = HMAllocObject(
ptiCurrent, pdesk, TYPE_WINDOW, sizeof(WND) + pcls->cbwndExtra);

handtabl.c中的函数维护着一张全局唯一的对象句柄表,HWND/HMENU等等类型的句柄都在此保存,只需在HMAllocObject函数调用时指明句柄类型和结构大小即可自动完成分配。支持的句柄类型列表可以在ntos\w32\ntuser\client\nt6\user.h:899找到,如下

以下为引用:

/*
* Object types
*
* NOTE: Changing this table means changing hard-coded arrays that depend
* on the index number (in security.c and in debug.c)
*/
#define TYPE_FREE 0 // must be zero!
#define TYPE_WINDOW 1 // in order of use for C code lookups
#define TYPE_MENU 2
#define TYPE_CURSOR 3
#define TYPE_SETWINDOWPOS 4
#define TYPE_HOOK 5
#define TYPE_CLIPDATA 6 // clipboard data
#define TYPE_CALLPROC 7
#define TYPE_ACCELTABLE 8
#define TYPE_DDEACCESS 9
#define TYPE_DDECONV 10
#define TYPE_DDEXACT 11 // DDE transaction tracking info.
#define TYPE_MONITOR 12
#define TYPE_KBDLAYOUT 13 // Keyboard Layout handle (HKL) object.
#define TYPE_KBDFILE 14 // Keyboard Layout file object.
#define TYPE_WINEVENTHOOK 15 // WinEvent hook (EVENTHOOK)
#define TYPE_TIMER 16
#define TYPE_INPUTCONTEXT 17 // Input Context info structure

#define TYPE_CTYPES 18 // Count of TYPEs; Must be LAST + 1

#define TYPE_GENERIC 255 // used for generic handle validation

HMAllocObject函数的功能主要分为以下几部分:

1.根据句柄类型不同,验证其创建标志有效性
2.尝试搜索一个空闲的可用句柄槽
3.当没有空闲句柄槽时扩展句柄表
4.根据句柄类型分配内存并填充句柄表槽
5.更新统计数据 (Debug版)

对我们来说重要的是第2-4部分。其中搜索句柄表可用槽的算法如下

以下为引用:

/*
* Find the next free handle
* Window handles must be even; hence we try first to use odd handles
* for all other objects.
* Old comment:
* Some wow apps, like WinProj, require even Window handles so we''ll
* accomodate them; build a list of the odd handles so they won''t get lost
* 10/13/97: WinProj never fixed this; even the 32 bit version has the problem.
*/
fEven = (bType == TYPE_WINDOW);
piheFreeHead = NULL;
do {
php = gpHandlePages;
for (i = 0; i < gcHandlePages; ++i, ++php) {
if (fEven) {
if (php->iheFreeEven != 0) {
piheFreeHead = &php->iheFreeEven;
break;
}
} else {
if (php->iheFreeOdd != 0) {
piheFreeHead = &php->iheFreeOdd;
break;
}
}
} /* for */
/*
* If we couldn''t find an odd handle, then search for an even one
*/
fEven = ((piheFreeHead == NULL) && !fEven);
} while (fEven);

gcHandlePages是全局句柄表的索引表项数,gpHandlePages全局句柄表的索引表。

以下为引用:

typedef struct _HANDLEPAGE {
ULONG_PTR iheLimit; /* first handle index past the end of the page */
ULONG_PTR iheFreeEven; /* first even free handle in the page -- window objects */
ULONG_PTR iheFreeOdd; /* first even odd handle in the page */
} HANDLEPAGE, *PHANDLEPAGE;

每个索引表项定义了一个句柄表的页,以及页内可用句柄的末端。因为历史原因,需要尽量保证HWND类型句柄为偶数。因此索引表里将最后可用的奇数和偶数句柄单独列出,并根据请求分配句柄类型不同,选择不同的优先搜索算法。如果实在无法保证HWND的偶数性才尝试分配其它奇数句柄。
当前句柄表如果完全用完,则调用HMGrowHandleTable (handtabl.c:704)函数扩展句柄表,并从新分配的句柄表页中分配句柄。实现算法如下:

以下为引用:

/*
* If there are no free handles we can use, grow the table
*/
if (piheFreeHead == NULL) {
HMGrowHandleTable();
/*
* If the table didn''t grow, get out.
*/
if (i == gcHandlePages) {
RIPMSG0(RIP_WARNING, "HMAllocObject: could not grow handle space");
return NULL;
}
/*
* Because the handle page table may have moved,
* recalc the page entry pointer.
*/
php = &gpHandlePages[i];
piheFreeHead = (bType == TYPE_WINDOW ? &php->iheFreeEven : &php->iheFreeOdd);
if (*piheFreeHead == 0) {
UserAssert(gpsi->cHandleEntries == (HMINDEXBITS + 1));
RIPMSG0(RIP_WARNING, "HMAllocObject: handle table is full");
return NULL;
}
}

在填充完句柄表项之后,将使用HMHandleFromIndex把一个索引转换成最终返回给用户的句柄。

以下为引用:

/*
* Change HMINDEXBITS for bits that make up table index in handle
* Change HMUNIQSHIFT for count of bits to shift uniqueness left.
* Change HMUNIQBITS for bits that make up uniqueness.
*
* Currently 64K handles can be created, w/16 bits of uniqueness.
*/
#define HMINDEXBITS 0x0000FFFF // bits where index is stored
#define HMUNIQSHIFT 16 // bits to shift uniqueness
#define HMUNIQBITS 0xFFFF // valid uniqueness bits

#define HMHandleFromIndex(i) ((HANDLE)(i | (gSharedInfo.aheList[i].wUniq << HMUNIQSHIFT)))
#define HMIndexFromHandle(h) (((ULONG_PTR)h) & HMINDEXBITS)
#define _HMPheFromObject(p) (&gSharedInfo.aheList[HMIndexFromHandle((((PHEAD)p)->h))])
#define _HMObjectFromHandle(h) ((PVOID)(gSharedInfo.aheList[HMIndexFromHandle(h)].phead))
#define HMUniqFromHandle(h) ((WORD)((((ULONG_PTR)h) >> HMUNIQSHIFT) & HMUNIQBITS))
#define HMObjectType(p) (HMPheFromObject(p)->bType)
#define HMObjectFlags(p) (gahti[HMObjectType(p)].bObjectCreateFlags)

#define HMIsMarkDestroy(p) (HMPheFromObject(p)->bFlags & HANDLEF_DESTROY)

从这段宏定义我们可以发现,返回给最终用户的句柄,实际上是由两部分组成的。低16bit是实际的句柄表索引;高16bit是一个唯一编号wUniq。这个句柄表项的唯一编号wUniq在函数HMInitHandleEntries (handtabl.c:529)初始化句柄表时会被初始化为1,而每次调用函数HMFreeObject (handtabl.c:1069)释放一个句柄时,此编号会被加一。这样一来,自动累加的每句柄表项使用计数器,加上全局唯一的对象句柄表索引,和起来就是一个在相当长时间内不会重复的32bit句柄。而在使用的时候也只需要使用HMIndexFromHandle宏轻松即可获得实际索引。MS相当完美的解决了索引重用的问题,无论是在时间还是空间上 :D



<< Home

This page is powered by Blogger. Isn't yours?