第一章 Windows內核概述

第一章 Windows內核概述

  這一章節描述了Windows內核知識中最重要的幾個概念,這些話題在這本書之後會有更詳細的描述,那些會與當前的主題密切相關。要確保你理解這個章節的概念,因為這些概念構成了任何驅動甚至用戶底層模式的基礎。


在這章中:

  • 進程(processes)
  • 虛擬內存(Virtual Memory)
  • 線程(Threads)
  • 系統服務(System Services)
  • 系統架構(System Architecture)
  • 句柄與對象(句柄和對象)

 進程

  進程是一個正在運行程序的容器和管理器,這經常被引用的術語「process runs」是不正確的。進程不會運行 – 進程負責管理。 線程是執行的代碼並且在技術上運行。從一個更高的角度來講,一個進程擁有如下的幾條:

  1. 一個可執行程序,它包含着原始代碼與數據,其被用於在進程中執行代碼;
  2. 一個私有的虛擬內存,被內存中的代碼以任何理由分配內存;
  3. 一個主令牌(a primary token),它是一個存儲着進程默認的安全上下文,被用於在線程中執行代碼(除非線程通過模擬採用不同的令牌);
  4. 一個存儲着可執行對象的私有句柄表,例如事件,信號量或者文件;
  5. 一個或多個可執行線程,一個正常用戶模式的進程被一個線程所創建(執行 classic main/ WinMain 函數).一個沒有線程的用戶進程大多數是無用的,在正常環境下會被內核所銷毀。

  這些進程的基本概念如下圖 1- 1

   一個進程是唯一被它的進程ID所標識的,只要內核進程存在,進程ID是保持獨一無二的。一旦進程被銷毀,這相同ID可能被新進程所拒絕.”認識到一個可執行文件本身不是進程的唯一身份”是非常重要的。例如,這兒有五個notepad.exe的實例運行在同一時間。每一個進程有它自己的地址空間,線程,句柄表以及它獨一無二的ID。這五個進程是由相同的文件(notepad.exe)所映射的,有着相同的初始化代碼和數據。1-2展示了系統快照所顯示的任務管理器細節 – 展示了五個notepad.exe實例,每一個有它自己的屬性。

 

虛擬內存

  每一個進程有它自己的虛擬,私有,線性的內存,這些地址空間一開始是空的(或者接近於空,因為可執行文件和ntdll.dll首先被映射進去,緊接着更多子系統的Dlls)。一旦主線程(main)開始執行,內存更有可能被分配,更多的Dlls被加載。這地址空間是私有的,這意味着其他進程無法直接訪問它。這地址空間從0開始(雖然從技術上來講,這最開始的64KB地址不能以任何方式被分配),一直到一個最大位 (其取決於進程和操作系統的位數),如下:

  • 對於32位的進程和操作系統,這進程地址默認為2GB;
  • 對於32位的進程和32位設置了用戶地址增加的操作系統(LARGEADDRESSAWARE 標誌在PE頭部),這進程地址空間可以達到3GB(依賴於正確的設置)。為了得到額外的地址空間,這程序被創建時必須有一個 LARGEADDRESSAWARE鏈接器標誌在它的頭上。如果沒有,這仍然將被限制在2GB。
  • 對於64位進程(運行在一個64位操作系統上),這個地址空間為8TB(Windows 8 或者更早期)或者128TB(Windows8.1或之後)。
  • 對於運行在64位系統上的32位進程,如果有LARGEADDRESSAWARE標誌,其空間為4GB,否則仍然為2GB。

  每一個進程有它自己的地址空間,這將讓任何進程的地址空間是相對的,而不是絕對的。例如,當嘗試判斷0x20000的內容時,這地址本身是不夠的;這個地址空間所相對的進程必須被指明。

  這個進程本身是被稱為虛擬的(virtual),這指的是這裡地址與一個正確的物理地址(RAM)之間存在一個間接的關係。在一個進程中的緩衝區可能被映射到物理地址,或者它可能暫時存儲在一個文件中(比如一個頁文件)。這詞”virtual”指的是「從一個執行的角度來看,這不需要知道這內存是否被存儲進RAM中」,如果這內存確實被映射進RAM中,這CPU可以直接訪問數據;否則,這CPU將引發一個頁異常,這將使內存管理器的頁錯誤處理程序從正確的頁文件中拷貝到RAM中,在緩衝的PTE中進行所需要的更改,之後CPU再次嘗試運行一下程序。1-3展示了兩個進程的物理內存的映射。

  內存管理的單位被稱為頁(page),與內存相關的屬性總是在一個頁的顆粒度上,比如它的保護。一個頁的大小是由CPU所決定的(在一些處理器中,它是可配置的),在任何情況下,內存管理器必須依照其這樣做。Windows所有支持的架構中,正常情況下頁大小是4KB(被稱之為小頁)。

  除了正常(小)頁表。Windows也支持大頁表,大頁表的大小時2MB(x86/x64/ARM64)和4MB(ARM).這是基於使用PDE來映射大頁而不是使用PTE。這可以更快速地進行轉化,最重要的是,其更好的使用了TLB緩存技術(Translation Lookaside Buffer) – 由CPU維護的近期所轉換的頁。在大頁的情況下,一個單獨的TLB目錄能顯着地映射更多的內存,相比小頁來講。

