《Windows內核原理與實現筆記》(一)Windows系統結構和基本概念

Windows內核結構

上圖是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子系統

其中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內核中的公共管理設施

Windows內核中的對象管理

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_<XXX>以外,在其他的5個根鍵中,真正存放系統設置信息的子樹是HKLM和HKU。HKLM存放有關係統全局的信息,包括5個子鍵,分別爲HARDWARE(硬件設置)、SAM(本地賬戶和組的信息)、SECURITY(系統全局範圍的安全策略和用戶權限設置)、SOFTWARE(系統中的全局配置信息,在系統引導時不需要)和SYSTEM(系統中的全局配置信息,在系統引導時需要,包括設備驅動程序和系統服務等)。HKU爲系統中每個加載過的用戶輪廓包含一個子鍵,也包含一個名爲.DEFAULT的子鍵,這是系統的默認輪廓,當登錄進程winlogon.exe爲第一次登錄到系統中的用戶創建輪廓時將以此爲基礎。

關於註冊表存儲結構,註冊表是由一組儲巢構成的,每個儲巢包含了一個由鍵和值構成的層次結構。上圖列出了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位指定了一個巢室在塊內的偏移。基於這樣的巢室索引結構,配置管理器將只爲每個儲巢映射那些需要用到的巢箱,而不是所有的巢箱。巢室目錄和巢室表仍然佔用換頁內存池的空間,但通常情況下,相比於整個儲巢文件,它們要小得多。配置管理器通過這種巢室映射的做法,可有效地降低註冊表數據的內存使用量。

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,它是在內核初始化過程中由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將它鏈接到主儲巢中。

接下來,利用加載塊參數,將有關當前這次引導的信息寫到註冊表中:

  • 調用CmpInitializeHardwareConfiguration,創建“\Registry\Machine\Hardware”節點,並且把硬件信息設置到註冊表中。
  • 調用CmpInitializeMachineDependentConfiguration函數,把與機器相關的配置數據設置到註冊表HARDWARE儲巢中。
  • 調用CmpSetSystemValues,將這次系統啓動的信息寫到註冊表中。
  • 調用CmpSetNetworkValue,將這次啓動的網絡信息寫到註冊表中。

因此,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<Xxx>函數供用戶模式應用程序使用,而Zw<Xxx>函數供內核代碼直接調用。NtOpenKey和NtQueryValueKey函數,其原型如下:

 這兩個函數的代碼位於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的通知塊成員,它描述了一個鍵對象的哪些事件以何種方式被通知到註冊方。由於配置管理器提供了這種變化通知能力,因而對於想要監視註冊表行爲的應用程序,它們無須頻繁地檢查註冊表來判斷感興趣的鍵是否已被修改。這對於一些安全保護或者註冊表行爲分析等程序有顯著的意義。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章