每个内核对象都只是一个内存块,它又操作系统内核分配,并只能由操作系统内核访问。这个内存块是一个数据结构,其成员维护着与对象相关的信息。少数成员(安全描述符和使用计数)是所有对象都有的,但其他大多数成员都是不同类型的成员特有的。由于内核对象的数据结构只能由操作系统内核访问,所以应用程序不能再内存中定位这些数据结构并直接更改内容。
使用计数:操作系统知道当前有多少个进程正在使用一个特定的内核对象,是因为每个内核对象都包含一个使用计数。初次创建一个内核对象得到时候,其使用计数设为1。如果一旦对象的使用计数变为0,操作系统内核就会销毁该对象。
安全属性:内核对象可以用一个安全描述符(security descriptor, SD)来保护。用于创建内核对象的所有函数几乎都有一个SECURITY_ATTRIBUTES结构的指针作为参数,大多数应用程序都只是为这个参数传入NULL,这样创建的内核对象具有默认
的安全属性。
一个进程在初始化时,系统将为它分配一个句柄表。这个句柄表进攻内核对象使用,不适用于用户对象或GDI对象。一个进程首次初始化的时候,其句柄表为空。当进程内的一个线程调用一个会创建内核对象的函数时,内核将为这个对象分配并初始化一个内存块。然后内核扫描进程的句柄表,查找一个空白的记录项并对其进行初始化。用于创建内核对象的任何函数都会返回一个与进程相关的句柄,这个句柄可又同一个进程中运行的所有线程使用。系统用索引来表示内核对象的信息保存在句柄表中的具体位置,要得到实际的索引值,句柄值应该除以4。调用一个函数时,如果它接受一个内核对象句柄作为参数,就必须把Create* 函数返回的值传给它。在内部,这个函数会查找进程的句柄表,或的目标内核对象的地址,然后以一种恰当的方式操纵内核对象的数据结构。
无论以什么方式创建内核对象,我们都要调用CloseHandle向系统表明我们已经结束使用对象,在内部,函数首先检查主调进程的句柄表,验证“传给函数的句柄值”标示的是“进程确实有劝访问的一个对象”。如果句柄是有效的,系统就将获得内核对象的数据结构的地址,并将结构中的“使用计数”成员递减。如果使用计数变为0,内核对象将被销毁,并从内存中去除。就在CloseHandle返回之前,它会清除进程句柄表中对应的纪录项——这个句柄现在对我们的进程来说是无效的,不要再试图用它。无论内核对象当前是否销毁,这个清除过程都会发生!一旦调用CloseHandle我们的进程就不能访问那个内核对象;但是,如果对象的使用计数还没有递减至0,它就不会被销毁。这是完全正常的;它表明另外还有一个或多个进程在使用该对象。当其他进程(通过调用CloseHandle)全部停止使用这个对象后,对象就会被销毁。假如忘记调用CloseHandle ,系统会确保进程使用的所有资源都被释放。对于内核对象,系统执行的操作时:当进程终止时,系统自动扫描该进程的句柄表,如果这个表中有任何有效的记录项(即进程终止前没有关闭的对象),操作系统会为我们关闭这些对象句柄。只要这些对象中有一个的使用计数递减至0,内核就会销毁对象。这适用于所有内核对象,资源(包括GDI对象在内)以及内存块。进程终止运行时,系统会确保我们的进程不留下任何东西。
跨进程边界共享内核对象:
(1)使用对象句柄继承
只有在进程之间有一个父—子关系的时候,才可以使用对象句柄继承。
为创建一个可继承的句柄,父进程必须分配并初始化一个SECURITY_ATTRIBUTES结构,并将这个结构的地址传给具体的Create函数:
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE; //使返回的句柄成为可继承的
HANDLE hMutex = ::CreateMutex(&sa,FALSE,NULL);
句柄表中的每一个记录项都有一个指明句柄是否可以继承的标志位。如果在创建内核对象的时候将NULL作为PSECURITY_ATTRIBUTES的参数传入,则返回的句柄是不可继承的,这个标志位为0.将bInheritHandle成员设为TRUE,则导致这个
标志位被设为1.
为了使用对象句柄继承,下一步是由父进程生成子进程。这是通过CreateProcess函数来完成的
BOOL WINAPI CreateProcess( __in LPCTSTR lpApplicationName,
__in_out LPTSTR lpCommandLine,
__in LPSECURITY_ATTRIBUTES lpProcessAttributes,
__in LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in BOOL bInheritHandles,
__in DWORD dwCreationFlags,
__in LPVOID lpEnvironment,
__in LPCTSTR lpCurrentDirectory,
__in LPSTARTUPINFO lpStartupInfo,
__out LPPROCESS_INFORMATION lpProcessInformation);
bInheritHandles,通常情况下,我们将该参数设为FALSE。这个值向系统表明:我们不希望子进程继承父进程句柄表中的”可继承的句柄“。如果向这个参数传递TRUE,子进程就会继承父进程的”可继承的句柄“的值。操作系统会创建新的
子进程,但不允许子进程立即执行它的代码。系统会为子进程创建一个新的,空白的句柄表。系统会遍历父进程的句柄表,对它的每个记录项进行检查。凡是会包含一个有效的”可继承的句柄“的项,都会被完整的复制到子进程的句柄表。在子
进程的句柄表中,复制想的位置与它在父进程的位置完全一样。它意味着:在父进程和子进程中,对一个内核对象进行标示的句柄中是完全一样的。除了复制句柄表的记录项,系统还会递增内核对象的使用计数,因为两个进程都在使用这个对象
。对象句柄的继承只会在生成子进程的时候发生。加入父进程后来又创建了新的内核对象,并同样将他们的句柄设为可继承的句柄。那么正在运行的子进程是不会继承这些新句柄的。为了使子进程得到它想要的一个内核对象的句柄值,最常见的
方式是将句柄值作为命令行参数传给子进程。
(2)为对象命名:
许多内核对象都可以进行命名。在创建内核对象的Create函数中的 pszName 传入NULL,向系统表明我们要创建一个未命名的内核对象。若要创建一个命名的内核对象,可以传入一个”以0 为终止符的名称字符串“。所有对象都共享同一个
命名空间,即使它们的类型并不相同。通过为对象命名来实现共享式,是否可以继承并非一个必要条件。
例如A进程通过CreateMutex函数创建一个名为 "JeffMutex"内核对象时,进程B可以通过CreateMutex函数打开这个内核对象,但两个进程中的句柄值不一定相同。内核对象的使用计数会递增。
(3)复制对象句柄:
该方法通过使用 DuplicateHandle函数实现:
BOOL WINAPI DuplicateHandle( __in HANDLE hSourceProcessHandle,
__in HANDLE hSourceHandle,
__in HANDLE hTargetProcessHandle,
__out LPHANDLE lpTargetHandle,
__in DWORD dwDesiredAccess,
__in BOOL bInheritHandle,
__in DWORD dwOptions);
这个函数获得一个进程的句柄表中的一个记录项,然后再另一个进程的句柄表中创建这个记录项的一个副本。它的第一个参数和第三个参数是内核对象句柄。这两个句柄本身必须相对于调用DuplicateHandle函数的那个进程。此外,这两个
参数标示的必须是进程内核对象。第二个参数是指向任何类型的内核对象的句柄。但是,它的句柄值一定不能与调用DuplicateHandle 函数的那个进程相关,相反,该句柄必须与第一个参数句柄所标示的那个进程相关。函数会将源进程中的
句柄信息复制到第三个参数所标示进程的句柄表中。
通常在只涉及两个进程的时候,才会调用DuplicateHandle函数:
HANDLE hObjInProcessS = CreateMutex(...);
HANDLE hProcessT = OpenProcess(...);
HANDLE hObjInProcessT ;
DuplicateHandle( GetCurrentProcess(), hObjInProcessS, hProcessT , &hObjInProcessT , 0, FALSE, DUPLICATE_SAME_ACCESS);
CloseHandle(hProcessT);
CloseHandle(hObjInProcessS);
还可以在同一个进程中使用这个函数为现有对象创建一个新句柄,并未这个新句柄指定新的权限。只要在第三个参数传递 GetCurrentProcess(), 该函数会返回一个伪句柄,该句柄始终标示主调进程。