系统会创建和处理几种类型的内核对象,比如事件对象、文件对象、互斥量对象等,每个内核对象都只是一个内存块,它由操作系统内核分配,并只能由操作系统内核访问。这个内存块是一个数据结构,其成员维护着与对象相关的信息。
应用程序如何操纵这些内核对象呢?Windows提供了一组函数可以访问这些内核对象。
调用一个会创建内核对象的函数后,函数会返回一个句柄,它标识了所创建的对象。为了增加操作系统的可靠性,这些句柄值是与进程相关的。所以将句柄值传给另一个进程中的线程时,可能会调用失败。
内核对象的所有者是操作系统内核,而非进程。当进程终止运行时,内核对象不一定会销毁。比如另一个进程正在使用我们的进程创建的内核对象。
操作系统内核知道我们当前有多少个进程正在使用一个特定的内核对象,因为每个对象都有一个使用计数,初次创建时为1,如果一旦对象使用计数变成0,操作系统内核就会销毁该对象。
内核对象可以用一个安全描述符来保护。安全描述符描述了谁拥有对象。
用于创建内核对象的所有函数几乎都有指向一个SECURITY_ATTRIBUTES结构的指针作为参数。如下所示:
HANDLE
WINAPI
CreateFileW(
_In_ LPCWSTR lpFileName,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwShareMode,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
_In_ DWORD dwCreationDisposition,
_In_ DWORD dwFlagsAndAttributes,
_In_opt_ HANDLE hTemplateFile
);
SECURITY_ATTRIBUTES结构如下所示:
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
一个进程初始化时,系统会为它分配一个句柄表,这个句柄表仅供内核对象使用。如下所示:
它只是一个由数据结构组成的数组。每个结构都包含指向一个内核对象的指针、一个访问掩码和一些标志。
索引 | 指向内核对象内存块的指针 | 访问掩码(包含标志位的一个DWORD) | 标志 |
1 | 0x????????? | 0x????????? | 0x????????? |
2 | 0x????????? | 0x????????? | 0x????????? |
... | ... | ... | ... |
当进程内的一个线程创建内核对象时,内核将为这个对象分配并初始化一个内存块。然后内核扫描进程的句柄表,查找一个空白的记录项。如上表所示,内核在索引1的位置找到空白记录项,并对其进行初始化。
下面的函数首先会检查主调进程的句柄表,验证当前的进程是否有权访问传入的句柄值。如果句柄是有效的,系统就会获得内核对象的数据结构的地址,并将结构中的“使用计数”成员递减。如果使用计数变为0,内核对象将被销毁,并从内存中去除。
BOOL CloseHandle(HANDLE hobject);
在很多时候,我们需要共享内核对象。比如:互斥量、信号量执行线程同步,有以下几种方法实现:
6.1使用对象句柄继承
只有在进程之间有一个父-子关系,才可以使用对象句柄继承。
当父进程创建一个内核对象时,父进程必须向系统指出希望这个对象的句柄是可以继承的。
注意:只有句柄是可以继承的,对象不能继承。
为了创建一个可继承的句柄,父进程必须分配并初始化SECURITY_ATTRIBUTES结构。示例:
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);
设置bInheritHandle 为TRUE,它会导致标志位被设为1。
索引 | 指向内核对象内存块的指针 | 访问掩码(包含标志位的一个DWORD) | 标志 |
1 | 0x????????? | 0x????????? | 0x????????? |
2 | 0xF000010 | 0x????????? | 1 |
... | ... | ... | ... |
凡是包含一个有效的“可继承的句柄”的项,都会被完整地复制到子进程的句柄表中,复制项的位置与它在父进程句柄表中的位置完全一样。系统还会递增内核对象的使用计数。
6.2为对象命名
很多函数都可以创建命名的内核对象,如下所示:这些函数的最后一个参数都是lpName。
HANDLE
WINAPI
CreateMutexW(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
_In_ BOOL bInitialOwner,
_In_opt_ LPCWSTR lpName
);
示例:进程A创建一个名称为mutex的互斥量
HANDLE hMutexA = CreateMutex(NULL, FALSE, TEXT("mutex"));
这个句柄并不是一个可继承的句柄。如果进程B执行以下代码:
HANDLE hMutexB = CreateMutex(NULL, FALSE, TEXT("mutex"));
这个时候,系统会去查找是否存在"mutex"的内核对象,由于A进程创建好了已存在,接着检查类型,都是互斥量对象,然后验证调用者是否拥有该对象的访问权限。肯定是可以访问的,系统接着会在进程B的句柄表中查找一个空白记录项,将其初始化为指向现有的内核对象。
进程B调用成功后,不会实际去创建一个互斥量对象。相反,会为进程B分配一个新的句柄值,它标识了内核中的一个现有的互斥量对象。系统还会递增内核对象的使用计数。
6.3复制对象句柄
WINBASEAPI
BOOL
WINAPI
DuplicateHandle(
_In_ HANDLE hSourceProcessHandle,
_In_ HANDLE hSourceHandle,
_In_ HANDLE hTargetProcessHandle,
_Outptr_ LPHANDLE lpTargetHandle,
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ DWORD dwOptions
);
这个函数获得一个进程的句柄表中一个记录项,然后再另一个进程的句柄表中创建这个记录项的一个副本。
示例:
HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);
HANDLE processT = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID);
HANDLE mutexT;
DuplicateHandle(GetCurrentProcess(), hMutex, processT, &mutexT, 0,
FALSE, DUPLICATE_SAME_ACCESS);