内核对象可以供系统和应用程序使用和管理各种各样的资源。内核对象只是内核分配的一个内存块,在该内存块中是一种数据结构,它的成员用于维护该内核对象的各种信息,个内核对象的数据结构不完全相同,并且只有内核才能直接访问该数据结构。 windows提供了一组函数,用于对这种数据结构进行操作。当我们调用函数创建内核对象时,该函数会返回一个标识该对象的句柄。通过将该句柄值传递给各个函数,即能够操作该内核对象。内核对象的句柄值与进程密切相关,只有通过特定的方法,才能让另一进程对你的内核对象进行访问。
内核对象通过引用计数来判断内核对象是否应该被撤销。当创建内核对象时,引用计数被置1;当另一个进程访问该内核对象时,引用计数加1;当进程结束时,该进程仍然打开的所有内核对象的引用计数都减1;当调用CloseHandle函数时,引用计数会减1,并将句柄表中该内核对象的索引清除;如果内核对象的引用计数为0,那么这个内核对象将被从内存中撤销。
内核对象能够得到安全描述符的保护。安全描述符用于描述谁创建了该内核对象,谁能够访问或使用该内核对象,以及谁无权访问该内核对象。在创建内核对象时,几乎没个函数都有一个参数为指向SECURITY_ATTRIBUTES结构的指针。如果将该参数设为NULL,那么该内核对象带有默认的安全性,即该对象的管理小组的任何成员以及该对象的创建者拥有访问该对象的全部访问权,其他所有人无访问权。该结构的形式如下:
typedef struct _SECURITY_ATTRIBUTES{
dword nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
}SECURITY_ATTRIBUTES;
- nLength:该结构体的大小;
- lpSecurityDescriptor:用于设定安全性;
- bInheritHandle:用于设定继承性,如果将该值设为TRUE,那么该内核对象就拥有继承性;反之亦然
当一个进程被初始化时,会分配一个空的句柄表。该句柄表只用于记录该进程打开的内核对象。其结构如下:
索 引 | 内核对象内存块 的指针 | 访问屏蔽 (标志位的DWORD) |
标识位 (标志位DWORD)
|
1 | 0X???????? | 0X???????? | 0X???????? |
2 | 0X???????? | 0X???????? | 0X???????? |
... | .... | ... | ... |
当进程被创建时,该句柄表为空;当进程的线程创建内核对象时,内核会给内核对象分配一块内存,并对该内存进行初始化。并且创建内核对象的进程会扫描句柄表,找到一个空的索引项,并将该索引项的指针成员设为指向内核对象内存块的地址,并对访问屏蔽和标志位进行设置。创建内核对象的函数会返回一个句柄值。该句柄值为指向该内核对象在句柄表中的位置,可能为索引,也可能为该对象在句柄表中的字节数,这要看具体的操作系统。
当关闭内核对象时,需要调用CloseHandle函数来结束对该内核对象的操作:
BOOL CloseHandle(HANDLE hobj);
如果传递给CloseHandle函数的句柄有效,那么通过句柄找到该内核对象的句柄表,并得到内核对象的内存地址,通过访问内存地址,对引用计数进行减1,如果这是引用计数为0,那么就会将内核对象从内存中撤销,并且不管是否引用计数为0,都会将该内核对象从进程中的句柄表中清除,那么该进程不再拥有对该内核对象的访问权。
如果忘记了调用CloseHandle函数,可能会造成内存泄露。但,当进程结束时,进程会将扫描句柄表,对那些还没有从句柄表中清除的内核对象进行引用计数减1,如果引用计数为0了,那么就会撤销该内核对象。当进程结束时,会对所有资源进行清除。
进程间共享内核对象
在父子进程之间,才能使用内核对象的继承性来共享内核对象。1)首先,在创建内核对象时,需要指定SECURITY_ATTRIBUTES结构体的 bInheritHandle为TRUE。这样这个内核对象的句柄就具有的继承性,但,内核对象是没有继承性的,并且创建的内核对象在句柄表中的标志位会被置1。 2)其次,要通过调用CreateProcess让 父进程 生成子进程,并且将参数heritHandle设为TRUE。这样子进程就继承了父进程的课继承的内核句柄值。子进程在创建时,也会分配一个空的句柄表,并且会遍历父进程的句柄表,找到可继承的内核句柄项目,并将它们复制到子句柄表的对应位置,注意位置必须相同。并且内核计数会加1。
但是需要注意的是,对象句柄的继承性只有在创建子进程时有用。也就是说,如果在子进程创建了以后,如果这时父进程又创建了一个可继承的内核对象,那么子进程是无法继承这个内核对象的。
子进程在句柄表里添加了内核对象索引后,就需要获得句柄值以便访问内核对象。子进程从父进程获取内核对象句柄值最常用的方法是:通过CreateProcess函数的命令行参数将句柄值从父进程传递给子进程。还有其他方法,不介绍。
可以通过使用SetHandleInformation函数来对句柄的标识进行设置,可以使用GetHandleInformation函数来获取句柄的标识。
进程间共享内核对象的第二种方法是:通过命名对象。某些内核对象在创建时,是可以给该对象命名的,该名字以“/0”结尾,最大长度了MAX_PATH(定义为260)。系统中的所用对象都共享一个命名空间,因此需要保证名字的唯一性。可以通过Create*函数来实现。当创建内核对象时命名,首先会在系统中检测这个名字是否存在,不存在的话,就会创建该对象,并返回句柄值;如果该名字存在,就会检查该名字的内核对象的类型是否与需要创建的对象的类型是否相同,然后会判断调用者是否拥有对该内核对象的访问权;如果有,那么就会在进程的句柄表中找到一个空项目添加已经存在内核对象的索引。注意,相同内核对象在不同进程中句柄值可能不同。如果类型不同或不能访问,那么就会创建失败。
也可以在另一个进程中通过Open*函数使用名字来打开一个已经存在的内核对象。Open*函数和Create*函数的唯一区别是:Create*函数会在系统没有改名字的内核对象时,创建一个内核对象;而Open*函数没有时,会运行失败。
进程将共享内核对象的第三种方法是:使用该函数DuplicateHandle。