当系统提供给我们很多 API 调用的时候,事实上我们是请求系统为我们做很多事情,比如很多由系统进行维护的数据结构,我们是无法进行操纵的,必须通过系统提供的API进行操纵,这也就是所谓内核对象的由来。

内核对象是什么

内核对象,事实上也是内存中的内存块而已,只不过:这些对象内存区域只能由系统进行修改(由内核所拥有),因此才叫它做内核对象。

内核对象通过引用计数来表示其是否还有用,如果某个内核对象的引用计数为0,那么内核就会销毁这个对象。当一个内核对象刚刚创建的时候,其引用计数为 1,其他的进程引用此对象的时候,他的引用计数就会加 1。

安全性

有一个数据结构叫做 SECURITY_ATTRIBUTES,我们在创建内核对象的 API 的时候经常会用到,我们会在这个数据结构中指明我们要创建的内核对象安全性设置。

typedef struct _SECURITY_ATTRIBUTES
{
DWORD nLength,
LPVOID lpSecurityDescriptor;
BOOL bInherttHandle;
} SECURITY_ATTRIBUTES;

多数情况下,我们会将需要这个参数的地方设置为 NULL,应用默认的设置。

Handle

每个进程都一个 Handle 表,这个表用来干什么呢。用来存储对于进程内核对象的引用,其结构是 索引,指针 形式的一个数组。

因此,Handle 是进程相关的。其只是一个进程对象表中的索引而已。

内核对象的继承

只有在进程间有父子关系的时候,内核对象才能被继承。事实上,这里继承的意思是指:对于父子进程对同一个 Handle 其指向的内核对象是一致的。具体的实现是:父进程在创建子进程的时候,会根据父进程中的 Handle 表复制一份到子进程去,当然,只有那些我们创建的时候指定了 bInherttHandle 为 TRUE 的内核对象才会被复制过去;同时,这个操作还会增加内核对象的引用计数。

进程的内核对象句柄表

当一个进程初始化的时候,系统会为其分配一个 Handle 表。这个 表 只为内核对象而使用,不是为了 用户对象或者 GDI 对象而存在的。这个表很简单

Index 指向内核对象的指针 访问掩码 标志
1 0x???????? 0x???????? 0x????????
2 0x???????? 0x???????? 0x????????

内核对象建立

当进程初始化的时候,其句柄表是空的。当进程中的某个线程调用了建立内核对象的函数是,内核会为此对象分配内存,然后初始化此对象。接着,内核会扫描进程的句柄表,找到一个空的项,比如找到了索引为 1 的位置,然后就进行填充这一项。

因此,句柄其实是线程相对的,只去当前进程中的所有线程间可用。句柄的值一般来说应该会被 4 整除。

在进程间共享内核对象

内核对象句柄是进程相关的,因此想要共享内核对象比较困难。有三种方式可以进行句柄的共享。

使用对象句柄继承

对象句柄的继承只在进程间有父子关系的时候适用。在这种场景下,父进程的句柄表内有多个记录,接着父进程准备 spawn 一个子进程,给予子进程访问这些内核对象的权限。父进程需要进行几步工作。

父进程在建立内核对象的时候,其必须为其建立的内核对象指定可继承属性。

SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE; // Make the returned handle inheritable.

HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);
Index Pointer to Kernel Object**Memory Block** Access Mask (DWORD of Flag Bits) Flags
1 0xF0000000 0x???????? 0x00000000
2 0x00000000 (N/A) (N/A)
3 0xF0000010 0x???????? 0x00000001

看起来父进程的句柄表是这个样子,句柄3是可以继承的。

建立进程的时候进行句柄的继承指定。

BOOL CreateProcess(
PCTSTR pszApplicationName,
PTSTR pszCommandLine,
PSECURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles,
DWORD dwCreationFlags,
PVOID pvEnvironment,
PCTSTR pszCurrentDirectory,
LPSTARTUPINFO pStartupInfo,
PPROCESS_INFORMATION pProcessInformation);

也就是将 bInheritHandles 指定为 TRUE。

