程式扎記: [ Windows DDP ] Windows 記憶體管理 : 記憶體管理概念

標籤

2010年12月29日 星期三

[ Windows DDP ] Windows 記憶體管理 : 記憶體管理概念


前言 :
編寫 Windows 驅動之前, 需要讀者進一步理解 Windows 作業系統是如何管理和使用記憶體. 在此只討論 32 位元平台下 Windows 作業系統的相關知識. 這裡先解釋實體記憶體位址, 虛擬記憶體位址, 處理程序和驅動的關係, 然後在後面列出操作記憶體的範例.

實體記憶體概念 (Physical Memory Address) :
PC 上有三條匯流排, 分別是資料匯流排, 位址匯流排和控制匯流排. 32 位元的 CPU 的定址能力無 4GB 位元組. 用戶最多可以使用 4GB 的真實實體記憶體. PC 中會擁有很多裝置, 其中很多裝置都提供了自己的裝置記憶體. 例如顯示卡就會提供自己的顯示記憶體. 這部分記憶體會映射到 PC 的實體記憶體上, 也就是讀寫這段實體位址, 其實會讀寫裝置記憶體位址, 而不會讀寫實體記憶體位址, 一個裝置可以有好幾塊裝置記憶體映射到實體記憶體上.


虛擬記憶體地址概念 (Virtual Memory Address) :
雖然可以定址 4GB 的記憶體, 而在 PC 裡往往沒有如此多的真實實體記憶體. 作業系統和硬體 (這裡指的是 CPU 中的記憶體管理單元 MMU) 為使用者提供了虛擬記憶體的概念. Windows 的所有程式 (包含Ring0 層和 Ring3 層的程式) 可以操作的都是虛擬記憶體. 之所以稱為虛擬記體, 是因為對它的所有操作最終都會變成一系列對真實記憶體的操作.
在此簡單地介紹一下轉換的過程, 這有助於理解 Windows 的記憶體管理. 在 CPU 中有一個重要的暫存器 CR0, 它是 32 位元的暫存器, 其中一個位元 (PG 位元) 是負責告訴系統是否分頁的. Windows 在啟動前會將它的 PG 位置設為 1, 即 Windows 允許分頁. DDK 中有個巨集 PAGE_SIZE 記錄著分頁大小, 一般為 4KB. 4GB 的虛擬記憶體會被分割成 1M (4GB/4KB=2^20) 分頁單元.
其中有一部分單元會和實體記憶體對應起來, 即虛擬記易體中第 N 個分頁單元對應著實體記憶體第 M 個分頁單元. 這種對應不是一一對應, 而是多對一的映射, 多個虛擬記憶體頁可以映射同一個實體記憶體頁. 還有一部分單元會被映射成磁碟上的檔案, 並標記為髒的 (Dirty). 讀取這段虛擬記憶體的時候, 系統會發出一個異常, 此時會觸發異常處理函式, 異常處理函式會將這個頁的磁碟檔讀入記憶體, 並將標記設置為 不髒. 讓經常不讀寫的記憶體頁, 可以交換 (swap) 成檔案, 並將此頁設置為 dirty. 還有一部分單元什麼也沒有對應 (空的)
Windows 之所以這樣設計, 是基於以下兩個原因 :
* 第一是虛擬的使用增加了記憶體的大小. 不管 PC 是否有夠 4GB 的實體記憶體, 作業系統總會有 4GB 的虛擬記憶體. 這就允許使用者申請更多的記憶體, 當實體記憶體不夠的時候可以透過將不常用的虛擬記憶體頁交換成檔, 等需要時後再去讀取.
* 第二是使用不同處理程序的虛擬記憶體互不干擾, 為了讓系統可以同時執行不同的處理程序, Windows 作業系統讓每個處理程序看到的虛擬記憶體都不同. 這個方法就使不同的處理程序會有不同的實體記憶體到虛擬記憶體的映射. 例如處理程序 A 和處理程序 B 的記憶體位址0X40000會完全不同. 修改 A 處理程序這個位址, 不會影響到處理程序 B. 因為 A 處理程序的這個位址可能是映射的是一段實體記憶體位址, 而 B 的這個位址映射的是另一個實體記憶體位址.

