内核对象 Windows via C/C++
1. 内核对象概述内核对象是操作系统的基础,系统内部的内核对象有:令牌(token)、事件(event)、文件(file)、文件映射(file-mapping), I/O完成端口(I/O completion port), 作业(job), 邮件mailslot, mutex, pipe, process, semaphore, thread, waitable timer, thread pool worker factory等。内核对象有如下的特点:1. 它只能通过内核访问。用户程序只能通过句柄进行操作,而且句柄是对进程特定的(不能在进程间共享),但可以通过某种方式进行传递。2. 它使用引用计数。3. 它的创建函数都带有一安全参数,这是与GDI等对象的最重要区别。可以使用WinObj工具查看所有的内核对象类型。下面是创建文件映射对象的代码示例:
SECURITY_ATTRIBUTES sa;sa.nLength = sizeof(sa);
//用于版本化sa.lpSecurityDescriptor = pSD;
// 已初始化的SD的地址sa.bInheritHandle = FALSE;
// 不能在进程间共享HANDLE
hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, &sa, PAGE_READWRITE, 0,
1024, TEXT("MyFileMapping"));
如果你是操作一个存在的内核对象,一般情况下你都要在第一个参数值上传递你想要的操作。如:
HANDLE hFileMapping = OpenFileMapping(FILE_MAP_READ, FALSE, TEXT("MyFileMapping"));
2. 进程的内核对象表当一个进程创建时,系统为它分配一个句柄表,这个表只用于内核对象而不用于UI或GDI对象。这个表的结构等是未文档化的。它像下面这个样子:
Index 内在块指针 访问标识 标识
1 0x???????? 0x???????? 0x????????
2 0x???????? 0x???????? 0x????????...
开始时这张表是空的。当进程创建内核对象时就会在这张表中分配相应的内容。所有的创建内核对象都返回句柄(如CreateThread, CreateFile, CreateFileMapping, CreateSematics等)。这个句柄值除4后(右移2位,这两位是Microsoft内部使用)就是句柄表中真正的索引(当然这是未文档化的,有可能会改变)。所以第一个有效的句柄值是4,而创建失败一般返回的句柄值是0或-1(很少,如CreateFile,你应该与INVALID_HANDLER_VALUE比较)。进程调用CloseHandler关闭内核对象,当调用这个函数后,它检查传递的句柄是否有效(在句柄表中检查),如果有效就取得该对象的内存地址并减少其引用计数(如果计数为零就从内存中清除该对象)。否则该函数返回FALSE,如果进程处于调试中,该函数会引发0xC0000008异常)注意当调用CloseHandler后,该句柄不能再使用(无效的或是已经是其它类型的对象了)。
3. 跨进程共享内核对象一些跨进程共享内核对象的情况:
(1) 文件映射对象允许你在同机器上不同进程间共享数据块
(2) Mailslot和命名管道允许你在不同机器上的进程间通过网络传递数据块
(3) Mutex, Semaphore和事件等对象允许你在不同进程间进行同步共享方式有: 继承(父子进程间),命名对象以及复制对象句柄
(1). 继承方式: 通过安全描述符结构体的bInheritHandler值设置为true就可以让它在父子进程间继承。SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
// 使得返回的句柄可继承HANDLE
hMutex = CreateMutex(&sa, FALSE, NULL);
如果句柄是可继承的,它在进程的句柄表中的Flags值是0x00000001。当然要让子进程真正使用这个句柄,父进程还需要把它传递给子进程,方法是在调用CreateProcess时把bInheritHandles参数值设置为TRUE。这时系统会遍历父进程句柄表,把可继承的句柄复制到子进程的句柄表中。之后父进程的句柄与子进程的句柄就没什么关系了,也就是说父子进程都需要关闭句柄。这个过程也意味着如果父进程在创建子进程之后新建了其它的内核对象,这些对象不会被原子进程继承。
偶而时候你可能想改变内核对象的标识,如父进程只能让直接进程继承而不让孙子进程继承,这时可以调用SetHandleInformation函数,例如要打开和关闭对象的继承标识,分别调用如下代码:
SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, 0);
要查看一个内核对象句柄是否是可继承的,可用如下方式:
DWORD dwFlags;
GetHandleInformation(hObj, &dwFlags);
BOOL fHandleIsInheritable = (0 != (dwFlags & HANDLE_FLAG_INHERIT));
(2) 命名对象
在创建内核对象的所有函数中,其最后一个参数是一PCTSTR类型的字符串,它表示对象的名字。如果创建时这个参数是NULL,就创建了匿名对象,这就是上面的情况。如果是非NULL的字符串,就创建命名对象。这个名字是全局空间(整个机器内共享,而且所有对象都共享)。例如下面的代码
HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("JeffObj"));
HANDLE hSem = CreateSemaphore(NULL, 1, 1, TEXT("JeffObj"));
DWORD dwErrorCode = GetLastError();
它对hSem返回NULL,而GetLastError()返回6。
如果创建内核对象时传递的名字已经在系统中存在同类型对象的名字,它就会返回那个名字的对象。这样就可以进行进程间共享了。另一种方式是调用OpenXXX方法使用命名的内核对象。如果不存在该名字的对象或该名字的对象的类型不一样,它返回NULL(这两种情况,错误代码是不一样的)。
在创建命名对象时,名字的选取是很关键的,但Microsoft并没有这方面的指导。
命名对象有很多用途,其中之一是用于防止一个程序的多人实例运行,代码如下:
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) {
// 已经有一个实例在运行了
// 关闭对象并退出
CloseHandle(h);
return(0);
}
//这个程序的第一个实例在运行
...
//退出前关闭句柄
CloseHandle(h);
return(0);
}
注意,这种情况在有终端服务的情况下有所改变,运行终端服务的机器对于内核对象有多个命名空间:对所有客户会话的一个全局命名空间,这个命名空间一般用于服务。以及对于每个客户会话一个自己的命名空间。这样确保多个会话运行同样的多个程序而不会相互影响。远程桌面和快速用户切换等功能都使用终端服务的。
在Vista以前的系统中,在任何用户登录前,services开始在第一个会话,它是非交互的。而在Vista系统中,只要一个用户登录,程序都启动在一个新会话中(与Session0不同,它是服务进程的,这些核心组件一般都有高的优先级。所以编写服务程序如果要与用户程序进行内核对象共享,要注意这些问题,具体见文档)。
如果要知道你的进程运行在哪个终端服务会话中,可用ProcessIdToSessionId查看,如下代码:
DWORD processID = GetCurrentProcessId();
DWORD sessionID;
if (ProcessIdToSessionId(processID, &sessionID)) {
tprintf(
TEXT("进程 '%u' 运行在终端服务会话 '%u'"),
processID, sessionID);
} else {
// 如果没有足够的权限它会失败,但这种情况在这里不会出现,因为我们在自己的进程ID
tprintf(
TEXT("Unable to get Terminal Services session ID for process '%u'"),
processID);
}
当然,在终端服务中,程序也可通过在名字前加Global\\强制命名内核对象进入全局命名空间。要显式地表示对象在本地命名空间的,可在名字前加Local\\,如下代码:
HANDLE h = CreateEvent(NULL, FALSE, FALSE, TEXT("Global\\MyName"));
HANDLE h = CreateEvent(NULL, FALSE, FALSE, TEXT("Local\\MyName"));
上面的命名内核对象有一种问题:任何程序都可以创建一个命名对象,这样如果某个程序要实现单例运行而创建了一个内核对象,这种情况下另一程序也创建了同名的内核对象时,该单例程序就无法正常运行了。这是DoS攻击的一种在Vista中有一种机制使得人创建的命名内核对象永远不会和其它程序创建的对象冲突,要使用定制的前缀并把它作为人的私有命名空间,如Global和Local,服务进程会确保为内核对象定义一边界描述符来保护命名空间。下面是检查实例的代码:
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
(3) 复制内核对象最后一种跨进程边界共享内核对象的方法是用DuplicateHandle函数进行对象复制,简单地说,它就是一个进程的内核句柄表中找出一项,复制到另一个进程的内核句柄表。该复制有三个进程内核对象参与,下面的代码显示了这个过程
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
还有另一种使用复制方法的情况:设想你有一个可读写的文件映射对象,但你需要调用一个函数,而该函数只允许对该文件映射对象进行写的访问,这时你也可以使用这种机制。具体代码如下:
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)