在Windows操作系统中我们常常接触的有三种对象类型:
- Windows内核对象 (事件对象,文件对象,进程对象,I/O完成端口对象,互斥量对象,线程对象等等):由执行体(Excutive)对象管理器(Object Manager)管理,内核对象结构体保存在系统内存空间(0x80000000-0xFFFFFFFF),句柄值与进程相关。
- Windows GDI对象 (画笔对象,画刷对象等):由Windows子系统管理,句柄值在系统,会话范围 (system-wide / session-wide) 有效。
- Windows USER对象 (窗口对象,菜单对象等) :由Windows子系统管理,句柄值在系统,会话范围 (system-wide / session-wide) 有效。
内核对象是Windows操作系统内核分配和访问的对象,每个内核对象对应于一个内存块,该内存块只能由内核分配,访问和释放。该内存块是一个数据结构,维护着与对象相关的信息。少数成员是所有内核对象所共有的,如:安全描述符和使用计数,其他多数成员则是每种内核对象所特有的。
内核对象有很多种,如:访问令牌,事件,文件,文件映射,作业,互斥体,管道,进程,线程,信号量,可等待计时器以及线程池工厂等。可使用一些工具来观察,如:WinObj,Process Explorer等。
内核对象在应用程序中只能通过Windows的一组函数Create*/Open*/CloseHandle以及相应的内核对象句柄(*Handle)来访问。
使用计数
内核对象的所有这是内核,而非进程,而且进程操作内核对象的唯一手段就是用句柄作为参数来调用相应的Windows API(Create*/Open*/CloseHandle)。
Windows操作系统可由此来实现对内核对象的使用计数,就是说同一个内核对象,Windows操作系统知道有多少进程在引用它。只有当引用计数变为0的时候,内核对象才被销毁,因此,一个内核对象的生命周期有可能比创建它的进程生命周期要长。
内核对象安全性
内核对象可以用一个安全描述符(security descriptor,SD)来保护。它描述了谁拥有对象,哪些用户和用户被允许访问或使用此对象;哪些组和用户被拒绝访问此对象。通常在编写服务器应用程序的时候使用。
用于创建内核对象的函数几乎都有指向一个SECURITY_ATTRIBUTES 结构的指针作为参数,如下面的CreateFileMapping :
HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName);
大多数应用程序只是为这个参数传入NULL,这样创建的内核对象具有默认的安全性——具体包括哪些默认的安全性要取决于当前进程的安全令牌(security token)。
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES;
除了内核对象,进程还可能使用一些其他对象,如菜单,窗口,鼠标,笔刷和字体,这些属于用户对象或GDI对象,而非内核对象。一个简单的方法来判断是否是内核对象,就看其Create*方法有没有指定安全信息的参数。
何为内核对象句柄
内核对象句柄(32位Windows进程中句柄为32位,64位进程中,句柄也变成64位)是一个不透明值,其具体意义也可能与Windows不同版本的具体实现相关,不过能够确定的是:
- 句柄是进程相关的,一个进程的句柄传给其他进程是没有意义的;
- 句柄可以在同一进程中的所有线程使用;
- 句柄包含了进程句柄表的索引值,所以通过句柄就能找到对应的内核对象所在的地址
很多人对句柄到底是什么东西很疑惑。有人说是指针有人说是索引。其实句柄仅仅是独立于每个进程的句柄表的一个索引。在每个进程中都存在一个句柄表,列出了所有本进程内可以使用的句柄。它只是一个由特定数据结构组成的数组,每个结构都包含一个指向内核对象的指针、访问掩码、继承标识等,而句柄仅仅是句柄表数组的下标。
由于每个进程都存在句柄表,因此句柄是独立于进程的,虽然将一个进程的句柄传给另一个进程不一定会失败,但是它引用的是另一个进程完全不同的内核对象。
内核对象句柄表
一个进程在初始化时,系统将为它分配一个句柄表。这个句柄表仅供内核对象使用,不适用于用户对象和GDI对象,句柄表的结构大致包括:
1:索引
2:指向内核对象内存块的指针
3:访问掩码
4:标志
创建一个内核对象
一 个进程首次初始化时,其句柄表为空。当进程的某个线程调用了一个会创建内核对象的函数时,内核将为这个对象分配内存,并扫描进程句柄表,找到一个空白的记 录项,并对其进行初始化。指针成员被设置成内核对象的数据结构的内部内存地址,访问掩码被设置成拥有完全访问权限,标志也会被设置,根据句柄的继承性。由于句柄值实际是作为进程句柄表的索引来使用的,所以这些句柄是与当前所使用的进程相关的,无法供其它进程使用。如果我们真的在其它进程使用它,那么实际引用的知识那个进程的句柄表中位于同一个索引的内核对象——只是索引值相同而已,我们根本不知道它会指向什么对象。
调用函数 来创建一个内核对象时,如果调用失败,那么返回的句柄值通常为0(NULL),有几个函数调用失败时返回的是-1,即INVALID_HANDLE_VALUE;所以在检查函数返回值时务必相当仔细。
关闭内核对象
我们通过调用函数:
BOOL CloseHandle(HANDLE hobject);
来表明结束使用内核对象,该函数将清除进程句柄表中对应的记录项,并且递减该内核对象的“使用计数” ,如果该内核对象的"使用计数"减为“0”,那么操作系统销毁该内核对象,即将对象从内存中清除,但是“使用计数”大于“0”,该内核对象将继续保留,因为还有其它进程拥有该内核对象的访问权限。
通常,在创建一个内核对象时,我们会将相应句柄保存到一个变量中。将此变量作为参数调用CloseHandle()函数后,最好能同时将这个变量设为NULL。
在不需要再使用对象时,释放相应对象句柄是一个好习惯,如果句柄没有及时释放,可能引起内存泄露。
当进程终止运行,操作系统会确保此进程所使用的所有资源都被释放。进程终止时,系统扫描进程句柄表,操作系统将关闭所有句柄表中的有效记录项。只要这些对象中有一个的“使用计数”减为0,内核就会销毁对象。
进程终止时,系统也将保证进程拥有的所有资源(GDI对象等)以及内存块都将得到正确清除。