探究Windows 2000/XP原型PTE
WebCrazy(http://webcrazy.yeah.net)
記憶體管理可以說是作業系統實現中最重要的環節,也是最為複雜的一環節。對於相對貧乏的記憶體資源,記憶體共用也成了一個很重要的有效手段。Windows 2000/XP在此方面的實現藉助於一個稱為原型PTE(Prototype PTE,PPTE)的軟體機制。在《小議Windows NT/2000分頁機制》中我詳細的介紹了Intel X86實現分段、分頁的硬體PTE工作方式。我們來回顧一下這種機制:
假設我們的一個進程映射了從虛擬位址0xXXXXXXXX(假設位於分配粒度上)開始的4M空間,而這4M空間當前都相應的映射了實際的實體記憶體(鑒於Lazy evaluation等的先進思想,這種情況在Windows 2000/XP中比較少見)。我們將這4M空間分成1000塊的4K(PAGE_SIZE,X86處理器決定),對於第n個4K(0<=n<1000),其虛擬位址(0xXXXXXXXX+n*4K),我們都有一個對應的硬體PTE,指出目前這4K駐留於實體記憶體的位置。通過由PDBR(CR3寄存器)與虛擬位址可定位這個硬體PTE(具體請參閱《小議Windows NT/2000分頁機制》)。
現在讓我們來考慮這樣一種情況,我們有一個檔案其大小也為4M,我們知道通常我們要使用這個檔案都要將它讀入記憶體。試想同時有兩個或更多的進程需讀寫這個檔案,這就需要解決記憶體共用的問題。實際上就算當前只有一個進程訪問這個檔案,對於這種潛在的需要共用的檔案,Windows 2000/XP均會事先考慮共用情況。她通過一個稱為Section的核心對象來實現這樣的目的。仔細想想,這種情況下記憶體共用決不僅僅是記憶體資源的充分利用,就算我們可以為每個進程各分配4M空間,但是這將導致各個進程某種時刻可能得不到這個檔案的最新內容。這是非常糟糕的情況。在內部Windows 2000/XP利用原型PTE來解決這樣的情況。基於硬體PTE相同的原理,對於這樣一個4M的檔案,在映射這個檔案時,Windows 2000/XP同樣的將這個檔案分成1000塊,每塊4K(PAGE_SIZE)大小。然後從頁交換區分配1000個DWORD,每個DWORD值都是原型PTE,它們組成原型PTE表。對於這個檔案的第n個4K(0<=n<1000),如果當前其駐留在實體記憶體中的話,其對應的PPTE的Valid位(bit 0,與硬體PTE一致)為1,然後這個PPTE的Page Frame Number(PPTE的高20位)用於指示實體記憶體。如果當前其仍然在磁碟中的話,Valid位為0。針對這種情況,通過PPTE的高20位(PFN Entry),尋找Page Frame Datbase(由MmPfnDatabase定位),通過PFN Entry的Subsection PTE(windbg中稱為restore pte,《Inside Windows 2000》中稱為original pte,Windows XP內部稱為Subsection PTE),定位Subsection,然後通過Subsection指向的Control Area的FILE_OBJECT,與PPTE在PPTE表的位移n,通過公式:
PFN Entry Subsection PTE->Subsection->Control Area->FileObject + n * 4K
定位所要訪問的檔案位移,這樣Windows 2000/XP使用頁面調入IO讀入這頁內容,更新PPTE表的這個PPTE。以上的這一系列定位轉換演算法,如Subsection PTE如何定位Subsection,我將另行介紹。上面的描述解決了一個非常重要的問題,我們不需要更新所有引用這一頁面的進程的硬體PTE,因為此時所有進程的PTE均指向PPTE,我們只要更新PPTE就能達到目的。至於進程PTE如何指向PPTE,下面我會涉及到這個內容。這兒你只要有一個概念,進程的PTE為了指向PPTE,肯定是一個Invalid PTE,即bit 0為0,而且其bit 10為1(PPTE標誌,具體請看我在《探尋Windows NT/2000 Copy On Write機制》列出的HARDWARE_PTE_X86結構)。
對於PPTE,因為X86處理器沒有提供這樣一種方式,像處理硬體PTE一樣,由CPU直接進行地址轉換。Windows 2000/XP記憶體管理器在處理Page Fault時,通過軟體機制來類比這種實現,這可以說是硬體PTE與PPTE的一個本質區別。
應該重點提出的是PPTE存在於頁交換區(由MmPagedPoolStart與MmPagedPoolEnd指定的位置,從虛擬位址0xE1000000開始),其本身也有可能被Page Out,Windows 2000/XP通過MiCheckProtoPtePageState判斷是否被Page Out,還有頁交換區的起始地址0xE10000000將用於從無效PTE轉化成原型PTE所在的地址,這等一下我會介紹到的。
照例我們用SoftICE來驗證一下我們前面的描述:
:bpint e
只要我們截獲這個硬體中斷,我們就知道肯定發生了Page Fault,但是我們並不能確定這都是由於指向PPTE的無效PTE導致的。事實上Copy On Write等等其他機制,均會發生Page Fault(《探尋Windows NT/2000 Copy On Write機制》有詳細討論)。但是正如我們前面提及的PPTE的bit 10為1,我們還是很容易的判定一個Page Fault是不是由於指向PPTE的無效PTE導致的。由於發生Page Fault的虛擬位址由CR2寄存器指定,經過幾次嘗試以後,我們繼續以下的討論:
Break due to BPINT 0E (ET=2.23 Seconds)
:cpu
Processor 00 Registers
----------------------
CS:EIP=0008:801648A4 SS:ESP=0010:FCBEADC8
EAX=C002100B EBX=77E74A02 ECX=00000102 EDX=00000000
ESI=00085108 EDI=000493E0 EBP=0140FF74 EFL=00000006
DS=0023 ES=0023 FS=0038 GS=0000
CR0=8000003B PE MP TS ET NE PG
CR2=77D3BB26 //發生Page Fault的虛擬位址。
.
.
.
:page 77d3bb26
Linear Physical Attributes
77D3BB26 NP 01A714F6
從PTE值01A714F6的bit 10為1我們知道這是一個指向PPTE的無效PTE。通過query命令我們可以找到CR2指定的地址,位於模組rpcrt4.dll中。從下面可以看到:
:query 77d30000
Context Address Range Flags MMCI PTE Name
explorer 77D20000-77D8E000 07100001 FF8D1328 E169C580 rpcrt4.dll
結合我文章開始的介紹,通過以下的計算:
:? (77d3bb26-77d20000)/1000*4+e169c580
unsigned long = 0xE169C5EC, -513161748, "/xE1i/xC5/xEC"
我們可以得到其實PTE 01A714F6應該指向0xE169C5EC位置。這時候由MMCI指向的Control Area,根據我上面提到的計算公式,即可以讀出rpcrt4.dll位移(0xE169C5EC-0XE169C580)/4*1000處,即0x1B000處的4K位元組,讀入虛擬位址77D3B000中((0xE169C5EC-0XE169C580)/4*1000+77D20000),而CR2指定的地址77D3BB26肯定在這4K之中。
其實這樣我們已經描述了MmAccessFault處理指向PPTE的無效PTE的一個典型過程。這裡只是示範了原型PTE指向的頁面未駐留在實體記憶體的情況,試想如果我們的頁面已經在實體記憶體了,我們還有必要去費時的尋找VAD嗎?這就要涉及到無效的PTE如何定位原型PTE,所以我一直使用指向PPTE的無效PTE的叫法。《Inside Windows 2000》中指出指向PPTE的無效PTE的具體格式,但我發現其描述的不盡正確,我一直深信像作者那樣能觸及Windows 2000代碼的人肯定不會有什麼問題,所以我在理解PPTE時一直卡在此處。後來通過反組譯碼實現時發現實際上通過下面的方式來計算PPTE的位置:
(PTE>>2) & 0x3FFFFE00 + (PTE & 0x000000FF) << 1 + 0xE1000000
其中PTE為指向PPTE的無效PTE,0xE10000000是頁交換區的起始地址。同樣我們使用上面的例子來示範這個演算法:
上面的無效PTE為01A714F6,有了這個值,我們可以得到:
PPTE Address = (0x01A714F6 >> 2) & 0x3FFFFE00 + (0x01A714F6 & 0x000000FF) << 1 + 0xE1000000
= 0x0069C53D & 0x3FFFFE00 + 0xF6 << 1 + 0xE1000000
= 0x69C400 + 0x1EC + 0xE1000000
= 0xE169C5EC
與我們通過VAD尋找到的PPTE位置0xE169C5EC一致。
為了更好的理解PPTE,我們再來看一個例子。我們知道在Windows 2000/XP中ntdll.dll是個非常重要的dll,只要作業系統正常啟動,ntdll肯定會被多個進程共用。我們用SoftICE作如下分析:
:query -x 77f50000
Context Address Range Flags MMCI PTE Name
smss 77F50000-77FF8000 07100005 80E6FA50 E131F9E8 ntdll.dll
.
.
.
explorer 77F50000-77FF8000 07100005 80E6FA50 E131F9E8 ntdll.dll
.
.
.
:addr smss
:mod ntdll
hMod Base PEHeader Module Name File Name
77F50000 77F500E8 ntdll /WINDOWS/system32/ntdll.dll
根據ntdll的基地址77F50000,我們查看其硬體PTE:
:dd 1df*1000+350*4+c0000000 l 4 //詳細請參考《小議Windows NT/2000分頁機制》
0010:C01DFD40 02267027 02F2E005 02F2F005 00C7E4FA 'p&.............
從smss進程的這些頁表,我們很容易知道ntdll.dll第1至3個4K均駐留於實體記憶體地址中,因為它們都是有效硬體PTE,而第四個PTE(00C7E4FA),雖然其是一個無效PTE(bit 0為0),但由於其是一個指向PPTE的PTE(bit 10為1),所以我們不能僅憑此PTE是個無效PTE,就斷定ntdll.dll的第4個4K就不在實體記憶體中。我們要進一步的分析這個PTE,找出指向的PPTE判斷這第4個4K是不是真的就是在磁碟中。OK,通過上面提及的演算法,我們很容易的算出PPTE Address為E131F9F4,我們來看看這個PPTE的值:
:dd e131f9f4 l 4
0010:E131F9F4 02F30121 02F31121 02F32121 02F33121 !...!...!!..!1..
從值02F30121我們這時就可以判定這第4個4K也存在於物理地址中,位於Page Frame Number為02F30的實體記憶體中,剩下的就是查PFN Database了。
我們也可以來查看查看explorer進程的ntdll.dll映射情況,來驗證一下這種情況:
:addr explorer
:dd 1df*1000+350*4+c0000000 l 4
0010:C01DFD40 02267025 02F2E025 02F2F025 02F30025 %p&.%...%...%...
這回清楚了吧。文章開頭我提及:“我們不需要更新所有引用這一頁面的進程的硬體PTE,因為此時所有進程的PTE均指向PPTE,我們只要更新PPTE就能達到目的了”。從中我們也可以看到ntdll.dll的第4個4K實際上位於實體記憶體中,但Windows 2000/XP並沒有更新每個引用此頁面的PTE,就正如smss進程一樣。而PPTE卻已經指向其實際地址了。當smss進程首次訪問這個地區時,記憶體管理器才將02F30025(假設屬性與explorer進程使用這頁的屬性一樣且為考慮訪問位標誌)這個有效硬體PTE更新上面的00C7E4FA,現在一切都明朗了吧。
本文雖然著重點在於介紹PPTE,但實際上我已將Section對象的內部機制說得非常清楚。這也是我原先將文章標題定為剖析Section之類的。關於PPTE,我的理解也經曆了較多時間,主要是目前這部分資料實在是沒有,僅有的《Inside Windows 2000》在沒深入介紹的同時其指向PPTE的無效PTE格式未明確指出(特別是加上0xE1000000,這讓我吃盡了苦頭),本文介紹的這個格式我已經在Windows 2000及XP上測試過,實際上本文的兩個例子一個是在Windows 2000 Server Build 2195,另一個在XP專業版Build 2600上示範的。
在這次介紹PPTE後,我們來回顧一下記憶體管理器內部的幾個千絲萬縷的聯絡:
FILEOBJECT的SECTION_OBJECT_POINTERS->DataSectionObject或SECTION_OBJECT_POINTERS->ImageSectionObject(決定於Section對象映射的檔案的開啟檔案)指向Control Area,同時進程描述這檔案對應的虛擬位址的VAD的MMCI成員(SoftICE叫法)也指向這個Control Area,Control Area底下存在一至多個SubSection,SubSection指向PPTE,PPTE table一般位於Control Area指向的Segment結構的底部。Section對象指向Segment;進程Page Table指向PPTE;這一切現在已描述的比較清楚了。還有一個主要的聯絡,即PFN Entry的Restore PTE(Original PTE)指向Subsection,這個關係我將在下次予以介紹。
從《小議Windows NT/2000分頁機制》到今天這篇介紹PPTE,我對Windows 2000/XP的記憶體管理部分才有了比較深入的理解,至於未提及到的Working Set等概念也是非常重要的。經曆過很多的模糊,對記憶體管理器也總算有了些許概念了。所有討論均基於自己的理解,對錯請多多指教(tsu00@263.net)。