上图是windows内核的组成结构
如图Windows内核分三层,与硬件直接打交道的是硬件抽象层HAL,这一层把所有与硬件相关代码逻辑隔离到一个专门模块中,从而是上层尽可能独立于硬件平台。HAL是一个独立动态链接库,windows带了多个如Hal.dll,halacpi.dl等,这是根据高级配置和电源接口高级可编程中断控制器之类的区别,只有一个会被选中选中之后拷贝改名为hal.dll,hal才是真正的硬件抽象层,例如自旋锁和中断是在hal实现。内核只需简单实用其导出函数。
HAL之上是内核层,有时候成为微内核,这是大内核中的小内核,是ntoskrnl.exe的下层部分,上传是执行体,接近HAL层,这层是包含了基本的操作系统原语和功能,如进程线程,线程调度,中断和异常处理,同步对象和各种机制,还负责同步处理器直接行为。windows内核实现了抢占式线程调度机制,就是线程按优先顺序,分配到处理器上,每个线程有基本优先级,也有动态优先级,高优先级线程可抢断低优先级线程,windows内核按面向对象思想,管理两种对象,分发器对象(dispatcher object)和控制对象,分发器对象实现各种同步功能,这些对象状态会影响线程电镀。分发器对象包括event,mutant,Semaphore,process,thread,queue,gate,timer。控制设备对象被用于控制内核操作,不影响线程调度,包括异步过程调用APC,延迟过程调用DPC,中断对象等。
在内核层之上是执行体层,这一层是提供上层应用程序或内核驱动程序直接调用的功能和语义,Windows内核的执行体包含一个对象管理器,用于一致地管理执行体中的对象。执行体层和内核层位于同一个二进制模块中,即内核基本模块,其名称为ntoskrnl.exe。执行体是ntoskrnl上层,包含进程线程管理器,内存管理器,安全引用监视器,IO管理器,缓存管理器。配置管理器。即插即用管理器。电源管理器。
既然在同一个模块,内核层和执行体层分工是,内核层实现操作系统基本机制,而所有策略决定则留给执行体。执行体中的对象绝大多数封装了一个或者多个内核对象,并通过某种方式比如对象句柄,暴露给应用程序。这种设计体现了机制与策略分离的思想。
Windows内核为用户模式代码提供一组系统服务,供应用程序使用内核中的功能。应用程序通常并不直接调用这些系统服务,而是通过一组系统DLL,最终通过ntdll.dll切换到内核模式下的执行体API函数中,以调用内核中的系统服务,ntdll.dll是链接用户模式代码和内核模式系统服务的桥梁,对于内核提供的每个系统服务,该DLL都提供一个相对应的存根函数,这些存根函数的名称以NT作为前缀,比如NtCreatProcess等,此外ntdll.dll还提供了很多系统的支持函数,比如映像加载器函数(以Ldr)为前缀,WIndows子系统进程通信函数(以Csr为前缀),调试函数(以Dbg为前缀),系统事件函数(以Etw为前缀),以一般的运行支持函数(以Rtl为前缀)和字符串支持函数。
执行体API函数若果先前模式时用户模式要做参数检查,指针还要做内存校验。比如probeforwrite(long)。
每个线程都维护着一个状态值,说明它以前的处理器模式是用户模式还是内核模式。
即插即用管理器
这类模型驱动程序称为WDM,一用三种类型,总线驱动,功能驱动,过滤驱动程序。
windows原生文件系统NTFS(NT File System),驱动是ntfs.sys。另一个是FAT,windows支持两种形式的过滤驱动程序:一种直接插入到设备栈中,能够看到每一个经过设备栈文件的I/O请求,另一种基于Windows提供的过滤管理器驱动程序(FltMgr)的I/O过滤框架,称为文件系统小过滤驱动程序,并不出现在文件系统设备栈,通过回调方式响应FltMgr的事件。
分区是存储设备上连续存储区域,卷是值扇区逻辑集合,卷内部扇区可能是来自一个分区,或者多个分区,文件系统是卷内部的逻辑结构。
所以从应用程序-->存储设备应该是,文件系统,卷管理部分,分区管理,磁盘驱动。
磁盘设备是典型的即插即用,最下层总线驱动程序,最上层分区管理器驱动程序,负责通知即插即用管理器当前磁盘有哪些分区,因而系统中的磁盘管理器可以接收到有个分区创建和删除。
有Windows套接字,Winsock,WinInet(高层网络API,包括FTP,HTTP).
命名管道(named pipe)和油槽(mailslot)。用于不同进程间通信,支持不同机器上进程通信,前者连接方式,后者非连接方式通信,可以广播。
NetBIOS是早期网络API,支持有连接,无连接通信。
RPC是网络通信标志,分布式系统基础组件,RPC建立在其他网络API上,比如命名管道Winsock。Windows的RPC支持异步通信。
这些网络API提供了用户模式的dll,通过dll发出网络请求,将接收到的请求给驱动,通常要么通过系统服务如油槽管道等给,要么是I/O管理器对象给驱动。
winsock是windows最重要的网络API,用户模式包含了一个dll,即ws2_32.dll,还定义了一个可扩展的框架,允许第三方插入传输服务提供者和名字空间服务提供者。以支持更多的传输服务和名称解析或地址映射能力。Winsock默认支持TCP/IP,IPX/SPX,AppleTalk和ATM协议,提供的传输服务和名字空间服务通过内核驱动afd.sys实现网络通信。
在内核模式网络API驱动通过传输驱动程序接口(TDI),与协议驱动通信。TDI实际上预定了一组I/O请求,如名字解析,建立连接等。网络API驱动程序是TDI客户,传输协议驱动实现了TDI接口,称为TDI传输器。TDI用户和TDI传输器之间松耦合,多对一。
Windows中,网络协议与网络适配器驱动是分开的,协议驱动程序独立于任何一个网络适配器,真正发送和接收数据是通过网络适配器进行。协议驱动通过统一结构与适配器去哦多功能进行通信就是NDIS(Network Driver Interface Specification)
符合NDIS的网络适配器驱动称为NDIS驱动,或NDIS小端口驱动。Windows提供了NDIS库即ndis.sys,作为协议驱动和NDIS驱动程序桥梁,随系统发行。
NDIS客户即TDI传输器利用NDIS提供的功能,对将要发送给NDIS驱动程序格式化,并发给NDIS驱动,而NDIS驱动程序则利用NDIS库,接收请求和送回应答。NDIS驱动被封标准设备驱动程序,通过NDIS库与NDIS客户通信,I/O管理器不介入。
其中windows子系统包含内核模式和用户模式,内核模式部分核心是Win32k.sys,包含2部分,窗口管理和图形设备接口,窗口管理负责收集分发消息,控制窗口显示和管理屏幕输出。图形设备接口部分包含各种形状绘制及文本输出功能。用户模式部分包括Windows子系统csrss.exe以及一组动态链接库。csrss.exe进程主要负责控制台窗口的功能,以及创建或删除进程和线程等。子系统dll则被直接链接到应用程序进程中,包括kernel32.dll,use32.dll,gid32.dll,advapi.dll等,负责实现已经文档化的Windows API函数。
Win32k向用户代码提供系统服务,另一方面也跟Windows内核紧密融合在一起,通过向内核注册一组callout函数,介入到内核的线程进程管理处理逻辑中,同时接受电源事件。对于每个线程,一旦调用win32k.sys的任何一个服务,就变成了GUI线程,从而纳入Windows子系统的线程和进程管理范畴。
以下1模式这些系统进程在Windows操作系统扮演重要角色:
系统空闲进程(Idle),PID为0,每个处理器有一个
System进程,xp,server2003PID为4,包含了内核模式系统线程
会话管理器(Session manager smss.exe),这是Windows系统创建的第一恶搞用户模式进程。承担创建环境变了等,启动了子系统进程csrss.exe和winlogon.exe。
登陆进程winlogon.exe
windows子系统进程csrss.exe,负责提哦给你子系统环境,包括提供控制台窗口功能,以及创建删除进程和线程。
本地安全权威子系统进程Isass.exe。负责本地系统安全策略。
shell进程explorer.exe。windows默认Shell,提供了系统与用户打交道的各种节目包括开始菜单,任务栏等。
服务控制管理器services.exe,负责管理windows的系统服务。
在windows内核结构中,进程线程的核心机制是在微内核中实现,而管理机制在执行体中实现,符合机制与策略分工的原则。
如线程调度由微内核来完成,线程进程创建各种管理属性设置由执行体完成。因此关于进/线程数据结构和函数属于微内核范畴,另外一些属于执行体范畴。比较典型的例子是KPROCESS和KTHREAD是微内核中进程和线程的数据结构,EPROCESS和ETHREAD是执行体中进程和线程的数据结构。KeAttachProcess/KeStackAttachProcess是微内核中将线程附载到指定进程的函数,而PspCreateThread/PspCreateProcess是执行体中创建线程/进程的函数。
windows实现了基于优先级的抢占式线程调度算法,每个线程有一个基本优先级和动态优先级。
中断是外部设备/异常是CPU内部产生的打断指令流。所以区别在于中断发生于当前指令流并无实质联系,异常则是当前指令流执行的直接结果。而且中断是异步的,异常是同步的。
windows使用0-31来表示IRQL,中断请求级别,数值越大,优先级越高,如果发生中断时,IRQL等于或者低于当前级别,则该中断被屏蔽,直到IRQL降下来,IRQL=0表示普通线程,称为PASSIVE_LEVEL或被动级别,它的优先级最低,可被任意其他级别中断打断,IRQL=1表示异步过程调用(APC),成为APC_LEVEL,它仅比PASSIVE_LEVEL高,因此,在一个线程插入一个APC对象可以打断该线程执行,IRQL=2表示处理器正在做两件事情之一:正在进行线程调度,比如选择新的线程;正在处理一个硬件中断的后半部分,在windows中称为延迟过程调用(DPC Deferred Procedure Call),因此IRQL为2也被称为DISPATCH/DPC级别,或者简单称为DISPATCH_LEVEL。
DPC是一个重要概念,IRQL等于DISPATCH_LEVEL,高于PASSIVE_LEVEL,和APC_LEVEL,因此优先于任何一个线程相关函数,屏蔽了线程调度。低于所有的硬件中断,所以不屏蔽任何一个硬件中断。之所以称为延迟的,过程调用,因为它往往被用来执行一些相对于当前优先级别的任务不那么紧急的事情,比如硬件中断的后半段,典型的用法是timer定时器。比如在时钟中断服务例程中,负责更新中断时间,系统时间判断西戎的定时器是否有定时器到期有发出DISPATCH_LEVEL的软中断请求(中断线程和APC)。
与DPC不同,APC属于线程相关,只能在特定线程环境中被执行。所以也在特定地址空间执行,而且APC高于PASSIVE_LEVEL,所以优于线程本身指令流,当一个线程获得执行权,APC例程会立即执行,所以APC适合实现各种异步通知事件,例如I/O的完成通知可以使用APC实现。
异常既可以处理器硬件产生,也可以软件流产生。内核模式用户模式都可能发生异常,根据异常时处理器模式不同,异常分发(exception dispatch)过程也不同:
内核模式下,异常分发器首先将异常交给内核调试器处理,若不存在内核调试器或者内核调试器没有处理该异常,则尝试分发到一个基于帧的异常处理器(frame-based exception handler),基于帧的异常处理器是一种异常处理技术,他将异常处理器与“栈帧(stack frame)”关联起来,当发生异常时,异常分发器将根据当前栈帧来查找与之关联的异常处理器,如果未能找到这样的异常处理器,则异常分发器将该一次再次交给内核调试器,若这次该异常仍未能被处理,则认为是一个严重错误,系统奔溃。
用户模式下,异常分发器首先判断进程的调试端口是否有效,有效则发送消息至调试端口,然后等待应答,否则将异常给内核调试器。如果异常仍未得到处理,则将控制转到用户模式下,由用户模式的异常分发器(ntdll.dll中的KeUserExceptionDispatcher函数)寻找一个基于帧的异常处理器。如果仍未得到处理,则再次回到内核模式下,这次,内核模式异常处理器首先尝试调试端口,若异常仍未被处理,则再次尝试当前进程的异常端口。连接进程异常端口的是windows子系统,因此,windows子系统在异常处理最后时刻有机会处理他所属进程的异常,如果它也不能处理此异常,则该进程被终止。
同步是为了解决并发访问。windows操作系统提供多种多种同步机制,根据执行环境中的IRQL值大于APC_LEVEL或者等于PASSIVE_LEVEL,可以将同步分为“不依赖线程调度的同步机制”和“基于线程调度的同步机制”两类。
当IRQL大于APC_LEVEL时,Windows提供了一些方便的同步保护机制,供内核自身或设备驱动程序使用。IRQL大于APC_LEVEL时典型的同步机制如下:
提升IRQL。根据IRQL的定义,当处理器在某个IRQL上运行时,他只能被更高IRQL的中断打断,意味着,不用担心低IRQL的过程会抢占掉当前执行过程。这种做法在单处理器系统上可以做的更好,但是多处理器系统上,提升IRQL还不够,往往还需要其他同步机制(如自旋锁)。
互锁操作。利用Intel X86处理器提供的lock指令前缀,可以实现基于整数操作的保护,确保一个操作以原子方式进行。这种操作只能在小粒度数据上(可以达到64位内存单元)进行同步保护,而且是指令级保护。
无锁的单链表。windows利用64位互锁指令来实现无锁的单链表数据结构,支持多核,多处理器环境。
自旋锁(spin lock)。自旋锁本身是一种忙等。为了获得自旋锁,处理器持续监测锁状态。直至可用。此时即使有线程调度执行,APC排队,也没机会执行。所以自旋锁通常用于IRQL大于等于DISPATCH_LEVEL的代码。windows还提供一些自旋锁的扩展:执行体自旋锁(支持共享和独占的语义),排队自旋锁(queued spin lock)和栈内排队自旋锁(in-stack queued spin lock)。
另一种同步是PASSIVE_LEVEL上线程之间的同步。当一个线程的执行条件不满足,进入等待状态。系统将控制权交给满足执行条件单没有得到处理器资源的线程。以后当该线程执行条件满足时,有机会继续执行。这里的执行条件正是windows提供的线程同步机制中的语义。windows定义了统一的机制支持各种线程同步原语:分发器对象(dispatcher object),其数据结构头部为DISPATCH_HEADER。
windows使用等待块(wait block)来描述“一个线程正在等待一个分发器对象变成有信号状态”。对于每个处于等待状态的线程,它由一个等待块链表。链表中的每个节点代表该线程正在等待一个分发器对象,而对于每个分发器对象,它也有一个等待块链表。链表中每个节点代表了正在等待该对象的一个线程。所以当一个分发器对象变成有信号状态时,系统循着此对象的等待块链表,就知道该解除那个或者哪些线程,唤醒他们。线程进入等待的条件(若等待多个对象,则须指明等待任何其中之一或者等待所有对象)及分发器对象的状态(解除一个线程或者所有线程),决定了等待条件该如何被满足。
windows 实现了以下分发器对象:
事件(event):分为事件通知对象和事件同步对象,区别在于,当事件对象变成有信号状态时,是解除所有正在等待该对象的线程,还是只唤醒第一个以WaitAny方式等待该对象的线程。
突变体(mutant):突变体是Windows内核中互斥体的实现,如果推按体对象为无信号状态,则一定被某个线程“占有”,否则可满足任何一个线程的等待要求。突变体记录了“所有者”线程信息,通常可用于实现“锁”。
信号量(semaphore):信号量有一个计数器,用于控制最多可以有多少个线程共享一个资源。当计数器达到预定最大值时,信号量将变成无信号状态。
队列对象(queue):这是Windows内核中用于支持线程池的机制,其数据结构为KQUEUE,通常可以用于控制一项任务的并发程度。它的典型用途是I/O完成端口。
进程对象:Windows的内核进程对象本身也是一个分发器对象,当进程对象被初始创建时,为无信号状态,进程结束为有信号状态。
线程对象:同理如上。
定时器对象:定时器对象既是一个像DPC一样的例程,也是个可等待的分发器对象,当设定的时间到期时,定时器变成有信号状态。
门对象(gate object):在windows中,门对象和门等待时线程调度器的特殊支持,它绕过了以上分发器对象同步过程中的很多步骤。唤醒一个门等待的线程几乎是以最快捷的方式进行,线程调度器会直接将线程插入到某个处理器的就绪队列中。
除此之外,还有更丰富的同步机制:包括,快速互斥体(fast mutex),守护互斥体(guarded mutex),执行体资源(executive resource)和推锁(push lock)。
Windows中资源管理采纳了面向对象的思想。
每个对象都分为对象头对象体。对象头包含对象管理所需要的基本信息,包括名称类型,引用计数,安全描述符等,每种对象需要一个对应的类型对象(OBJECT_TYPE),通常在初始化过程调用ObCreateObjectType函数构建这种对象类型,完成相应全局变量的初始化。
第二个参数
可以看到在调用ObCreateObjectType创建对象类型,除了数据,还指定了基本的增删改等基本操作。之后内核就可以调用ObCreateObject来创建此种对象了。
参数ObjectType只是确定了对象头,对象体大小在ObjectBodySize。这个函数返回时,指向对象体开始位置(不是对象头),对象体格式特定于某种对象类型,由相应类型对象的诸多过程来维护。
对象管理器使用对象头中包含的信息管理这些对象,在对象头中除了对象名称和对象类型,有两个重要信息,指针计数,记录了内核本身(也包括驱动程序)引用该对象的次数。句柄计数,记录了有多少个句柄引用此对象。这些句柄可能出现在不同进程中。
对象管理器提供了一些通用服务,可以应用在任何类型的对象上,所以类型对象不需要为此种类型的对象提供所有在OBJECT_TYPE_INITIALIZER定义中出现的方法。
对象构造由两部分完成(1)调用ObCreateObject,根据指定的类型对象来完成对象头初始化,并且按指定大小分配对象体内存(2)完成对象体的初始化。前者统一完成,猴子根据不同类型对象有自己的初始化逻辑。
Windows允许以名称的方式管理对象。为了这样,Windows必须维护一套全家的名称查询机制,ObpDirectoryObjectType类型对象就说实现这一机制的关键。
Windows内部维护了一个对象层次目录(系统全局名字空间),其根目录对象是由全局变量ObRootDirectoryObject来定义。在WRK中,通过查询NTCreateDirectoryObject函数被调用情况,可以看到一系列ObjectTypes等对象子目录创建情况。
对象管理器提供一些基本操作用于在名字空间增删改查比如ObpLookupDirectoryEntry(指定目录查找一个名字),另外一个重要操作是ObpLookupObjectName,从指定目录或根目录,递归找一个对象。管理器的打开和引用对象,插入对象(ObOpenObjectByName)等都是通过ObpLookupObjectName实现。
在ObpLookupObjectName的代码逻辑中,可以看到进程设备表(DeviceMap),而且在目录对象的数据结构OBJECT_DIRECTORY中也有一个名为DeviceMap的成员,指向一个DEVICE_MAP。DEVICE_MAP的含义是,它定义了一个DOS设备名字空间,比如驱动器字母(C:/D:)和一些外设(com1),当对象管理器看到一个以“\??\”这样的名称,它会利用进程DeviceMap来获得相应的对象目录,然后进一步解析剩余名称字符串。
对象管理器中的对象是执行体对象,位于系统地址空间中,因而所有进程都可以访问这些对象,但是在进程地址空间中运行的用户模式代码不能用指针的方式引用这些对象,而是在调用系统服务时通过句柄来引用执行体对象。句柄时进程范畴的概念。在内核中将一个句柄转换成对应的对象。可通过ObReferenceObjectByHandle函数来完成。该函数负责从当前进程环境或内核环境句柄表中获得指定的对象引用。
关于对象的内存结构和生命周期,对象分为对象头对象体,头部结构是OBJECT_HEADER,对象体因对象而异,所以看到很多函数在接受对象作为参数时,类型为PVOID,而对象头和体通过ObpAllocateObject函数可知在同一块内存中。
从对象体转换到对象头,可以通过
对象是通过引用计数实现管理生命周期,一旦计数为零,则生命周期结束,对象的引用计数来源于2个方面,第一个来源是内核中的指针引用,一旦内核中新增了以恶搞对象引用,则对象引用计数需要增1,如果不在引用,则减1。第二个来源是进程打开一个对象获取一个句柄,它以后通过此句柄来引用此对象。对象头信息中准确记录有多少个句柄指向该对象,当1句柄不再被使用时,句柄计数减一。两种作用是在函数ObpIncrementHandleCount/ObpDecrementHandleCount中完成的。
Windows系统很多组件都是可以配置的,内核组建通常支持一些参数,甚至有些完全依赖于系统配置信息。例如I/O管理器和即插即用管理器在初始化阶段根据系统设置来例句和加载设备驱动程序。Windows操作系统提供了一个称为“注册表”的中心存储设施来作为系统的配置和管理中心。应用程序和捏合通过访问注册表来读写设置Windows同时提供API供访问注册表,API接到注册表访问请求,转发给系统服务。在内核中,执行体包含一个称为“配置管理器(configuration manager)”组件,是注册表的真正实现。注册表由一组称为储巢(hive)的文件构成,每个储巢内部包含一个树形层次结构,每个储巢可以想象成一个文件系统。
windows注册表是树状结构,每个节点是一个键值。注册表值可以多种类型,绝大多说注册表值类型为REG_DWORD(32位整数),REG_BINARY(二进制数据)和REG_SZ(字符串)。还有REG_LINK(符号链接,执行另一个键或值)。
除了HKEY_PERFORMANCE_
关于注册表存储结构,注册表是由一组储巢构成的,每个储巢包含了一个由键和值构成的层次结构。上图列出了Windows Server 2003系统中各个储巢的注册表路径和文件路径。一个系统的储巢列表存放在HKLM\SYSTEM\CurrentControlSet\Control\hivelist键下,如下图所示。当系统初始化时,HKLM\SYSTEM总是先被加载进来,然后配置管理器找到hivelist键,继而加载其他储巢,并创建注册表根键,将这些储巢链接起来,从而建立起完整的注册表结构。
储巢的内部结构类似于一个文件系统,而储巢相当于是一个磁盘分区。储巢的基本分配单元称为块(block),类似于文件系统定义的簇(cluster)。当储巢为了存储新的数据而需要扩展时,它总是按照块的粒度来增长。在Windows中,注册表的块的大小为4KB(4096B)。储巢的第一个块称为基本块,它包含了储巢文件标识、最新序列号、最后一次写操作的时间戳、储巢格式的版本号、校验和,以及储巢的内部文件名。储巢中的注册表数据是按照巢室(cell)来组织的。巢室可大可小,具体取决于它的类型和数据,每个巢室可以存放一个键、值、安全描述符、子键列表或者值列表,对应的巢室分别称为键巢室、值巢室、安全描述符巢室、子键列表巢室和值列表巢室。巢室在储巢文件中的偏移称为该巢室的索引(cell index),其他巢室可以利用此巢室索引来引用它,从而建立起巢室之间的关系。
配置管理器使用了一种类似于Intel x86处理器的页表映射的做法来解决巢室地址转译,一个32位的巢室索引被分成四个组成部分:存储类型、巢室目录索引、巢室表索引和块内偏移。存储类型有两种可能:稳定的(stable,最高位用0表示)和易失的(volatile,最高位用1表示)。每个储巢在内存中有两个巢室目录,分别对应于稳定的和易失的配置数据;每个巢室目录有1024项,每一项指向一个巢室表;每个巢室表包含512个表项,每一项指向一个块。由于配置管理器用巢箱来管理内存分配,而巢箱总是以块为边界(4KB),所以,巢室索引的最后12位指定了一个巢室在块内的偏移。基于这样的巢室索引结构,配置管理器将只为每个储巢映射那些需要用到的巢箱,而不是所有的巢箱。巢室目录和巢室表仍然占用换页内存池的空间,但通常情况下,相比于整个储巢文件,它们要小得多。配置管理器通过这种巢室映射的做法,可有效地降低注册表数据的内存使用量。
配置管理器是执行体中的组件,它的实现依赖于内存管理器和缓存管理器(以及文件系统),这意味着它必须要在这些组件初始化以后才能正常工作;然而,在系统初始化的早期(比如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,它是在内核初始化过程中由Phase1InitializationDiscard函数调用的。
CmInitSystem1函数(参见base\ntos\config\cmsysini.c文件)负责完成以下事项:
初始化配置管理器的全局变量,包括各种链表和同步对象。
创建注册表键的类型对象CmpKeyObjectType,CmInitSystem1通过调用CmpCreateObjectTypes函数来完成。
创建主储巢CmpMasterHive,这是一个易失储巢,代表了注册表的根。创建储巢的函数为CmpInitializeHive。
用CmpCreateRegistryRoot函数建立起注册表的根:在主储巢中创建节点“\REGISTRY”,并创建一个键对象指向该节点,然后将该对象插入到对象名字空间的根下面。
调用NtCreateKey函数创建“\REGISTRY\MACHINE”和“\REGISTRY\USER”节点。
调用CmpInitializeSystemHive函数创建系统储巢。在CmpInitializeSystemHive函数中,它根据ntldr传递进来的已加载的原始SYSTEM储巢映像,来初始化内存中的SYSTEM储巢。CmpInitializeSystemHive函数调用CmpInitializeHive来初始化SYSTEM储巢,并调用CmpLinkHiveToMaster将它链接到主储巢中。
调用CmpCreateControlSet函数,根据加载信息创建符号链接“\Registry\Machine\System\CurrentControlSet”。
调用CmpInitializeHive,创建HARDWARE储巢,这是一个易失储巢。然后调用CmpLinkHiveToMaster将它链接到主储巢中。
接下来,利用加载块参数,将有关当前这次引导的信息写到注册表中:
因此,CmInitSystem1函数将注册表结构初步建立起来,它构造了主储巢、HKLM\SYSTEM和HKLM\HARDWARE三个储巢,并且也建立起与这次启动有关的符号链接和配置信息,为系统的进一步初始化提供了基本的配置信息。
再来看注册表的进一步初始化。数组CmpMachineHiveList包含6个储巢,对应于表2.6中的前6个储巢。这些储巢(包括HKLM\SYSTEM和HKLM\HARDWARE)是由会话管理器进程(smss.exe)通过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来完成实际的加载和链接操作。
以上讨论了配置管理器的初始化以及Windows注册表的建立过程。储巢是配置管理器的核心概念,也是注册表存储结构中的文件实体。WRK包含了配置管理器的完整代码,储巢的数据类型为CMHIVE,其内嵌的HHIVE成员是它的数据管理结构。
巢内部的数据管理类似于一个文件系统,它的数据存储单元按照巢箱来分配,而巢箱以块(4KB大小)为边界;储巢内部的逻辑数据结构为巢室,巢室有不同的类型,其大小亦不尽相同。在配置管理器的实现中,巢室的数据结构为HCELL,巢箱的数据结构为HBIN。空闲的巢箱形成一个空闲链表。实际上,HHIVE数据结构包含两个Storage成员,分别对应于稳定的储巢和易失的储巢;在Storage成员中,有空闲巢箱链表,以及一套用于转译巢室索引的巢室目录和巢室表。
注册表的层次结构形成了一个名字空间,配置管理器定义了一个以“Key”命名的对象类型,从而将该名字空间与对象管理器的全局名字空间整合起来。配置管理器在初始化阶段调用CmpCreateObjectTypes函数,创建了类型对象全局变量CmpKeyObjectType。配置管理器充分利用了对象管理器提供的对象管理框架,让注册表中的每个键自动成为对象管理器中的一个对象。对于每个打开的注册表键,配置管理器分配一个键控制块(key control block),其数据结构为CM_KEY_CONTROL_BLOCK,它包含了该控制块所引用的键节点所在的储巢和巢室索引。配置管理器将所有的键控制块放在一张散列表(全局变量CmpCacheTable)中,因而可以快速地根据名称来搜索已有的键控制块。散列表CmpCacheTable实际上是一个包含2048个元素的数组,散列表的键ID是由键控制块所引用的键对象的名称通过计算而获得。每个键控制块然后被放到散列表的相应桶中,放到同一个散列桶中的所有键控制块形成一个链表。
当内核或应用程序访问一个注册表键时对象管理器和配置管理器的名称解析过程。这涉及两个常用的操作:系统服务NtOpenKey和NtQueryValueKey,或者ZwOpenKey和ZwQueryValueKey。根据内核函数的命名约定,我们知道,Nt
这两个函数的代码位于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\CurrentControlSet\services”。
由于配置管理器已经在全局名字空间的根下创建了一个名为“REGISTRY”的键对象,所以,当ObpLookupObjectName函数解析一个注册表键的全路径名称时,它首先在根目录下找到“REGISTRY”键对象,然后调用键对象类型的Parse方法来解析剩余的名称字符串。键对象类型的Parse方法CmpParseKey函数。CmpParseKey函数的实现并不难理解,它首先调用CmpBuildHashStackAndLookupCache函数,在散列表中查找已经打开的键对象,若能直接找到,则无须进一步名称解析;否则,需要顺序解析剩余的名称串,对于路径上的每一个子键,逐个为它们创建键控制块(通过调用CmpCreateKeyControlBlock函数)。最后,CmpParseKey调用CmpDoOpen函数打开此注册表键,并根据需要创建一个键控制块。
ObOpenObjectByName函数接收到一个指向键对象的句柄,键对象的数据结构为CM_KEY_BODY,其内部指向一个键控制块。如果两个应用程序打开同一个注册表键的话,它们都会接收到一个键对象,但这两个键对象指向一个公共的键控制块。键控制块有一个引用计数用于跟踪一个键被多少个客户引用。当引用计数为零时,表明该键控制块已不再被使用了,于是配置管理器将它从散列表中移除,并且回收该键控制块。
NtQueryValueKey函数相对要简单得多,因为它的参数KeyHandle已经指示了要查询哪个键,所以,它只需调用ObReferenceObjectByHandle函数即可获得目标键的键对象。然后它调用CmQueryValueKey函数从目标键中读取指定的值的信息。
最后值得一提的是,配置管理器提供了注册表键的变化通知机制。应用程序通过调用NtNotifyChangeKey或NtNotifyChangeMultipleKeys系统服务,可以监视一个或多个注册表键的创建、删除和修改动作。实现注册表键变化通知机制的关键在于,每个键对象都有一个类型为CM_NOTIFY_BLOCK的通知块成员,它描述了一个键对象的哪些事件以何种方式被通知到注册方。由于配置管理器提供了这种变化通知能力,因而对于想要监视注册表行为的应用程序,它们无须频繁地检查注册表来判断感兴趣的键是否已被修改。这对于一些安全保护或者注册表行为分析等程序有显著的意义。