頁狀態

在虛擬內存中的每一個頁都會有如下三種狀態:

  • Free – 這種頁不允許以任何方式被分配;這什麼也沒有。任何試圖訪問該頁的都會造成一個異常訪問衝突。在新創建的進程中大部分頁都是Free屬性的。
  • Commited – Free的相反,一個已分配的頁,可以在沒有任何保護屬性下成功訪問(例如,寫入一個只讀頁會造成訪問衝突)。提交的頁常常是有RAM或一個文件(例如一個頁文件)所映射的。
  • Reserved – 這個頁沒有被提交,但是這個地址範圍被保留,可能用於以後的提交。從CPU的角度來看,它是等同於Free – 任何嘗試訪問的都會造成一個異常訪問衝突。然而,嘗試使用VirtualAlloc函數(或者NtAllocateVirtualMemory,這種比較原生的Api)來進行空間的分配將不會在保留區分配。在後面的章節「Thread Stacks」 中描述了使用保留內存來維護連續虛擬地址空間的比較經典的例子。

系統內存

  內存空間的低地址被進程所使用。然而一個指定的線程正在執行時,它所附屬的進程從0地址到上一節描述的上限都是可見的。操作系統,然而,必須也位於某個地方 – 該地方是操作系統所支持的高地址空間,其詳細描述如下:

  • 在32位沒有設置用戶空間增長的操作系統中,這操作系統位於高2GB的虛擬空間中,從地址0x80000000到0xFFFFFFFF。
  • 在32位配置了用戶空間增長的操作系統中,這操作系統位於剩餘的地址空間中。例如,如果這系統被配置成每個進程3GB的用戶空間(最大值),這OS佔據最高的1GB(從0xC0000000到0xFFFFFFFF),受這種空間地址減少的是文件緩存系統。
  • 在64位Win8,Server 2012以及更早的操作系統上,OS佔據了高8TB的虛擬地址空間。
  • 在64位Win8.1,Server 2012 R2 和之後的操作系統上,OS佔據了高128TB的虛擬空間。

  系統空間不是相對於進程的  – 畢竟,相同的「系統」,相同的內核,相同地服務於每一個進程的進程(例外的是一個基於會話的系統內存,但是在本章中並不重要)。系統中的任何地址是絕對的而不是相對的,因為它對於每個進程內容看起來是相同的。當然,從用戶模式去實際地訪問系統空間會導致一個異常訪問衝突。

  系統空間是內核本身、硬件抽象層(Hareware Abstraction Layer HAL)和內核驅動加載時所存在的地方。因此,內核驅動自動地保護來免於用戶模式的直接訪問。這也意味着他們存在一個整個系統的潛在影響。例如,一個內核驅動泄漏內存,這塊內存不能被釋放即使驅動卸載。用戶模式的進程,另一方面,無法泄漏任何東西當超出其生命周期。這內核是負責關閉和釋放一個死進程的任何私有東西(所有句柄關閉,全部的私有內存被釋放)。

