Windows核心编程【3】小结

第三章 内核对象


一、何为内核对象

1、作为Windows软件开发人员,我们经常都要创建、打开和处理内核对象。系统会创建和处理几种类型的内核对象,比如访问令牌(access token)对象、事件对象、文件对象、文件映射对象、I/O完成端口对象、作业对象、mailslot对象、mutex对象、pipe对象、进程对象、semaphore对象、线程对象、waitable timer对象以及thread pool worker factory对象等等。可以用WinObj查看所有内核对象类型列表。


2、每个内核对象都只是一个内存块,它由内核分配,并只能由内核访问。这个内核块是一个数据结构,其成员维护着与对象相关的信息。少数成员(安全描述符、使用计数器等等)是所欲对象都有的,但其他大多数成员都是不同的对象类型特有的。


3、内核对象的数据结构只能由内核访问,应用程序不能在内存中定位这些数据结构并直接更改其内容,只能通过Windows提供的一组函数来访问这些内核对象。


4、内核对象的所有者是内核,而非进程。每个对象都包含一个使用计数器(usage count),另一个进程获得对现有内核对象的访问后,内核计数器会递增,反之,一个停用(closehandle)内核对象,则会递减,直至内核对象计数变为0,则内核就会销毁该对象。即使进程忘记停用对象引用,在进程终止时,操作系统也会递减该进程引用到的所有内核对象(见下面句柄表)。


5、内核可以用一个安全描述符(SD)来保护,其描述了谁(通常是它的创建者)拥有对象;哪些用户和用户运行访问或使用此对象;以及哪些组和用户拒绝访问此对象。SD通常在编写服务器应用程序的时候使用。


6、用于创建内核对象的所有函数几乎都有指向一个SECURITY_ATTRIBUTES结构的指针作为参数。大多数应用程序只是为这个参数传入NULL,这样创建的内核对象具有默认的安全性,具体包括哪些默认的安全性,要取决于当前进程的安全令牌(security token)。但是,也可以分配一个SECURITY_ATTRIBUTES结构,初始化它,再将它的地址传给这个参数。


