问题: 什么是内核对象?
答:内核对象实际上时由内核分配的一块内存,而且只能由内核来访问。它时一个数据结构,成员包含有关于该对象的信息。一些成员对于所有对象类型都是一样的,比如对象名称、安全描述、使用计数等,而大部分时针对某一特定对象类型的。比如进程对象ID、基本优先级和返回码。
内核对象
windows操作系统创建和管理着已下几种类型。
对象英文名称 | 对象中文名称 |
Event objects | 事件对象 |
File-mapping objects | 文件映射对象 |
File objects | 文件对象 |
Mailslot objects | 邮件槽对象 |
Mutex objects | 互斥对象 |
Pipe objects | 管道对象 |
Process objects | 进程对象 |
Semaphore objects | 信号量对象 |
Thread objects | 线程对象 |
问题:内核对象产生的途径是什么?
不同内核对象通过不同得到Win32API函数产生
- 计数
- 安全
内核对象能被一个安全描述保护着,这个安全描述描述了谁创建了该对象,谁能访问或使用该对象,谁对该对象的访问要被拒绝。
几乎所有创建核心对象的函数都有一个指向SECURITY_ATTRIBUTES结构的指针作为参数。一般而言都只是将这个参数设置为NULL,而这样创建的内核对象使用了缺省的安全属性。缺省的安全属性表明只有管理员和对象的创建者对它有完全的访问权限,其他都不能访问该对象。
typedef struct _SECURITY_ATTRIBUTES{ DWORD nLength; LPVOID lpSecurityDescriptor; BOOL bInheritHandle; }SECURITY_ATTRIBUTES;
结构中lpSecurityDescriptor是描述安全的
SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = pSD; sa.bInheritHandle = FALSE; HANDLE hFileMapping = CreateFileMapping((HANDLE) 0xffffffff,&sa,PAGE_READWRITE,0,1024,"MyFileMapping");
- 进程的内核对象句柄表
进程初始化时,系统为进程分配一张句柄表。这个句柄表只用于内核对象,而不用于用户或GDI对象。
进程首次初始化时,句柄表时空白的。如果进程一个线程调用了创建内核对象的函数,内核就为该对象分配一块内存,并初始化它;然后内核扫描进程的句柄表来寻找一个空白项,找到空白项后进行初始化。
所有创建内核对象的函数都返回一个进程相关的句柄,因为时进程相关的,所以能够在本进程中所有线程中使用。这个句柄值实际上时进程的句柄表中知名内核对象的信息存取之处的索引。
因为时进程相关的,句柄值时进程句柄表中的索引,所以在其他进程中不能正确使用。
创建内核对象的函数如果失败,返回的句柄值通常是0(NULL)。通常原因是系统的可用内存太少了,或者发生了安全性问题。不过有几个函数失败时返回的句柄时-1(INVALID_HANDLE_VALUE),比如CreateFile没能打开指定文件时就返回-1而不是0。在判断是否调用成功时要看情况比较:
HANDLE hMutex = CreateMutex(...); if(hMutex == INVALID_HANDLE_VALUE) { //下列代码将永远不会执行 //因为CreateMutex失败时返回的是NULL而不是INVALID_HANDLE_VALUE }
- 关闭内核对象
不管内核对象是如何创建的,都是通过调用CloseHandle来告诉操作系统已经完成了对对象的操作。BOOL CloseHandle(HANDLE hobj);
如果忘记了调用CloseHandle,可能会发生内存泄漏。不过当进程终结时,操作系统将释放所有资源。
进程间共享内核对象
需要共享内核对象的原因:
- 文件映像对象允许运行在同一计算机上的两个进程共享数据库块。
- 邮件槽和命名管道允许应用程序在联网的计算机的进程之间传送数据块。
- 互斥量、信号量和事件允许不同进程中的线程同步他们的行动。
共享内核对象的3种机制:
- 对象句柄继承
- 对象句柄命名
- 对象句柄复制
- 对象句柄继承
当进程存在父子关系时才能使用对象句柄继承。在这种情况下,父进程拥有一个或多个内核对象句柄,它决定派生出一个子进程,给子进程访问它的内核对象权限。要实现这种操作,父进程必须执行若干操作。
首先,父进程创建内核对象时,它必须告诉系统它想让该对象的句柄能够被继承。注意,内核对象的句柄是可继承的,内核对象本身却不是。
要创建一个可继承的句柄,父进程必须分配和初始化一个SECURITY_ATTRIBUTES结构,并把该结构的地址传递给特定的Create*函数。这个SECURITY_ATTRIBUTES结构的bInheritHandle需要为TRUE。
下面,用父进程派生出子进程。调用CreateProcess函数时参数BOOL bInheritHandles需要为TRUE,子进程这时候将继承父进程的可继承的句柄值。
创建子进程时,操作系统也会像创建任何新进程一样,给一张新的、空的进程的句柄表。由于CreateProcess的bInheritHandles参数为TRUE,系统将多做一件事:查看父进程的句柄表,将每条可继承的句柄表项拷贝到子进程的句柄表种。拷贝到子进程句柄表种的位置同父进程的句柄表一模一样。
除了拷贝句柄表项之外,系统还增加内核对象的使用计数。
***注意:对象句柄继承只发生在派生子进程的那一时刻,如果父进程要创建带可继承句柄的内核对象,这些新句柄将不能被已经运行着的子进程继承。
对象句柄继承有个特点:子进程并不知道已经继承了句柄。因此,需要通过几个方式将句柄值传给子进程。一般有3种方式:1.将句柄值作为命令行参数传给子进程。2.使用其他形式的进程间通信来从父进程向子进程传递继承的内核对象句柄值3.使用父进程的环境变量,因为子进程能继承父进程的环境变量表,可以容易地调用GetEnvironmentVariable函数来得到被继承的对象句柄值。
- 命名对象
第二种进程间共享内核对象的方法是给对象命名。虽然不是全部,但是很多内核对象能够被命名。
创建内核对象时,将参数lpszName赋值为以0结尾的字符串名字,而不是NULL。就实现了对内核对象命名。
***注意:在系统种,互斥量、事件、信号量、文件映射和等待记时器都共享一个名字空间。因此,要创建一个名为"JeffObj"对象,不能保证系统中没有名为"JeffObj"对象存在。
如果进程A使用CreateMutex创建了一个"JeffMutex"互斥对象后,当进程B调用CreateMutex(NULL,FALSE,"JeffMutex")后,系统首先检查是否已经存在一个名为"JeffMutex"的内核对象。因为存在名字叫这个名字的对象,内核会继续检查对象的类型。通过检查发现名为"JeffMutex"的对象也是一个互斥对象,系统就认为进程B对CreateMutex的调用成功了。也就是说,系统在进程B的句柄表中找到一个空表项,然后初始化该表项指向已经存在的内核对象。
***注意:进程A和进程B没有继承关系,虽然都是操作“JeffMutex"互斥对象,但是A和B进程中的句柄值很可能时不同的值,两个进程分别使用了自己的句柄值。
当然,除了Create*函数还可以用Open*函数来创建通过名字的共享对象。不过Create*在没有这个名字的内核对象时创建这个内核对象,而Open*在没有命名的对象时返回失败。
- 复制对象句柄
在进程之间共享内核对象句柄的最后一个技术是使用DuplicateHandle函数。
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle, HANDLE hSourceHandle,
HANDLE hTargetProcessHandle, LPHANDLE lpTargetHandle,
DWORD dwDesiredAccess, BOOL bInheritHandle,DWORD dwOptions);
这个函数取得某个进程句柄表中的一个表项,然后把它拷贝到另一个进程的句柄表中。
第1个和第3个参数,hSourceProcessHandle和hTargetProcessHandle是内核对象句柄。他们本身必须是相对于调用该函数的进程的,而且,着两个参数必须标识进程内核对象。如果传递其他类型的内核对象句柄,函数就会失败。第2个参数hSourceHandle是任意类型的内核对象的句柄。这个句柄值不是相对于调用该函数的进程,必须相对于句柄hSourceProcessHandle所标识的进程。第4个参数lpTargetHandle是一个HANDLE变量的地址,它将接受得到源句柄信息的拷贝的表项的索引值。返回的该句柄值是相对于由hTargetProcessHandle所标识的进程。