線程  

  執行代碼的真正實體是線程,一個線程包含進一個進程,使用進程所暴露的資源來工作(例如虛擬空間和內核對象的句柄),一個線程擁有如下最重要的信息:

  • 當前訪問模式,用戶或者內核。
  • 執行體內容,包含着CPU寄存器和執行狀態。
  • 一個或兩個棧,用於本地變量分配和調用管理。
  • 本地存儲線程(TLS)數組,它提供了使用統一語義來存儲私有線程數據的方法。
  • 基礎優先級和一個當前(動態)的優先級。
  • 處理器親核性(Affinity),其表示該線程被允許運行在哪個處理器上。

  一個線程有如下幾個最重要的狀態:

  • Running – 當前的執行代碼運行在一個(本地)處理器上。
  • Ready – 等待執行的分配,因為所有相關的處理器都是在忙碌或者不可使用的。
  • Waiting – 在處理前等待事件發生,一旦事件發生,這線程將變為就緒狀態。

  1-4展示了這些狀態的狀態圖。括號里的數字表示了狀態序號,這可以用一個例如Performance Monitor工具所查看。注意就緒狀態有一個被稱為 Deferred Ready的同級狀態,與之非常類似,是實際存在的用於最小化某些內部鎖定的。

 

 線程棧 Thread Stack

  當執行時,每一個線程都有一個它所使用的的棧,被用於存儲本地變量,傳遞函數的參數以及在函數調用前存儲的函數調用地方。一個現場至少有一個棧位於系統(內核)空間,它是非常小的(默認是12KB在32位操作系統,24KB在64位操作系統)。一個用戶模式的線程可以擁有第二個棧在它的用戶空間,並且相對是比較大的(默認可以增長到1MB)。在1-5中展示了三個用戶模式線程和它們的棧的例子,在1-5中,線程1和2在進程A,線程3在進程B。

  內核棧總是存在於RAM中當線程處於運行和就緒狀態,這理由是非常微妙的,將在之後的章節被討論。用戶模式的棧,另一方面,是被寫出去的,就像其他用戶模式的內存。

  就用戶模式棧的大小而言,它的處理是與內核棧不同的。它以一個非常小的提交的內存開始(可能只有一個頁這麼大),剩餘的空間是被保留的內存,這意味着不能以其他任何方式分配。這種設計思想能夠在進程需要更多棧空間的情況下讓棧增長。為了滿足這種需求,在已經提交部分的下一個頁(有時更多的頁)被標記為一種特殊的稱為PAGE_GUARD的保護 – 這是一個保護頁。如果這個線程需要更多的棧空間,它將寫入這個頁來拋出一個被內存管理器所處理的異常。這內存管理器之後移除這個頁保護然後將提交到頁,並且將其下一個頁置為保護頁。在這種方式下,如果需要,這棧將會增長,這整個棧內存並不會全部被提交上。1-6展示了用戶模式的棧增長的方式。

 

  一個線程用戶模式的棧大小由如下所決定的:

  • 這個可執行映像文件有一個棧提交和保留的值在它的PE頭中。如果一個線程並不會指定替代值這將會被標記為默認的。
  • 當一個線程被CreateThread(或相似的函數)所創建時,調用者可以執行它所需要的棧大小,預先提交的大小或者保留的大小(但兩者不可同時設置),這依賴於函數所提供的標誌位;如果指定為零則會和上面的默認值一致。

 

系統服務(系統調用) 

   應用需要執行多種多樣的非純計算性操作,例如分配內存,打開文件,創建線程等。這些操作只能由最終運行在內核模式的代碼執行。因此用戶模式的代碼是如何執行這些操作的呢?讓我們舉一個經典的例子:一個運行Notepad程序的用戶使用文件才來提出打開文件的請求。Notepad的代碼通過調用Windows文檔中CreateFile的API作為回應。CreateFile是在kernel32.dll中實現的,Kernel.dll一個Windows子系統Dlls。這個函數仍然運行在用戶模式,因此沒有辦法直接打開一個文件。在之後的一些錯誤檢查中,它調用NtCreateFile,一個執行在Ntdll.dll中的函數,Ntdll.dll是運行被稱為「原生API」的基礎DLL,其是用戶層最底層的代碼。這(官方未文檔化的)Api轉換到了內核模式。在實際轉換之前,它放入寄存器中一個數字,被稱為系統調用號(EAX on Intel/ADM 結構中),之後它使用一個CPU指令(syscall on x64 or sysenter on x86)來完成實際的內核轉換,其跳入一個預先設計好的被稱為系統服務派發函數的例程中。

  這系統服務派發函數,相反,使用存儲在EAX寄存器中的值,作為一個進入SSDT表的索引。利用這張表,代碼跳入系統服務(系統調用)中。對於我們的NotePad例子中,SSDT入口將指向 I/O 管理器的NtCreateFile函數,注意這個函數和在Ntdll.dll中有着相同的名字和相同的參數。一旦系統服務被完成,這線程將通過執行指令 sysenter/systemcall來返回用戶模式。1-7描述了這一系列事件。

 

