windows内核开发学习笔记四十五:内核配置管理器

        配置管理器是执行体中的组件,它的实现依赖于内存管理器和缓存管理器(以及文件系统),这就意味着它必须要在这些组件初始化以后才能正常工作;然而在系统初始化的早期(比如I/O子系统的初始化),windows已经需要使用注册表中的配置信息了,但此时配置管理器尚未被初始化。windows的做法是,在内核初始化以前,内核加载器(ntldr)已经将整个HKLM\SYSTEM储巢作为一个只读文件加载到了内存中,因而配置管理器在完全初始化以前只需要直接把巢室索引加上该储巢的内存映射地址,就可以得到巢室的内存地址。这一做法有一个限制,即在配置管理器完全初始化以前,系统只能访问HKLM\SYSTEM中的设置,换句话说,windows必须初始化早期用到的各种设置存放在HKLM\SYSTEM中。

        配置管理器建立完全的注册表视图分为三个阶段来完成:第一,在内核初始化阶段,建立起HKLM\SYSTEM和HKLM\HARDWARE储巢;第二,由会话管理器(smss.exe进程)建立起HKLM\SAM、HKLM\SECURITY、HKLM\SOFTWARE和HKU\DEFAULT储巢;第三,当加载用户轮廓时建立起HKU\<用户的 SID>储巢,这是由登录进程(winlogon.exe)来完成的。第一阶段可以看作配置管理器的初始化,以及注册表的临时初始化;第二阶段可以看作注册表中系统部分的初始化;第三阶段可以看作注册表中用户部分的初始化。

        首先,第一阶段的初始化,它发生在一个关键点上:在内核初始化过程中,在对象管理器和缓存管理器的初始化以后,但在I/O子系统初始化以前。内核在这个点以前,不能访问在注册表中任何信息;而在这个点以后,可以访问HKLM\SYSTEM和HKLM\HARDWARE中的设置。执行这一初始化过程的函数为CmInitSystem1,它是在内核初始化过程中由Phase1 InitializationDiscard函数调用的。

        CmInitSystem1函数负责完成以下事项:

  • 初始化配置管理器的全局变量,包括各种链表和同步对象。
  • 创建注册表键的类型对象CmpKeyObjectType,CmInitSystem1通过CmpCreateObjectTypes函数来完成,请参考cmsysinit.c文件中CmpCreateObjectTypes函数代码,从中可以看到键对象的一些方法,如Parse方法为CmpParseKey。
  • 创建储巢CmpMasterHive,这是一个易失储巢,代表了注册表的根。创建储巢的函数为CmpInitializeHive,其代码位于base\ntos\config\cminit.c文件中。
  • 调用CmpCreateRegistryRoot函数建立起注册表的根:在主储巢中创建节点“\REGISTRY”,并创建一个键对象指向该该节点,然后将该对象插入到对象名字空间的根下面。
  • 调用NtCreateKey函数创建“\REGISTRY\MACHINE”和“\REGISTRY\USER”节点。
  • 调用CmpInitializeSystemHive函数创建系统储巢。在CmpInitializeSystemHive函数中,它根据ntldr传递进来的已加载的原始SYSTEM储巢映像,来初始化内存中的SYSTEM储巢。CmpInitializeSystemHive函数调用CmpInitializeHive来初始化SYSTEM储巢,并调用CmpLinkHiveToMaster将它链接到主储巢中。
  • 调用CmpCreateControlSet函数,根据加载信息创建符合链接“\Registry\Machine\System\CurrentControlSet”。
  • 调用CmpInitializeHive,创建HARDWARE储巢,这是一个易失的储巢,然后调用CmpLinkHiveToMaster将它链接到主储巢中。
  • 接下来,利用加载块参数,将有关当前这次引导的信息写到注册表中:
    • 调用CmpInitializeHardwareComfiguration函数,创建“\Registry\Machine\Hardware”节点,并且把硬件信息设置到注册表中。
    • 调用CmpInitializeMachineDependentComfiguration函数,把与机器相关的配置数据设置到注册表HARDWARE储巢中。
    • 调用CmpSetSystemValues,将这次系统启动的信息写到注册表中。
    • 调用CmpSetNetworkValue,将这次启动的网络信息写到注册表中。

        因此,CmInitSystem 1函数将注册表结构初步建立起来,它构造了主储巢、HKLM\SYSTEM和HKLM\HARDWARE三个储巢,并且也建立起与这次启动有关的符号链接和配置信息,为系统的进一步初始化提供了基本的配置信息。

        其次是注册表的进一步初始化。数组CmpMachineHiveList包含6个储巢,这些储巢(HKLM\SYSTEM和HKLM\HARDWARE)是由会话管理器进程(smss.exe)通过NtInitializeRegistry系统服务加载和初始化的。参见base\ntos\config\ntapi.c文件中NtInitializeRegistry函数的代码,在一次正常的启动过程中,它调用CmpCmdInit函数执行注册表的进一步初始化。在正常启动情形下,CmpCmdInit函数调用CmpInitializeHiveList来初始化储巢列表中的指定储巢,以及建立相应的符号链接。

        由于CmpInitializeHiveList是在会话管理器进程环境中执行的,而加载和初始化储巢的动作必须在System进程中完成,因此,CmpInitializeHiveList会为储巢列表中的每个储巢创建一个系统线程,由该系统线程来初始化该储巢,系统线程的主例程为CmpLoadHiveThread,参数为每个储巢在CmpMachineHiveList数组中的索引。

        在CmpLoadHiveThread函数中,对尚未加载的储巢,包括HKLM\SAM、HKLM\SECURITY、HKLM\SOFTWARE和HKU\DEFAULT,它会调用CmpInitHiveFromFile来完成储巢的加载和初始化;而对于已经被初始化的非易失储巢,即HKLM\SYSTEM,则调用CmpOpenHiveFiles打开系统储巢文件,因为在此之前系统储巢文件实际上一直没有被通过文件系统打开过,经过这一步后,系统储巢被完全初始化。

        随着系统的进一步引导,当需要特定于用户的配置信息时,注册表的HKU子树下的用户储巢也必须建立起来。这些储巢是按需加载和初始化的,由登陆进程(winlogon.exe)在建立起用户环境时完成,譬如当用户登陆到系统中,或者系统以特定的用户身份来启动一个进程或服务时。Winlogon通过NtLoadkey系统服务将一个储巢文件链接到注册表中,而NtLoadKey又进一步调用CmLoadKey来完成实际的加载和链接操作。

        CmLoadKey函数实现比较直接了当,其代码位于base\ntos\config\cmapi.c文件中。它首先调用ObOpenObjectByName打开注册表中目标键对象的句柄,如果该键对象已经存在,则此调用可以锁住它,以便进行“是否已被加载”的检查。然后,CmLoadKey调用CmpCmdHiveOpen调用CmpInitHiveFromFile函数来加载并初始化一个储巢。最后,CmLoadKey调用CmpLinkHiveToMaster函数,将新储巢链接到主储巢的恰当之处。

        刚刚讨论了配置管理器初始化以及windows注册表的建立过程。储巢是配置管理的核心概念,也是注册表存储结构中的文件实体。WRK包含了配置管理器的完整代码,储巢的数据类型为CMHIVE,其内嵌的HHIVE成员是它数据管理结构。储巢内部的数据管理类似于一个文件系统,它的数据存储单元按照巢箱来分配,而巢箱以块(4KB大小)为边界;储巢内部的逻辑数据结构为巢室,巢室有不同的类型,其大小也不尽相同。在配置管理其的实现中,巢室的数据结构为HCELL,巢箱的数据结构HBIN。空闲的巢箱形成一个空闲链表。实际上,HHIVE数据结构包含两个Storeage成员,分别对应于稳定的储巢和易失的储巢;在Storeage成员中,有空闲巢箱链表,以及一套用于转译巢室索引的巢室目录和巢室表。关于这些数据结构的定义,可以参考base\ntos\inc\hivedata.h文件的定义和说明。

        注册表的层次结构形成了一个名字空间,配置管理器定义了一个以“Key”命名的对象类型,从而将该名字空间对象管理器的全局名字空间整合起来。配置管理器在初始化阶段调用CmpCreateObjectTypes函数,创建了类型对象的全局变量CmpKeyObjectType。配置管理器充分利用了对象管理器提供的对象管理框架,让注册表中的每个键自动成为对象管理器中的对象。对于每个打开的注册表键,配置管理器分配一个键控制块(Key control block),其数据结构为CM_KEY_CONTROL_BLOCK,它包含了该控制块所引用的键节点所在的储巢和巢室索引,配置管理器将所有的键控制块放在一张散列表(全局变量CmpCacheTable)中,因而可以快速地根据名称来搜索已有的键控制块。散列表CmpCacheTable实际上是包含2048个元素的数组,散列表的键ID是由键控制块所引用的键对象的名称通过计算而获得。每个键控制块然后被放到散列表的相应桶中,放到同一个散列桶中的所有键控制形成一个链表。关于散列表CmpCacheTable的实现和用法,请参考base\ntos\config\cmsubs.c中的CmpInsertKeyHash、CmpRemoveKeyHash和CmpCreateKeyControlBlock函数。

        内核程序或者应用程序访问一个注册表键时对象管理器和配置管理器的名称解析过程中,涉及两个常用的操作:系统服务NtOpenKey和NtQueryValueKey,或者ZwOpenKey和ZwQueryValueKey。根据内核函数的命名约定,Nt<xxx>函数供用户模式应用程序使用,而Zw<xxx>函数供内核代码直接调用,这两组函数的功能时等价的,下面把NtOpenKey和NtQueryKey函数进行一下说明。

