内核对象是什么?
首先,内核对象是什么?引用《Windows核心编程》原文:
“每个内核对象都只是一个内存块,它由操作系统内核(Ring0)分配,并只能由操作系统内核访问。”
内核对象存在于进程虚拟地址空间的高地址(32位:0x80000000-0xFFFFFFFF),它由三个部分组成:对象头(_OBJECT_HEADER)、对象体(_EPROCESS/_ETHREAD)和附加信息。
(书中提到的引用计数和安全描述符存在于对象头中)
由于内核对象只能由操作系统内核访问,应用程序(Ring3)不可直接访问,因此,操作系统提供了一组API函数用于对内核对象的操作。内核对象被创建时,函数返回一个句柄,用于标识所创建的对象,这些API函数可通过句柄对对象进行访问。句柄值和进程是相关的(每个进程各自维护一个句柄表),所以,依靠进程间通信传递句柄值并使用,便可能会发生错误(在不同进程同一句柄值可能标识不同的内核对象)。
多进程共享内核对象
多进程共享内核对象有三种方式:内核对象句柄继承、命名对象和复制对象句柄。
父子进程句柄继承
内核对象句柄继承的关键在于两个进程必须具有父子关系,即一个进程生成另外一个进程。子进程成功继承并能使用继承的内核对象需满足两点条件:一是父进程需设置该内核对象为可继承对象;二是父进程需将句柄值通过某种进程间通信的方式(命令行参数等)传递给子进程。
需要注意的是:
1.父进程句柄表中“可继承句柄”会复制到子进程句柄表相应位置,也就是说它们的句柄值是相同的。这也是父进程和子进程共享内核对象的前提。
2.内核对象句柄的继承只发生在子进程生成时,之后父进程再生成的“可继承对象”不再继承给子进程。
3.子进程需要通过某种方式获得句柄值,否则继承无效(无法对内核对象进行操作)。
测试代码:
创建一个可继承的事件内核对象,在创建子进程的时候选择继承,并将事件对象的句柄值以命令行参数(argv)传入子进程。子进程得到句柄值,并调用SetEvent函数。事件授信,父进程弹框提示成功。
代码关键点:
1.允许内核对象继承
2.通过某种方式传给子进程句柄值
//父进程代码
#include<tchar.h>
#include<windows.h>
VOID _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
TCHAR BufferData[20] = { 0 };
STARTUPINFO StartupInfo = { 0 };
StartupInfo.cb = sizeof(STARTUPINFO);
PROCESS_INFORMATION ProcessInfo = { 0 };
//创建安全描述符结构体,并设置句柄可继承(第三参数)
SECURITY_ATTRIBUTES SecurityAttributes{ sizeof(SECURITY_ATTRIBUTES),NULL,TRUE };
//创建一个可继承的事件内核对象
HANDLE EventHandle = CreateEvent(&SecurityAttributes, TRUE, FALSE, NULL);
if (EventHandle == NULL)
{
OutputDebugString(_T("事件创建失败\r\n"));
goto Exit;
}
else
{
_tprintf(_T("EventHandle:%d\r\n"), EventHandle);
//将句柄值格式化成字符串
_stprintf_s(BufferData, _T("%d"), EventHandle);
_tprintf(_T("BufferData:%s\r\n"), BufferData);
//创建子进程
BOOL IsOk = CreateProcess(_T("SubProcess.exe"),
(LPTSTR)BufferData, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &StartupInfo, &ProcessInfo);
if (IsOk == FALSE)
{
OutputDebugString(_T("创建子进程失败\r\n"));
}
//解除父进程对子进程资源的占有 //为何?
if (ProcessInfo.hProcess != NULL || ProcessInfo.hThread != NULL)
{
CloseHandle(ProcessInfo.hProcess);
CloseHandle(ProcessInfo.hThread);
}
//等待事件授信
WaitForSingleObject(EventHandle, INFINITE);
//子进程设置这个事件则授信,并弹出MessageBox
MessageBox(NULL, _T("子进程触发事件,继承成功"), _T("Success"), 1);
_tprintf(_T("Input AnyKey To Exit\r\n"));
_gettchar();
}
Exit:
if (EventHandle != NULL)
{
CloseHandle(EventHandle);
}
}
//子进程代码
#include<tchar.h>
#include<windows.h>
VOID _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
HANDLE EventHandle = 0;
//命令行参数是单字的,将字符串格式化为句柄值(数字)
_stscanf_s((TCHAR*)argv[0], _T("%d"), &EventHandle);
_tprintf(_T("EventHandle:%d\r\n"), EventHandle);
__try
{
if (EventHandle != NULL)
{
SetEvent(EventHandle);
}
//停顿5s,查看一下传进来的句柄值
Sleep(5000);
if (EventHandle != NULL)
{
CloseHandle(EventHandle);
}
ExitProcess(0);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
int LastError = GetExceptionCode();
}
}
多进程共享内核对象——命名对象
测试代码:
进程1创建命名事件对象,进程2使用相同名称创建事件对象,并调用SetEvent函数。事件授信,进程1弹窗提示成功。
//进程1
#include<windows.h>
#include<tchar.h>
VOID _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
HANDLE EventHandle = CreateEvent(NULL, TRUE, FALSE, _T("666"));
if (EventHandle == NULL)
{
OutputDebugString(_T("事件创建失败\r\n"));
goto Exit;
}
WaitForSingleObject(EventHandle, INFINITE);
MessageBox(NULL, _T("子进程触发事件"), _T("Success"), 1);
_tprintf(_T("Input AnyKey To Exit."));
_gettchar();
Exit:
if (EventHandle != NULL)
{
CloseHandle(EventHandle);
}
}
//进程2
#include<windows.h>
#include<tchar.h>
VOID _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
HANDLE EventHandle = CreateEvent(NULL, TRUE, FALSE, _T("666"));
if (EventHandle != NULL)
{
SetEvent(EventHandle);
}
//停顿5S,查看一下传进来的句柄值
Sleep(5000);
if (EventHandle != NULL)
{
CloseHandle(EventHandle);
}
ExitProcess(0);
}
补充
一、句柄表
一个进程初始化时会生成一个专供内核对象使用的句柄表,句柄表本质是一个结构体数组。结构体(记录项)包含指向内核对象头(_OBJECT_HEADER)的指针、访问掩码和句柄的属性(继承属性和禁止关闭)等。
创建内核对象时进程会为其在句柄表中分配一个记录项,函数返回内核对象的句柄(句柄表索引的四倍)。