系統總體架構

  1-8展示了Windows的總體結構,由內核模式和用戶模式所組成。

 

  下面是出現在圖1-8中的快速摘要:

  • User Processes: 正常的進程都依賴於映射文件,執行在系統上,正如Notepad.exe、cmd.exe、explorer.exe實例等。
  • Subsystem Dlls: 子系統DLLs是運行子系統API的動態鏈接庫,一個子系統是內核能夠被公開的特殊的視圖。從技術上來講,從Windows8.1開始,這兒有一個單獨的子系統 – Windows子系統。這個子系統Dll是包含着很多知名的文件,比如kernel32.dll,user32.dll,gdi32.dll,advapi32.dll,combase.dll或者其他更多。這包含着Windows文檔化的絕大多數API。
  • Ntdll.dll:一個全系統的DLL,運行着Windows的原生API。這是運行在用戶模式最底層的代碼。它最重要的作用是通過系統調用來轉換成內核模式。Ntdll也運行着堆管理器,映像加載和一部分用戶模式的線程池。
  • Service Processes:服務進程是普通的Windows進程,它與服務控制管理器(SCM,運行在Services.exe中)進行通訊,並且允許在他們的聲明周期中進行一些控制。SCM可以開始、停止、暫停和發送消息向這些服務中。執行在一個特殊的Windows賬戶中的典型服務 – 本地系統、網絡服務和本地服務。
  • Executive:執行體是NtOskernl.exe的上層,它掌管絕大多數的內核代碼。這包含了絕大數的各種管理器 – 對象管理器,內存管理器,I/O管理器,即插即用管理器,電源管理器,配置管理器等,它比底層內核要大得多。
  • Kernel:這內核層執行了大多數基礎的,內核模式下時間敏感的OS代碼。這包含了線程調度,重點和異常已發以及各種內核原語(互斥體和信號量等)。一些內核代碼是用特定的CPU機器語言所編寫的,以提高效率並可以直接訪問CPU特定的細節
  • Device Drivers:設備驅動是可加載的內核模塊,它們的代碼執行在內核模式並且有着全系統的權限,這本書致力於寫特定類型的內核驅動。
  • Win32k.sys:「Windows子系統的內核模式組件」,本質上這是一個內核模塊(驅動)來處理Windows用戶界面部分和一個比較典型地圖形設備(GDI)APIs。這意味着所有的窗口操作(CreateWindowEx,GetMessage,PostMessage等)是由這個組件所處理的。這其餘的系統很少涉及UI的相關知識。
  • HaderWare Abstraction Layer(HAL):這HAL是一個硬件之上的抽象層。它允許設備驅動程序使用不需要中斷控制器或DMA控制器等詳細和特定知識的api。通常來講,這層是對於驅動設備處理硬件設備最有用的一層。
  • System Processes:系統進程是一種概括性屬於,用來描述進程通常「就在那兒」,在那些無法直接進行通訊的地方做着他們的事情。儘管如此,它們還是非常重要的,事實上對於操作系統的良好運行顯得至關重要。中斷它們中的一些可能是指明的,造成一個系統崩潰。一些系統進程是原生進程,意味着他們僅適用一些原生API(由Ntdll執行的API),例如smss.exe、Lsass.exe、Winlogon.exe、Services.exe以及其他進程。
  • Subsystem Process:Windows的子系統進程,運行着Csrss.exe的映像,它可以被視為在Windows系統下內核進程管理的助手。他是一個非常重要的進程,這意味着如果被殺死,這系統將會崩潰。正常來說每一個會話都存在一個Csrss.exe實例,因此在一個一般系統中存在着兩個實例 – 一個用於會話0,另外一個用於用戶登錄會話(典型的為1)。雖然Csrss.exe是Windows子系統管理器,它的重要程度遠不僅僅於此。
  • Hyper-V Hypervisor:這Hyper-V系統管理程序存在於Win10和Server2016以及之後的系統,如果他們支持虛擬化基礎安全(VBS),VBS提供一個額外的安全層,這兒實際上是由Hyper-V控制的虛擬機。VBS超出了本書的範圍,對於更多信息,你可以查閱《windows Internals》這本書。

 

 句柄與對象

   Windows內核暴露了各種類型的對象來讓運行在用戶模式的進程、內核本身以及內核模式的驅動所使用,這些類型的實例是在系統空間的數據結構,當由用戶或內核模式代碼請求時,其創建由對象管理器(Executive的部分)。對象被引用計數 – 僅有當最後一個引用的句柄被釋放之後,這個對象被銷毀,並從內存中釋放。

  因為這些對象位於系統空間,他們不能被用戶模式直接訪問。用戶模式必須使用一個被稱為句柄的間接訪問機制。一個句柄是存儲在進程句柄表中的實體,該實體邏輯上指向一個存儲在系統空間的內核對象。有各種 Create* 或者  Open* 函數來創建/打開內核對象並且檢索句柄背後的這些對象。例如,這 CreateMutext 用戶模式的函數允許創建或打開一個互斥體(依賴於這個對象是否被命名或者存在),如果成功,這函數返回一個這個對象的句柄。如果返回值為零則意味着這是一個無效句柄(這個函數調用失敗)。這 OpenMutex 函數,另一方面,試圖打開一個已經被命名的互斥體,如果這個命名的互斥體不存在,這函數調用失敗返回NULL(0)。

  內核(驅動)代碼也可以使用一個句柄或者直接指向對象的內核指針。這選擇通常基於你希望調用的API。在一些情況下,一個從用戶模式傳入驅動的句柄必須通過ObReferenceObjectByHandle函數來轉換成一個指針。我們將在之後的章節中討論這些細節。

  【大多數函數在失敗的情況下返回零,但有些不是。值得注意的是,CreateFile函數返回INVALID_HANDLE_VALUE(-1)如果失敗的情況下。】

  句柄值是4的倍數,第一個有效的句柄值為4;0從來不會是一個有效的句柄值。

  當創建/打開對象時,內核模式代碼可以使用句柄,但它們也可以直接使用指向內核的指針。當某個指定的API需要它時,這會被完成。內核模式代碼可以使用ObReferenceObjectByHandle函數來得到一個指向對象的指針。如果成功,這對象的索引值會被增加,因此「如果一個用於句柄的用戶模式代碼決定關閉它,然而內核模式代碼使用者一個指向對象的指針」是沒有危險的,這不會造成內核的指針指向的對象被釋放。不管句柄擁有者是誰,這個對象可以被安全的訪問,直到內核代碼調用ObDerefenceObject,這會將引用計數減少;如果內核代碼忘記調用這個,那會操作資源泄漏,直到下次操作系統重啟才會被解決。

  所有的內核對象都被計數,這對象管理器維護了句柄數值和對於這個句柄總共的引用次數。一旦這個對象不再需要,它的客戶端應該關閉句柄(如果一個句柄被用於訪問對象)或者不引用這個對象(如果內核客戶端使用指針)。從這點來講,這個代碼應該考慮到它的句柄/指針無效的。對象管理器將會銷毀這個對象當它的引用計數到達零時。

  每一個對象指針指向一個對象類型,它擁有者這個類型本身的信息,這意味着每一種類型對象都存在着單一的對象的類型。這些變量也作為導出的全局內核變量所公開,它們中的一些在內核頭中被定義,在某些特定的情況下這是非常有用的,在之後的章節中我們會看到。