在建立子进程的时候,子进程不会马上执行。首先,系统会子进程建立一个空的句柄表;接着,系统会遍历父进程的句柄表,将可继承的句柄复制到子进程的句柄表内。注意会将可继承的句柄复制到子进程的句柄表内的相同位置;然后,会增加句柄引用的内核对象的引用计数加1。

但是,对象句柄的继承有一个非常奇怪的特性:子进程并不知道其已经继承了一些句柄。对象句柄的继承只在子进程期望其从父进程获取一个内核对象的访问权限时有用。因此呢,我们需要一种方法来告知子进程其已经继承了句柄。

最常用的方式就是在子进程的命令行参数中进行传递句柄值;还有一种方式就是父进程将句柄放在一个环境变量内,同时环境变量的名字应该是子进程已知的。

改变句柄的标志

有的时候父进程建立了一个可进程的内核对象的,但是其只想要其某些子进程进行继承,为了改变一个句柄的继承标志,我们需要使用 SetHandleInformation 函数:

BOOL SetHandleInformation(
HANDLE hObject,
DWORD dwMask,
DWORD dwFlags);

dwMask 用来告诉函数需要改变句柄的哪个标志:

#define HANDLE_FLAG_INHERIT            0x00000001
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002

dwFlags 用来表示改变成什么值。

SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, 0);

命名对象

第二种方法是为内核对象进行命名。很多——不是全部——内核对象可以被命名。

HANDLE CreateMutex(
PSECURITY_ATTRIBUTES psa,
BOOL bInitialOwner,
PCTSTR pszName);

HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
BOOL bInitialState,
PCTSTR pszName);

HANDLE CreateSemaphore(
PSECURITY_ATTRIBUTES psa,
LONG lInitialCount,
LONG lMaximumCount,
PCTSTR pszName);

HANDLE CreateWaitableTimer(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
PCTSTR pszName);

HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName);

HANDLE CreateJobObject(
PSECURITY_ATTRIBUTES psa,
PCTSTR pszName);

当最后一个参数 pszName 不是 NULL 的时候,我们只能通过句柄继承或者 DuplicateHandle 来进行共享。

但是需要注意, pszName 传递的名称如果在内核内已经存在了,就会出错。

我们来看看如何进行共享,首先我们在进程 A 中建立一个内核对象:

HANDLE hMutexProcessA = CreateMutex(NULL, FALSE, TEXT("JeffMutex"));

然后我们的进程 B 也建立一个命名对象:

HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, TEXT("JeffMutex"));

这时候,系统会首先找一下是不是有一个叫 “JeffMutex” 的内核对象,然后再检查内核对象的类型,接着再检查进程 B 是不是拥有内核的访问权限。如果检查都通过,系统会在进程B 的句柄表内新增一个项,然后用找到的数据进行填充。这样,进程B 也就用有了对内核对象的引用了。

还有一个可选的途径是调用 Open 函数,而不是调用 Create 函数:

HANDLE OpenMutex(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenEvent(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenSemaphore(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenWaitableTimer(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenFileMapping(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenJobObject(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

因为微软并没有提供如何建立一个唯一的对象名称,所以呢,如果我们试图建立相同的名称的内核对象就会出错。推荐使用 GUID 来进行表示。

经常,命名对象用来禁志一个应用的多个实例存在。

int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine,
int nCmdShow) {
HANDLE h = CreateMutex(NULL, FALSE,
TEXT("{FA531CC1-0497-11d3-A180-00105A276C3E}"));
if (GetLastError() == ERROR_ALREADY_EXISTS) {
// There is already an instance of this application running.
// Close the object and immediately return.
CloseHandle(h);
return(0);
}

// This is the first instance of this application running.
...
// Before exiting, close the object.
CloseHandle(h);
return(0);
}

句柄复制

最后一种方式就是使用 DuplicateHande函数:

BOOL DuplicateHandle(
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
PHANDLE phTargetHandle,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions);

这个函数将一个进程句柄表内的项目,复制到另外一个进程的句柄表。最常见的情况是,DuplicateHandel 会涉及到三个运行中的进程。

  • hSourceProcessHandle 源进程内核对象句柄
  • hSourceHandle 源进程内的句柄
  • hTargetProcessHandle 目的进程内核对象句柄
  • phTargetHandle 目的进程中的句柄。