预计阅读本页时间:-
11.3.3 对象管理器的实现
对象管理器也许是Windows可执行过程中一个最重要的组件,这也是为什么我们已经介绍了它的许多概念。如前所述,它提供了一个统一的和一致的接口,用于管理系统资源和数据结构,如打开文件、进程、线程、内存部分、定时器、设备、驱动程序和信号。更为特殊的对象可以表示一些事物,像内核的事务、外形、安全令牌和由对象管理器管理的Win32桌面。设备对象和I/O系统的描述联系在一起,包括提供NT名字空间和文件系统卷之间的链接。配置管理器使用一个Key类型的对象与注册配置相链接。对象管理器自身有一些对象,它用于管理NT名字空间和使用公共功能来实现对象。在这些目录中,有象征性的联系和对象类型的对象。
由对象管理器提供的统一性有不同的方面。所有这些对象使用相同的机制,包括它们是如何创建、销毁以及定额分配值的占有。它们都可以被用户态进程通过使用句柄访问。在内核的对象上有一个统一的协议管理指针的引用。对象可以从NT的名字空间(由对象管理器管理)中得到名字。调度对象(那些以信号事件相关的共同数据结构开始的对象)可以使用共同的同步和通知接口,如WaitForMultipleObjects。有一个共同的安全系统,其执行了以名称来访问的对象的ACL,并检查每个使用的句柄。甚至有工具帮助内核态开发者,在使用对象的过程中追踪调试问题。
理解对象的关键,是要意识到一个(执行)对象仅仅是内核态下在虚拟内存中可以访问的一个数据结构。这些数据结构,常用来代表更抽象的概念。例如,执行文件对象会为那些已打开的系统文件的每一个实例而创建。进程对象被创建来代表每一个进程。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
一种事实上的结果是,对象只是内核数据结构,当系统重新启动时(或崩溃时)所有的对象都将丢失。当系统启动时,没有对象存在,甚至没有对象类型描述。所有对象类型和对象自身,由对象管理器提供接口的执行体的其他组件动态创建。当对象被创建并指定一个名字,它们可以在以后通过NT名字空间被引用。因此,建立对象的系统根目录还建立了NT名字空间。
对象结构,如图11-17所示。每个对象包含一个对所有类型的所有对象的某些共性信息头。在这个头的领域内包括在名字空间内的对象的名称,对象目录,并指向安全描述符代表的ACL对象。

对象的内存分配来自由执行体保持的两个堆(或池)的内存之一。在有(像内存分配)效用函数的执行体中,允许内核态组件不仅分配分页内核内存,也分配无分页内核内存。对于那些需要被具有CPU 2级以及更高优先级的对象访问的任何数据结构和内核态是对象,无分页内存都是需要的。这包括ISR和DPC(但不包括APC)和线程调度本身。该pagefault处理也需要由无分页内核内存分配的数据结构,以避免递归。
大部分来自内核堆管理器的分配,是通过使用每个处理器后备名单来获得的,这个后备名单中包含分配大小一致的LIFO列表。这些LIFO优化不涉及锁的运作,可提高系统的性能和可扩展性。
每个对象标头包含一个配额字段,这是用于对进程访问一个对象的配额征收。配额是用来保持用户使用较多的系统资源。对无分页核心内存(这需要分配物理内存和内核虚拟地址)和分页的核心内存(使用了内核虚拟地址)有不同的限制。当内存类型的累积费用达到了配额限制,由于资源不足而导致给该进程的分配失败。内存管理器也正在使用配额来控制工作集的大小和线程管理器,以限制CPU的使用率。
物理内存和内核虚拟地址都是宝贵的资源。当一个对象不再需要,应该取消并回收它的内存和地址。但是,如果一个仍在被使用的对象收到新的请求,则内存可以被分配给另一个对象,然而数据结构有可能被损坏。在Windows执行体中可以很容易发生这样的问题,因为它是高度多线程的,并实施了许多异步操作(例如,在完成特定数据结构之上的操作之前,就返回这些数据结构传递给函数的调用者)。
为了避免由于竞争条件而过早地释放对象,对象管理器实现了一个引用计数机制,以及引用指针的概念。需要一个参考指针来访问一个对象,即便是在该物体有可能正要被删除时。根据每一个特定对象类型有关的协议里面,只有在某些时候一个对象才可以被另一个线程删除。在其他时间使用的锁,数据结构之间的依赖关系,甚至是没有其他线程有一个对象的指针,这些都能够充分保护一个对象,使其避免被过早删除。
1.句柄
用户态提到内核态对象不能使用指针,因为它们很难验证。相反内核态对象必须使用一些其他方式命名,使用户代码可以引用它们。Windows使用句柄来引用内核态对象。句柄是不透明值(opaque value),该不透明值是被对象管理器转换到具体的应用,以表示一个对象的内核态数据结构。图11-18表示了用来把句柄转换成对象的指针的句柄表的数据结构。句柄表增加额外的间接层来扩展。每个进程都有自己的表,包括该系统的进程,其中包含那些只含有内核线程与用户态进程不相关的进程。