Object Names

  有一些類型的對象時有名字的,這些名字被用於使用合適的Open函數通過名字來打開對象。注意並不是所以的對象都有名字;例如,進程和線程並沒有名字 – 它們有IDs。這就是為什麼OpenProcess和OpenThread函數需要一個進程/線程的標識符(一個數字)而不是一個字符串名稱。另一個有點奇怪的沒有名字的對象是一個文件。文件名並不是一個對象名 – 它們是不同的概念。

  從用戶模式的代碼,調用一個帶有名字參數的Create函數,如果這名字不存在,它將創建一個內核對象,但是如果它存在,則將打開這個內核對象。在之後的例子中,調用GetLastError返回ERROR_ALREADY_EXISTS,這表明這並不是一個新對象,這返回句柄已經是一個指向一個已存在對象的新句柄。

  提供給Create函數的名稱實際上不是對象的最終名稱。它的前綴是\Sessions\x\BaseNamedObjects,其中x是調用者的會話ID。如果這個會話為0,這個名字以\BaseNamedObjects為前綴。如果調用發生在一個AppContianer中(通常是一個普通的Windows平台程序),則這個前綴字符串會更加負責,由一個唯一的AppContainer SID所組成:\Sessions\x\AppContainerNamedObjects\{AppContainerSID}。

  上面所有的都意味着對象名稱是相對於會話的(在某些情況下是與AppContainer-package相關聯的)。如果一個對象一定要被跨會話共享,它可以在Session0中創建,通過以Global\前綴的名稱;例如,以Global\MyMutex為名字來通過CreateMutex函數創建一個互斥體,這將創建在\BaseNamedObjects下。注意AppContainers沒有使用Session0對象命名空間的權力。可以使用Sysinternals WinObj 工具來查看這層次結構,如1-9所展示的。

  圖1-9展示了對象管理的命名空間,這由被命名的對象層次結構所組成。這整個結構存儲在內存,由對象管理器(Executive的部分)所操控。注意未命名的對象不是這個結構的部分,這意味着WinObj無法查看全部的存在的對象,而是那些被創建出來有名字的對象。

  每一個進程都存在一個存儲着內核對象(無論是否被命名)的私有句柄表,這可以被查看通過Process Explorer或者 handles Sysinternals 工具。如圖1-10,Process Explorer的屏幕快照所展示了在進程中的句柄。在句柄視圖中默認展示的列只有對象類型和名字。然而,如圖1-10,其他有效的列也被展示出來。

  

  默認來說,Process Explorer僅展示了帶有名字的對象句柄(至於Process Explorer’s名字的定義,我們之後再來討論)。如果想顯示一個進程中的所有句柄,可以在 Process Explorer’s View 菜單選擇 Show Unnamed Handles and Mappings。

  在句柄視圖中的各個列提供了每個句柄的更多信息,這些句柄值和對象類型是自己所解釋的。這名字列是非常複雜的。它展示了對於互斥體、信號量、事件、節區、ALPC端口、任務、計時器或者其他較少被使用的對象的真正名稱。然而,其他所展示出來的與其真正的名稱對象存在着不同的含義:

  • 進程和線程,其所展示的是其唯一的ID。
  • 對於文件對象它展示了文件名(或者設備名)的文件對象的指針,這不是和對象名稱相同的,因為沒有辦法通過文件名來獲取這個指向文件對象的句柄 – 只能通過創建一個新的文件對象來訪問這相同的底層的文件或設備(假設文件對象允許共享它)。
  • (註冊)鍵值對象的名稱是用來展示這個註冊值的路徑。這不是一個名字,理由與文件對象一致。
  • 目錄對象展示了這個路徑,而不是一個真正的對象名稱。一個目錄不是一個文件系統對象,而是一個對象管理器目錄 – 這些可以非常容易通過Sysinternals WinObj 工具所看到。
  • 令牌對象名稱是展示了這個令牌所存儲的使用者名稱。

