前言
本章讲述了Microsoft Windows操作系统中四种基本的机制,它们对于系统的管理和配置至关重要,它们是:
- 注册表(Registry)
- 服务(Service)
- 统一的背景进程管理器(UBPM)
- Windows管理设施(WMI)
- Windows诊断基础设施(WDI)
4.1 注册表(Registry)
注册表在Windows系统的配置和控制方面扮演了一个非常关键的角色。
- 它既是系统全局设置的存储仓库,(HKLM等根键)
- 也是每个用户的设置信息的存储仓库。(HKU,HKCU等根键)
虽然大多数人都把注册表想象成存储在磁盘上的静态数据
,
但是,在这一节中你将会看到,注册表也是Windows执行体和内核所维护的各种内存中数据结构的一个窗口
。
我们首先从总体上介绍注册表的结构,并讨论一下它所支持的数据类型,简要地浏览一下Windows在注册表中维护的关键信息。然后,我们进一步讨论配置管理器( Configuration Manager)的内部机理。
这里的配置管理器是负责实现注册表数据库的执行体组件。我们将会涉及的话题有:
- 注册表在磁盘上的内部结构、
- 当一个应用程序请求配置信息时Windows如何获得相应的信息
- 以及Windows采用了哪些手段来保护这一关键的系统数据库。
4.1.1 查看和修改注册表
一般来说,你应该永远不需要直接编辑注册表:
- 如果存储在注册表中的应用程序设置和系统设置需要手工修改,那么,它们应该有一个对应的用户界面来控制其修改。
然而,正如你在本书中已经多次看到过的那样,有些高级设置和调试设置并没有相应的编辑用户界面。因此,Windows中包含了图形用户界面(GUI)和命令行工具,用于查看和修改注册表。
Windows随带了一个主要的GUI工具可用于编辑注册表,Regedit.exe
,还有一些命令行注册表工具。例如,Reg.exe
具有导入、导出、备份和恢复注册表键的能力,也可以比较、修改和删除注册表键和值。它也可以设置或者查询在UAC虚拟化中所用到的各种标记。而Regini.exe
则允许你根据一个包括ASCII或Unicode配置数据的文本文件来导入注册表数据.
WDK (Windows DriverKit)也提供了一个可以再分发的组件:Offreg.dll,称为离线注册表库
。通过该库,可以以二进制格式来加载注册表储巢文件,以及在这些储巢文件上应用各种操作,因而绕过了Windows所要求的常规注册表操作的逻辑:
- 先加载、再映射的过程。
这种用法主要是为了帮助离线访问注册表,比如出于完整性检查或有效性检查的目的
。如果底层的数据不必对系统可见的话,这种用法还可以获得性能上的优势,因为对数据的访问是通过本地文件I/O来完成,而不是通过注册表系统调用来完成的。
4.1.2 注册表用法
配置数据主要在四个时间点上被读取:
-
在初始的引导过程中
,引导加载器读入配置数据和引导设备驱动程序的列表,以便在初始化内核以前将它们加载到内存中。因为BCD(引导配置数据库,BootConfiguration Database)实际上存放在注册表储巢中,有人可能会争辩说,对注册表的访问发生在更早的时候(当引导管理器显示操作系统列表的时候)。 -
在内核引导过程中
,内核读取有关的设置信息,这些设置信息指定了应该加载哪些设备驱动程序,以及各个系统部件(比如内存管理器和进程管理器(process manager))该如何配置以及该如何调整系统的行为。 -
在登录过程中
,Explorer和其他的Windows组件从注册表中读取到每个用户的参数选择,包括网络驱动器字母映射、桌面墙纸、屏幕保护程序、菜单行为和图标的摆放,以及可能更重要的是,哪些启动程序需要激发起来,以及哪些文件最近被访问过。 -
在应用程序启动过程中
,它们读取系统全局范围的设置,比如所有可选安装的组件列表和许可数据,还读取一些针对每个用户的设置信息,可能包括菜单和工具栏的摆放,以及最近访问过的文档的列表。
然而,在其他时间点上也可以读取注册表,比如在响应注册表值或键的修改的时候
。
注册表提供了异步回调机制,这是优先采用的接收注册表变化通知的方法,尽管如此,仍然有些应用程序会通过不停查询的做法,来监视它们存放在注册表中的设置信息,以便自动地让更新过的设置信息起作用。然而,一般来说,在一个空闲的系统上,注册表不应该有任何活动,显然这样的应用程序违反了注册表的最佳实践原则。(Sysinternals的Process Monitor是一个追查这种活动和错误应用程序的极佳工具。)
在下面的情形下,注册表通常会被修改:
- 虽然算不上是修改,但是,注册表的初始结构和许多默认设置是由一个原型版本的注册表来定义的,此原型版本随Windows的安装介质一起发行,
在安装的时候被拷贝到一个新的系统中
。 -
应用安装工具创建了默认的应用程序设置
,以及一些可反映出安装配置选项的设置信息。 -
在设备驱动程序的安装过程中
,即插即用系统在注册表中创建了一些设置,告诉I/O管理器如何启动此驱动程序;
还创建了其他一些用于配置该驱动程序如何操作的设置(有关设备驱动程序如何被安装到系统中的更多信息,参见本书下册第8章“I/O系统”)。 -
当你通过用户界面改变了应用程序或者系统设置时
,这些改变通常被保存在注册表中。
4.1.3 注册表数据类型
注册表是一个数据库,其结构类似于磁盘卷的结构。注册表包含了键(key)和值( value)。键类似于磁盘的目录,而值则好比是磁盘上的文件。键是一个容器,可以包含其他的键(子键)或值。而值存储的是数据。最顶级的键是根键(root key)。在本节中,我们将相互交换着使用子键和键这两个词。
键和值两者都借用了文件系统的命名规范。因此,可以用名称“trade\mark”唯一标识一个存储在名为“trade”的键下面的、名为“mark”的值。此命名方案的一个例外是,每个键都有一个未命名的值。Regedit将未命名的值显示为“(默认)”。
注册表的值存储了不同种类的数据,它们可以是表4.1中所列出的12种类型之一。大多数注册表值是REG_DWORD、REG_BINARY或REG_Sz。REG_DWORD类型的值可以存储整数或者布尔值(开/关值);REG_BINARY值可以存储超过32位的整数值,或者诸如加密后的口令之类的原始数据;REG_Sz值存储字符串(当然是Unicode),可以表达诸如名称、文件名、路径和类型等元素。
REG_LINK类型特别值得注意,因为它让一个键可以透明地指向另一个键。当你搜索注册表而碰到一个链接时,路经搜索过程会在此链接的目标处继续进行。例如,如果\Root1\Link是一个指向\Root2\RegKey的REG_LINK值,并且RegKey包含了值RegValue,那么,可以有两条路径标识RegValue:\Root1\Link\RegValue和\Root2(RegKey\RegValue
。
正如下一小节将要解释的那样,Windows自己也显式地使用了注册表链接:
- 六个注册表根键中有三个其实是链接,它们指向另三个非链接的根键中的子键。
4.1.4 注册表逻辑结构
利用注册表中存储的数据,你可以画出注册表的组织结构。
总共有6个根键(你不能加入新的根键,也不能删除已有的根键)可以存储信息,如表4.2所示。
为什么根键的名称都以“H”开头?因为根键的名称代表了指向键(KEY)的Windows句柄(H)。正如第1章“”中所提及的那样,HKLM是用于HKEY_LOCAL_MACHINE的缩写。表4.3列出了所有的根键以及它们的缩写。下面的几小节详细地解释了这六个根键中每一个根键的内容和用途。
4.1.4.1 HKEY_CURRENT_USER
HKCU根键包含了与当前本地登录用户的参数和软件配置有关的数据。
它指向当前登录用户的用户轮廓,位于硬盘上的\Users\<用户名>\Ntuser.dat
中(要想知道根键是如何被映射到硬盘文件的,请参见本章后面的“注册表的内部机理”一节)。无论何时当一个用户的轮廓信息被加载的时候(比如在登录时候,或者当一个服务进程运行在某个特定用户名的环境中时),HKCU就会被创建出来,映射到HKEY_USERS下该用户的键上。
表4.4 列出了HKCU下面的一些子键。
4.1.4.2 HKEY_USERS
HKU为系统中每个加载的用户轮廓和用户类注册数据库包含了一个子键。它也包含一个名为HKU\DEFAULT
的子键;这是一个链接,指向该系统的轮廓(对于那些运行在本地系统账户下的进程,它们会用到该轮廓信息;本章后面的“服务”一节中更加详细地讲述了有关该轮廓的信息)。譬如,这是Winlogon使用的轮廓,所以,改变了该轮廓中的桌面背景设置,就可以在登录屏幕上体现出来。当一个用户第一次登录到一个系统中,并且他的账户不依赖于一个漫游域轮廓(也就是说,用户的轮廓是在域控制器的指示下,从一个中心网络位置上获得的)的时候,系统以%SystemDrive%\Users\Default
下存放的轮廓为基础,为她的账户创建一个轮廓。
系统将这些轮廓存储在哪里呢?
此位置是由注册表值HKLM\Software\Microsoft\WindowsNT\CurrentVersion\ProfileList\ProfilesDirectory
来定义的,其默认设置是%SystemDrive%\Users。ProfileList键也存放了一个系统上所有出现过的轮廓的列表。针对每个轮廓的信息存放在一个对应的子键下面,其名称反映了该轮廓所对应账户的安全标识符
(SID,Security ldentifier)(有关SID的更多信息,参见第6章“安全性”)。在每个轮廓的键所存储的数据中,sid值中存放的是该账户SID的二进制表示,而在ProfilelmagePath目录中存放的则是该轮廓所对应的储巢( hive,参见本章后面的“储巢”一节中的介绍)的硬盘路径。Windows在用户轮廓管理对话框中显示了一个系统中存储的轮廓列表,如图4.1所示。你只需在控制面板的System小程序的AdvancedSystem Settings中的Advanced标签视图的User Profiles部分单击Setting,就可以看到该对话框。
4.1.4.3 HKEY_CLASSES_ROOT
HKCR包含了三种类型的信息:
- 文件扩展名关联
- COM类注册信息
- 以及UAC(User AccountControl)虚拟化注册表根。(关于UAC的更多信息,请参见第6章。)
针对每个已注册的文件扩展名,都有一个对应的键。大多数键中包含一个REG_Sz值,指向HKCR中的另一个键,此键中包含了与该扩展名所代表的文件类相关联的信息。
例如,HKCR.xls指向了在另一个键(比如HKCU.xls\Excel.Sheet.8)中包含的有关MicrosoftExcel文件的信息。HKCR中其他的键包含了已注册到当前系统上的COM对象的详细配置信息。UAC虚拟化注册表位于VirtualStore键下,它与HKCR中保存的其他种类的数据没有关系。
HKEY_CLASSES_ROOT下的数据有以下两个来源途径:
- 针对每个用户的类注册数据:位于HKCU\SOFTWARE\Classes下(被映射至磁盘文件
\Users\<username>\AppData\Local\Microsoft\Windows\Usrclass.dat
)。 - 整个系统范围的类注册数据:位于HKLM\SOFTWARE\Classes下。
针对每个用户的注册数据为什么要与系统全局范围的注册数据分开呢?
其原因在于
- 这样做以后,可漫游的轮廓就能够包含这些定制的用户数据。
- 它同时也关闭了一个安全漏洞:非特权用户无法改变或者删除系统全局版本HKEY_CLASSES_ROOT中的键,因此也就无法影响到系统中应用程序的操作。非特权用户和应用程序可以读取系统全局范围的数据,也可以在系统全局数据中增加新的键和值(在它们的用户数据中这些键和值也被镜像过来了),但是它们只能修改其私有数据中已有的键和值。
4.1.4.4 HKEY_LOCAL_MACHINE
根键HKLM包含了所有系统全局范围的配置子键:
- BCD00000000
- COMPONENTS(根据需要动态加载)、
- HARDWARE
- SAM
- SECURITY
- SOFTWARE和SYSTEM
HKLM\BCD00000000
HKLM\BCD00000000
子键包含了引导配置数据库(BCD)信息,这是作为一个注册表储巢被加载进来的。该数据库代替了以前在Windows Vista以前使用的Boot.ini文件,为每次安装Windows的引导配置数据增加了极大的灵活性和隔离性。(有关BCD的更多信息,请参考本书下册第13章“启动和停机”。)
在BCD中的每一项,比如一次安装的Windows,或者针对这一次安装的命令行设置,都保存在Objects子键下,它或者是一个可通过GUID来引用的对象(针对引导项的情形),或者是一个称为元素(element)的数值子键
。这些原始元素,绝大多数在MSDN Library的BCD参考材料部分有文档描述,它们定义了各种命令行设置或者引导参数。与每个元素子键相关联的值对应于相应的命令行标记或者引导参数的值。
BCDEdit命令行工具使得你可以通过元素和对象的符号名称来修改BCD。它也提供了关于所有可用的引导选项的大量帮助信息;不幸的是,它只能工作在本地系统环境中。因为注册表可以被远程打开,也可以从储巢文件中导入进来,所以,你可以利用注册表编辑器来修改或读取一台远程计算机的BCD。下面的实验显示了你可以利用注册表编辑器来打开内核调试功能。
HKLM\COMPONENTS
子键HKLM\COMPONENTS
包含的信息与CBS(Component Based Servicing
,基于组件的服务)栈有关。此CBSs栈包含了各种文件和资源,它们是Windows安装映像(用于自动化的安装包或者OEM预安装包)的一部分,或者是一次活动的Windows安装的一部分。为了服务目的而定义的CBS AP利用此注册表键中的信息来标识出已安装的组件和它们的配置信息。无论何时当单独地或者成组地安装、更新或者移除组件(分别称为单元unit或包package)时,这些信息都会被用到。因为该键可能会非常大,所以,为了优化系统资源,如果CBs栈在服务一个请求时,它只是被动态地加载到系统中,然后根据需要被卸载。
HKLM\HARDWARE
子键HKLM\HARDWARE维护了有关当前系统中的遗留硬件,以及一些从硬件设备至驱动程序的映射关系的描述信息。在现代系统上,只有一些外设,比如键盘、鼠标和ACPIBIOS数据,有可能在这里能找得到。设备管理器工具(从控制面板中运行System小程序,然后单击DeviceManager〉使你可以查看注册表硬件信息,这些信息只是简单地从HARDWARE键中读取出来的值(尽管它主要使用了HKLM\SYSTEM\CurrentControlSet\Enum树)。
HKLM\SAM
HKLM\SAM
存放了本地账户和组的信息,比如用户口令、组定义和域关联信息。作为域控制器运行的Windows Server系统将域账户和组的信息存放在活动目录(Active Directory)中。所谓活动目录,是一个用于存放域范围的各种设置和信息的数据库(本书并没有介绍活动目录)。在默认情况下,SAM键上的安全描述符被配置成:即使管理员账户也不能访问。
HKLM\SECURITY存放了系统全局范围的安全策略和用户权限分配。HKLM\SAM被链接到HKLM\SECURITY\SAM下的SECURITY子键。在默认情况下,你不能查看HKLM\SECURITY或者HKLM\SAM\SAM键的内容,因为这些键的安全设置只允许System账户才能访问(本章后面将会极其详细地讨论System账户)。如果你想要检查这些键的话,你可以改变此安全描述符,以便允许管理员组中的用户可以读访问这些键,或者,你可以利用PsExec,在本地系统账户中运行Regedit。然而,仅仅看一眼这些数据是不会有多大用处的,因为这些数据是没有文档说明的,其中的口令已经通过一个单向映射关系加了密,也就是说,你是无法从加密形式得到一个口令的。
HKLM\SOFTWARE
HKLM\SOFTWARE
是Windows存储系统全局配置信息的地方,这些配置信息在系统引导的时候并不需要。而且,第三方应用程序也将它们的系统全局范围的设置存放在这里,比如应用程序文件和目录的路径、授权许可信息和过期日期信息。
HKLM\SYSTEM
HKLM\SYSTEM
包含了引导系统所需要的全局配置信息,比如该加载哪些设备驱动程序、该启动哪些服务。因为这些信息对于启动系统是至关重要的,所以,Windows也在该键下维护了这份信息的一份拷贝,称为“last known good control set(最后已知的好控制集)”。由于维护了这样一份拷贝,因此,万一对当前控制集做了修改以后,系统无法正常引导了,管理员可以选择以前的一份正常工作的控制集。关于Windows何时声明当前控制集是“好”的,更详细的信息请参见本章“接受当前引导和最后已知的好控制集”一节。
4.1.4.5 HKEY_CURRENT_CONFIG
HKEY_CURRENT_CONFIG只是一个链接,指向HKLM\SYSTEM\CurrentControlSet\ HardwareProfiles\Current下的当前硬件轮廓。Windows不再支持硬件轮廓,但是该键仍然存在,以便支持那些遗留下来的、可能依赖于该键的应用程序。
4.1.4.6 HKEY_PERFORMANCE_DATA
在Windows上,注册表也是访问性能计数器值的机制,无论这些计数器是从操作系统组件中来的,还是从服务器应用程序中来的。通过注册表来访问性能计数器,一个额外的好处是,远程性能监视工作几乎可以“免费”完成,因为注册表很容易通过常规的注册表API就可以访问到。
只需打开一个名为HKEY_PERFORMANCE_DATA的特殊的键,并且查询该键下面的值,就可以直接访问注册表性能计数器信息。如果在注册表编辑器中查寻此键,是无法找到的;这个键只能通过编程的方式,利用Windows注册表函数,例如RegQueryValueEx,才可以访问到。性能信息实际上并没有存储在注册表中,注册表函数利用该键来获得从性能数据提供者那里提供的信息。
也可以利用性能数据帮助器(PDH,Performance Data Helper)API(Pdh.dIl)中的性能数据帮助器函数来访问性能计数器信息。图4.2显示了在访问性能计数器信息过程中涉及的组件。
4.1.5 事务型注册表(TxR)
感谢内核事务管理器(KTM,KernelTransaction Manager;更多信息参见第3章“系统机制”中关于KTM的章节),开发人员在执行注册表操作时,可以通过访问直接的API就可以获得鲁棒的、错误恢复的能力,并且与非注册表操作,比如文件或数据库操作,链接起来。
有三个API支持事务方式的注册表修改:
- RegCreateKeyTransacted
- RegOpenKeyTransacted
- RegDeleteKeyTransacted
这些新的例程与对应的非事务型例程一样,也带同样的参数,只是额外加入了一个新的事务句柄参数。开发人员在调用了KTM函数CreateTransaction以后就可以提供此句柄了。
在一个事务方式的创建或打开操作以后,所有后续的注册表操作,比如创建、删除或者修改该键中的值,都将是事务方式的。然而,在事务方式键的子键上执行的操作并不是自动事务方式的,这也是为什么第三个API,RegDeleteKeyTransacted要存在的原因。它使得可以以事务方式来删除子键,这是RegDeleteKeyEx在常规方式下做不到的。
如同其他的KTM操作一样,这些事务操作的数据也通过公共的日志文件系统(CLFS,common loggingfile system)服务写到了志文件中。在事务本身被提交或者回滚(这两种情形可以通过编程来发生,也可以是电源失败或者系统崩溃导致的后果) 以前,凡是在事务句柄上执行的键、值和其他的注册表修改对于通过非事务API来执行操作的外部应用程序都是不可见的。而且,事务是相互隔离的,在一个事务内所做的修改,在该事务被提交以前,在其他的事务内部,或者该事务外部,都是不可见的。
注:
一个非事务的写者在冲突的情况下将使一个事务中途失败一一例如,如果在一个事务内部创建了一个值,后来,当该事务仍然活动的时候,一个非事务的写者试图在同样的键下创建一个值。非事务的操作将会成功,而在该冲突的事务内的操作将会被丢弃。
TxR资源管理器实现的隔离级别(ACID中的“1”)是“读-提交”(read-commit),这意味着在事务被提交之后其所做的修改对其他的读者 (事务的或非事务的)立即起作用。这种机制对于那些熟悉数据库事务的人来非常重要,因为在数据库的事务中,隔离级别是“可预测的读(predictable-read)”(或者游标-稳定的,cursor-stability,这是在数据库的文章资料中的法)。对于“可预测的-读”隔离级别,在你读取了事务内部的一个值以后,后续再读这个值,都会取回同样的数据。“读-提交”并没有这种保证。这样的一个后果是,注册表事务不能被用于对一个注册表值进行递增或递减操作。
为了对注册表做永久的修改,已经使用了事务句柄的应用程序必须调用KTM函数CommitTransaction。(如果应用程序决定要撤销其所做的修改,比如在失败的路径上,那么,它可以调用RollbackTransaction AP1。)然后,这些修改对于常规的注册表API也将是可见的。
注:
如果一个用CreateTransaction来创建的事务句柄在该事务被提交之前关闭了(并且也没有其他的句柄指向这一事务),那么,系统将会回滚该事务。
TxR除了用到KTM提供的CLFS以外,还把它的内部日志文件存储在系统卷上的%SystemRoot%(System32\Config\Txr
文件夹中;这些文件有一个regtrans-ms扩展名,默认是隐藏的。即使你的系统上没有安装第三方的应用程序,该目录中也会包含一些文件,因为Windows Update
和CBS(Component BasedServicing)
利用TxR来以原子方式往注册表中写数据以避免系统失败,或者在一次不兼容的系统更新过程中造成组件数据不一致。事实上,如果你看一看其中的某些事务文件,应该能够看到那些正在被执行事务的键的名称。
有一个全局的注册表资源管理器(RM,resource manager)
,它服务于所有在引导时候被挂载上来的储巢。对于每一个被显式挂载的储巢,都会对应地创建一个RM。对于使用注册表事务的应用程序,RM的创建是完全透明的,因为KTM确保参与同一个事务的所有RM,在两阶段递交/失败协议中会协调工作。对于全局的注册表RM,CLFS日志文件被存储在前面所提到的System32\Config\Txr目录中。对于其他的储巢,日志文件存储在与储巢相同的目录中。它们是隐藏的文件,遵从同样的命名规则,结束后缀为.regtrans-ms。日志文件名称的前缀是它们所对应的储巢的名称。
4.1.6 监视注册表活动
因为系统和应用程序如此严重地依赖于配置设置来指导它们的行为,所以,系统和应用程序的失败可能是由于注册表数据的改变或者安全性而导致的。当系统或者应用程序未能读取到它们认为总是能够访问到的设置数据时,它们可能不会正常工作,显示一些隐藏了根源的错误消息,甚至崩溃。如果在系统或应用程序失败以后,不理解它们是如何访问注册表的,那么,要想知道哪些注册表键或者值被错误配置了,那基本上是不可能的。
在这样的情况下, Windows sysinternals (http://technet.microsoftcom/sysinternals)的进程监视工具(ProcessMonitor)也许能提供答案。
进程监视器允许你监视注册表的一举一动。对于每一次注册表访问,进程监视器都会向你显示执行这一次访问的进程:这一次访问的时间、类型和结果:以及在执行这次访问时刻线程的栈。这些信息极其有助于看清楚应用程序和系统是如何依赖于注册表的,也有助于发现应用程序和系统将配置设置信息存储在哪里,以及可以诊断出那些“因漏掉了注册表键或值而发生”的应用程序问题。进程监视器包含了高级的过滤和加亮显示功能,所以你可以快速地将注意力集中在与特定的键或值有关的活动上,或者与特定进程有关的活动上。
4.1.6.1 进程监视器(Process Monitor)内部机理
进程监视器依赖于一个设备驱动程序,该设备驱动程序是在运行时刻从它的可执行映像文件中提取得到、然后再启动起来的。它第一次执行的时候,要求当前运行它的账户具有LoadDriver特权,以及Debug特权;在同一次引导会话中,以后再执行的时候只需要Debug特权就可以了,因为一旦该驱动程序被加载以后,它就会驻留在系统中。
4.1.7 进程监视器诊断技巧
有两种基本的进程监视器诊断技巧对于发现与注册表有关的应用程序或系统问题特别有效,它们是:
- 在进程监视器的痕迹数据中,查看应用程序失败以前所做的最后的事情:这个动作可能直指问题本身。
- 将失败了的应用程序的进程监视器痕迹数据,与一个正常运行的系统的痕迹数据进行比较。
按照第一种方法,先运行进程监视器,再运行应用程序。在失败发生的时候,回到进程监视器中,停止记录数据(按下Ctrl+E)。然后,回到日志的末尾,找到该应用程序失败(或者崩溃,或者挂起,或者其他的失败行为)以前最后执行的一些操作。从最后一行开始往回查找,检查被引用到的文件、注册表键,或者两者兼而有之一一通常,这将有助于查明问题的原因所在。
当应用程序在一个系统上可以工作,而在另一个系统上却失败的时候,考虑使用第二种方法。分别在正常工作的系统上和失败的系统上捕获到该应用程序的痕迹数据,并且将这些输出数据保存到一个日志文件中。然后用Microsoft Excel打开好的和坏的日志文件(在Import向导中接受默认选项),删除其中的前三列(如果你不删除这前三列,比较的时候将会显示出每一行都不相同,因为前三列中包含了一些每次运行都不相同的信息,比如时间和进程ID)最后,比较结果日志文件(你也可以用WinDiff来比较,在Windows SDK中包含了该工具)。在进程监视器的痕迹数据中,如果在Result一列中有“NAMENOTFOUND”或者“ACCESSDENIED”的值,则这样的记录项应该是你要仔细探查的。当一个应用程序试图读取一个不存在的注册表键或者值的时候,NAME NOTFOUND就会被报告出来。在许多情况下,漏掉一个注册表键或者值是无关紧要的,因为如果进程未能从注册表中读取到它想要的设置,它只需简单地使用默认的值就可以。然而,在有些情况下,应用程序期望能找到非默认的值,如果非默认值不存在的话,则应用程序就会失败。
访问拒绝错误是一种常见的、与注册表有关的应用程序失败的根源,当应用程序没有权限访问一个它想要的键时,就会发生这种错误。如果应用程序不检查注册表操作的结果,或者不执行恰当的错误恢复过程,则应用程序就会失败。
一个可能值得怀疑的常见结果字符串是BUFFER OVERFLOW
。它并不表明在收到此错误的应用程序中有一个缓冲区溢出漏洞。相反,配置管理器利用它来通知一个应用程序,它所指定的用于存放一个注册表值的缓冲区太小,因而容不下该值。应用程序开发人员通常利用这种行为,来确定一个缓冲区到底应该分配多大才能存放一个值。他们首先用一个0长度的缓冲区来执行一个注册表查询操作,结果返回一个缓冲区溢出错误,以及该查询操作期望读取的数据的长度。然后,应用程序按照所指示的大小值分配一个缓冲区,重新读取该值。因此,你应该可以看到,返回BUFFER OVERFLOW的操作总是伴有一次成功的重复操作。
在一个利用进程监视器来诊断实际问题的例子中,它使得一个用户避免了完全重装他的Windows系统。其症状是,如果用户在启动InternetExplorer之前,不手工拨号打开Internet连接的话,Internet Explorer就会在启动的时候挂起。此Internet连接被设置成系统的默认连接,所以,一启动Internet Explorer,应该就会引发自动拨号到Internet (因为internet Explorer被设置成:一启动就显示一个默认主页)。
在检查Internet Explorer启动活动的进程监视器日志的过程中发现,从Internet Explorer被挂起的点开始往前回溯,有一个针对HKCU\SoftwarelMicrosoft\RAS Phonebook下的键的查询操作。用户报告说,在此之前他卸载了与该键关联的拨号程序,并且手工创建了拨号连接。因为拨号连接的名称与卸载的拨号程序的名称不相符,所以,看起来似乎是拨号卸载程序并没有删除该键,因而引起InternetExplorer被挂起。在删除了该键以后,InternetExplorer又正常工作。
4.1.8 在非特权账户下或者登录/注销过程中记录活动
一种常见的应用程序失败的情形是,应用程序在一个具有Administrative组成员资格的账户下运行时可以正常工作,但是在一个非特权用户的账户下运行时却不能正常工作。正如前面所介绍的,为了执行进程监视器,需要一些在正常情况下未分配给标准用户账户的安全特权,但是,利用Runas命令,在一个管理员账户下执行进程监视器,你就可以捕获到在非特权用户的登录会话中执行的应用程序的痕迹数据。
如果一个注册表问题与账户的登录或注销有关,那么,你也可以采取特别的步骤,以便利用进程监视器来捕获一个登录会话中有关这些阶段的痕迹数据。当一个用户注销的时候,在本地系统账户下运行的应用程序并不会终止,所以,你可以利用这一事实,让进程监视器在一个先注销随后又登录的过程中一直保持运行。你或者利用Windows内置的At命令并指定/interactive标志,或者利用Sysinternals的PsExec工具,就可以在本地系统账户下启动进程监视器。
PsExec工具的使用例子如下:
psexec -i0 -s -d c:\procmon.exe
-i0开关指示PsExec,让进程监视器的窗口出现在会话0的交互式控制台上:-s开关让PSExec在本地系统账户下运行进程监视器:-d开关让PSExec激发进程监视器,并且无须等待它终止就可以退出。当你执行了该命令时,它执行起来的进程监视器实例在你注销以后仍然还存活着,当你登录回来时,此进程监视器实例仍然出现在桌面上,因此它可以捕获到注销和登录这两个动作的注册表活动。
在登录、注销、引导或者停机过程中监视注册表的另一种办法是利用进程监视器的“记录引导”特性,你只要在Options菜单中选择“Log Boot”命令就可以打开该特性。下一次当你引导系统的时候,进程监视器的设备驱动程序就会从系统引导的早期开始,将注册表的活动记录到%SystemRoot\Procmon.pml中。它会一直在该文件中记录注册表的活动,直到磁盘空间耗尽、系统停机或者你运行进程监视器为止。在Windows系统上,一个存储了启动、登录、注销和停机的注册表痕迹的日志文件,通常情况下在50MB到150MB之间。
4.1.9 注册表的内部机理
在这一小节中,你将会了解到,配置管理器(即实现了注册表的执行体子系统)是如何组织注册表的磁盘文件的。我们将会检查:当应用程序或者其他的操作系统组件读取或者改变注册表键和值的时候,配置管理器是如何管理注册表的。我们还将讨论一些注册表管理机制,配置管理器试图利用这些机制来确保注册表总是处于一种可恢复的状态,即使当注册表被修改时系统崩溃了也可以被恢复。
4.1.9.1 储巢(Hive)
在磁盘上,注册表并不是简单的一个大文件,而是一组称为储巢(hive)
的独立文件。每个储巢包含了一棵注册表树,有一个键作为该树的根,或者作为该树的起点。子键和它们的值存储在根的下面。你可能会认为,注册表编辑器工具所显示的根键与储巢中的根键一定是相互关联的,但实际情况并不是这样的。表4.5列出了注册表储巢,以及它们对应的磁盘文件名。除了用户轮廓的储巢以外,其他所有储巢的路径名都被编码进了配置管理器中。当配置管理器加载储巢的时候,包括系统轮廓的储巢,它都会在HKLM\SYSTEM\CurrentControlSet\Control\Hivelist
子键下的注册表值中记录下每个储巢的路径,如果储巢被卸载的话,则删除对应的路径。它创建了根键,并且将这些储巢链接起来,以便建立起你所熟悉的、注册表编辑器所显示的注册表结构。
你会注意到,表4.5中列出的某些储巢是易变的,它们没有对应的关联文件。系统完全在内存中创建和管理这些储巢,因此这些储巢是临时的。系统在每次引导时创建这些易失的储巢。易失储巢的一个例子是HKLM\HARDWARE
储巢,它保存了有关物理设备和这些设备分配到的资源的信息。系统每次引导时,就会进行资源分配和硬件检测,所以,不在硬盘上存储这些数据是合理的。
4.1.9.2 储巢的大小限制
在有些情况下,储巢的大小是有限制的。例如,Windows对于HKLM\SYSTEM储巢的大小是有限制的。它之所以这样做,是因为,在引导过程开始之初,当虚拟内存换页机制尚未启用时,Winload要将整个HKLM\SYSTEM储巢读入到物理内存中。Winload还要将Noskrnl和引导设备驱动程序加载到物理内存中,所以,它必须要对分配给HKLMSYSTEM的物理内存数量进行限制
(关于Winload在启动过程中所扮演的角色的更多信息,请参见本书下册第13章)。在32位系统上,Winload允许该储巢可以达到最高400MB或者该系统物理内存数量的二分之一,取决于哪个更小一点。在x64系统上,低限是1.5GB。在itanium系统上,低限是32MB。
4.1.9.3 注册表符号链接
一种被称为注册表符号链接(symbolic link)
的特殊键使得配置管理器有可能将键链接起来,从而将注册表组织起来。符号链接是一个键,它指导配置管理器到达另一个键。因此,HKLM\SAM键是一个符号链接,它连接到SAM储巢的根所在的键上。符号链接是通过在RegCreateKey或RegCreateKeyEx调用中指定REG CREATE LINK参数而创建出来的。在内部,配置管理器创建了一个名为SymbolicLinkValue的REG_LINK值,其中包含目标键的路径。因为该值的类型是REG LINK而不是REG SZ,所以,它对于Regedit是不可见的一一然而,它仍然是磁盘上注册表储巢的一部分。
4.1.9.4 储巢结构
配置管理器从逻辑上将一个储巢分成一些称为块 (block)
的分配单元,其方式类似于文件系统将一个磁盘分成簇。
根据定义,注册表块的大小为4096字节 (4KB)。当新的数据要扩展一个储巢时,该储巢总是按照块的粒度来增加。一个储巢的第一个块是基本块(base block)。
基本块包含了有关该储巢的全局信息,包括:
- 一个特征签名regf (将该文件标识成储巢)、
- 更新的序列号、
- 一个时间戳(显示了该储巢上最后一个写操作发生的时间)、
- 有关Winload修复或恢复注册表的信息、
- 储巢格式版本号、
- 校验和,
- 以及该储巢文件的内部文件名(例如,DevicelHarddiskVolume1\WINDOWS\SYSTEM32\CONFIGSAM)。
当我们讲述如何将数据写到个储巢文件中的时候,我们将会说明更新的序列号和时间戳的重要性。
储巢格式版本号指明了该储巢内部的数据格式。为了与Windows 2000的漫游轮廓兼容除了System和Software,对于所有其他的储巢,配置管理器使用储巢格式版本1.3(该版本将称的前4个字符缓存在巢室索引结构内部,以便于快速查找,以此来提升搜索的性能);而对于system和Software储巢,它使用版本1.5,因为新格式对于大的值(支持超过1MB的大值)和搜索(不再缓存一个名称的前4个字符,而是使用整个名称的散列值来降低冲突)作了特别的优化。
Windows将一个储巢所存储的注册表数据组织在一种称为巢室 (cell)
的容器中。一个巢室可以容纳一个键、一个值、一个安全描述符、一列子键,或者一列键值。在巢室数据开始处的一个4字节字符标记描述了该巢室数据的类型,作为一个特征签名。表4.6详细列出了每个巢室数据类型。巢室的头是一个指定了该巢室大小的域(作为1的补数,不出现在CM 结构中)。当一个巢室加入到一个储巢中,而且该储巢必须进行扩展才能包含该巢室时,系统创建一个称为巢箱 (bin)
的分配单元。
一个巢箱是新巢室正好扩展到下一个块或页面边界的大小。系统将巢室的尾部和巢箱的尾部之间的任何空间都看成是空闲的空间,因而可以将它再分配给其他的巢室。巢箱也有头部,其中包含了一个特征签名hbin、一个记录了该巢箱在储巢文件中偏移量的域,以及该巢箱的大小。
通过使用巢箱,而不是巢室,来跟踪注册表的活动部分,Windows可以最小化其管理杂务。例如,在通常情况下系统分配和释放巢箱的频率,比分配和释放巢室的频率要低得多,这使得配置管理器可以更加有效地管理内存。当配置管理器将一个注册表储巢读进内存的时候,它读入整个储巢,包括空的巢箱,但是它可以选择在后面将它们丢弃掉。当系统在储巢中加入或者删除巢室的时候,储巢可能会包含空的巢箱,并且它们散布在活动的巢箱之间。这种情形类似于磁盘碎片,系统在磁盘上创建和删除文件的时候,就会产生磁盘碎片。当一个巢箱变成空的时候,配置管理器将任何相邻的空巢箱也加入到此空巢箱中,从而形成一个尽可能大的连续空巢箱。配置管理器也将相邻的已被删除的巢室连接起来,以便形成更大的空闲巢室(配置管理器只有当一个储巢尾部的储箱变成空闲的时候才会缩短该储巢。你可以通过Windows的RegSaveKey和RegReplaceKey函数,先备份注册表,再恢复注册表,从而达到压缩注册表的目的;Windows Backup工具就使用了这些函数)。
储巢的结构是通过一些链接建立起来的,这些链接称为巢室索引 (cellindex)。每个巢室索引是一个巢室在储巢文件中的偏移,再减去基本块的大小。因此,巢室索引就像是一个指针,从一个巢室指向另一个巢室,配置管理器将巢室索引解释成相对于储巢起始处的偏移。例如,正如你在表4.6中所看到的,用于描述一个键的键巢室,它所包含的一个域指定了其父键的巢室索引;子键的巢室索引还指定了另一个巢室,它描述了那些隶属于该子键的子键。子键列表巢室包含了一列巢室索引,这些巢室索引指向其子键的键巢室。因此,假如你想要找到子键A的键巢室,并且A的父键是B,那么,你必须首先利用键B的巢室中的子键列表巢室索引,找到包含键B所有子键列表的那个巢室,然后利用该子键列表巢室中的巢室索引列表找到键B的每个子键巢室。对于每个子键巢室,检查该子键的名称(键巢室存储了键的名称)是否符合你想要找的那个子键,在这个例子中即子键A。
巢室、巢箱和块之间的区别可能很容易让人混淆,所以,我们现在来看一个简单的注册表储巢的布局示例,以帮助澄清它们之间的区别。在图4.3中,示例性的注册表储巢文件包含了一个基本块和两个巢箱。第一个巢箱是空的,第二个巢箱包含了几个巢室。从逻辑上讲,该储巢只有两个键:一个是根键Root;另一个是Root的子键,即Sub Key。Root有两个值:Val1和Va2。通过一个子键列表巢室,可以定位到根键的子键;通过一个值列表巢室,可以定位到根键的值。第二个巢箱中的空闲空间是空的巢室。图4.3并没有显示这两个键的安全巢室,它们也应该出现在一个储巢中。
为了优化对于值和子键的搜索过程,配置管理器按照字母表顺序来存储子键列表巢室。然后,当配置管理器要在一个子键列表中查找一个子键时,它可以执行二分搜索。配置管理器在列表的中间检查要查找的子键,如果按照字母表的顺序,配置管理器正在查找的子键的名称位于中间子键名称的前面,则配置管理器知道该子键位于子键列表的前半部分:否则该子键位于子键列表的后半部分。这样的切分过程一直进行下去,直到配置管理器找到了该子键,或者没有找到匹配的子键。然而,值列表巢室并非是排序的,所以,新的值总是被加到列表的尾部。
4.1.9.4 巢室映射表
如果储巢从来不增长,那么,配置管理器就可以在一个储巢的内存版本中执行所有的注册表管理工作,就好像该储巢是一个文件一样。给定一个巢室索引,配置管理器只需简单地将巢室索引 (即在储巢文件中的偏移) 加到内存中储巢映像的基地址上,就可以计算出该巢室在内存中的位置。
在系统引导的早期Winload对于SYSTEM储巢正是这样处理的: Winload将整个SYSTEM储巢作为一个只读储巢读入到内存中,并且将巢室索引加上内存中储巢映像的基地址,从而可以定位到这些巢室。不幸的是,随着储巢中加入新的键和值,储巢会随之增长,这意味着系统必须申请换页池的内存来存储新的巢箱,在这些巢箱中包含新加入的键和值。因此,在内存中存储注册表数据的换页池不必是连续的。
为了处理内存中存放储巢数据的非连续内存地址,配置管理器采用了一种类似于Windows内存管理器用来将虚拟内存地址映射为物理内存地址的策略。配置管理器采用一种两层方案,如图4.4所示,它接受一个巢室索引 (即储巢文件中的一个偏移)作为输入,返回该巢室索引所在的块的内存地址,以及该巢室所在的块的内存地址。前面提到过,一个巢箱可以包含一个或者多个块,储巢以巢箱为粒度进行增长,所以,Windows总是用一块连续区域的内存来代表一个巢箱。因此,一个巢箱内部的所有块都出现在同一个缓存管理器视图的内部。
为了实现这种映射,配置管理器将一个巢室索引从逻辑上分成多个域,就如同内存管理器将一个虚拟地址分成多个域一样。Windows将一个巢室索引的第一个域解释成一个储巢的巢室映射表目录中的一个索引。巢室映射表目录包含1024个项目,每个项目指向一张巢室映射表,每张巢室映射表包含512个表项。在巢室映射表中的表项是由巢室索引中的第二个域来指定的。该表项指定了此巢室的巢箱内存地址和块内存地址。并不是所有的巢箱都必须映射到内存中,如果查找一个巢室得到的结果地址为0,则配置管理器将该巢箱映射到内存中,如果有必要的话,将它维护的LRU表中的另一个巢箱解除映射。
在转译过程的最后一步,配置管理器将巢室索引的最后一个域解释成上述指定块中的一个偏移量,以便精确地定位到内存中的巢室。当一个储巢初始化的时候,配置管理器动态地创建这些映射表,为储巢中的每一个块指定一个表项:当储巢的大小有必要改变时,它也会从巢室目录中删除和增加映射表。
4.1.9.5 注册表名字空间和操作
配置管理器定义了一个“键对象 (keyobject)”的对象类型,从而将注册表的名字空间与内核的总名字空间整合在一起。配置管理器在Windows名字空间的根上插入了一个名为“注册表(Registry)”的键对象,用作指向注册表的基本入口点。
Regedit 按照HKEY_LOCAL_MACHINESYSTEM\CurrentControlSet
这样的形式来显示键的名称,但是,Windows子系统将这样的名称转译成相应的对象名字空间的形式(比如Registry\MachinelSystem\CurrentControlSet
)当Windows对象管理器解析此名称时,它首先研到Registry名称的键对象,然后将名称中剩下的部分交给配置管理器。配置管理器接管了名称解析过程,它查找自己内部的储巢树,以便找到期望的键或值。在我们介绍典型的注册表操作的控制流程以前,我们首先来讨论键对象和键控制块 (key controlblock)。无论何时,当一个应用程序打开或者创建一个注册表键的时候,对象管理器会给应用程序一个句柄,让它通过此句柄来引用该键。对应于一个键对象的句柄是配置管理器在对象管理器的帮助下分配的。通过对象管理器的对象支持,配置管理器得以充分利用对象管理器提供的安全性和引用计数功能。
对于每个打开的注册表键,配置管理器也分配一个键控制块。键控制块保存了该键的名称,包含了该控制块所引用的键节点的巢室索引,还包含一个标志。该标志指明,当该键最后一个句柄关闭的时候,配置管理器是否需要删除该键控制块所引用的键巢室。Windows把所有的键控制块都放在一张散列表中,从而可以快速地按照名称来搜索已有的键控制块。键对象指向它所对应的键控制块,所以,如果两个应用程序打开同一个注册表键的话,则每个应用程序都会收到一个键对象,并且这两个键对象指向一个公共的键控制块。
当一个应用程序打开一个已有的注册表键的时候,控制流程从应用程序在一个注册表API中指定了该键的名称开始(该注册表API调用了对象管理器的名称解析例程)对象管理器碰到了在名字空间中属于配置管理器的注册表键对象以后,将路径名称交给配置管理器。配置管理器在键控制块散列表中执行一次查找。如果找到了相应的键控制块,则无须再进一步工作:否则,这次查找的结果也让配置管理器获得了与所查找的键最相近的键控制块,它继续利用内存中的储巢数据结构来搜索键和子键,以便找到指定的键。如果配置管理器找到了该键巢室,它就会搜索键控制块树,以确定该键是否是打开的(被同一个应用程序或者另一个应用程序打开)。此搜索过程已经作了优化,以便总是从最近的,并且已经有一个打开的键控制块的祖先开始找起。例如,如果一个应用程序要打开\Registry\Machine\Key1\Subkey2,并且\Registry\Machine已经打开了,那么,解析例程使用\Registry\Machine的键控制块作为起点。如果该键是打开的,则配置管理器增加已有的键控制块的引用计数。如果该键尚未被打开,则配置管理器分配一个新的键控制块,并且将它插入到树中。然后,配置管理器分配一个键对象,使该键对象指向此键控制块,再将控制返回给对象管理器,对象管理器则返回一个句柄给应用程序。
当一个应用程序创建一个新的注册表键时,配置管理器首先找到此新键的父键的键巢室然后,配置管理器对于此新键将要存入的储巢中的空闲巢室列表进行搜索,以确定是否有足够大的巢室来容纳此新键巢室。如果没有足够大的空闲巢室,则配置管理器分配一个新的巢箱,并将它用于新键巢室,然后将巢箱尾部的任何空间也放到空闲巢室列表中。新键巢室中填充了各种有关的信息,包括该键的名称,配置管理器将该键巢室加入到其父键的子键列表巢室的子键列表中。最后,系统也将父巢室的巢室索引保存在新子键的键巢室中。
配置管理器利用键控制块的引用计数来决定何时应该删除键控制块。如果有多个句柄引用了一个键控制块中的键,则当所有这些句柄都关闭时,键控制块中的引用计数变为0,这表明该键控制块不再被需要了。如果一个应用程序调用一个API来删除该键,并设置了删除标志那么,配置管理器可以从该键的储巢中删除对应的键,因为它知道没有其他的应用程序将该键保持为打开的状态。
4.1.9.6 稳定可靠的存储
为了确保非易失性的注册表储巢(每个都是一个磁盘文件)总是处于一种可恢复的状态配置管理器使用了日志储巢 (log hive)。每个非易失性储巢都有一个关联的日志储巢,它是一个隐藏文件,与对应的储巢有同样的基本文件名,扩展名为logN。为了确保总能向前进行,配置管理器使用了一种双日志方案。可能会有两个日志文件:.og1和.log2。如果出于某种原因,.log1已经被写入了,但是在将脏数据写到主日志文件的时候发生了失败,那么,下一次发生刷新操作时,就会切换到.og2,累积起来的脏数据也会写到.log2。如果那也失败了,则累积的脏数据.og1中的数据,以及在此期间被弄脏的数据)被保存在.og2中。因此,下一次还会再次使用.log1,直到成功地写入到主日志文件中。如果期间不发生失败,则只使用.log1。
例如,如果你检查你的系统中的%SystemRoot%(System32\Config目录(将“Show HiddenFiles And Folders”文件夹选项选中),你将会看到System.log1、Sam.log1以及其他的log1和log2文件。当一个储巢初始化的时候,配置管理器分配一个位阵列,其中每一个位代表了该储巢中一个512字节大小的部分,或者称为扇区(sector)。这个阵列称为脏扇区阵列 (dirty sectorarray),因为阵列中的“on”位表示系统已经修改了内存里该储巢中的对应扇区,因此系统必须将该扇区写回到储巢文件中(“off”位表示对应的扇区是最新的,与内存中储巢的内容一致)。
当创建一个新的键或值,或者修改一个已有的键或值的时候,配置管理器在该储巢的脏扇区阵列中对于所改变的扇区做上标记。然后,配置管理器调度一个延迟的写操作,或者称为储巢同步(hive sync)。执行该储巢延迟的写操作的系统线程在储巢同步请求之后5秒钟被唤醒,它把所有储巢的脏储巢扇区从内存中写到磁盘上的储巢文件中。因此,系统同时也会刷新“在发出储巢同步请求的时间点和真正执行储巢同步的时间点”之间的这段时间内的所有注册表修改操作。当一个储巢同步发生时,下一个储巢同步至少要等到5秒钟以后。
注:
API函数RegFlushKey的名称指明了,该函数只是将某个键的已修改数据刷新到磁盘上,但是它实际上也会触发一次完全的注册表刷新动作,这对系统会有显著的性能影响。出于这一原因以及注册表的机制自动可以保证已修改的数据在几秒钟之内会进入到稳定可靠的存储体中所以,应用程序员应该避免使用这一API。
如果延迟写出器(lazy writer)只是简单地把一个储巢的所有脏扇区写到储巢文件中,并且在写操作过程中系统崩溃了,那么,储巢文件将处于一种不一致(破坏的)和不可恢复的状态。为了避免这样的情形发生,延迟写出器首先将储巢的脏扇区阵列和所有的脏扇区写到储巢的日志文件中,如果有必要的话可以增加日志文件的大小。然后,延迟写出器更新该储巢基本块中的一个序列号,再将脏扇区写到储巢中。当延迟写出器结束时,它更新该基本块中的另一个序列号。因此,如果在写储巢操作的过程中系统崩溃的话,则下一次重新引导时,配置管理器将会注意到,储巢的基本块中的两个序列号不匹配。于是,配置管理器可以用储巢的日志文件中的脏扇区来更新该储巢,从而使储巢向前滚过去。然后,该储巢被更新,并且其状态是一致的。
Windows的引导加载器(Boot Loader)也包含了一些与注册表可靠性有关的代码。例如它可以在内核被加载到系统中以前解析Svstem.iog文件,并执行一些修复工作来维持注册表的一致性。而且,在某些特定的储巢破坏情形下(比如,储巢的基本块、巢箱或者巢室中包含的数据未能通过一致性检查),配置管理器可以重新对破坏的数据结构进行初始化,在此过程中可能会删除一些子键,然后继续正常的操作。如果它不得不求助于自治愈操作的话,则它会弹出一个系统错误对话框来通知用户。
4.1.9.7 注册表过滤
Windows内核中的配置管理器实现了一个强大的注册表过滤模型,使得像进程监视器(Process Monitor)这样的工具可以方便地监视注册表的活动。当一个驱动程序使用回调机制时,它可以向配置管理器注册一个回调函数。配置管理器在执行注册表系统服务之前或者之后,调用该驱动程序的回调函数,所以,该驱动程序可以完全地看到并且控制对注册表的访问。回调机制还有其他一些用途,比如反病毒软件产品扫描注册表数据以检查病毒,或者防止未授权的进程修改注册表。
注册表回调也跟高位值 (altitude)的概念有关系。高位值是一种控制方法,它针对不同厂商在注册表过滤栈上注册一个“高度”,所以,系统调用每个回调例程的顺序是确定的、正确的。这可以避免这样一种情形:反病毒软件产品在加密软件产品运行其回调函数来解密注册表键数据之前对这些键进行扫描。按照Windows的注册表回调模型,这两种类型的软件工具都被分配了一个基本的高位值,分别对应于它们所做的过滤任务的类型 – 在这个例子中,分别是加密和扫描。其次,开发这些类型软件工具的公司,必须向Microsoft登记,以便在它们自己的组内,它们不会与相似的或者竞争的产品发生冲突。
注册表过滤模型也包含这样的能力:完全接管注册表操作的处理过程(从而绕过配置管理器,不让它处理相应的注册表请求),或者将一个操作重定向为另一个操作(比如Wow64的注册表重定向)。此外,修改一个注册表操作的输出参数或者返回值,这也是完全能做到的.最后,驱动程序也可以根据自己的目的,针对一个键或者一个操作,分配或标记上该驱动程序特有的信息。一个驱动程序可以在一个创建操作或者打开操作过程中,创建并分配这样的环境数据:配置管理器将在该键的每次后续操作过程中,记住并返回此环境数据。
4.1.9.8 注册表优化
配置管理器作了一些非常显著的性能优化。首先,实际上每一个注册表键都有一个安全描述符,它可以起到保护该键访问的作用。然而,为储巢中的每一个键都保存一个唯一的安全描述符拷贝将是十分低效的,因为同样的安全设置往往应用在注册表的整棵子树上。当系统将新的安全性作用到一个键上时,配置管理器检查该键所在的储巢中的安全描述符池(其中包含了该储巢中用到的独一无二的安全描述符):它为该键共用任何已有的描述符,从而确保在一个储巢中任何一个独一无二的安全描述符至多只有一份拷贝。
配置管理器也对一个储巢中键或者值的名称的存储方式做了优化。尽管注册表具备完全的Unicode能力,它使用Unicode编码方式来表述所有的名称,但是,如果一个名称中只包含ASCI字符,那么,配置管理器在该储巢中,按照ASCII形式来存储该名称。当配置管理器读入此名称时(比如当执行名称查询时),它在内存中将该名称转换成Unicode形式。按照ASCI形式来存储名称,可以显著地降低一个储巢的大小。
为了使内存使用量尽可能地减到最小,键控制块中并没有存储完整的键路径名。相反,它们只是引用了一个键的名称。例如,一个引用了\Registry\System\Control的键控制块只是引用了名称Control,而不是全路径名。进一步的内存优化是,配置管理器使用键名称控制块来存储键的名称,对于所有具有同样名称的键,它们的键控制块共用同样的键名称控制块。为了优化性能,配置管理器将键控制块名称存储在一个散列表中,以便于快速查询。
为了能够快速地访问键控制块,配置管理器将频繁被访问的键控制块存储在一个缓存表中,该表也被配置成一张散列表。当配置管理器需要查找一个键控制块时,它首先检查此缓存表。最后,配置管理器还有另一个缓存:延迟的关闭表。它存储的键控制块是应用程序关闭的,所以,一个应用程序可以很快地重新打开一个刚刚被关闭的键。为了优化查询操作,这些缓存表是针对每个储巢来存储的。当配置管理器将最近被关闭的键控制块加入到延迟的关闭表中时,它也移除掉该表中那些最老的键控制块。
4.2 服务(Service)
几乎每一个操作系统都有一种在系统启动时刻启动进程的机制,这些进程提供了一些不依赖于任何交互式用户的服务。在Windows中,这样的进程称为服务 (service)或者Windows服务(Windows Service),因为它们依赖于WindowsAPI与系统进行交互。Windows服务类似于UNIX的守护进程,它们通常实现了客户/服务器应用的服务器一方。
类比linux 使用service/systemctl enable命令查看的服务一样的 开机自动启动。
Windows服务的一个例子可能是Web服务器,因为不管是否有人登录到机器上,它必须保持运行:当系统启动的时候,它必须开始运行,这样,管理员就不用总是记着,也不用待在机器跟前,将服务启动起来。
Windows服务是由三个组件构成的:
- 服务应用
- 服务控制程序(SCP,service controlprogram),
- 以及服务控制管理器(SCM,service controlmanager)。
首先,我们讲述服务应用、服务账号,以及SCM的操作。然后我们将解释,那些自动启动的服务是如何在系统引导的过程中被启动起来的。我们还将介绍当一个服务在启动过程中失败时SCM所采取的步骤,以及SCM停掉服务的方法。
4.2.1 服务应用
像web服务器这样的服务应用至少包含一个作为Windows服务而运行的可执行程序。若用户想要启动、停止或者配置一个服务,他可以使用SCP。虽然Windows内置的SCP提供了一般性的启动、停止、暂停和继续功能,但是,有些服务应用包含了它们自己的SCP,管理员通过这些SCP,可以指定一些特定于他们所管理的服务的特殊设置。
服务应用也只是简单的Windows可执行程序(GUI风格或者控制台风格)加上一些代码来接收SCM的命令,以及将应用的状态反馈回SCM。因为大多数服务没有用户界面,所以它们都是按照控制台程序来创建的。
当你安装一个包含有服务的应用时,该应用的安装程序必须向系统注册它的服务。为了注册该服务,安装程序调用Windows的CreateService函数,这是一个在Advapi32.dll(%SystemRoot%(System32\Advapi32.dll)中实现的、与服务有关的函数。Advapi32,即“高级API(AdvancedAPI)”DLL,实现了所有的客户端SCMAPI。
当一个安装程序通过调用CreateService来注册服务时,就会发送一个消息给该服务将要驻留的机器上的SCM。然后,SCM为该服务在HKLM\SYSTEM\CurrentControlSet\Services下创建一个注册表键。Service键是SCM数据库的非易失部分。针对每个服务的键定义了该服务所在的可执行映像文件的路径,以及一些参数和配置选项。
在创建了一个服务以后,一个安装程序或者管理应用程序可以通过StartService函数来启动该服务。因为有些基于服务的应用也必须在引导过程中进行初始化才能工作,所以,像下面这样的情形也就不足为奇了:安装程序将一个服务注册成一个自动启动的服务,并且请用户重新引导整个系统,以便完成安装过程,并且让SCM在系统引导过程中启动此服务。
当一个程序调用CreateService时,它必须指定许多参数,由这些参数来描述该服务的特征这样的特征包括该服务的类型(它是运行在自已独立的进程中,还是与其他的服务共享一个进程)、该服务可执行映像文件的位置、一个可选的显示名、一个可选的账户名和口令(用于在特定账户的安全环境中启动该服务)、一个启动类型(指定该服务是在系统引导时自动启动,还是在SCP的指示下手工启动)、一个错误代码(指示了如果该服务在启动时检测到错误的话该如何反应),以及一些可选的信息 (如果该服务自动启动的话),这些信息指定了该服务相对于其他的服务该何时启动。
SCM将每一个特征存储为该服务的注册表键下的一个值。图4.5显示了一个服务的注册表键的例子。
请注意,Type值包含了三个可适用于设备驱动程序的值:设备驱动程序、文件系统驱动程序,以及文件系统识别器。这些值是Windows设备驱动程序使用的,这些设备驱动程序也将它们的参数作为注册表数据存储在Services键中,SCM负责启动那些Stat值为SERVICE_AUTO START或 SERVICE DEMAND_START的驱动程序所以SCM数据库中包含了驱动程序的信息,这也就不足为奇了。Windows 服务使用其他的类型:SERVICE WIN320WNPROCESS和SERVICE WIN32 SHARE PROCESS这两者是斥的。可容纳多个服务的可执行程序指定的是SERVICE_WIN32SHARE_PROCESS类型。
让一个进程运行多个服务的好处是,可以节省下在单独进程中运行每一个服务所需要的系统资源。一个潜在的缺点是,如果在同一个进程中运行的一组服务中的任何一个发生了错误,并因此而终止了该进程,那么,该进程的所有服务也随之而终止。而且,另一个限制是,所有的服务必须运行在同一个账户下(然而,如果一个服务利用了服务安全围护机制 (servicehardening),那么它可以限制在某些情况下自己出现在共享进程中不会遭受恶意攻击)。
当SCM启动一个服务进程时,该进程必须立即调用SartServiceCtrlDispatcher函数StartServiceCtrlDispatcher接受一个入口点列表,每个入口点对应于该进程中的一个服务。每个入口点是由它所对应的服务的名称来标识的StartServiceCtrlDispatcher创建了一个命名管道来跟SCM进行通信,在建立了该通信管道以后,它等待SCM通过该管道发送过来的命令。每次SCM启动一个属于该进程的服务时,它发送一个“服务启动”命令。StartServiceCtrlDispatcher函数对于所接收到的每个启动命令,创建一个线程(称为服务线程),由该线程来调用所启动服务的入口点函数,并实现该服务的命令循环。StartServiceCtrlDispatcher一直在等待来自SCM的命令,只有当该进程的所有服务都停止时它才会将控制返回至该进程的main函数,以便服务进程在退出以前做一些资源清理工作。
每个服务的入口点的第一个动作是调用RegisterServiceCtrlHandler函数。该函数接收一个指向某个函数的指针,并且将该指针保存起来,此函数指针称为控制处理器(controlhandler)。该服务实现此函数以便处理各种来自SCM的命令。RegisterServiceCtrlHandler并不与SCM通信但它为StartServiceCtrlDispatcher函数将此函数存储在本地进程内存中服务入口点维续初始化该服务,包括分配内存、创建通信端点,以及从注册表中读入私有的配置数据。正如前面所解释过的,大多数服务都遵从的一个惯例是,将服务的参数保存在其服务注册表键下的名为Parameters的子键下面。
在入口点初始化该服务的过程中,它必须利用SetServiceStatus函数来定期地给SCM发送状态消息,以指明该服务的启动过程正在如何进行。当入口点完成了初始化以后,服务线程通常进入一个循环,等待来自客户应用的请求。例如,Web服务器会初始化一个TCP监听套接字,等待进来的HTTP连接请求。
一个服务进程的主线程,即在StartServiceCtrlDispatcher函数中执行的线程,接收到针对该进程中各个服务的SCM命令,再调用目标服务的控制处理器函数(由RegisterServiceCtrlHandler 保存在该进程中)。SCM命令包括停止、暂停、恢复、询问和停机,或者应用程序定义的命令图4.6显示了一个服务进程的内部组织结构。图中的进程宿纳了一个服务,它包含两个线程:主线程和服务线程。
4.2.2 服务账户
对于服务开发人员和管理员来说,一个服务的安全环境是一个非常重要的考虑,因为它规定了该服务进程可以访问哪些资源。除非一个服务安装程序或者管理员指定了特殊的设置,否则,绝大多数服务运行在本地系统账户 (localsystem account)的安全环境下(该账户有时候被显示成SYSTEM,而其他时候则被显示成LocaSystem)。另有两个内置的账户是:网络服务(networkservice)和本地服务 (localservice)账户。从安全角度而言,这两个新的账户比本地系统账户具有的能力要少一些:那些不要求本地系统账户能力的Windows内置服务都运行在适当的服务账户之下。下面的小节介绍了这些账户的一些特殊之处。
本地系统账户
本地系统账户也正是核心的Windows用户模式操作系统组件运行时所在的账户,这样的组件包括会话管理器(%SystemRoot%\System32\Smss.exe)、Windows子系统进程(Csrss.exe)本地安全权威进程 (%SystemRoot%\System32\Lsass.exe )和Logon进程 (%SystemRoot%\System32\Winlogon.exe)。关于后两个进程的更多信息,请参见第6章。
从安全的角度来看,本地系统账户有非常强大的能力一一当涉及本地系统上的安全能力时,它比任何本地的或者域的账户要强大得多。该账户有以下一些特征。
它是本地管理员组中的一个成员。表4.8显示了本地系统账户所属的那些组(有关在对象访问检查时如何利用组成员关系的更多信息,请参见第6章)。
- 它有权利赋予几乎每一种特权(甚至包括那些通常并不赋予本地管理员账户的特权比如创建安全令牌的特权)。表4.9列出了分配给本地系统账户的特权 (第6章讲述了每一种特权的用途)。
- 绝大多数文件和注册表键都赋予本地系统账户完全的访问权限(即使它们没有赋予全部的访问权限,在本地系统账户下运行的进程也可以利用“接管所有权 (take-ownership)”的特权来获得访问权限)。
在本地系统账户下运行的进程按照默认的用户轮廓(HKU\DEFAULT)来运行。因此它们不能访问存储在其他账户的用户轮廓中的配置信息。 - 当一个系统是Windows域中的一个成员时,本地系统账户包含了一个服务进程运行时所在计算机的机器安全标识符(SID)。因此,一个运行在本地系统账户中的服务通过利用它的计算机账户,就可以自动地在同一个域林中的其他机器上得到身份认证(一个林 (forest) 是指一组域)。
- 除非该机器账户被特别赋予了对某些资源(比如网络共享体、命名管道,等等)的访问权限,否则,一个进程只能访问那些允许空会话(也即,不需要安全凭证的连接)访问的网络资源。你可以在特定的允许空会话的计算机上,通过HKLM)SYSTEM\CurrentControlSet(Services\lanmanserver parameters下的 NullSessionPipes和NullSessionShares注册表值来指定这些共享体和管道
网络服务账户
网络服务账户的用途是,供那些“既希望利用计算机账户来向网络上其他的机器认证身份(就好像本地系统账户所具有的认证能力那样),但是又不需要管理员组的成员所属权,或者也不需要用到那些分配给本地系统账户的诸多特权”的服务来使用的。因为网络服务账户并不属于管理员组,所以,运行在网络服务账户中的服务在默认情况下,比起运行在本地系统账户下的服务,只能访问很少量的注册表键,以及文件系统中的文件夹和文件。而且,只被赋予少量的特权,也限制了一个被攻破的网络服务进程的能力范围。例如,一个运行在网络服务账户下的进程不可能加载一个设备驱动程序或者打开任意的进程。
网络服务账户和本地系统账户之间的另一个差别是,运行在网络服务账户中的进程使用了网络服务账户的轮廓。
网络服务轮廓的注册表部分被加载在HKU\S-1-5-20下面,
而构成此注册表部分的文件和目录则位于%SystemRoot%\ServiceProfiles\NetworkService中。
一个运行在网络服务账户下的服务是DNS客户,它负责解析DNS名称,也负责找到域控制器。
本地服务账户
本地服务账户几乎等同于网络服务账户,两者重要的区别在于,前者只能访问那些允许匿名访问的网络资源。表4.9显示了网络服务账户与本地服务账户具有同样的特权;表4.8显示了两者属于同样的组,唯一的例外是网络服务账户属于NetworkService组,而不是LocalService组。运行在本地服务账户中的进程所使用的轮廓被加载到HKU\S-1-5-19中,以及存储在%SystemRoot%\ServiceProfiles\LocalService中。
运行在本地服务账户中的服务例子包括: 允许远程访问本地系统注册表的远程注册表服务(Remote Registry Service),和执行NetBIOS名称解析任务的LmHosts服务。
在哪个账户中运行服务
由于刚才提到的这些限制,有些服务需要使用一个用户账户的安全凭证来运行。你可以在创建一个服务的时候,对该服务进行配置,让它在选定的账户下运行;你也可以利用Windows Services MMC加载件 (snap-in)来指定一个账户和口令,让该服务运行在此账户下在Services加载件中,在一个服务上右键单击,再选择Properties,并单击Logn标签,然后选择ThisAccount选项,如图4.7所示。
以最小特权来运行
服务通常会受制于“要么全部、要么什么也没有 (all-or-nothing)”的模型,这意味着,对于服务进程运行时所在的账户,所有可使用的特权对于该进程中运行的服务都是可用的,而该服务实际上可能只要求这些特权中的一个子集。
为了更好地遵从最小特权原则,即windows只给服务分配它们真正需要的特权,开发人员可以为他们的服务指定所需要的特权,这样SCM创建一个安全令牌,其中只包含这些特权。
注: 为一个服务指定的特权必须是它运行时所在服务账户可以使用的特权的子集。
服务开发人员使用changeServiceConfig2API来指明他们期望的特权列表。此API将这一信息保存在注册表中该服务的Parameters键中。当服务启动的时候,SCM读取该键,把这些特权加入到该服务运行时所在进程的令牌中。
如果有一个RequiredPrivileges值,并且该服务是一个独立的服务(作为一个专有进程来运行),那么,SCM创建一个令牌,仅包含该服务所需要的特权。
若一个服务只是一个多服务的服务进程中的一部分(绝大多数服务是Windows的一部分),并且指定了所要求的特权,那么,SCM计算出这些特权的并集,将它们组合起来构成服务宿主进程的令牌。换句话讲,只有那些没有被服务组中任何一个服务指定的特权将会被删除。如果此注册表值不存在,那么,SCM假定该服务与最小特权原则不兼容,或者要求所有的特权才可以工作,除此之外别无选择。在这种情况下,SCM创建一个完全的令牌,其中包含所有的特权,这样的模型也不会提供额外的安全性。要想剥离几乎所有的特权,服务只需指定Change Notify特权。
服务隔离
虽然限定一个服务能够访问的特权可以有助于削弱一个被攻破的服务进程危害其他进程的能力,但是,它并没有做任何努力,将该服务与它运行所在账户在通常条件下可以访问的资源隔离开来。正如前面所提到的,本地系统账户对关键的系统文件、注册表键和系统中其他一些受保护的对象具有完全的访问权,因为它们的访问控制列表(ACL)把访问权授予此账户了。
有时候,访问这其中某些资源对于一个服务的操作确实是至关重要的,但其他的对象应该要针对该服务保护起来。以前的做法是,通过在本地系统账户中运行以获得所需要的资源为了避免这样做,一个服务将运行在一个标准用户的账户之下,同时把系统对象加入到ACL中这大大增加了恶意代码攻击系统的风险。另一种方案是,创建专门的服务账户,并且为每个账户(与某个服务关联) 设置专门的ACL,但这种做法很容易形成一个管理瓶颈。
Windows现在将这两种做法结合到一个更加可管理的方案之中:它允许服务运行在一个非特权的账户中,但是仍然可以访问特定的特权资源,又不降低这些对象的安全性。以一种类似于Windows Vista之前的第二种方案,一个对象的ACL现在可以直接为一个服务设置许可而不要求专门的账户。相反地,SCM生成一个服务SID来代表一个服务,此SID可以用于为像注册表键和文件这样的资源设置许可。服务SID是在宿纳服务的进程令牌的组SID中实现的。它们是在系统启动过程中,为每个通过ChangeServiceConfig2API来请求服务ID的服务而生成的。在服务宿主进程(包含多个服务的进程) 的情形下,该进程的令牌将包含所有服务的服务SID,这些服务都是与该进程关联的服务组的一部分,也包含那些尚未启动的服务,因为在一个令牌被创建以后就无法再增加新的SID。
作为对象访问的细粒度控制方法,为每个服务有一个SID,其有用性超出了仅仅有能力为系统上的各个对象增加ACL项和许可设置。我们最初讨论的情形是,系统上可被某个给定账户访问的特定对象,必须要保护起来,以免被同样账户中运行的服务访问。随着我们讲述到现在这个程度,通过服务SID,只需把与服务SID相关联的Deny项放在每个需要被保护的对象上,就可以阻止这一问题。这显然不是一个可容易管理的方法。
为了避免使用Deny访问控制项(ACE)作为防止服务访问那些该服务运行所在账户具有访问权的资源另外还有两种类型的服务SID:受限制的服务SID(SERVICE SID TYPE_RESTRICTED)和非受限的服务SID (SERVICE SID TYPE UNRESTRICTED),后者是默认的,我们现在来看一看它的使用情形。
非受限的服务SID是作为默认启用的组所有者SID而创建的,进程令牌也被赋予一个新的ACE:为服务登录SID赋予完全的权限许可,这就使得该服务可以继续跟SCM进行通信。(一个主要的用途是,在服务启动或停止过程中,允许或禁止该进程内部的服务SID。)
另一方面,受限制的服务SID,将服务宿主进程的令牌转变成一个写限制的令牌(关于令牌的更多信息,参见第6章),这意味着,只有给服务SID赋予显式写访问权的对象才对该服务是可写的,不管该服务运行在哪个账户下。由于这个原因,运行在该进程中的所有服务(属于同一个服务组)必须具有受限制的SID类型;否则,具有受限制SID类型的服务将不能启动。出于兼容性的原因,一旦此令牌变成写限制的,就会加入三个额外的SID:
- 全局SID加入进来,以便允许对通常情况下任何人无论如何(最重要的是,加载路径中的特定DLL)都可以访问的对象进行写访问。
- 服务登录SID加入进来,以便允许该服务可以与SCM进行通信。
- 写限制的SID加入进来,以便允许对象可以显式地允许任何写限制的服务对它们进行写访问。例如,ETW (Event Tracing for Windows)在它的对象上使用此SID,以便允许任何写限制的服务能够生成事件。
图4.8显示了一个服务宿主进程的例子,该进程包含的服务已经被标记为具有受限制的服务SID。例如,BFE (Base FilteringEngine)负责应用Windows的防火墙过滤规则,它是该服务的一部分,因为这些规则被存放在注册表键中,而这些注册表键是必须要保护的,以避免在服务被攻破的情况下它们被恶意写访问。(例如,这可能会导致一个服务攻击手法,禁止向外流量的防火墙规则,从而与攻击者建立起双向通信。)
对于一个服务本来可以写访问的那些对象 (通过继承进程账户所具有的访问许可来获得写访问权限),在阻止了对它们的写访问以后,受限制的服务SID解决了最初我们展示过的那个问题的另一个方面,因为用户无须做任何事情就可以防止一个服务在特权账户下运行,避免对关键的系统文件、注册表键或其他对象的写访问,从而限制了这些服务可能被攻破之后暴露出来的进一步攻击。
Windows也允许在防火墙规则中引用服务SID,将它们链接到表4.10所述的三种行为之一。
交互式服务和会话0隔离
对于在Windows中常见的,在本地系统账户、本地服务账户和网络服务账户下运行的服务一个限制是,它们不能(如果不在MessageBox函数中使用一个特殊标志的话,稍后会讨论这一标志)在交互式用户的桌面上显示对话框或者窗口。这一限制并非是由于它们运行在这些账户下所导致的直接结果,而是由于Windows子系统将服务进程分配给窗口站的做法所导致的结果。利用会话的概念,在一个称为会话零隔离 (Session Zero lsolation)的模型中,这一限制被进一步得到了增强:此模型的一个结果是,这些服务不能直接与用户的桌面进行交互。Windows子系统将每个Windows进程与一个窗口站(windowstation)关联起来。窗口站包含了桌面,桌面包含了窗口。在一个控制台上只有一个窗口站是可见的,它可以接收用户的鼠标和键盘的输入。在终端服务环境中,每个会话有一个窗口站是可见的,但是所有的服务都作为控制台会话的一部分来运行。Windows将可见的窗口站命名为WinSta0,所有的交互式进程都访问WinSta0。
除非另行指定,否则,Windows子系统都将运行在本地系统账户下的服务跟一个名为Service-0x0-3e7S的非可见窗口站关联起来,所有的非交互式服务都共享此窗口站。此名称中的数字3e7,代表了LSASS(本地安全权威进程)分配给登录会话的登录会话标识符:对于运行在本地系统账户下的非交互式服务,SCM都使用此登录会话。
凡是配置了要运行在某个用户账户(也就是说,不是本地系统账户)下的服务,都运行在一个不同的非可见窗口站中,此窗口站是用LSASS分配给该服务的登录会话的登录标识符来命名的。图4.9显示了Sysinternals Winobj工具的一个输出示例,它显示的是Windows专门放置窗口站对象的对象管理器目录。从其中可以看到交互式窗口站(WinSta0)非交互式系统服务窗口站 (Service-0x0-3e7S)。
不管一个服务是运行在用户账户下,或是运行在本地系统账户下,或是本地服务账户下,还是运行在网络服务账户下,只要它没有运行在可见的窗口站中,就不能接收来自用户的输入,或者在控制台上显示窗口。实际上,如果一个服务要在该窗口站上弹出一个普通的对话框,那么,该服务就好像挂起了一样,因为没有用户能看到该对话框,当然,用户也就无法提供键盘或者鼠标的输入来解除对话框,从而无法让服务继续执行下去。
注: 在过去,即使一个服务被标记为非交互式的,它也有可能在MessageBox API中使用特殊的标志MBSERVICENOTIFICATION或MB DEFAULT DESKTOPONLY,从而在交互式窗口站上显示消息。现在,由于会话隔离的原因,任何使用此标志的服务将会立即接收到IDOK返回值,但消息框却永远不会显示出来。
在很少的情况下,一个服务可以有充分的理由通过对话框或窗口来与用户进行交互。为了将一个服务配置成有权利与用户进行交互,SERVICE INTERACTIVEPROCESS修饰符必须出现在该服务的注册表键的Tvpe参数中(注意,凡是被配置成在一个用户账户下运行的服务,不能被标记为交互式的)。当SCM启动一个被标记为交互式的服务时,它在本地系统账户的安全环境中激发起该服务的进程,但是将该服务与WinSta0连接起来,而不是与非交互式的服务窗口站连接在一起。
用户进程也将与服务运行在同一个会话中的,服务进程与WinStao的连接使得该服务可以在控制台上显示对话框和窗口,也允许这些窗口可以响应用户的输入,因为它们与交互式服务共享窗口站。然而,只有属于系统的进程和Windows服务才在会话0中运行,所有其他的登录会话,包括控制台用户的登录会话,都运行在其他不同的会话中。因此,会话0中的进程所显示的窗口对用户是不可见的。
这一额外的边界有助于抵挡住粉碎攻击(shatter attack),在这种攻击中,一个低特权的应用程序向同一个窗口站上可见的窗口发送窗口消息,以便挖掘出拥有该窗口的高特权进程中的软件错误,从而使得在高特权进程中执行代码。
为了与那些依赖于用户输入的服务之间保持兼容性,Windows包含一个服务,用于当一个服务显示一个窗口时可以通知用户。交互式服务检测(UIODetect)服务寻找会话0的WinStao窗口站的主桌面上的可见窗口,并且在控制台用户的桌面上显示一个通知对话框,使用户可以切换到会话0,并且查看该服务的UI。(这类似于连接到一个本地终端服务会话,或者切换不同的用户。
注交互式服务检测机制纯粹是为了应用兼容性而设计。强烈建议开发人员不要使用交互式服务而使用一个二级的、非特权的辅助应用与用户进行通信和交互。当接收到UI输入以后,在这一辅助应用与配置目的的服务之间可以使用本地RPC或COM。
图4.10中显示的对话框例子包含了进程名称、当UI消息显示时的时间,以及要被显示的窗口的标题。一旦用户连接到会话0,一个类似的对话框也会出现,以提供一个回到用户会话的入口。在图中,显示窗口的服务是Microsoft Paint,它是由Sysinternals的PsExec工具显式地启动起来的,在PsExec工具的参数选项中指定了在会话0中运行画图程序。你可以自己利用下面的命令来试一试:
psexec -s -i 0 -d mspaint.exe
此命令告诉PsExec,在会话0中 (0)以系统进程的形式(-s来运行画图程序,并且立即返回,而不是等待该进程结束(-d)。
如果单击“ViewThe Message”,可以切换到会话0的控制台(在控制台上通过一个类似的窗口再切换回来)。
4.3.3 服务控制管理器
SCM的可执行文件是%SvstemRoot%\System32\Servicesexe,如同大多数服务进程一样,它也是作为一个Windows控制台程序来运行的。Wininit进程在系统引导的早期将SCM启动起来(有关引导过程的细节,请参见本书下册第13章)。SCM的启动函数,即SvcCtrlMain,会有条不紊地将那些被配置成自动启动的服务激发起来。
SvcCtrlMain首先创建一个名为SvcctrlStartEvent A3752DX的同步事件,该事件被初始化成无信号状态。只有当SCM完成了必要的步骤来准备接收SCP的命令以后,它才将该事件设置成有信号状态。SCP用来与SCM建立对话的函数是OpenSCManager。OpenSCManager等待SvcctrlStartEvent_A3752DX事件变成有信号状态,因而,SCP在SCM完成初始化以前,不必总是试图与SCM联络。
接下来,SvcCtrlMain开始做它自己的事情,并调用ScGenerateServiceDB,该函数负责建立起SCM的内部服务数据库。ScGenerateServiceDB 读取并存储 HKLMSYSTEM)CurrentControlSet\ControlServiceGroupOrder\List的内容,这是一个REGMULTI SZ值,它列出了定义好的服务组的名称和顺序。如果一个服务或者设备驱动程序需要控制其相对于其他组中的服务的启动顺序,那么,该服务的注册表键包含一个可选的Group值。例如,Windows网络栈是由底向上建立起来的,所以,涉及网络的Windows服务必须指定Group值,以便在启动序列中将它们放在网络设备驱动程序的后面。SCM内部创建了一个组列表,此列表保留了SCM从注册表中读入的组顺序。这些组包括(但不限于)NDIS、TDI、Primary Disk、Keyboard Port和Keyboard Class。加载件和第三方应用程序甚至可以定义它们自己的组,并且将这些组加入到列表中。例如,Microsoft Transaction Server添加了一个名为MS Transactions的组。
然后,ScGenerateServiceDB扫描HKLM\SYSTEM\CurrentControlSet\Services
中的内容,并且在服务数据库中为它所碰到的每一个键创建一个条目。数据库条目包含了所有为一个服务而定义的、与服务有关的参数,以及一些记录了该服务状态的域。SCM既为设备驱动程序添加条目,也为服务添加条目,因为SCM将所有标记为自动-启动的服务和驱动程序都启动起来,并且对于那些标记为引导-启动和系统-启动的驱动程序,检测其启动失败的情况。它还提供了一种方法,让应用程序可以查询驱动程序的状态。在任何用户模式进程执行起来以前,I/O管理器将那些标记为引导-启动和系统-启动的驱动程序加载到系统中,因此,凡是这些启动类型的驱动程序,都在SCM启动之前被加载到系统中。
ScGenerateServiceDB读取一个服务的Group值,以确定它是否是一个组的成员,并且将这个值与先前创建的组列表中该组的条目关联起来。此函数也通过查询该服务的DependOnGroup和DependOnService注册表值,读取到该服务的组相依性和服务相依性,并将它们记录到数据库中。图4.11显示了SCM如何组织服务条目和组顺序列表。注意,服务列表是按字母顺序来排列的。此列表之所以按字母序来排列,是因为,SCM根据Services注册表键来创建此列表,而windows按字母顺序来存储注册表键。
在服务启动过程中,SCM调用LSASS(比如,在一个非本地系统账户中登录一个服务),所以SCM等待LSASS发出LSA RPC SERVER ACTIVE同步事件,当LSASS完成了初始化时它就会发出此信号。Wininit也启动LSASS进程,所以,LSASS的初始化与SCM的初始化是同时进行的,LSASS和SCM谁先完成初始化的顺序是不确定的。
然后,SvcCtrlMain 调用ScGetBootAndSystemDriverState来扫描服务数据库,以寻找那些自动-启动的和系统-启动的设备驱动程序条目。
ScGetBootAndSystemDriverState确定一个驱动程序是否已成功启动,它的做法是,在名为Driver的对象管理器名字空间目录中查找该驱动程序的名称。当一个设备驱动程序被成功地加载时,I/O管理器将该驱动程序的对象插入到名字空间中该目录下面,所以,如果它的名称没有出现的话,则它还没有被加载进来。在图4.12中,Winobj显示了Driver目录的内容。SvcCtrMain将所有尚未启动的、包含在当前轮廓中的驱动程序的名称记录在一个名为ScFailedDrivers的列表中。
在启动那些“自动-启动”的服务以前,SCM还要执行其他的一些步骤。它创建一个名为PipelNtsvcs的远过程调用(RPC)命名管道,然后,RPC激发一个线程,在该管道上监听来自SCP的进入消息。然后,SCM将它的初始化完成事件SvcctrlStartEvent A3752DX设置成有信号状态。注册一个控制台应用程序停机事件处理器,以及通过RegisterServiceProcess向Windows子系统进程进行注册,这些都为SCM做好了系统停机的准备。
网络驱动器字母
SCM除了作为服务接口的角色以外,它还有另一项完全不相关的责任:无论何时当系统创建或者删除一个网络驱动器字母连接时,它都要通知系统中的GUI应用程序。SCM等待MPR(Multiple Provider Router)用信号通知一个命名的事件 BaseNamedObjects ScNetDrvMsg,当应用程序将一个驱动器字母分配给一个远程网络共享文件夹,或者删除了一个远程共享文件夹的驱动器字母分配时,MPR就会用信号通知此事件(有关MPR的更多信息,请参见第7章“网络”)。
当MPR用信号通知该事件时,SCM调用Windows函数GetDriveType来查询已连接的网络驱动器字母的列表。如果在事件信号之间该列表改变了的话,则SCM发送一个类型为WMDEVICECHANGE的Windows广播消息。SCM使用DBT DEVICEREMOVECOMPLETE或DBT DEVICEARRIVAL作为该消息的子类型。此消息主要是给WindowsExplorer使用的,所以,它可以更新任何已打开的Computer窗口,显示出网络驱动器字母的存在与否。
4.2.4 服务启动
SvcCtrlMain调用SCM函数ScAutoStartServices来启动所有已被指定为“自动-启动”(在Start值中)的服务(除了那些延迟的自动-启动服务)。ScAutoStartServices也会将“自动-启动”的设备驱动程序启动起来。为了避免混淆,你应该将术语“服务”看成是“服务和驱动程序”除非另行指定。ScAutoStartServices中按照正确顺序来启动服务的算法分阶段进行处理,每个阶段对应于一个组,这些阶段按照HKLM\SYSTEMCurrentControlSetlControNServiceGroupOrdelList注册表值中存储的组顺序所定义的序列进行处理。如图4.13所示,List值包含了组的名称,其顺序正是SCM启动这些组的顺序。因此,将一个服务分配给一个组,这对于属于其他组的其他服务,在启动顺序上没有任何影响
当一个阶段开始时,ScAutoStartServices将所有属于此阶段的组的那些服务条目标记出来以便启动它们。然后,ScAutoStartServices循环检查这些标记出来的服务,看它是否能将每-个服务启动起来。
作为检查过程的一部分,它要看这个服务是否被标记为延迟的自动-启动若是的话,SCM将在后面的步骤中启动该服务。(延迟的自动-启动服务必须不属于任何一个组).作为检查过程的另一部分,它根据这个服务的注册表键中的DependOnGroup值是否存在,来确定这个服务是否依赖于其他的组。如果存在这种相依性,则它所依赖的那个组必须已经初始化,该组中至少有一个服务已经成功启动起来。如果在组启动序列中,该服务所依赖的那个组,比该服务所属的组还要迟,那么,SCM记录下该服务有一个“循环相依性”错误。如果ScAutoStartServices正在检查的是一个Windows服务,或者一个“自动启动”的设备驱动程序那么,它接下来检查该服务是否依赖于其他一个或者多个服务,如果是的话,它查看这些服务是否已经启动了。服务相依性是由一个服务的注册表键中的DependOnService值来指定的如果一个服务依赖于其他一些服务,并且这些服务所属的组位于ServiceGrouporderl List的后面,那么,SCM也会产生一个“循环相依性”错误,并且不会启动该服务。如果该服务依赖于同一个组中的任何其他尚未启动的服务,则该服务将被跳过去。
当一个服务的相依性已经被验证通过以后,ScAutoStartServices在启动该服务之前还要做最后的检查,看该服务是否是当前引导配置的一部分。当系统以安全模式来引导时,SCM要确保:在适当的安全引导注册表键中,该服务或者是按名称来标识的,或者是按组来标识的。注册表中有两个安全引导键:Minimal和Network,它们位于HKLM\SYSTEMCurrentControlSetControlSafeBoot的下面;SCM到底检查哪一个,取决于用户引导的是哪一种安全模式。如果用户在专门的引导菜单(在引导过程中,按下F8就可以看到该菜单)中选择的是Safe Mode或者Safe Mode With Command Prompt,则SCM使用的是Minimal键;如果用户选择的是Safe ModeWith Networking,则SCM使用的是Network键。若SafeBoot键下存在一个名为Option的字符串值,则不仅表明了当前系统是按安全模式引导起来的,而且也指出了用户选择的安全模式类型。有关安全引导的更多信息,请参见本书下册第13章中的“安全模式”一节。
一旦SCM决定要启动一个服务,它就调用ScStartService,该函数对于服务和设备驱动程序分别采取不同的步骤。当ScStartService启动一个Windows服务时,它首先读取该服务的注册表键中的ImagePath值,以确定该服务进程的映像文件名。然后,它检查该服务的Type值,如果此值是SERVICE WINDOWS SHARE PROCESS (0x20),那么,SCM保证: 该服务运行所在的进程如果已经启动了的话,其登录的账户一定与该服务的指定启动账户相同。(这也确保了,该服务没有被配置成错误的账户,比如LocalService账户,但映像路径却指向一个正在运行的5vchost,比如netsvcs,它以LocalSystem来运行。)一个服务的ObjectName注册表值存储了该服务应该在哪个账户下运行。如果一个服务没有ObjectName值,或者它的ObjectName值为LocalSystem,则该服务运行在本地系统账户中。
SCM在一个称为映像数据库(image database)的内部数据库中,检查是否有针对该服务的ImagePath值的条目,以便验证该服务的进程尚未在其他的账户下被启动起来。如果在映像数据库中没有找到此ImagePath值的条目,则SCM创建一个这样的条目。当SCM创建一个新的条目时,它还将该服务的登录账户名,以及该服务的ImagePath值中的数据也存储起来。SCM要求Windows服务有一个magePath值。如果一个服务没有ImagePath值,则SCM报告一个错误,表示它不能找到该服务的路径,从而无法启动该服务。如果SCM在映像数据库中找到了一个与该服务的ImagePath数据相匹配的条目,那么,它要保证:它当前正在启动的服务的用户账户信息与数据库条目中存储的信息是相同的一一一个进程只能以一个账户的身份来登录,所以,当一个服务指定的账户名与同一个进程中已经启动起来的其他服务的账户名不相同时,SCM会报告一个错误。
SCM调用ScLogonAndStartlmage来登录一个服务(如果该服务的配置中指定了的话),并启动该服务的进程。SCM通过调用LSASS函数LogonUserEx来登录那些并非运行在系统账户下的服务。LogonUserEx在通常情况下要求一个口令,但是SCM告诉LSASS,该口令保存在注册表中HKLM\SECURITYPolicySecrets的下面,是该服务的LSASS“秘密”(记住,涉及安全的内容通常是不可见的,因为它的默认安全设置只允许系统账户访问)。当SCM调用LogonUserEx时,它指定了登录类型参数为服务登录,所以,LSASS在Secrets子键的名为_SC服务名>的子键下查找口令。
当SCP配置一个服务的登录信息时,SCM利用LsaStorePrivateData函数来指示LSASS将一个登录口令保存到Secrets子键下。在登录成功以后,LoonUserEx给调用者返回一个句柄,指向个访问令牌。Windows使用访问令牌来代表一个用户的安全环境,以后,SCM将该访问令牌与实现此服务的进程关联起来。
在成功登录以后,如果此账户的轮廓信息还没有被加载的话,SCM通过调用UserEnv DLL(%SystemRoot%\System32\Userenvdl)的LoadUserProfile函数将此账户的轮廓信息加载进来HKLM\SOFTWARE\Microsoft(Windows NT\CurrentVersio\Profilelist<用户轮键>ProfileImagePath值包含了LoadUserProfile需要加载的注册表储巢在磁盘上的位置,从而使得该储巢中的信息成为该服务的HKEYCURRENTUSER键。
交互式的服务必须打开Winta0窗口站,但是,ScLogonAndStartlmage在允许一个交互式服务访问WinSta0以前,它要查 HKLMSYSTEMCurrentControlSet\ControWindowsNolnteractiveServices值是否已被设置。管理员设置此值以后,可以防止那些被标记为交互式的服务在控制台上显示窗口。在无人值守的服务器环境中,此选项是非常合适的,因为在这种环境中,交互式服务的会话o UI Discovery通知是没有人来响应的。
下一个步骤是,如果该服务的进程尚未被启动(例如,为了另一个服务),则ScLogonAndStartlmage为该服务激发一个进程。SCM通过Windows函数CreateProcessAsUser来启动此进程,并且将该进程的状态设置为挂起状态。接下来,SCM创建一个命名管道,以后它通过该管道与服务进程进行通信,它分配给管道的名称为\Pipe\Net\NtControlPipeX,这里X是一个数字,每次SCM创建一个管道,该数字就会递增。然后,SCM通过ResumeThread函数来恢复服务进程的执行,并且等待该服务连接到它的SCM管道上。如果注册表值HKLM\SYSTEM\CurrentControlSet\ControServicesPipeTimeout存在的话,则它决定了SCM等待一个服务调用StartServiceCtrlDispatcher并连接过来的时间长度,如果在这么长时间里没有等到则SCM就会放弃,终止该进程,并得出结论:该服务未能启动。如果ServicesPipeTimeout不存在,则SCM使用默认的30秒作为超时间隔值。SCM对于它所有的服务通信都使用同样的超时间隔值。
当一个服务通过它的管道连接到SCM时,SCM向该服务发送一个启动命令。如果该服务未能在超时间隔内肯定地响应此启动命令,SCM就会放弃,并且转移到启动下一个服务。当一个服务没有对启动请求作出响应时,SCM并不会像一个服务在超时间隔内没有调用StartServiceCtrlDispatcher的情形那样终止该进程,相反,它会在系统的事件日志(Event Log)中记录一个错误,指明该服务未能及时地启动起来。
如果SCM调用ScStartService启动的服务有一个Type注册表值为SERVICE KERNEL DRIVER或SERVICE_FILE_SYSTEM_DRIVER,那么,该服务确实是一个设备驱动程序,所以,ScStartService调用ScLoadDeviceDriver来加载该驱动程序。ScLoadDeviceDriver首先使SCM进程具有加载驱动程序的安全特权,然后调用内核服务NtLoadDriver,将该驱动程序的注册表键中的ImagePath值的数据传递过去。与Windows服务不同的是,驱动程序不需要指定lmagePath值:如果该值不存在的话,SCM通过将驱动程序的名称附加在字符串%SystemRoot%(System32\Drivers\的后面就可以构造一个映像文件路径。
ScAutoStartServices继续循环处理同属于一个组的服务,直到所有这些服务要么被启动起来,要么产生相依性错误。这种循环处理方式是SCM根据一个组中的服务的DependonService相依性来自动对它们进行顺序处理的。SCM在较早的循环中启动那些被其他服务依赖的服务,跳过那些依赖于其他服务的服务,而在后续的循环中再启动这些服务。注意:SCM忽略了Windows服务的Tag值,你可能会在HKLM\SYSTEM\CurrentControlSet\Services键的子键下面看到这些值:I/0管理器利用Ta值来排列一个组内的引导-启动(boot-start)和系统-启动 (systemstart)的设备驱动程序的启动顺序。一旦SCM完成了ServiceGroupOrder\List值中列出的所有组。
的启动阶段,它再为那些属于其他组(未列在该值中)的服务执行一个单独的阶段,最后,再
为那些不属于任何一个组的服务执行一个阶段。SCM在处理了自动-启动的服务以后,调用ScinitDelayStart,该函数将一个延迟的工作项目加入队列中,该工作项目与一个专门的辅助线程相关联,它负责处理所有由于被标记为“延迟的自动-启动”而被ScAutoStartServices忽略掉的服务。此辅助线程将在一段时间的延迟之后执行,默认的延迟是120秒,但是通过在HTML\SYSTEM\CurrentControlSet\Control中创建一个AutoStartDelay值,可以覆盖这一默认值。就如同非延迟的自动-启动服务一样,SCM在这些延迟的自动-启动服务的启动过程中也执行同样的动作。
延迟的自动-启动服务
延迟的自动-启动服务使得Windows可以处理越来越多的当用户登录时候需要启动的服务这些服务数量大了以后,使得引导过程陷于停顿,用户需要等待很长时间才能从桌面得到响应。自动-启动服务的设计目标主要是为那些在引导过程早期必需的服务,因为其他的服务要依赖它们。一个很好的例子是RPC服务,所有其他的服务都要依赖它。另一个用途是,允许一个服务不被留意到就可以启动起来,比如Windows Update服务。因为许多自动-启动服务都落在第二类别中,所以,将它们标记为延迟的自动-启动可以让关键的服务启动得更快,而且在引导以后当用户登录时可以更快地准备好用户的桌面。此外,这些服务在后台模式下运行,这也降低了它们的线程、I/O和内存的优先级。把一个服务配置成延迟的自动-启动,要求调用ChangeServiceConfig2AP1。你也可以使用scexe的qc位选项来检查一个服务的这一标志状态
注如果一个非延迟的自动-启动服务有一个延迟的自动-启动服务作为它的依赖服务之一,那么这一延迟的自动-启动标志将被忽略,该服务将被立即启动,以满足相依性要求。
当SCM完成了启动所有这些自动启动的服务和驱动程序,以及设置了延迟的自动-启动的工作项目时,它发信号通知\BaseNamedObjects(SC_AutoStartComplete事件。这一事件被Windows Setup程序用于在安装过程中衡量启动过程。
4.2.5 启动错误
如果一个驱动程序或者服务报告一个错误,作为对SCM启动命令的响应,那么该服务的注册表键的ErrorControl 值决定了SCM如何作出反应。如果ErrorControl值是SERVICE_ERROR_IGNORE0),或者ErrorControl值没有被指定,那么,SCM只是简单地忽略该错误,继续处理它的服务启动工作即可。如果ErrorControl值是SERVICE ERROR NORMAL(1),那么,SCM向系统事件日志中写入一个事件,它这样说:“由于以下错误,故<服务名>服务未能启动 (The service failed to start due to the following error):”。该服务向SCM返回一个Windows错误代码作为启动失败的原因;SCM在事件日志记录中包含了此Windows错误
代码的文本表示。图4.14显示了这样的事件日志记录,它报告了一个服务启动错误。
如果一个服务报告的ErrorControl值是SERVICE_ERROR SEVERE(2)或SERVICE_ERRORCRITICAL(3),那么,SCM会在事件日志中加入一条记录,然后调用内部函数ScRevertToLastKnownGood。此函数将系统的注册表配置切换到一个名为“最后已知的好控制集 (last known good)”的版本,此版本是该系统最后一次成功引导的注册表配置。然后,它利用执行体中实现的NtShutdownSystem系统服务来重新启动该系统。如果该系统当前已经是在利用“最后已知的好控制集”来引导了,那么,该系统仅仅重新启动而已。
4.2.6 接受当前引导和“最后已知的好控制集"
除了启动服务以外,系统也要让SCM来决定:应该何时将系统的注册表配置HKLMSYSTEM\CurrentControlSet保存为“最后已知的好控制集”。CurrentControlSet键包含了Services键,作为它的一个子键,所以,CurrentControlSet包含了SCM数据库的注册表表示形式。它也包含了Control键,其中保存了许多内核模式和用户模式的子系统配置信息。在默认情况下,一次成功的引导是由“所有自动-启动服务的成功启动”和“一次成功的用户登录”构成的。如果由于一个设备驱动程序在引导过程中使系统崩溃了,或者由于一个自动-启动的服务报告了一个启动错误(其ErrorControl值为SERVICE ERROR SEVERE或SERVICE ERROR CRITICAL)因而导致系统停止,则引导就失败了。
很显然。SCM知道什么时候它已经完成了所有自动-启动服务的成功启动,但是Winlogon(%SystemRoot%\System32\Winlogon.exe)必须通知它什么时候有了一次成功的登录。当一个用户登录的时候,Winlogon调用Notlfy8ootConfigStatus函数,而NotifyBootConfigStatus则会向SCM发送一个消息。在所有自动-启动的服务都成功启动以后,或者CM接收到了NotifyBootConfigStatus发送过来的消息以后(看哪个来得更晚一些),SCM训用系统函数NtlnitializeRegistry,将当的注册表的启动配置保存下来。
第三方软件开发人员可以用他们自己的定文来代替Winlogon对于一次成功登录的定文。例如,运行Microsoft SQL Server的系统可能要等到SQL erver能够接受或者处理数据库事务之后才认为引导成功了。开发人员只要编写一个引导确认程序并且安装该程序,就可以强加上他们自己的对于一次成功引导的定义,安装方法很简单,只要让注册表键HKLM\SYSTEM\CurrentControlSet\ControNBootVerificationProgram中存储的值指向它在磁盘上的位置即可。而且,安装的引导确认程序必须禁止Winlogon对于NotifyBootConfi5tatus的调用其做法是,将HKLM\SOFTWARE\Microsoft)Windows NTCurrentVersion\Winlogon\ReportBootk设置为0。在一个引导确认程序被安装到系统中以后,SCM在完成了所有自动-启动的服务以后会激发该程序,然后等待它调用NotlfybootConfigStatus,再保存“最后已知的好控制集”Windows维护了CurrentControlSet的几份持贝,CurrentControlSet实际上是一个注册表符号链接,它指向其中的一份拷贝,这些控制集有着形如HKLM\SYSTEMYControlSetnnn这样的名称,其中nnn是诸如001或者002这样的数值。HKLM\SYSTEM\Select键中包含的值指出了每一份控制集的角色。例如,若CurrentControlSet 指向ControlSet001,那么Select下的Current值就是1.Select下的LastKnownGood值包含了“最后已知的好控制集”的数值,这是最后一次用来成功引导的控制集。在你的系统上另一个可能出现在Select键下的值是Failed,它指向最后一次引导不成功并且即使在“最后已知的好控制集”的帮助下也未能成功引导而不得不退出的控制集。图4.15显示了一个系统的控制集和Select值。
NtinitializeRegistry取出“最后已知的好控制集”中的内容,并且与CurrentControlSet键树中的控制集进行同步。如果这是系统第一次成功引导,则“最后已知的好控制集”并不存在,于是系统将创建一个新的控制集,如果“最后已知的好控制集”已经存在了,则系统只是简单地
用它与CurrentControSet之间的差值对它进行更新。“最后已知的好控制集”对于因CurrentControl5et的一个变化而引起随后引导失败的情形是非常有帮助的,比如在HKLM\SYSTEM)Control下面有一个调节系统性能的值被修改了,或者新增加了一个服务或设备驱动程序。用户可以在引导过程的早期按下F8,得到一个菜单,然后在菜单中直接选择使用“最后已知的好控制集”,将系统的注册表配置回滚到最后一次系统成功引导时的配置。本书下册第13章将会更加详细地讨论如何使用“最后已知的好控制集”和其他的恢复机制来诊断系统启动时出现的问圈。
4.2.7 服务失败
在一个服务的注册表键中,可以有可选的FailureActions和FailureCommand值,这是SCM在该服务的启动过程中记录下来的。由于SCM向系统进行了登记,因而当服务进程退出时系统可以发信号通知SCM。当一个服务进程意外终止时,SCM确定该进程中运行的是哪些服务,并且根据其与失败有关的注册表值中指定的恢复步骤来采取行动。而且,一个服务能够为在崩溃或意外服务终止过程中请求的失败动作也是受限制的,因为有些其他的问题,比如内存泄漏,也可能会导致服务失败。
如果一个服务进入SERVICE STOPPED状态,并且返回给SCM的错误代码不是ERROR SUCCESS,那么,SCM将检查该服务是否设置了FailureActionsOnNonCrashFailures标志,并执行同样的恢复动作就好像该服务崩溃了一样。为了使用这一功能,该服务必须通过ChangeServiceConfig2API来进行配置,将FailureActionsOnNonCrashFailures设置为1:或者系统管理员使用Scexe工具的Failureflag参数,将FailureActionsOnNonCrashFailures设置为1。默认的值为0,SCM将会继续保持windows早期版本中为所有其他服务那样的行为。
一个服务可以为SCM配置的动作包括重新启动该服务、运行一个程序,以及重新引导计算机。而且,一个服务可以指定当它第一次服务进程失败、第二次失败,以及后续失败时可以采取的失败动作,它也可以指定:如果该服务要求重新启动的话,那么,SCM在重启该服务之前要等待多长时间。IISAdmin ervice的服务失败动作是导致SCM运行ISReset应用程序,该程序会执行一些清理工作,然后重新启动该服务。你利用Services MMC加载件中一个服务的Properties对话框中的Recovery标签页面,就能很容易地管理该服务的恢复动作,如图4.16所示。
4.2.8 服务停机
当Winlogon调用Windows的ExitWindowsEx函数时,ExitWindowsEx向Windows子系统进程Csrss发送一个消息,以便调用srss的停机例程。Csrss对所有活动的进程进行循环,通知它们系统正在停机。对于除了SCM以外的每一个系统进程,Csrss都要等待到由HKUNDEFAULT\ControPaneDesktolWaitToKillAppTimeout指定的秒数(默认值为20秒),以便让该进程退出,然后再转移到下一个进程。当Csrss碰到SCM进程时,它也会通知SCM进程系统正在停机,但是使用一个专门针对SCM的超时间隔值。Csrss使用进程ID来识别SCM,当SCM在系统初始化过程中利用RegisterServicesProcess函数向Csrss登记时,Csrss将SCM的进程ID保存下来了。SCM的超时间隔值之所以不同于其他的进程,是因为,Csrss知道,SCM与那些在停机时需要执行清理工作的服务进行通信,所以,管理员可能需要单独调节SCM的超时间隔值。SCM的超时间隔值位于HKLM\SYSTEM\CurrentControlSet\ControlWaitTokilServiceTimeout注册表值中,其默认值是12秒。
凡是在初始化时请求SCM发送停机通知的Windows服务,SCM的停机处理器 (shutdownhandler)负责给它们发送停机通知。SCM函数ScShutdownAlIServices在SCM服务数据库中进行循环,找到那些希望得到停机通知的服务,并且给每一个这样的服务发送一个停机命令。对于发送了停机命令的每一个服务,SCM记录下该服务的“等待提示(waithint)”值,这是该服务在向SCM登记时指定的一个值。SCM跟踪记录了它所接收到的最大的等待提示值。在发送了停机消息以后,SCM开始等待,要么等到它所通知的服务有一个已经退出,或者等到指定的最大等待提示值已经过去。
如果等待提示值已经到期,仍然没有哪个服务退出,那么,SCM确定是否它所等待退出的一个或者多个服务已经向SCM发送了一个消息,告诉SCM该服务正在进行停机处理。如果至少有一个服务正在进行之中,那么,SCM再等待一个等待提示值周期。SCM继续执行此等待循环直至要么所有的服务都退出,要么在等待提示值周期内,它所等待的服务全都没有通知它停机的进度。
当SCM忙于告诉服务停机并等待它们退出的过程中,Csrss也在等待SCM退出。如果srss的等待结束了(即WaitToKillServiceTimeout时间到期),但SCM还没有退出,那么,Csrss杀掉SCM,继续它的停机过程。因此,未能及时停机的服务将被杀掉。这一逻辑可以让系统在面对那些由于不正确设计而永远无法完成停机过程的服务的情况下,也可以正常停机,但这也意味着,那些要超过20秒才能完成停机的服务将无法完成其停机操作。
而且,因为停机顺序是不确定的,所以,那些可能依赖于其他服务先停机的服务(称为停机相依性)无法将这种停机相依性报告给SCM,可能永远也没有机会来完成清理工作。为了满足这些需求,Windows实现了预停机通知和停机顺序来应对这两种情形引发的问题。对于通过SetServiceStatus API来请求预停机通知的服务,将使用与停机通知同样的机制来发送预停机通知,并且SCM将等待这些预停机通知被确认。
这些通知背后的思路是,将那些可能要花较长时间来完成清理工作的服务(比如数据库服务器服务)做好标志,并且给它们更多的时间来完成其工作。SCM将发送一个进度查询请求,并等待3分钟,以便让一个服务来响应此通知。如果该服务没有在此时间间隔内作出响应,它将在停机过程中被杀掉;否则,它可以根据其需要一直运行,只要它继续响应SCM即可。
参与预停机的服务也可以指定一个相对于其他预停机服务的停机顺序。依赖于其他服务先停机的服务(比如,组策略服务需要等待Windows Update先结束)可以在HKLMSYSTEM\CurrentControlSet\ControPreshutdownOrder注册表值中指定它们的停机依赖性。
4.2.9 共享的服务进程
在单独的进程中运行每一个服务,而不是在可能的情况下让多个服务共享同一个进程,必然会浪费系统资源。然而,共享进程也意味着,如果该进程中的任何一个服务出现了错误因而导致进程退出,则该进程中所有的服务都终止了。
在Windows的内置服务中,有些运行在它们自己的进程中,而有些则与其他的服务共享一个进程。例如,LSASS进程包含了与安全相关的一些服务,比如安全账户管理器(SamSs)服务、网络登录(Netlogon)服务,以及Crypto Next Generation (CNG) Keylsolation (Keylso)服务。
有一个名为“服务宿主(Service Host)”的“通用”进程(SvcHost,%SystemRoot%(System32Svchost.exe)可以包含多个服务。SvcHost的多个实例可以运行在不同的进程中。在SvcHost进程中运行的服务有电话 (TapiSrv) 服务、远过程调用 (RpSs)服务和远程访问连接管理器(RasMan)服务。运行在SvcHost中的服务被实现为DLL,Windows在该服务的注册表键中包含了一个形如“%SystemRoot%System32\svchost.exe -k netsvcs”这样的ImagePath定义。该服务的注册表键还必须有一个名为ServiceDll的注册表值,它位于Parameters子键下面,其中指向该服务的DLL文件。
共享同一个SvcHost进程的所有服务必须指定同样的参数(比如,在上一段例子中的“-knetsvcs”),所以,它们在SCM的映像数据中只有一个记录。当SCM在服务启动过程中碰到第个ImagePath为SvcHost且带有特定参数的服务时,它创建一个新的映像数据库记录,并且用此参数激发一个SvcHost进程。新的SvcHost进程带有此参数,它在HKLMSOFTWARE)Microsoft\Windows NT\CurrentVersion(Svchost下查找一个以此参数作为名称的注册表值。SvcHost读取该值的内容,并且将它解释成一个服务名称列表:当SvcHost向SCM登记时,它通知SCM,自己宿纳着这些服务。
当SCM在服务启动过程中碰到一个SvcHost服务,并且其ImagePath与映像数据库中已有的记录相匹配时,它不会再激发第二个进程,而是向早先为了该lmagePath值而启动起来的SvcHost发送一个启动命令,让它启动该服务。原先的SvcHost进程读入该服务注册表键中的ServiceDII参数,将该DLL加载到它的进程中,以便启动此服务。
表4.11列出了Windows上所有默认的服务组,有些服务向每一个组都进行了注册。
4.2.9 服务标记
使用服务宿主进程的一个缺点是,要计量某个特定服务的CPU时间和使用率,以及各种资源的使用率,都比较困难,因为每个服务共享了内存地址空间、句柄表,针对每个进程的CPU计量值也是与同一个服务组中其他的服务共享的。虽然在服务宿主进程内部总存在一个线程属于一个特定的服务,但是,这种关联性并不总是很容易建立起来。例如,一个服务有可能使用辅助线程来完成它的操作,或者该线程的启动地址和栈并没有暴露出该服务的DLL名称,这就使得很难确切地判断出一个线程在做哪种工作,以及它可能属于哪个服务。
Windows实现了一个称为服务标记的服务属性,当一个服务被创建的时候,或者当在系统引导过程中服务数据库被生成的时候,SCM调用ScGenerateServiceTag来生成服务标记。这一属性只是一个简单的索引,它标识了相应的服务。此服务标记被保存在每个线程的线程环境块(TEB)的SubProcessTag域中(关于TEB的更多信息,请参见第5章“进程和线程”),它也会被传播到由主服务线程创建的所有其他线程中(除了那些通过线程池API间接创建的线程以外)。虽然服务标记被保持在SCM的内部,但是有几个Windows工具,像Netstat.exe(你可以用此工具来显示哪些程序打开了哪些网络端口),利用未文档化的API来查询服务标记,并且将它们映射到相应的服务名称上。因为TCP/IP栈保存了创建TCP/IP端点的线程的服务标记,所以,当你通过-b参数来运行Netstat时,Netstat可以报告出由服务创建的端点的服务名称。另一个你可以用来查看服务标记的工具是ScTagQuery (来自 Winsider Seminars & Solutions Inc.www.winsidersscom/tools/sctagquery/sctagquery.htm)。它可以向SCM查询每个服务标记的映射关系,并且按照全系统范围或每个进程的角度来显示它们。它也可以向你显示,一个服务宿主进程内部的所有线程属于哪些服务。(这是有条件的:这些线程必须有一个适当的服务标记与它们相关联。)通过这种方法,如果你有一个失控的服务正在消耗大量的CPU时间,那么,即使线程的启动地址或者栈没有显然的服务DLL与之关联,你也可以标识出罪魁服务。
4.3 统一的后台进程管理器(UBPM)
传统上,随着Windows操作系统在各种特性上复杂性的增加,各种各样的Windows组件负责管理所宿纳的任务或者后台任务:
- 从前面讲述的服务控制管理器
- (SCM)到任务调度器(TaskScheduler),
- DCOM服务器激发器 (DCOM Server Launcher),
- 以及WMI提供者一一所有这些组件都在负责其宿纳的跨越进程的代码的执行。
今天,Windows实现了一个统一的后台进程管理器(UBPM,Unified Background Process Manager)
,它处理这些机制中的两种(至少两种,到目前为止):
- SCM
- 任务调度器
为这些组件提供了访问UBPM功能的能力。
UBPM是在Servicesexe中实现的,与SCM在同样的位置上,但是它作为一个独立的库,在RPC基础上提供了它自己的接口(类似于即插即用管理器,它也运行在Services.exe中,但是是一个独立的组件)。它通过一个公开的导出DLL (Ubpm.dll)来提供对此接口的访问。Ubpm.dll通过新的触发器API(TriggerAPI,已经被加入到SCM中了)暴露给第三方服务开发人员。然后SCM加载一个自定义的SCM扩展DLL (Scextdll),它再调用进入Ubpm.dli中。这一层间接性是为了支持MinWin而需要的,在MinWin中Scextdll并未被加载,SCM只提供了最小的功能。图4.17描述了这一体系结构。
4.3.1 初始化
UBPM是由SCM初始化的,这发生在当它的Ubpminitialize导出函数被SCM扩展DLL中的SzExtInitializeTerminateUbpm调用的时候。同样地,UBPM被实现成一个DLL,运行在SCM的环培而不是它自己的独立进程。
UBPM首先建立起它的内部工具库,作为初始化的开始。通过利用Windows新版本中诸多的改进措施,UBPM使用一个线程池来处理许多进来的事件(后面我们将会看到),这使得UBPM可以有很好的伸缩性:从单个辅助线程,到1000个辅助线程(根据最多处理10,000个消费者)。
接下来,UBPM初始化它的内部跟踪支持,这可以在HKLM\SOFTWARE\MicrosoftlWindowsNT\CurrentVersion\Tracing\UBPM\Regular
键中用一些标志值进行配置。这对于使用WPP跟踪机制(在Windows Driver Kit中有介绍)来调试和监视UBPM的行为很有帮助。
之后,建立起事件管理器,它将被UBPM后来的组件用于报告内部事件状态。事件管理器注册了一个GUID:TASKCHED,将来可通过此GUID来使用ETW事件,事件管理器把它的状态记录到一个TaskScheduler.log文件中。
下一个步骤对UBPM至关重要,它要初始化它自己的实时ETW消费者,这也是UBPM完成其任务的中心机制,因为它接收到的几乎所有数据都是通过ETW事件过来的。UBPM启动一个安全模式下的ETW实时会话,这意味着它将是唯一能够接收其事件的进程:它把此会话的名称命名为UBPM。为了接收与时间变化相关的通知,它也启用了第一个内置的提供者(由内核所拥有)。
然后它将一个事件回调函数UbpmpEventCallback与进来的事件关联起来,并创建一个消费者线程UbpmpConsumeEvents,该线程等待SCM用于通知“自动-启动事件已完成”的事件(此前已经提到过)。一旦等待完成,消费者线程调用ProcessTrace,该函数调用进入到ETW,并阻塞线程,直到ETW痕迹数据完成(通常,只有UBPM退出)。另一方面,随着每一个ETW事件的到来,事件回调函数会接收每个事件,并根据下一小节将要介绍的算法来处理该事件。
ETW自动回放所有在ProcessTrace被调用之前漏掉的事件,这意味着在引导过程中的内核事件也将会立即进来,并正确地进行处理。UBPM也在SCM的自动-启动事件上等待,这可以确保当这些事件进来的时候,至少有很多服务已经注册过要消费这些事件:否则,太早地启动痕迹跟踪将会导致有些事件还没有已注册的消费者,从而这些事件被丢掉。
最后,UBPM建立一个与TaskHost通信的本地RPC接口,这里TaskHost是UBPM的第二个组件稍后将进一步介绍。UBPM也建立起它自己的本地RPC接口,这一接口暴露的API可以让各种服务使用UBPM的功能(比如注册触发器提供者、生成触发器和通知,等等)。这些API是在Ubpm.dll库中实现的,它们使用RPC与Services.exe中UBPM代码的RPC接口进行通信。
当UBPM退出的时候,相反的动作按照相反的顺序执行,以便将系统重置到它以前的状态。
4.3.2 UBPM API
UBPM让Windows服务使用UBPMAPI,它提供下面的机制让它们使用:
- 注册或者注销一个触发器提供者,以及打开和关闭一个指向此提供者的句柄。
- 生成一个通知或触发器。
- 设置和查询一个触发器提供者的配置
- 向一个触发器提供者发送控制命令。
4.3.3 提供者注册
提供者是通过SCM扩展DLL来注册的,它使用ScExtpRegisterProvider
函数,此函数通过ScExtGenerationNotification 来调用。这打开一个指向UBPM的句,并调用UbmpRegisterTriggerProviderAPI。当一个服务注册一个提供者时,它必须为这个提供者定义-个唯一的名称和GUID,以及一些必要的标志来定义此提供者(例如,使用ETW提供者标志)。
而且,提供者也可以有一个友好名称以及一段描述。一旦注册完成,该提供者被插入到UBPM的提供者链表中,提供者的总数随之递增;如果这是一个设置了disabled标志而尚未启动的ETW提供者,那么该提供者的GUID将在UBPM在初始化阶段激活的实时ETW痕迹中被启用。另外也创建一个提供者块,其中包含了所有在注册过程中获取到的有关提供者的信息。
既然提供者已经被注册了,就可以使用打开和关闭API来递增该提供者的引用计数,并返回它的提供者块。而且,如果此提供者并未被注册成禁用的状态,那么,这也是第一个指向它的引用,它的GUID在实时ETW痕迹中被启用。
类似地,注销一个提供者将禁用它的GUID,并且将它从提供者链表中解除链接关系:一旦所有的引用都关闭了,则提供者块也相应地被删除。
4.3.4 消费者注册
服务消费者的注册工作最初是由ScExtRegisterTriggerConsumer回调暴露出来的,此回调函数是SCM扩展DLL提供的。它的任务是接收所有由SCM格式化的触发器信息(由服务开发者提供,根据MSDNAPI文档“Service TriggerEvents”,可以查阅MSDN获得),并且将这些信息转换成UBPM内部使用的原始数据结构。一旦所有的处理都已经完成,SCM扩展DLL将这一触发器包装起来,将它与两个动作关联起来: UBPM Start Service和UBPM Stop Service。
ScheduledTasks服务利用了UBPM的能力,它通过一个内部的UBPM单体类(UBPM SingletonClass,调用进入Ubpmdll)提供了类似的功能。它允许其内部的RegisterTaskAP!也可以注册触发器的消费:它也对其内部的数据进行类似的处理,不同之处在于它使用了UBPM StatEXE动作。接下来,为了真正地完成注册操作,也为了打开一个指向UBPM的句柄,要检查一下是否该消费者已经注册过了(不允许改变已有的消费者),最后通过UbpmRegisterTriggerConsumerAPI注册消费者。
触发器消费者的注册是由UbpmTriggerProviderRegister完成的,它验证此注册请求,把提供者的GUID加入到提供者链表中,并且启用此提供者,使得ETW痕迹会话现在也可以接收关于此提供者的事件。
4.3.5 TaskHost
TaskHost接收SCM中运行的UBPM发过来的命令。在初始化时刻,它打开UBPM在初始化时候创建的本地RPC接口,然后无限循环,等待此通道中过来的命令。
当前支持四种命令,它们可通过TaskHostSendResponseReceiveCommand RPCAPI发送过来:
- 停止宿主
- 启动一个任务。
- 停止一个任务
- 终止一个任务。
而且,所宿纳的任务是通过TaskHostReportTaskStatus RPCAPI来提供的,该API使得这些任务可以在调用UbpmReportTaskStatus的时候将它们的当前执行状态通知UBPM。
所有基于任务的命令实际上在内部都是由一个通用的COM任务库来实现的,它们实质上都会导致创建或销毁相应的COM组件。
4.3.6 服务控制程序
服务控制程序是标准的Windows应用程序,它用到了SCM服务管理函数,包括CreateServiceOpenService、StartService、ControlService、QueryServiceStatus和DeleteService。为了使用这些SCM函数,SCP必须首先调用OpenSCManager函数,以打开一个通向SCM的通信通道。在调用该打开函数的时候,SCP必须指定它想要执行的动作的类型。例如,如果一个SCP只是简单地想要枚举并显示SCM数据库中出现的服务,那么,它在调用OpenSCManager时请求“枚举服务”访问许可。SCM在初始化过程中,创建了一个代表SCM数据库的内部对象,并且利用Windows的安全功能,通过一个安全描述符来保护该对象。此安全描述符指定了哪些账户可以用什么样的访问许可来打开该对象。例如,此安全描述符指示Authenticated Users组可以以“枚举服务”的访问许可来打开该SCM对象。然而,只有管理员组才可以请求以创建或删除服务的访问方式来打开该对象。
如同SCM数据库对象一样,SCM也为这些服务本身实现了安全功能。当一个SCP利用CreateService函数来创建一个服务时,它指定一个安全描述符,SCM在内部将此安全描述符与该服务在服务数据库中的记录关联起来。SCM将此安全描述符保存在该服务注册表键的Security值中,SCM在初始化过程中扫描注册表的Services键时,读入此Security值,所以,即使机器重新引导以后,安全设置也会一直有效。如同SCP必须要在OpenSCManager调用中指定它将要以什么类型的方式来访问SCM数据库一样,SCP也必须在OpenService调用中告诉SCM,它想要怎样访问一个服务。一个SCP可以请求的访问方式有:能够查询一个服务的状态,可以配置、停止和启动一个服务。
你可能最熟悉的SCP是Windows 自带的Services MMC加载件,位于%SystemRoot%\System32\Filemgmtdll中。Windows也包含了一个命令行服务控制程序Sc.exe(Service Controller tool,服务控制器工具),前面我们已经多次提到过。
有时候,SCP在SCM实现的服务策略之上又叠加了一层服务策略。一个很好的例子是,当一个服务被手工启动时,Services MMC加载件实现了超时的策略。该加载件显示一个进度条表示一个服务正在启动过程中。服务在响应SCM命令(比如启动命令)时,通过设置它们的配置状态来反映它们的进度情况,从而可以间接地与SCP进行交互。SCP通过QueryServiceStatus函数来查询此状态。它们可以辨别何时一个服务主动更新了此状态,何时一个服务像是被挂起了,因此,SCM可以采取适当的动作来通知一个用户该服务当前的状况。
4.4 Windows管理设施(WMI)
Windows管理设施(WMl,Windows ManagementInstrumentation)是WBEM(Web-BasedEnterprise Management)的一个实现,而WBEM则是DMTF(Distributed Management Task Force,一个工业界联盟)定义的一个标准。WBEM标准包含了一套可扩展的、针对企业的数据采集和数据管理设施的设计方案,此设计方案具有很强的灵活性和扩展性;为了管理具有任意多个组件的本地和远程系统,这样的灵活性和扩展性是非常必要的。
4.4.1 WMI体系结构
WMI是由四个主要的部件构成的,如图4.18所示。这四个部件是管理应用程序、WMI基础设施、提供者和被管理的对象。管理应用程序是Windows应用程序,它们访问有关被管理对象的数据,并且显示这些数据,或者对它们进行处理。管理应用程序的一个简单例子是,利用WMI而不是性能API来获得有关性能信息的性能工具。另一个稍微复杂一点的例子是一个企业管理工具,它使得管理员可以自动完成计算机设备的清查工作,即将他们企业中每台计算机的软硬件配置信息收集到指定的地点。
开发人员通常将管理应用程序的目标定义为采集有关特定对象的数据,以及管理这些对象。一个对象可能代表了一个组件,比如网络适配器设备,也可能代表了一组组件,比如一台计算机(计算机对象可能包含了网络适配器对象)。提供者需要为管理应用程序可能感兴趣的对象,定义和导出它们的表示形式。例如,网络适配器的厂商可能想要在Windows包含的网络适配器的WMI支持中,再加入一些与他们的适配器有关的属性,从而允许管理应用程序可以直接查询和设置适配器的状态和行为。在有些情况下(比如,针对设备驱动程序),Microsoft提供了一个具有专属API的提供者,以帮助开发人员用最小的编程努力为他们自己管理的对象实现一个提供者。
WMI基础设施是把管理应用程序和提供者绑定在一起的黏合剂,其核心是CIM对象管理器(CIMOM,Common Information Model Object Manager)(本章后面将会讲述CIM)。此基础设施也被用作对象-类的存储体,在许多情况下,还被用作永久对象属性的存储管理器。WMI将此存储体,或者说仓库,实现为磁盘上的数据库,称为CIMOM对象仓库。作为此基础设施的一部分,WMI支持几个API; 管理应用程序通过这些API可以访问对象的数据,而提供者则通过这些API来提供数据和类的定义。
Windows程序和脚本(比如Windows PowerShell)使用WMICOMAPI来直接与WMI打交道这是最为基本的管理API。其他的API建立在此COMAPI基础之上其中包括针对Microsoft Access数据库应用程序的ODBC (Open Database Connectivity)配接接口。数据库开发人员使用WMIODBC配接接口,来嵌入对于他们自己数据库中的对象数据的引用。然后,开发人员可以通过数据库查询,来得到基于WM的数据,从而很容易生成相应的数据报告。WMI Activex控件支持另一层API。Web开发人员使用Activex控件,来为WMI数据构造出基于Web的界面。另一个管理API是WMI脚本API,主要为了在基于脚本的应用程序和Microsoft Visual Basic程序中使用。wMI的脚本支持也是为了所有的Microsoft程序设计语言技术而存在的。
如同针对管理应用程序的情形一样,WMICOM接口也是提供者最为基本的API。然而,不像管理应用程序是COM客户,提供者是COM或者分布式COM (DCOM)服务器(也就是说,提供者实现了COM对象,而WMI与这些COM对象打交道)。WMI提供者的可能实现方式有,被加载到WMI管理器进程中的DLL、单独的Windows应用程序,或者Windows服务。Microsoft包含了许多内置的提供者,它们供应的数据来自于大家熟悉的各种数据源,比如性能API、注册表事件管理器、活动目录、SNMP和现代的设备驱动程序。WMISDK让开发人员可以开发第三方WMI提供者。
4.4.2 提供者
WBEM的核心是DMTF设计的CIM规范。CIM规定了管理系统如何从系统管理的角度出发将一台计算机的方方面面展示给一台计算机上的应用程序或者设备。提供者的开发人员使用CIM来表达那些“属于他们想要管理的应用中的各种部件”。开发人员使用“可管理对象的格式”(MOF,Managed Object Format)语言来实现CIM表示。
提供者除了定义一些类来表达对象以外,还必须作为接口将WMI与这些对象连接起来。WMI根据这些提供者所支持的接口特性对它们进行分类。表4.12列出了WMI提供者的分类。注意,每个提供者可以实现一个或者多个特性,因此,举例来说,一个提供者既可以是一个类,也可以是一个事件提供者。为了清楚地阐述表4.12中给出的特性定义,我们来看一个实现了其中多个特性的提供者。事件日志(Event Log)提供者支持几个对象,包括事件日志计算机(EventLog Computer)、事件日志记录 (Event Log Record)和事件日志文件 (Event Log File)。事件日志(Event Log)是一个实例 (Instance)提供者,因为它可以为它的类中的某几个类定义多个实例。其中一个可以定义多个实例的类是事件日志文件类(Win32_NTEventlogFile);事件日志提供者为系统中的每一个事件日志 (即系统事件日志(System Event Log)、应用事件日志Application Event Log]或安全事件日志(Security Event Log))定义该类的一个实例.
事件日志提供者定义了实例数据,让管理应用程序可以枚举它的记录。为了使管理应用程序可以使用WMI来备份和恢复事件日志文件,事件日志提供者为事件日志文件对象实现了备份和恢复方法。这样做使得事件日志提供者成了一个方法(Method)提供者。最后,管理应用程序可以注册成:无论何时当一条新的记录被写入到任一个事件日志中时它可以接收到通知。因此,当事件日志提供者使用WMI事件通知机制来告诉WMI事件日志记录已经到达时,它就成了一个事件(Event)提供者。
4.4.3 公共信息模型(CIM)和可管理对象的格式语言
CIM也遵从像C++和Java这样的面向对象语言的步骤,在这些语言中,它们用类来表示问题中的数据。由于采用了类的概念,因而开发人员可以使用强大的建模技术,包括继承和复合。子类可以继承父类的属性,也可以增加自己的特征,还可以覆盖从父类继承过来的特征。如果一个类从另外一个类继承了属性或特征,则称前一个类派生自后一个类。类也能够复合起来:开发人员可以构造一个包含了多个其他类的类。
DMTF提供了多个类,这些类作为WBEM标准的一部分。这些类是CIM的基本语言,它们表达了可适用于所有管理领域的对象。这些类是CIM核心模型的一部分。核心类的一个例子是CIM_ManagedSystemElement。该类包含少数几个基本属性,这些属性标识了像硬件设备这样的物理组件,以及像进程和文件这样的逻辑组件。这样的属性有标题、说明、安装日期和状态。所以,CIM_LogicalElement和CIM_PhysicalElement这两个类都承了CIM_ManagedSystemElement类的属性。这两个类也是CIM核心模型的一部分。WBEM标准将这些类称为抽象类,因为它们存在的唯一理由是为了让其他的类继承它们(也就是说,抽象类不存在对象实例)。因此,你也可以把抽象类想象成专门用于为其他类定义属性的模板。
第二种类别的类表达了那些特定于管理领域但是与具体实现无关的对象。这些类构成了公共模型 (common model),它们被认为是核心模型的一个扩展。公共模型类的一个例子是CIM_FileSystem类,它继承了CIM_LogicalElement的属性。因为几乎每一个操作系统,包括Windows、Linux和其他的UNIX变种,都依赖于基于文件系统的结构化存储,所以,CIM FileSystem类是公共模型中一个很恰当的组成部分。
最后一种类别的类是扩展模型 (extended model),它是由一些附加于公共模型之上的与特定技术相关的类组成的。Windows定义了一大堆这种类来表达那些特定于Windows环境的对象。因为所有的操作系统都将数据存储在文件中,所以,CIM公共模型包含了CIM_LogicalFile类。CIM_DataFile类继承了CIM_LogicalFile类,另外,Windows针对其页面文件和快捷文件这两种文件类型分别加入了Win32PageFile和Win32 ShortcutFile文件类。
事件日志 (Event Log)提供者大量地使用了继承技术。图4.19显示了WMI CIM Studio的一个视图,这是随WMIAdministrative Tools一起带的类浏览器(你可以从Microsoft Web站点的Microsoft下载中心获得WMIAdministrative Tools)。你可以看到,事件日志提供者依赖于继承技术,该提供者的Win32_NTEventlogFile类派生自CIM_DataFile。事件日志文件是具有额外特性的数据文件,这些额外的特性与事件日志有关,比如日志文件名(LogfileName)、该文件所包含的记录的数量 (NumberOfRecords)。类浏览器中显示的树反映了Win32 NTEventlogFile具有多层继承性:CIM_DataFile派生自CIM_LogicalFile,而CIM_LogicalFile又派生自CIM_LogicalElement.而CIM LogicalElement又派生自CIM_ManagedSystemElement。
正如前面所述,WMI提供者的开发人员使用MOF语言来编写他们的类。下面的输出显示了事件日志提供者的Win32_NTEventlogFile类的定义,图4.19中选中的正是其父类CIM_DataFile.请注意观察,图4.19中右边窗格中列出的属性,和下面的MOF文件中这些属性的定义,两者是怎样关联起来的。CIM Studio使用黄色的箭头来标记那些因继承而得来的属性。因此,你在Win32_NTEventlogFile的定义中看不到这些特定的属性。
dynamic: ToInstanceprovider(“MS NT EVENTLOC PROVIDER”),Locale(1033),UUID(“[8502C57B-5FBB-11D2-AAC1-006008C78BC73”)]class Win32_NTEventlogFile:CIM DataFile
[read] string LogfileName;[read,write] uint32 MaxFileSize;
[read] uint32 NumberOfRecords;volatie,VaTueMap[“0”“1…365"“4294967295]] string OverWritePolicy;read,write,Units(“Days"),Range(“0-365 4294967295”)] uint32 0verwrite0utDated;read.[read] string Sources[];[implemented,Privileges{“SeSecurityPrivilege”,“SeBackupPrivilege”]] uint32 CTearEvent1og([in] string ArchiveFileName);[implementedPrivileges[“SeSecurityPrivilege”,“SeBackupPrivilege”]] uint32 BackupEventloa([in] string ArchiveFileName);
一个值得再看一看的术语是dynamic(动态的),这是出现在上面MOF文件中的、用于说明Win32_NTEventlogFile类的指示符。Dynamic意味着,当一个管理应用程序查询该类的对象的属性时,WMI基础设施向WMI提供者询问与该对象相关联的属性的值。Static(静态的)类是指WMI仓库中的类:WMI基础设施引用WMI仓库就可以得到这些值,无须向提供者询问这些值。因为更新WMI仓库是一个相对昂贵的操作,所以,对于那些属性频繁变化的对象,动态提供者更加高效。
在WMI开发人员使用MOF编写了他们的类以后,他们可以通过几种不同的方式将这些类定义提供给WMI。WDM驱动程序的开发人员可将一个MOF文件编译成一个二进制MOF(BMF)文件(这是一种比MOF文件更加紧凑的二进制表示形式),并且可以选择动态地将BMF文件交给WDM基础设施,或者静态地在二进制文件中包含BMF文件。另一种方法是,提供者编译该MOF文件,并且通过WMICOMAPI,将这些定义交给WMI基础设施。最后一种做法是,提供者可以使用MOF编译器(Mofcompexe)工具,将一个类编译之后的表示形式直接交给WMI基础设施。
4.4.6 WMI名字空间
类定义了对象的属性,而对象则是一个系统上的类实例。WMI使用一个名字空间将对象组织起来,该名字空间包含了几个子名字空间,WMI按照层次结构来组织对象。管理应用程序在访问一个名字空间中的对象以前,必须先与该名字空间建立连接。
WMI将名字空间的根目录命名为root。所有安装的WMI都有四个预定义的名字空间,它们位于root的下面,分别是CIMV2、Default、Security和WMI。在这四个名字空间中,有的还有其他的名字空间。例如,CIMV2包含了Applications和ms_209这两个名字空间作为自己的子名字空间。有时候提供者还定义了它们自己的名字空间。你可以在Windows中看到root下面的WMI名字空间(这是Windows设备驱动程序WMI提供者定义的名字空间)。
不像文件系统的名字空间是由目录和文件的层次结构构成的,WMI名字空间只有一层的深度。不像文件系统那样使用名称来标识文件或目录,WMI使用对象的属性来标识对象,它把这些属性定义成键(key)。管理应用程序指定类名和键名来找到一个名字空间内部的特定对象。因此,一个类的每个实例必须由它的键值唯一标识。例如,事件日志提供者使用Win32_NTLogEvent类来表达事件日志中的记录。该类有两个键:一个是字符串Logfile,另一个是无符号整数RecordNumber。如果一个管理应用程序要向WMI查询事件日志记录的实例,则它可以利用能标识出这些记录的键对来获得它们。应用程序利用特定的语法来引用一条记录,在下面的例子对象路径名中你可以看到这种语法:\DARYL\root\CIMV2:Win32_NTLogEvent.Logfile="Application"RecordNumber=“1”
该名称中的第一个部件(\DARYL)标识了该对象所在的计算机,第二个部件(\root\CIMV2)是该对象所处的名字空间。类名紧跟在冒号的后面,键名和对应的值跟在句点的后面。逗号把两个键值分隔开。
WMI提供了一些接口让应用程序可以枚举一个特定类中所有的对象,也可以发出一些查询请求以获得某个类中符合查询条件的那些实例。
4.4.7 类关联
许多对象类型彼此之间以某种方式联系在一起。例如,一个计算机对象可以有一个处理器、软件、一个操作系统、活动的进程,等等。WMI允许提供者构造一个关联类 (associationclass)来表达两个不同的类之间的逻辑连接。关联类将一个类与另一个类关联起来,所以,这样的类仅有两个属性:一个类名和Ref修饰符。
下面的输出显示了一种关联:
- 事件日志提供者的MOF文件将Win32_NTLogEvent类与Win32_ComputerSystem类关联起来。给定一个对象,管理应用程序可以查询与其关联的对象。按照这种方式,提供者可以定义出一个对象层次结构。
[dynamic: ToInstanceprovider(“MS_NT_EVENTLOG_PROVIDER”): ToInstance EnumPrivilegeslSeSecurityPrivilege"}:ToSubCTass,Locale(1033): ToInstance,UUID"[8502C57F-5FBB-11D2-AAC1-006008C78BC7}"):ToInstance,Association: DisableOverride ToInstance ToSubClass]class Win32_NTLogEventComputer
ToSubCTass] Win32 ComputerSystem ref Computer;[key,read:[key,read: ToSubClass] Win32_NTLogEvent ref Record;
图4.20 显示了WMI对象浏览器(WMIAdministrativeTools包含的另一个工具),它显示的是CIMV2名字空间中的内容。Windows的系统组件往往把它们的对象放在CIMV2名字空间中。对象浏览器首先找到Win32 ComputerSystem对象的实例ALEX-LAPTOP,这是代表整个计算机的对象。然后,对象浏览器获得那些与Win32 ComputerSvstem相关联的对象,并且将它们显示在ALEX-LAPTOP的下面。对象浏览器的用户界面利用双箭头文件夹图标来显示关联对象。关联类类型的对象显示在该文件夹的下面。
在对象浏览器中可以看到,事件日志提供者的关联类Win32_NTLogEventComputer位于ALEX-LAPTOP的下面,同时,Win32_NTLogEvent类有大量的实例。参照前面的代码输出,可以验证MOF文件定义了Win32NTLogEventComputer 类,将Win32 ComputerSystem 与Win32_NTLogEvent类关联起来。在对象浏览器中选择Win32_NTLogEvent的一个实例,可以在右侧窗格的Properties表单中看到该类的属性。虽然Microsoft提供对象浏览器的用意是帮助WMI开发人员检查他们的对象,但是管理应用程序也可以执行同样的操作,并以一种更加易于理解的方式来显示对象的属性或者收集到的信息。
4.4.8 WMI实现
WMI服务运行在一个共享的Svchost进程中,它在本地系统账户下执行。它将提供者加载到一个专门宿纳提供者的进程Wmiprvse.exe中。Wmiprvse.exe作为RPC服务进程的一个子进程被激发起来。WMI在本地系统账户、本地服务账户或者网络服务账户下执行Wmiprvse,具体在哪个账户下取决于代表此提供者实现的WMI Win32Provider对象实例的HostingModel属性值。当该提供者被从缓存中移除以后,也就是说,它接收到最后一个提供者请求以后一分钟,Wmiprvse进程即退出。
大多数WMI组件在默认情况下驻留在%SystemRoot%System32和%SystemRoot%\System32Wbem下,包括Windows MOF文件、内置的提供者DLL和管理应用程序WMI DLL。打开%SystemRoot%(ystem32Wbem目录,你可以找到Ntevt.mof,这是事件日志提供者的MOF文件。你还可以找到Ntevt.dll,这是事件日志提供者的DLL文件,WMI服务要用到它。
%SystemRoot%\System32 Wbem下面的目录存放了仓库文件日志文件和第三方的MOF文件。WMI使用一个私有版本的Microsoft JET数据库引警来实现它的仓库,即CIMOM对象仓库此数据库文件默认驻留在%SystemRoot%\System32Wbem\Repository中。
WMI用到了大量的注册表设置存放在该服务的HKLMSOFTWARE\Microsoft\WBEMCIMOM注册表键中,比如某些特定参数的阀值和最大值。
设备驱动程序使用特殊的接口来向WMI提供数据,以及接受来自WMI的命令(称为WMI系统控制命令)。这些接口是WDM的一部分,本书下册第8章“I/0系统”将会讲述。因为这些接口是跨平台的,所以它们位于\root\wMI名字空间的下面。
WMIC
Windows包含了一个工具Wmic.exe,它使得你可以从一个能感知WMI的命令行外壳中与WMI进行交互。所有的WMI对象和它们的属性,也包括它们的方法,都可以通过该外壳进行访问,这使得WMIC成为一个高级的系统管理控制台。
4.4.9 WMI安全性
WMI在名字空间层次上实现了安全性。如果一个管理应用程序成功地连接到一个名字空间,那么,此应用程序可以查看和访问该名字空间中所有对象的属性。管理员可以使用WMIControl应用程序来控制哪些用户可以访问哪个名字空间。在内部,这一安全模型是通过ACL和安全描述符来实现的,这是在Windows平台上实现访问检查的标准安全模型的一部分。(有关访问检查的更多信息,请参见第6章。)
为了启动WMIControl应用程序,你可以从Start菜单中,选择ControlPanel,再选择SystemAnd Maintenance,Administrative Tools,以及Computer Management。接下来,打开Services AndApplications分支,右键单击WMI Control,选择Properties,就可以激发WMI Control Properties对话框,如图4.21所示。为了配置名字空间的安全性,单击Security标签页,选中一个名字空间,再单击Security。通过WMIControlProperties对话框中的其他标签页,你可以修改注册表中存储的性能和备份设置。
4.5 Windows诊断基础设施(WDI)
Windows诊断基础设施 (WDl,Windows Diagnostic infrastructure)可以帮助在用户干预最小的情况下检测、诊断和解决一些常见的问题场景。Windows组件实现了一些触发器,通过这些触发器让WDI激发起与特定场景有关的诊断模块,以检测到一个问题场景的存在。触发器可以指示出当前系统正在接近或者已经到达了一种有问题的状态。一旦诊断模块识别出一个根原因(rootcause),它可以调用一个问题解决器 (problemresolver)来处理该问题。解决方案可能很简单,比如改变一个注册表设置,或者与用户进行交互以便完成恢复步骤或者配置改变。最后,WDI的主要角色是,为Windows组件提供一个统一的框架来完成各种与自动化的问题检测、诊断和解决相关的任务。
4.5.1 WDI设施
Windows或者应用程序组件必须加入专门的设施代码,以便当一个问题场景发生的时候可以通知WDI。这些组件可以以同步方式等待诊断的结果,或者继续操作,让诊断过程以异步方式进行。WDI实现了两种不同类型的设施API(instrumentation API)来支持这些模型:
基于事件的诊断,这可以用于最小侵入式的诊断设施方案,它可以被加入到一个组件中,而无须对该组件的实现做任何改变。WDI支持两种基于事件的诊断:简单的场景和启动-停止场景。在简单的场景中,代码中的一个点对失败负责任,并且升起(raise一个事件来触发诊断过程。在启动-停止场景中,整个代码路径都被认为是危险的,因而被加上设施代码以便于诊断。在场景的开始处,向一个名为DiagLog的实时ETW(Event Tracing for Windows)会话升起一个事件;同时,一个称为场景事件映射器(SEM,Scenario Event Mapper)的内核设施允许一组额外的ETW痕迹记录到WDI环境日志记录器中。第二个事件也被升起来,以标志该诊断场景的结束,此时SEM禁止详细的日志跟踪。这种“即时跟踪”机制使得详细跟踪的性能开销尽可能地小,同时又保持足够的环境信息,以便WDI在无须重现一个问题的情况下找到根原因(若一个失败应该发生的话)。
按需诊断,允许应用程序根据它们的需要来请求诊断、参与诊断、当诊断完成的时候接收通知,并根据诊断的结果来调整它的行为。当诊断过程需要在特权安全环境中执行的时候,按需诊断设施尤为有用。WDI可以在跨越信任和进程边界时对执行环境的转换加上代码设施,它也支持在必要时候模仿调用者。
4.5.2 诊断策略服务
诊断策略服务(DPS,Diagnostic Policy Service,%SystemRoot%System32Dps.dll)实现了绝大多数WDI场景的后端。DPS是一个多线程的服务(运行在vchost中),它接受按需场景的诊断请求,也监视和守护着通过DiagLog递交的诊断事件。(看图4.22,它显示了DPS和其他的WDI关键组件之间的关系。 作为对这些请求的响应,DPS激发起适当的诊断模块,该模块融合了特定领域的知识,比如如何找到一个网络问题的根原因。而且,DPS使所有与该场景有关的环境信息,让诊断模块通过所捕获的痕迹数据加以使用。诊断模块对这些数据执行一次自动化的分析,也可以请DPS激发第二个称为解决器的模块,由它负责解决该问题,如果可能的话悄悄地进行。
DPS也为诊断场景控制和强制应用组策略设置。你可以使用组策略编辑器(Group PolicyEditor,%SystemRoot%System32\Gpedit.msc)来配置诊断选项和自动恢复选项的设置信息。你可以通过 Computer Configuration 、Administrative Templates、System 、Troubleshooting And Diagnostics来访问这些设置,如图4.23所示
4.5.2 诊断功能
Windows实现了几个内置的诊断场景和一些工具。下面是一些例子:
-
磁盘诊断功能,包含了存储类驱动程序(%SystemRoot%\System32\Driver\Classpnp.sys)0中“Self-MonitoringAnalysis and Reporting Technology(SMART)”的代码,监视磁盘的健康情况。当检测到即将发生磁盘失败以后,WDI通知和指导用户。而且,Windows监视由关键系统文件的磁盘破坏而引起的应用程序崩溃。此诊断功能利用Windows的文件保护机制,自动地从备份缓存(若可能的话)恢复这些已被损坏的系统文件有关Windows存储管理的更多信息,请参见本书下册第9章“存储管理”。
-
网络诊断功能,扩展了WDI,能处理不同种类的与网络相关的问题,比如文件共享.Internet访问、无线网络、第三方防火墙和一般的网络连接问题。有关网络的更多信息,参见第7章“网络”。
-
资源耗尽保护,包括Windows内存泄漏诊断,以及Windows资源耗尽检测和解决。这些诊断功能可以检测到何时内存的提交限制值已经到达它的最大值,并提醒用户出现了这样的情况,包括消耗内存和资源最多的一些消费者。然后,用户可以选择终止这些应用程序以试图释放一些资源。有关内存提交限制和虚拟内存的更多信息,参见本书下册第10章“内存管理”。
-
Windows内存诊断工具,可以由用户在启动时候从引导管理器中手工执行,或者在-次系统崩溃之后(分析的结果是,可能是由于RAM错误而导致崩溃)由WER(WindowsErrorReporting,Windows错误报告)自动推荐执行。有关引导过程的更多信息,参见本书下册第13章。
-
Windows启动修复工具,试图自动地修复某些常见的不能引导系统的错误类型,比如不正确的BCD设置、损坏的磁盘结构(比如MBR或引导扇区),以及错误的驱动程序。当系统引导不成功的时候,引导管理器自动地激发起启动修复工具《若已经安装的话),它也包含手工的恢复选项,以及可以访问命令提示环境。有关启动修复工具的更多信息,参见本书下册第13章。
-
Windows性能诊断功能,包括Windows引导性能诊断、Windows停机性能诊断、Windows待机/恢复性能诊断,以及Windows系统响应性能诊断。根据特定的时间阀值和这些机制的内部行为期望,windows可以检测到由于很慢的性能而引起的问题并且将问题记录在事件日志中,继而WDI使用这些日志来提供解决方案和运行步骤供用户修复相应的问题。
-
程序兼容性助手(PCA,Program Compatibility Assistant),使遗留的应用程序可以在新版本的Windows上执行而不用担心兼容性问题。PCA检测到由于版本检查不匹配而引起的应用程序安装失败,以及由于不正确的二进制文件和用户账户控制 (UAC)设
置而引起的运行时失败。PCA试图从这些失败中恢复过来,其做法是,为应用程序使用适当的兼容性设置,这些设置将在应用程序下次运行的时候生效。而且,PCA维护了一个程序数据库,其中的程序都有已知的兼容性问题,并且,PCA在程序启动的时候将这些潜在的问题通知用户。文章来源:https://www.toymoban.com/news/detail-444848.html
4.6 本章总结
到现在为止,我们已经讨论了Windows的总体结构,以及该结构赖以建立起来的各种核心系统机制,还讨论了各种核心管理机制。有了这些基础作为准备,我们现在可以更加详细地挖掘单独的执行体组件,首先从进程和线程开始这一挖掘历程。文章来源地址https://www.toymoban.com/news/detail-444848.html
实验
实验:在一个空闲的系统上查看注册表的活动
实验:使用进程监视器来找到应用程序的注册表设置
实验:离线方式或远程编辑BCD
实验:观察轮廓加载和卸载
实验:手工加载和卸载储巢
实验:查看储巢句柄
实验: 查看储巢换页池的使用量
实验:查看键控制块
实验:查看服务所要求的特权
实验:查看进程中运行的服务
实验:查看UBPM触发器提供者
实验:查看哪些服务响应哪些触发器
实验:查看WMI类的MOF定义
实验:查看WMI名字空间
实验:使用WMI脚本来管理系统
实验:查看Wmiprvse的创建
总结
到了这里,关于[笔记]深入解析Windows操作系统《四》管理机制的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!