图11-19显示,句柄表最大支持两个额外的间接层。这使得在内核态中执行代码能够方便地使用句柄,而不是引用指针。内核句柄都是经过特殊编码的,从而它们能够与用户态的句柄区分开。内核句柄都保存在系统进程的句柄表里,而且不能以用户态存取。就像大部分内核虚拟地址空间被所有进程共享,系统句柄表由所有的内核成分共享,无论当前的用户态进程是什么。

用户可以通过Win32调用的CreateSemaphore或OpenSemaphore来创建新的对象或打开一个已经存在的对象。这些都是对程序库的调用,并且最后会转向适当的系统调用。任何成功创建或打开对象的指令的结果,都是储存在内核内存的进程私有句柄表的一个64位句柄表入口。表中句柄逻辑位置的32位索引返回给用户用于随后的指令。内核的64位句柄表入口包含两个32位字节。一个字节包含29位指针指向包头。其后的3位作为标志(例如,表示句柄是否被它创建的进程继承)。这3位在指针就位以前是被屏蔽掉的。其他的字节包含一个32位正确掩码。这是必需的因为只有在对象创建或打开的时候许可校验才会进行。如果一个进程对某对象只有只读的权限,那在表示其他在掩码中的权限位都为0,从而让操作系统可以拒绝除读之外对对象进行任何其他的操作。
2.对象名字空间
进程可以通过由一个进程把到对象的句柄复制给其他进程来共享对象。但是这需要复制的进程有到其他进程的句柄,而这样在多数情况中并不适用,例如进程共享的对象是无关的或被其他进程保护的。在其他情况下,对象即使在不被任何进程调用的时候仍然保持存在是非常重要的,例如表示物理设备的对象,或用户实现对象管理器和它自己的NT名字空间的对象。为了地址的全面分享和持久化需求,对象管理允许随意的对象在被创建的时候就给定其NT名字空间中的名字。然而,是由执行部件控制特定类型的对象来提供接口,以使用对象管理器的命名功能。
NT名字空间是分级的,借由对象管理器实现目录和特征连接。名字空间也是可扩展的,通过提供一个叫做Parse的进程程序允许任何对象类型指定名字空间扩展。Parse程序是一个可以提供给每一个对象类型的对象创建时使用的程序,如图11-20所示。

Open语句很少使用,因为默认对象管理器的行为才是必需的,所以程序为所有基本对象类型指定为NULL。
Close和Delete语句描述对象完成的不同阶段。当对象的最后一个句柄关闭,可能会有必要的动作清空状态,这些由Close语句来执行,当最后的指针参考从对象移除,使用Delete语句,从而对象可以准备被删除并使其内存可以重用。利用文件对象,这两个语句都实现为I/O管理器里面的回调,I/O管理器是声明了对象类型的组件。对象管理操作使得由设备堆栈发送的I/O操作能够与文件对象关联上,而大多数这些工作由文件系统完成。
Parse语句用来打开或创建对象,如文件和登录密码,以及扩展NT名字空间。当对象管理器试图通过名称打开一个对象并遭遇其管理的名字空间树的叶结点,它检查该叶结点对象类型是否指定了一个Parse语句。如果有,它会引用该语句,将路径名中未用的部分传给它。再以文件对象为例,叶子结点是一个表现特定文件系统卷的设备对象。Parse语句由I/O管理器执行,并发起在对文件系统的I/O操作,以填充一个指向文件的公开实例到该文件对象,这个文件是由路径名指定的。我们将在以后逐步探索这个特殊的实例。
QueryName语句是用来查找与对象关联的名字。Security语句用于得到、设置或删除该安全描述符的对象。对于大多数类型的对象,此程序在执行的安全引用监视器组件里提供一个标准的切入点。
注意,在图11-20里的语句并不执行每种对象类型最感兴趣的操作。相反,这些程序提供给对象管理器正确实现功能所需要的回调函数,如提供对对象的访问和对象完成时的清理工作。除了这些回调,对象管理器还提供了一套通用对象例程,例如创建对象和对象类型,复制句柄,从句柄或者名字获得引用指针,并增加和减去对象头部的参考计数。
对象感兴趣的操作都是在本地NT API系统调用,如NtCresteProcess、NtCreateFile或NtClose(关闭句柄所有类型的通用操作),如图11-9所示。
虽然对象名字空间对整个运作的系统是至关重要的,但却很少有人知道它的存在,因为没有特殊的浏览工具的话它对用户是不可见的。winobj就是一个这样的浏览工具,在www.microsoft.com/technet/sysinternals可免费获得。在运行时,此工具描绘的对象的名字空间通常包含对象目录,如图11-21列出来的及其他一些。

