Windows内核对象(2) -- 内核对象跨进程访问
虽然内核对象位于独立于进程之外的内核区域,我们在开发中却只能通过调用Win32 API传入HANDLE参数来操作内核对象(如SetEvent
等)。然而HANDLE句柄
只对当前进程有效,离开了当前进程该句柄就无效了(具体原因参考:Windows内核对象(1) – 内核对象与句柄)。所以说,跨进程访问内核对象的关键在于我们怎么跨进程访问句柄HANDLE
?
下面介绍几种方法来实现跨进程共享内核对象。
一、使用句柄继承的方式
只有进程之间有父子关系时,才可以使用句柄继承的方式。在这种情况下,父进程可以生成一个子进程,并允许子进程访问父进程的内核对象。为了使这种继承生效,父进程必须执行几个步骤:
(1). 父进程在创建一个内核对象时,父进程必须向系统指定它希望这个内核对象的句柄是可以继承的。为了创建一个可继承的内核对象,必须分配并初始化一个SECURITY_ATTRIBUTES
结构,如:
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE; // 可继承的
sa.lpSecurityDescriptor = NULL;
HANDLE h = CreateEvent(&sa, TRUE, FALSE, NULL);
(2). 父进程通过CreateProcess生成子进程,且指定bInheritHandles
为TRUE,从而允许子进程来继承父进程的那些“可继承的句柄”。
// 启动子进程TestB.exe,将句柄h作为启动参数传给进程TestB
//
TCHAR cmd_buf[MAX_PATH];
StringCchPrintf(cmd_buf, MAX_PATH, TEXT("TestB.exe %ld"), (long)h);
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
BOOL ret = CreateProcess(
NULL,
cmd_buf,
NULL,
NULL,
TRUE, // 指定子进程可以继承父进程的“可继承句柄”
0,
NULL,
NULL,
&si,
&pi
);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
由于我们传给bInheritHandles
参数的值是TRUE
,所以系统在创建子进程时会多做一件事情:它会遍历父进程的句柄表,对它的每一项进行检查,凡是包含一个有效的“可继承的句柄”的项,都会将该项完整的复制到子进程的句柄表。在子进程的句柄表中,复制项的位置与它在父进程句柄表中的位置完全一样(包含索引),这个就意味着:在父进程和子进程中,对一个内核对象进行标识的句柄值也是完全一样的。所以我们只需要通过某种方式(如上面示例中的启动参数的方式,或者环境变量的方式等任何进程间通讯的方式)将这个值告诉子进程,子进程就可以将该值转成HANDLE
,然后使用这个HANDLE
来调用系统API。
二、使用DuplicateHandle方式
明白DuplicateHandle的工作原理,需要先了解进程句柄表,可以参考Windows内核对象(1) – 内核对象与句柄)
2.1 DuplicateHandle功能
DuplicateHandle函数可以将指定“源进程的句柄表”中的某一项复制到“目的进程句柄表”中(除了索引),并且返回该项在目的进程句柄表中的索引(即HADNLE)。
可以在任何时候调用DuplicateHandle函数,DuplicateHandle对源句柄是否是可继承的没有要求。
函数声明如下:
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
LPHANDLE lpTargetHandle,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions
);
DuplicateHandle详细介绍可以参考MSDN:https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx
2.2 支持的句柄类型
DuplicateHandle函数不能复制所有类型的句柄,只能复制如下类型的句柄(从MSDN复制而来):
Object | Description |
---|---|
Access token | The handle is returned by the CreateRestrictedToken, DuplicateToken, DuplicateTokenEx, OpenProcessToken, or OpenThreadToken function. |
Change notification | The handle is returned by the FindFirstChangeNotification function. |
Communications device | The handle is returned by the CreateFile function. |
Console input | The handle is returned by the CreateFile function when CONIN$ is specified, or by the GetStdHandle function when STD_INPUT_HANDLE is specified. Console handles can be duplicated for use only in the same process. |
Console screen buffer | The handle is returned by the CreateFile function when CONOUT$ is specified, or by the GetStdHandle function when STD_OUTPUT_HANDLE is specified. Console handles can be duplicated for use only in the same process. |
Desktop | The handle is returned by the GetThreadDesktop function. |
Event | The handle is returned by the CreateEvent or OpenEvent function. |
File | The handle is returned by the CreateFile function. |
File mapping | The handle is returned by the CreateFileMapping function. |
Job | The handle is returned by the CreateJobObject function. |
Mailslot | The handle is returned by the CreateMailslot function. |
Mutex | The handle is returned by the CreateMutex or OpenMutex function. |
Pipe | A named pipe handle is returned by the CreateNamedPipe or CreateFile function. An anonymous pipe handle is returned by the CreatePipe function. |
Process | The handle is returned by the CreateProcess, GetCurrentProcess, or OpenProcess function. |
Registry key | The handle is returned by the RegCreateKey, RegCreateKeyEx, RegOpenKey, or RegOpenKeyEx function. Note that registry key handles returned by the RegConnectRegistry function cannot be used in a call to DuplicateHandle. |
Semaphore | The handle is returned by the CreateSemaphore or OpenSemaphore function. |
Thread | The handle is returned by the CreateProcess, CreateThread, CreateRemoteThread, or GetCurrentThread function |
Timer | The handle is returned by the CreateWaitableTimer or OpenWaitableTimer function. |
Transaction | The handle is returned by the CreateTransaction function. |
Window station | The handle is returned by the GetProcessWindowStation function. |
不同的事件类型对应的dwDesiredAccess
参数不同,具体参考MSDN。
2.3 使用示例
进程TestA源码
int main(int argc, char** argv) {
HANDLE h = CreateEvent(NULL, TRUE, FALSE, NULL);
// 启动子进程TestB.exe
//
TCHAR cmd_buf[MAX_PATH];
StringCchPrintf(cmd_buf, MAX_PATH, TEXT("D:\\TestB.exe"), (long)h);
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
BOOL ret = CreateProcess(NULL, cmd_buf, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
assert(ret);
assert(pi.hProcess);
HANDLE duplicated_h = NULL;
ret = DuplicateHandle(GetCurrentProcess(), h, pi.hProcess, &duplicated_h, 0, FALSE, DUPLICATE_SAME_ACCESS);
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
bool has_signal = WaitForSingleObject(h, 0) == WAIT_OBJECT_0;
assert(has_signal == true);
return 0;
}
子进程TestB源码
int main(int argc, char** argv)
{
long l = 0;
printf("Input Handle:");
scanf("%ld", &l);
HANDLE h = (HANDLE)l;
bool has_signal = WaitForSingleObject(h, 0) == WAIT_OBJECT_0;
assert(has_signal == false);
SetEvent(h);
return 0;
}
在父进程TestA中创建一个不可继承的事件 -> 然后启动子进程TestB -> 调用DuplicateHandle复制句柄项到TestB进程句柄表 -> 并向TestB输入句柄值 -> TestB访问该事件句柄,将事件置为有信号状态。
三、使用命名的内核对象的方式
3.1 实现原理
这种方式严格的说已经不是文章开头说到的跨进程访问句柄了,有点类似跨进程直接访问内核对象了。
该方式实现起来比较简单,就是在调用创建内核对象的Create***
函数时,通过pszName
参数为内核对象取一个名字。
如创建事件Event的函数CreateEvent
:
HANDLE WINAPI CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCTSTR lpName // 指定名称
);
HANDLE h = CreateEvent(NULL, TRUE, FALSE, TEXT("TestA_Obj"));
若在其他进程中要访问这个内核对象,只需要使用打开函数Open***
打开该内核对象,系统就会在进程的句柄表中插入一条记录,并返回这条记录的索引,也就是句柄。需要注意的是,在打开内核对象时需要留意返回值
和GetLastError
函数的返回值。由于内核对象是有访问权限的,有时候虽然这个名字的内核对象存在,但该进程却不见得有权限可以打开它,这个时候GetLastError
函数会返回失败的原因。
以打开事件的函数OpenEvent
为例:
HANDLE h = OpenEvent(READ_CONTROL, FALSE, TEXT("TestA_Obj"));
if (h == NULL) {
if (GetLastError() == ERROR_ACCESS_DENIED) { // 没有READ_CONTROL权限
}
}
3.2 全局命令空间
不同的会话(Session)有不同的内核对象命名空间(如windows服务程序位于Session 0
,而普通的用户进程位于Session 1
),要通过名称访问其他会话中的内核对象,需要在名称前面加上Session\<当前会话ID>
。Windows提供了一个全局的内核对象命名空间,处于任何会话中的进程都可以访问该命名空间,将内核对象放入全局命令空间的方式很简单:只需要在内核对象名称前加入Global\
即可。
如:
HANDLE h = CreateEvent(NULL, TRUE, FALSE, TEXT("Global\\TestA_Obj"));
上一篇: 怎么发面做包子?教你做简单又美味的包子
下一篇: 用c#实现简易的计算器功能实例代码