7、SECURITY_ATTRIBUTES结构(http://msdn.microsoft.com/en-us/library/windows/desktop/aa379560(v=vs.85).aspx),要先初始化长度(版本不同),该结构实际上只包含一个和安全性有关的成员。

Syntax

typedef struct _SECURITY_ATTRIBUTES {
  DWORD  nLength;
  LPVOID lpSecurityDescriptor;
  BOOL   bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;


8、应该习惯用最小安全性来创建内核对象,不要多于的权限,可以防止Windows版本更换权限审核变化。


9、除了使用内核对象,应用程序可能还要使用其他类型的对象,比如菜单、窗口、鼠标光标、画刷和字体。这些属于User对象或GDI(Graphical Device Interface)对象,而非内核对象。判断一个对象是不是内核对象,最简单的方式是查看这个对象的函数,几乎所有的创建内核对象的函数都有一个允许你指定安全属性信息信息的参数。


二、进程内核对象句柄表

1、一个进程在初始化时,系统将为它分配一个句柄表(handle table)。这个句柄表仅供内核对象使用,不适用于User或GDI对象。


2、句柄表细节尚无文档可以参考,随时会变,但是大概内容应该不会有太大变动的。


3、一个进程的句柄表,是一个由数据结构组成的数组。每个数据结构都包含指向一个内核对象的指针、一个访问掩码(access mask)和一些标志。



4、一个进程首次初始化的时候,其句柄表为空。当进程内的一个线程调用一个会创建内核对象的函数时,内核将为这个对象分配并初始化一个内存块。然后内核扫描进程的句柄表,查找一个空白的记录项(empty entry)。具体点说,指针成员会被设置成内核对象的数据结构的内部内存地址,访问掩码将被设置成拥有完全访问权限,标志也会设置。


5、创建内核对象的任何函数都会返回一个相对于进程的句柄,这个句柄可由同一个进程中运行的所有线程使用。句柄值实际应该除以4(或右移两位,以忽略Windows操作系统内部使用的最后两位),从而得到在进程句柄表中的真正索引(内核对象的信息将保存在这个位置处)。


6、如果传入一个无效的句柄,函数就会失败。错误码为ERROR_INVALID_HANDLE,由于句柄值实际是作为进程句柄表的索引来使用的,所以这些句柄是相对于当前这个进程的,无法供其他进程使用。


7、调用函数来创建一个内核对象时,如果调用失败,那么返回值的句柄值通常为0(NULL),这就是为什么第一个有效的句柄值为4的原因。之所以失败,可能是由于系统内存不足,或者遇到了一个安全问题。不过,有几个函数会范围-1,既是INVALID_HANDLE_VALUE。


8、无论以什么方式创建内核对象,都要调用CloseHandle向系统指出你已经结束使用对象。就在该函数返回之前,它会清除进程句柄表中的记录项。严格来说,应该在CloseHandle后,将保存句柄的变量设置为NULL,防止意外调用。(句柄表空白位置可能后来又填充了新的内核对象)


9、忘记调用CloseHandle,在进程运行期间,进程可能发生资源泄漏的情况。但是,当进程终止运行,操作系统会确保此进程所有资源都被释放。这适用于所有的内核对象、资源(包括GDI对象在内)以及内存块。


10、Process Explorer工具使用。(书上链接失效,给个Sysinternals的工具集官方链接:http://technet.microsoft.com/zh-cn/sysinternals/bb842062)


三、跨进程边界共享内核对象

1、很多时候,不同进程中运行的线程需要共享内核对象。下面罗列了一些实现。

a、利用文件映射对象,可以在同一台机器上运行的两个不同进程之间共享数据块。


b、借助mailslots和named pipes,在网络中的不同计算机上运行的进程可以相互发送数据块。


c、mutexes、semaphores和事件运行不同进程中的线程同步执行。例如,一个应用程序可能需要在完成某个人物之后,向另一个应用程序发出通知。


2、使用对象句柄继承来运行进程共享内核对象

a、只有进程之间有一个父子关系的时候,才可以使用对象句柄继承。在这种情况下,父进程有一个或者多个内核对象句柄可以使用,而且父进程决定生成一个子进程,并允许子进程访问父进程的内核对象。


b、当父进程创建一个内核对象时,父进程必须向系统指出它希望这个对象的句柄是可以继承的。(注意,继承的只是对象句柄,对象本身是不能继承的)既是SD的hInheritHandle属性要设置为TRUE。


c、在进程的句柄表记录项中保存的标志(句柄表中的每个记录项都有一个指明句柄是否可以继承的标志位),如果创建内核对象的时候将NULL传入,则返回的句柄是不可继承的,这个标志位为0.将bInheritHandle成员设为TRUE,则导致这个标志位被设为1.


d、系统会遍历父进程的句柄表,对它的每一个记录项进行检查。凡是包含一个有效的“可继承的句柄”的项,都会被完整地拷贝到子进程的句柄表。在子进程的句柄表中,拷贝项的位置与它在父进程句柄表中的位置是完全一样的。这是非常重要的一个设计,意味着:在父进程和子进程中,对一个内核对象进行标识的句柄值是完全一样的。

e、除了拷贝句柄表的记录项,系统还会递增内核对象的使用计数器。


f、第十三章主要讲到,内核对象的内容被保存在内核地址空间中——系统上运行的所有进程都共享这个空间。对于32位系统,这是0x80000000到0xffffffff之间的内存空间。对于64位系统,则是0x00000400 00000000到0xffffffff ffffffff之间的内存空间。


g、对象句柄继承还有个非常奇怪的特征:子进程并不知道自己继承了任何句柄。在子进程的文档中,应指出当它从另一个进程生成时,希望获得对一个内核对象的访问权,只有在这种情况下,内核对象的句柄继承才是有用的。


h、到目前为止,子进程为了判断自己期望的一个内核对象的句柄值,最常见的方式是将句柄值作为命令行参数传给子进程。子进程的初始化代码将解析命令行(通常是调用stscanf_s)并提取句柄值。之所以能这样实现的唯一原因是共享的内核对象的句柄值在父进程和子进程中是完全一样的。


i、也可以使用其他进程间通信技术将继承的内核对象句柄值从父进程传入子进程。让子进程完成初始化(第九章讨论的WaitForInputIdle函数)后,父进程将一条消息send或post到由子进程中的一个线程创建的一个窗口。


j、另一种方式是让父进程向其环境块添加一个环境变量,变量的名称应该是子进程知道去查找的一个名称,而变量的值应该是准备被子进程继承的那个内核对象的句柄值。然后子进程就能够轻松调用GetEnvironmentVariable来获得句柄值。如果子进程还要生成子进程,那么这种方式非常不错,因为环境变量可以反复继承的。

(其实本质上着三种方式都是进程间通信的方法小类)


k、可以改变句柄的标志,用于分别给不同的子进程继承不同的内核对象。SetHandleInformation函数。


3、为对象命名来运行进程共享内核对象

a、许多,但不是全部内核对象都可以进行命名的。


b、创建内核对象的函数最后一个参数为pszName的,传入NULL则是未命名(即匿名)内核对象。如果不传入NULL,则应该传入一个“以0来终止的名称字符串”的地址。这个名称可以长达MAX_PATH个字符(定义为260)。遗憾的是,MS没有提供任何专门的机制来保证为内核对象指定的名称是唯一的。更悲剧的是,所有这些对象都共享同一个命名空间,即使对象的类型不相同。创建已经存在的命名会返回NULL对象,而且错误码还是悲剧的6既是ERROR_INVALID_HANDLE。


c、至于如何使用这种方法,就是继续创建一个同名的的对象,然后系统会检查是否存在,如果存在,则会继续检查是否拥有对象的完全访问权限。如果是肯定的,那么系统就会在其句柄表中查找一个空白记录项,并将其初始化为指向现有的内核对象。如果对象的类型不匹配或调用者被拒绝访问,那么创建对象会失败,返回NULL。


d、内核句柄的具体权限,需要调用这些函数的扩展版本(Ex后缀)。


e、进程创建了一个已经存在的对象句柄值的时候,输入进去的安全属性信息和第二个参数会被忽略。而如何判断是否创建一个新的对象,则可以调用错误码,判断是否为ERROR_ALREADY_EXITS。


f、也可以通过Open系列函数来打开。如果没有找到该命名对象,则错误码为2既是ERROR_FILE_NOT_FOUND;如果找到了但是内核对象类型不对,则错误码为6既是ERROR_INVALID_HANDLE;如果名称对,类型对,系统会检查请求的访问权限是否允许,通过dwDesiredAccess来指定。


g、这个可以用来创建一个GUID对象,然后实现实例的单例程化。


~h、Terminal Services(终端服务)的情况和签名描述的稍微有所区别。在正在允许Terminal Services的计算机中,有多个用于内核对象的命名空间。其中一个是全局命名空间,所欲客户端都能访问的内核对象要放在这个命名空间中。这个命名空间主要由服务使用。此外,每个客户端会话(client session)都有一个自己的命名空间。并非只有服务端才会遇到这种情况,因为Remote Desktop(远程桌面)和Fast User Switching(快速用户切换)特性也是利用Terminal Services会话来实现的。


i、在Vista之前,不可能防范一个共享对象的名称被“劫持”,任何进程,即使是最低权限的进程,都能用任何指定的名称来创建一个对象。如果想确保程序创建的内核对象的名称永远不会和其它应用程序的名称冲突,或者想确保它们不会成为劫持攻击的目标,那么可以定义一个自定义的前缀,并把它作为自己的private命名空间使用,这和使用Global和Local前缀是相似的。负责创建内核对象的服务器进程将定义一个边界描述符(boundary description),它对命名空间的名称自身进行保护。


4、复制对象句柄来运行进程共享内核对象
a、DuplicateHandle函数,简单地说,这个函数获得一个进程的句柄表中的一个记录项,然后再另一个进程的句柄表中创建这个记录项的一个拷贝。涉及到系统中同时运行的三个不同的进程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值