HKEY_CURRENT_CONFIG
HKEY_CURRENT_CONFIG只是一个链接,指向HKLM\SYSTEM\CurrentControlSet\ HardwareProfiles\Current下的当前硬件轮廓。Windows不再支持硬件轮廓,但是该键仍然存在,以便支持那些遗留下来的、可能依赖于该键的应用程序。
HKEY_PERFORMANCE_DATA
在Windows上,注册表也是访问性能计数器值的机制,无论这些计数器是从操作系统组件中来的,还是从服务器应用程序中来的。通过注册表来访问性能计数器,一个额外的好处是,远程性能监视工作几乎可以“免费”完成,因为注册表很容易通过常规的注册表API就可以访问到。
只需打开一个名为HKEY_PERFORMANCE_DATA的特殊的键,并且查询该键下面的值,就可以直接访问注册表性能计数器信息。如果在注册表编辑器中查寻此键,是无法找到的;这个键只能通过编程的方式,利用Windows注册表函数,例如RegQueryValueEx,才可以访问到。性能信息实际上并没有存储在注册表中,注册表函数利用该键来获得从性能数据提供者那里提供的信息。
也可以利用性能数据帮助器(PDH,Performance Data Helper)API(Pdh.dIl)中的性能数据帮助器函数来访问性能计数器信息。图4.2显示了在访问性能计数器信息过程中涉及的组件。
事务型注册表(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。日志文件名称的前缀是它们所对应的储巢的名称。
监视注册表活动
因为系统和应用程序如此严重地依赖于配置设置来指导它们的行为,所以,系统和应用程序的失败可能是由于注册表数据的改变或者安全性而导致的。当系统或者应用程序未能读取到它们认为总是能够访问到的设置数据时,它们可能不会正常工作,显示一些隐藏了根源的错误消息,甚至崩溃。如果在系统或应用程序失败以后,不理解它们是如何访问注册表的,那么,要想知道哪些注册表键或者值被错误配置了,那基本上是不可能的。
在这样的情况下, Windows sysinternals (http://technet.microsoftcom/sysinternals)的进程监视工具(ProcessMonitor)也许能提供答案。
进程监视器允许你监视注册表的一举一动。对于每一次注册表访问,进程监视器都会向你显示执行这一次访问的进程:这一次访问的时间、类型和结果:以及在执行这次访问时刻线程的栈。这些信息极其有助于看清楚应用程序和系统是如何依赖于注册表的,也有助于发现应用程序和系统将配置设置信息存储在哪里,以及可以诊断出那些“因漏掉了注册表键或值而发生”的应用程序问题。进程监视器包含了高级的过滤和加亮显示功能,所以你可以快速地将注意力集中在与特定的键或值有关的活动上,或者与特定进程有关的活动上。
进程监视器(Process Monitor)内部机理
进程监视器依赖于一个设备驱动程序,该设备驱动程序是在运行时刻从它的可执行映像文件中提取得到、然后再启动起来的。它第一次执行的时候,要求当前运行它的账户具有LoadDriver特权,以及Debug特权;在同一次引导会话中,以后再执行的时候只需要Debug特权就可以了,因为一旦该驱动程序被加载以后,它就会驻留在系统中。
进程监视器诊断技巧
有两种基本的进程监视器诊断技巧对于发现与注册表有关的应用程序或系统问题特别有效,它们是:
- 在进程监视器的痕迹数据中,查看应用程序失败以前所做的最后的事情:这个动作可能直指问题本身。
- 将失败了的应用程序的进程监视器痕迹数据,与一个正常运行的系统的痕迹数据进行比较。
按照第一种方法,先运行进程监视器,再运行应用程序。在失败发生的时候,回到进程监视器中,停止记录数据(按下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又正常工作。
在非特权账户下或者登录/注销过程中记录活动
一种常见的应用程序失败的情形是,应用程序在一个具有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之间。
注册表的内部机理
在这一小节中,你将会了解到,配置管理器(即实现了注册表的执行体子系统)是如何组织注册表的磁盘文件的。我们将会检查:当应用程序或者其他的操作系统组件读取或者改变注册表键和值的时候,配置管理器是如何管理注册表的。我们还将讨论一些注册表管理机制,配置管理器试图利用这些机制来确保注册表总是处于一种可恢复的状态,即使当注册表被修改时系统崩溃了也可以被恢复。
储巢
在磁盘上,注册表并不是简单的一个大文件,而是一组称为储巢(hive)
的独立文件。每个储巢包含了一棵注册表树,有一个键作为该树的根,或者作为该树的起点。子键和它们的值存储在根的下面。你可能会认为,注册表编辑器工具所显示的根键与储巢中的根键一定是相互关联的,但实际情况并不是这样的。表4.5列出了注册表储巢,以及它们对应的磁盘文件名。除了用户轮廓的储巢以外,其他所有储巢的路径名都被编码进了配置管理器中。当配置管理器加载储巢的时候,包括系统轮廓的储巢,它都会在HKLM\SYSTEM\CurrentControlSet\Control\Hivelist
子键下的注册表值中记录下每个储巢的路径,如果储巢被卸载的话,则删除对应的路径。它创建了根键,并且将这些储巢链接起来,以便建立起你所熟悉的、注册表编辑器所显示的注册表结构。
你会注意到,表4.5中列出的某些储巢是易变的,它们没有对应的关联文件。系统完全在内存中创建和管理这些储巢,因此这些储巢是临时的。系统在每次引导时创建这些易失的储巢。易失储巢的一个例子是HKLM\HARDWARE
储巢,它保存了有关物理设备和这些设备分配到的资源的信息。系统每次引导时,就会进行资源分配和硬件检测,所以,不在硬盘上存储这些数据是合理的。
储巢的大小限制
在有些情况下,储巢的大小是有限制的。例如,Windows对于HKLM\SYSTEM储巢的大小是有限制的。它之所以这样做,是因为,在引导过程开始之初,当虚拟内存换页机制尚未启用时,Winload要将整个HKLM\SYSTEM储巢读入到物理内存中。Winload还要将Noskrnl和引导设备驱动程序加载到物理内存中,所以,它必须要对分配给HKLMSYSTEM的物理内存数量进行限制(关于Winload在启动过程中所扮演的角色的更多信息,请参见本书下册第13章)。在32位系统上,Winload允许该储巢可以达到最高400MB或者该系统物理内存数量的二分之一,取决于哪个更小一点。在x64系统上,低限是1.5GB。在itanium系统上,低限是32MB。
注册表符号链接
一种被称为注册表符号链接(symbolic link)
的特殊键使得配置管理器有可能将键链接起来,从而将注册表组织起来。符号链接是一个键,它指导配置管理器到达另一个键。因此,HKLM\SAM键是一个符号链接,它连接到SAM储巢的根所在的键上。符号链接是通过在RegCreateKey或RegCreateKeyEx调用中指定REG CREATE LINK参数而创建出来的。在内部,配置管理器创建了一个名为SymbolicLinkValue的REG_LINK值,其中包含目标键的路径。因为该值的类型是REG LINK而不是REG SZ,所以,它对于Regedit是不可见的一一然而,它仍然是磁盘上注册表储巢的一部分。
储巢结构
配置管理器从逻辑上将一个储巢分成一些称为块 (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并没有显示这两个键的安全巢室,它们也应该出现在一个储巢中。
为了优化对于值和子键的搜索过程,配置管理器按照字母表顺序来存储子键列表巢室。然后,当配置管理器要在一个子键列表中查找一个子键时,它可以执行二分搜索。配置管理器在列表的中间检查要查找的子键,如果按照字母表的顺序,配置管理器正在查找的子键的名称位于中间子键名称的前面,则配置管理器知道该子键位于子键列表的前半部分:否则该子键位于子键列表的后半部分。这样的切分过程一直进行下去,直到配置管理器找到了该子键,或者没有找到匹配的子键。然而,值列表巢室并非是排序的,所以,新的值总是被加到列表的尾部。