NTSTATUS NtOpenKey(

        __out PHANDLE KeyHandle,                       //返回的句柄

        __in  ACCESS_MASK DesiredAccess,    //访问模式

        __in POBJECT_ATTRIBUTES ObjectAttributes   //指定对象的属性

);

NTSTATUS NtQueryValueKey(

        __in HANDLE KeyHandle,                                 //要操作的注册表键

        __in PUNICODE_STRING ValueName,           //注册表键下值的名称

        __in KEY_VALUE_INFOMATION_CLASS KeyValueInfomationClass,   //指定待获取的信息的类型

        __out_bcount_opt(length) PVOID KeyValueInformation,                        //接收数据的缓冲区

        __in  ULONG Length,                                          //KeyValueInformation缓冲区的长度       

        __out PULONG ResultLength                          //实际返回的长度

};

        这两个函数的代码位于base\ntos\config\ntapi.c文件中。NtOpenKey系统服务收到的对象名称位于ObjectAttributes.ObjectName中,它检查KeyHandle和对象名称参数是否可以正确地访问,然后将打开注册表键对象的操作全盘交给对象管理器的ObOpenObjectByName函数来完成。这也可以证明注册表的接口与实现,都跟对象管理器的框架融合在一起。对象管理器中ObOpenObjectByName函数通过ObpLookupObjectName函数来完成对象打开操作,它层层递进解析一个名称串,若碰到目录对象,则在目录总查询剩余名称串;若碰到支持Parse方法的对象,则交给Parse方法来解析剩余的名称串。在NtOpenKey的情形中,它的ObjectAttributes参数已经指定了一个搜索目录,即RootDirectory;也可能直接从全局名字空间的根下开始查找,此时调用者应该指定注册表键的全路径名,注册表键全路径名以“\Registry”作为开始,如HKLM\SYSTEM\CurrentControlSet\services的全路径名为“\Registry\Machine\System\CurentControlSet\services”。

        由于配置管理器已经在全局的名字空间的根下创建一个名为“REGISTRY”的键对象(不是目录对象),所以当ObpLookupObjectName函数解析一个注册表键的全路径时,它首先在根目录下找到“REGISTRY”键对象,然后调用键对象类型的Parse方法来解析剩余的名称字符串。键对象类型的Parse方法为CmpParseKey函数。因此,注册表键的名称解析实际上是有CmpParseKey函数来完成的,它首先调用CmpBuildHanshStackAndLookupCache函数。需要顺序解析剩余的名称串,对于路径上的每一个子健,逐个为它们创建键控制块(通过调用CmpCreateKeyControlBlock函数)。最后,CmpParseKey调用CmpDoOpen函数打开此注册表键,并根据需要创建一个键控制块。

         ObOpenObjectByName函数收到一个指向键对象的句柄,键对象的数据结构为CM_KEY_BODY,其内部指向一个键控制块。如果两个应用程序打开同一个注册表键的话,它们都会收到一个键对象,但这两个键对象指向一个公共的键控制块。键控制块有一个引用计数用于跟踪一个键被多少个客户引用,当用户计数为零时,表明该键控制块已不再被使用了,于是配置管理器将它从散列表中移除,并且回收该键值控制块。

        NtQueryValueKey函数相对要简单的多,因为它的参数KeyHandle已经指示了要查询的键,只需调用ObReferenceObjectByHandle函数即可获得目标键的键对象。然后调用CmQueryValueKey函数从目标键中读取指定的值信息。CmQueryValueKey调用CmpFindValueByNameFromCache函数从键巢室的值列表中查找指定名称的值。配置管理器为每个注册表键在它的键控制块中保存一个值的缓存,用于存放它的值列表。CmpFindValueByNameFromCache调用CmpGetValueListFromCache获得此缓存,如果缓存尚未建立,则CmpGetValueListFromCache试图建立此缓存。然后,CmpFindValueByNameFromCache为值列表中的每个值调用CmpGetValueKeyFromCache,并检查值名称是否匹配,直至找到指定名称的值或所有的值都不匹配。

        除了NtOpenKey和NtQueryValueKey函数以外,配置管理器还提供了其他一些系统服务,包括NtCreateKey、NtDeleteKey、NtDeleteValueKey,NtEnumerateKey、NtFlushKey、NtQueryKey,NtRestoreKey、NtSaveKey、NtSetValueKey、NtLoadKey、NtReplaceKey和NtInitializeRegistry等。内核代码调用的是与这些服务对应的Zw<xxx>内核例程。配置管理器提供了注册表键的变化通知机制,应用程序通过调用NtNotifyChangeKey或NtNotifyChangeMultipleKeys系统服务,可以监视一个或者多个注册表键的创建、删除和修改动作。实现注册表键变化通知机制的关键在于,每个键对象都有一个类型为CM_NOTIFY_BLOCK的通知块成员,它描述了一个键对象的哪些事件可以以何种方式不通知到注册方。由于配置管理器提供了变化通知的功能,因而对应想要监视注册表行为的应用程序,它们无需频繁检查注册表来判断感兴趣的键是否被修改,这对安全保护或者注册表行为分析等程序,具有重要的意义。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jyl_sh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值