一个被奇怪地命名为\??的目录包含用户的所有MS-DOS类型的设备名称,如A:表示软驱,C:表示第一块硬盘。这些名称其实是在设备对象活跃的地方链接到目录\装置的符号。使用名称\??是因为其按字母顺序排列第一,以加快查询从驱动器盘符开始的所有路径名称。其他的对象目录的内容应该是自解释的。
如上所述,对象管理器保持一个单独的句柄为每个对象计数。这个计数是从来不会大于指针引用计数,因为每个有效的句柄对象在它的句柄表入口有一个引用指针。使用单独句柄计数的理由是,当最后一个用户态的引用消失的时候,许多类型的对象可能需要清理自己的状态,尽管它们尚未准备好让它们的内存删除。
以一个文件对象为例表示一个打开文件的实例。Windows系统中文件被打开以供独占访问。当文件对象的最后一个句柄被关闭,重要的是在那一刻就应该删除专有访问,而不是等待任何内核引用最终消失(例如,在最后一次从内存冲洗数据之后。)否则,从用户态关闭并重新打开一个文件可能无法按预期的方式工作,因为该文件看来仍然在使用中。
虽然对象管理器在内核具有全面的管理机制来管理内核中的对象生命周期,不论是NT API或Win32API的都没有提供一个引用机制来处理在用户态的并行多线程之间的句柄使用。从而多线程并发访问句柄会带来竞争条件(race condition)和bug,例如,可能发生一个线程在别的线程使用完特定的句柄之前就把它关闭了。或者多次关闭一个句柄。或者关闭另一个线程仍然在使用的句柄,然后重新打开它指向不同的对象。
也许Windows的API应该被设计为每个类型对象带有一个关闭API,而不是单一的通用NTClose操作。这将至少会减少由于用户态线程关闭了错误的处理而发生错误的频率。另一个解决办法可能是在句柄表中的指针之外再添加一个序列域。
为了帮助程序开发人员在他们的程序中寻找这些类似的问题,Windows有一个应用程序验证,软件开发商能够从Microsoft下载。我们将在11.7节介绍类似的驱动程序的验证器,应用程序验证器通过大量的规则检查来帮助程序员寻找可能通过普通测试无法发现的错误。它也可以为句柄释放列表启用先进先出顺序,以便句柄不会被立即重用(即关闭句柄表通常采用效果较好的LIFO排序)。防止句柄被立即重用的情况发生,在这些转化的情况下操作可能错误地使用一个已经关闭的句柄,这是很容易检测到的。
该设备对象是执行体中一个最重要的和贯穿内核态的对象。该类型是由I/O管理器指定的,I/O管理器和设备驱动是设备对象的主要使用者。设备对象和驱动程序是密切相关的,每个设备对象通常有一个链接指向一个特定的驱动程序对象,它描述了如何访问设备驱动程序所对应的I/O处理例程。
设备对象代表硬件设备、接口和总线,以及逻辑磁盘分区、磁盘卷甚至文件系统、扩展内核,例如防病毒过滤器。许多设备驱动程序都有给定的名称,这样就可以访问它们,而无需打开设备的实例的句柄,如在UNIX中。我们将利用设备对象以说明Parse程序是如何被使用的,如图11-22所示。