Accessing Existing Objects(訪問已存在的對象)

  在Process Explorer的句柄視圖中,Access列展示了被用於創建或打開這個句柄的訪問掩碼。這訪問掩碼是用於指定句柄是否允許執行某些操作。例如,如果客戶端代碼想終止一個進程,它必須首先使用 PROCESS_TEMINATE訪問掩碼,來調用OpenProcess函數來獲取一個句柄,否則這無法使用一個句柄來終止線程。如果調用成功,之後調用TerminateProcess一定會成功。這裡有一個通過進程ID來終止進程的例子:

  已解碼的Access列提供了訪問掩碼的文本描述(對於某些類型),使用它可以很容易識別出一個實際句柄的正確的訪問權限。

  雙擊這個句柄入口展示了這個句柄屬性,如圖-11展示了一個Event對象的屬性。

  圖1-11中的屬性包含了對象的名稱,它的類型,一個描述,在內核對象的地址,被打開的句柄次數和某些特殊的句柄信息,比如這個狀態和事件句柄信息。注意References並沒有執行該對象實際的未完成的引用次數。一個正確查看某個對象實際的引用次數是使用內核調試器的 !trueref 命令,效果如下:

 

   我們可以更能清楚地看到對象的屬性,這內核調試器在之後的章節會介紹。

  現在讓我們開始寫一個簡單的驅動,來展示和使用我們之後在該書中之後需要用到的工具。