使用者模式位址和核心模式位址 :
虛擬位址在 0~0x7FFFFFFF 範圍內的虛擬記憶體, 即低 2GB 的虛擬記憶體, 被稱為 使用者模式位址. 而 0x80000000~0xFFFFFFFF 範圍內的虛擬記憶體, 即高 2GB 的虛擬記憶體, 被稱為核心模式位址. Windows 規定執行在用戶態 (Ring3 層) 的程式, 只能存取使用者模式位址, 而執行在核心態 (Ring0 層) 的程式, 可以存取整個 4GB 的虛擬記體, 即使用者模式位址和核心模式位址.
Windows 的核心程式碼和 Windows 的驅動程式載入的位置都是在高 2GB 的內核位址裡, 所以一般的應用程式是不能存取到這些核心程式碼和重要資料的. 這大大提高了系統的穩健性.同時 Windows 作業系統在處理程序切換時, 保持核心模式位址是完全相同的. 也就是說, 所有處理程序的內核位址映射完全一致, 處理程序切換的時候, 只改變使用者模式位址的映射.


Windows 驅動程式和處理程序的關係 :
驅動程式可以看成是一個特殊的 dll 檔被應用程式載入到虛擬記憶體中, 只不過載入位址是核心模式位址, 而不是使用者位址. 它能存取的只是這個處理程序的虛擬記憶體, 而不能是其他處理程序的虛擬位址. 需要指出的是 Windows 驅動程式裡的不同常是執行在不同的處理程序中, DriverEntry 常式和 AddDevice 常式是執行在系統處理程序中. 這個處理程序是 Windows 第一個執行的處理程序. 當需要載入的時候, 這個處理程序中會有一個執行緒將驅動程式載入到核心模式位址空間內, 並呼叫 DriverEntry 常式.
而其他的一些常式, 例如 IRP_MJ_READ 和 IRP_MJ_WRITE 的派遣函式會執行於應用程式的 "context" 中. 所謂的執行在處理程序的 "context" 指的是執行在某個處理程序的環境中, 所能存取的虛擬位址是這個處理程序的虛擬位址.
這裡有個技巧可以方便看出程式碼是否執行於某個處理程序的 context 中. 在程式碼中列印一行 log 資訊, 這行資訊列印出當前處理程序的處理程序名. 如果當前處理程序是發起 I/O 請求的處理程序, 則說明在處理程序的 "context" 中. 下面列出的函式可以顯示當前處理程序的處理程序名, 藉由 DebugView 軟體在呼叫該常式同時查看 log 資訊 :
  1. VOID DisplayItsProcessName()  
  2. {  
  3.     // 得到當前處理程序  
  4.     PROCESS pEProcess = PsGetCurrentProcess();  
  5.     // 得到當前處理程序名稱  
  6.     PTSTR ProcessName = (PTSTR)((ULONG)pEProcess + 0x174);  
  7.     KdPrint(("%s\n", ProcessName);  
  8. }  
其中 PsGetCurrentProcess 函式 是得到當前執行的處理程序, 它是 EPROCESS 的結構體, 然而 EPROCESS 這個結構體是微軟沒有公開的結構體. 其 0X174 偏移的位置記錄一個字串指標. 有興趣讀者可以利用 WinDbg 工具查看該結構體內容.

分頁與非分頁記憶體 :
前面介紹過虛擬記憶體頁和實體記憶體頁的關係, Windows 規定有些虛擬記憶體頁面試可以swap到檔案中, 這類記憶體被稱為分頁記憶體. 而有些記憶體永遠不會被swap到檔案中, 這類記憶體被稱為非分頁記憶體.
當程式的插斷要求層在 DISPATCH_LEVEL 以上時 (包括 DISPATCH_LEVEL 層), 程式只能用非分頁式記憶體, 否則將導致 blue screen. 而在編譯 DDK 提供常式時, 可以指定某個常式和某個全域變數是載入分頁記憶體還是非分頁記憶體, 需要作如下定義 :
  1. #define PAGEDCODE code_seg("PAGE")  
  2. #define LOCKEDCODE code_seg()  
  3. #define INITCODE code_seg("INIT")  
  4.   
  5. #define PAGEDDATA data_seg("PAGE")  
  6. #define LOCKEDDATA data_seg()  
  7. #define INITDATA data_seg("INIT")  
如果將某個函式載入到分頁記憶體中, 我們需要在函式的實作加入如下程式碼 :
#pragma PAGEDCODE
  1. VOID SomeFunction()  
  2. {  
  3.     PAGED_CODE();  
  4.     // 做一些事情  
  5. }  
其中 PAGED_CODE() 是 DDK 提供的巨集, 它只在 check 版本生效. 它會檢驗這個函式是否執行低於 DISPATCH_LEVEL 的插斷要求層級, 如果等於或高於這個插斷要求層級, 將產生一個斷言. 如果讓函式載入到非分頁記憶體中, 需要在函式的實做中加入如下程式碼 :
  1. #pragma LOCKEDCODE  
  2. VOID SomeFunction()  
  3. {  
  4.     // 做一些事情  
  5. }  
還有一種特殊狀況, 就是某個常式需要在初始化時候載入記憶體, 然後就可以從記憶體中卸載掉. 這種情況指出在 DriverEntry 情況下, 尤其是 NT 式的驅動程式, DriverEntry 會很長, 佔據很大的空間, 為了節省記憶體, 需要及時從記憶體中卸載掉. 程式碼如下 :
  1. #pragma INITCODE  
  2. extern "C" NTSTATUS DriverEntry (IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING RegistryPath)  
  3. {  
  4.     // 做一些事情  
  5. }  
分配內核記憶體 :
Windows 驅動程式使用的記憶體資源非常珍貴, 非配記憶體時要盡量節約. 和應用程式一樣, 區域變數是存放在堆疊 (Stack) 空間的. 但堆疊空間不會像應用程式那麼大, 所以驅動程式不適合遞迴呼叫或者區域變數是大型結構體. 如果需要大型結構體, 請在堆積 (Heap) 中申請. 堆積中申請記憶體的函式有以下幾個, 原型如下 :
  1. PVOID ExAllocatePool (  
  2.     IN POOL_TYPE  PoolType,  
  3.     IN SIZE_T  NumberOfBytes  
  4. );  
  5. PVOID ExAllocatePoolWithTag(  
  6.     IN POOL_TYPE  PoolType,  
  7.     IN SIZE_T  NumberOfBytes,  
  8.     IN ULONG  Tag  
  9. );  
  10. PVOID ExAllocatePoolWithQuota(  
  11.     IN POOL_TYPE  PoolType,  
  12.     IN SIZE_T  NumberOfBytes  
  13. );  
  14. PVOID ExAllocatePoolWithQuotaTag(  
  15.     IN POOL_TYPE  PoolType,  
  16.     IN SIZE_T  NumberOfBytes,  
  17.     IN ULONG  Tag  
  18. );  
* PoolType 是個枚舉變數, 如果此值為 NonPagedPool, 則分配非分頁式記憶體. 如果值為 PagedPool, 則分配記憶體為分頁記憶體.
* NumberOfBytes 是分配記憶體大小, 注意最好是 4 的倍數.
* 返回值是分配的記憶體位址, 一定是核心模式位址. 如果返回 0 則表示分配失敗.

以上四個函式功能類似, 函式以 WithQuota 結尾的代表分配的時候按配額分配. 函式以 WithTag 結尾的函式和 ExAllocatePool 功能類似, 唯一不同的是多了一個 Tag 參數, 系統在要求記憶體外又額外地分配 4 個位元組的標籤. 在除錯的時候, 可以找出是否有標有這個標籤的記憶體沒有被釋放. 以上四個函式都需要 PoolType, 分別可以指定如下幾種 :
* NonPagedPool : 指定要求非配非分頁式記憶體
* PagedPool : 指定要求分配分頁記憶體.
* NonPagedPoolMustSucceed : 指定分配非分頁記憶體, 必須成功.
* DontUseThisType : 未指定.
* NonPagedPoolCacheAligned : 指定分配非分頁記憶體, 而且必須記憶體對齊.
* PagedPoolCacheAligned : 指定分配分頁記憶體, 而且必須記憶體對齊.
* NonPagedPoolCacheAlignedMustS : 指定分配非分頁記憶體, 而且必須記憶體對齊與必成功.

將分配記憶體進行回收的函式是 ExFreePool 和 ExFreePoolWithTag
This message was edited 13 times. Last update was at 19/12/2010 13:54:26

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!