1)当一个执行组件,如实现了本地系统调用NTCreateFile的I/O管理器,在对象管理器中称之为ObOpenObjectByName,它发送一个NT名字空间的Unicode路径名,例如\??\C:\foo\bar。
2)对象管理器通过目录和符号链接表搜索并最终认定\??\C:指的是设备对象(I/O管理器定义的一个类型)。该设备对象在由对象管理器管理的NT名字空间中一个叶节点。
3)然后对象管理器为该对象类型调用Parse程序,这恰好是由I/O管理器实现的lopParseDevice。它不仅传递一个指针给它发现的设备对象(C:),而且还把剩下的字符串\foo\bar也发送过去。
4)I/O管理器将创建一个IRP(I/O请求包),分配一个文件对象,发送请求到由对象管理器确定的设备对象发现的I/O设备堆栈。
5)IRP是在I/O堆栈中逐级传递,直到它到达一个代表文件系统C:实例的设备对象。在每一个阶段,控制是通过一个与这一等级设备对象相连的切入点传递到驱动对象内部。切入点用在这种情况下,是为了支持CREATE操作,因为要求是创建或打开一个名为\foo\bar的文件。
6)该设备对象中遇到指向文件系统的IRP可以表示为文件系统筛选驱动程序,这可能在该操作到达对应的文件系统设备对象之前修改I/O操作。通常情况下这些中间设备代表系统扩展,例如反病毒过滤器。
7)文件系统设备对象有一个链接到文件系统驱动程序对象,叫NTFS。因此,驱动对象包含NTFS内创建操作的地址范围。
8)NTFS将填补该文件中的对象并将它返回到I/O管理器,I/O管理器备份堆栈中的所有设备,直到lopParseDevice返回对象管理器(如11.8节所述)。
9)在对象管理器以其名字空间中的查找结束。它从Parse程序收到一个初始化对象(这正好是一个文件对象,而不是原来对象发现的设备对象)。因此,对象管理器为文件对象在目前进程的句柄表里创建了一个句柄,并对需求者返回句柄。
10)最后一步是返回用户态的调用者,在这个例子里就是Win32 API CreateFile,它会把句柄返回给应用程序。
可执行组件能够通过调用ObCreateObjectType接口给对象管理器来动态创建新的类型。由于每次发布都在变化,所以没有一个限定的对象类型定义表。图11-23列出了在Windows Vista中非常通用的一些对象类型,供快速参考。

进程(process)和线程(thread)是明显的。每个进程和每个线程都有一个对象来表示,这个对象包含了管理进程或线程所需的主要属性。接下来的三个对象:信号量、互斥体和事件,都可以处理进程间的同步。信号量和互斥体按预期方式工作,但都需要额外的响铃和警哨(例如,最大值和超时设定)。事件可以在两种状态之一:已标记信号或未标记信号。如果一个线程等待事件处于已标记信号状态,线程被立即释放。如果该事件是未标记信号状态,它会一直阻塞直到一些其他线程信号释放所有被阻止的线程(通知事件)的活动或只是第一个被阻止的线程(同步事件)。也可以设置一个事件,这样一种信号成功等待后,它会自动恢复到该未标记信号的状态而不是处在已标记信号状态。
端口、定时器和队列对象也与通信和同步相关。端口是进程之间交换LPC消息的通道。定时器提供一种为特定的时间区间内阻塞的方法。队列用于通知线程已完成以前启动的异步I/O操作,或一个端口有消息等待。(它们被设计来管理应用程序中的并发的水平,以及在使用高性能多处理器应用中使用,如SQL)。
当一个文件被打开时,Open file对象将会被创建。没打开的文件,并没有对象由对象管理器管理。访问令牌是安全的对象。它们识别用户,并指出用户具有什么样的特权,如果有的话。配置文件是线程的用于存储程序计数器的正在运行的周期样本的数据结构,用以确定程序线程的时间是花在哪些地方了。
段用来表示内存对象,这些内存对象可以被应用程序向内存管理器请求,将应用程序的地址空间映射到这个区域中来。它们记录表示磁盘上的内存对象的页的文件(或页面文件)的段。键表示的是象管理名字空间的注册表名字空间的加载点。通常只有一个名为\REGISTRY关键对象,负责链接到注册表键值和NT名字空间的值。
对象目录和符号链接完全是本地对象管理器的NT名字空间的一部分。它们是类似于和它们对应的文件系统部分:目录允许要收集一些相关的对象。符号链接允许对象名字空间来引用一个对象名字空间的不同部分中的对象的一部分的名称。
每个已知的操作系统的设备有一个或多个设备对象包含有关它的信息,并且由系统引用该设备。最后,每个已加载设备驱动程序在对象空间中有一个驱动程序对象。驱动程序对象被所有那些表示被这些驱动控制的设备的实例共享。
其他没有介绍的对象有更多特别的目的,如同内核事务的交互或Win32线程池的工作线程工厂交互。