小議Windows NT/2000分頁機制
WebCrazy(tsu00@263.net)
記憶體管理是作業系統最重要的一部分,其決定了作業系統的效能。Intel X86採用分段、分頁的記憶體機制,Windows NT/2000充分利用了此項先進的機制。段內IA-32體系使用頁目錄(Page Directory)與頁表(Page Table,SoftICE中Page命令可以顯示出頁目錄與頁表等)形成對4G地址的定址能力。文中未特別說明,均討論運行平台為Intel 32位處理器的Windows NT/2000,所提及的Windows也僅指Windows NT/2000。Windows中不管使用者態與核心態的程式碼片段、資料區段與堆棧段基址均為0,文中提到的邏輯地址(由段基址與位移量兩部分組成)與線性地址值是相等的。由於對於使用者來說線性地址是其可見的,若我未特別指出物理地址,所說的地址也僅指線性地址。
頁目錄(PDE)由1024項組成,每項均指向一頁表(PTE),每一頁表也由1024個頁組成,IA-32體系每頁大小為4K,所以可定址範圍為4G(1024*1024*4K)。Windows中每個進程都擁有其各自的進程地址空間,即擁有其各自的頁目錄與頁表。每個進程均使用線性地址C0300000H指向其特定的頁目錄所在的地址,而頁目錄中每項(即頁表)均依次排列線上性地址C0000000H處,每個頁表均佔用4K(1024*4)位元組,如第一個頁表位於C0000000H處,而第二個頁表位於C0000000+1000H(4K),即C0001000中,依次類推,計算公式即為C0000000H+頁目錄位移值(線性地址的高10位)*1000H,在下面我將利用此公式。當然以上描述的前提是每個頁表均位於實體記憶體中(由頁目錄中每項中的P位指定),這也是為什麼IA-32使用兩級頁表的原因,否則每個進程除其代碼與資料等外還額外需要4M(4*1024*1024)的儲存空間。
上面的機制即實現了物理地址定址,也就實現了在Windows NT/2000中物理地址與線性地址的相互轉換(雖然CPU在對記憶體操作時只需要線性地址轉換成物理地址,但我們在剖析器代碼等仍需要物理地址轉換成線性地址)。照例先看看SoftICE的分析吧:
// addr explorer命令後以下操作將只在進程explorer的私人進程空間進行
:addr explorer
// 顯示explorer進程頁目錄所在物理地址,即進程切換至explorer後PDBR(CR3)中的值
:addr
CR3 LDT Base:Limit KPEB Addr PID Name
.
.
.
00C10000 FF9FC920 036C explorer
.
.
.
/*
線性地址的格式:0-11位對應1頁(4096位元組)的位移量
12-21位對當前頁表中1024項中定址
22-31位對頁目錄進行定址
高20位(12-31位)又稱為楨
根據上面提及的公式,即可以得到物理地址高20位的值,再加上線性地址頁表位移(作為物理
地址的低12位),即實現了線性地址轉化為物理地址,用公式表示為:
@(C0000000h+PDE*1000h+PTE*4)&0fffff000h+PO
=@(C0000000h+4*(PDE*400h+PTE))&0fffff000h+PO
=@(C0000000h+(PDE>>10d+PTE)<<2d)&0fffff000h+PO
=@(C0000000h+(LA>>12d)<<2d)&0fffff000h+PO
=@(C0000000h+(LA&0xFFFFF000)>>10d)&0fffff000h+PO
上式中用PDE與PTE分別代表Page Directory與Page Table的位移值,用LA代表給定的線性
地址,用PO代表LA的低12位,用h與d分別代表16/10進位,@後表示取後地址指標中的內容。
如此分析後,線性地址C0300000所對應的頁目錄為300h,頁表為300h,位移量為0
則 C0000000h+PD*1000h+PT*4+PO=C0000000+300h*1000h+300h*4+0
*/
:dd c0000000+300*1000+300*4 l 4
0010:C0300C00 00C10063 01A31063 00000000 0141F163 c...c.......c.A.
|
|_低12位(0-11)063為屬性位、Intel保留位與系統(OS)使用位
// 顯示C0300000的物理地址(00C10000)
dword(@(c0000000+300*1000+300*4))&fffff000+C0300000&00000fff
00C10000
//用SoftICE驗證
:phys dword(@(c0000000+300*1000+300*4))&fffff000+C0300000&00000fff
C0300000
:page C0300000
Linear Physical Attributes
C0300000 00C10000 P D A S RW
其實上面最後一條命令就可以實現所有其它指令的功能,下面我列出實現的程式碼片段:
// 線性地址->物理地址
// SoftICE中Page命令可以實現此功能
// 一個線性地址對應唯一的物理地址
// 此函數若返回0說明此線性地址未對應物理地址
ULONG LinearAddressToPhysicalAddress(ULONG lAddress)
{
unsigned int *pAddr;
unsigned int *PageDirectoryEntry=(unsigned int *)0xC0300000;
unsigned int *PageTableEntry=(unsigned int *)0xC0000000;
//判斷頁目錄是否有效,第0位(P)為存在位,請參閱相關書籍
if((!(PageDirectoryEntry[lAddress>>22]&0xFFFFF000))
&&(!(PageDirectoryEntry[lAddress>>22]&0x00000001)))
return 0;
//@(C0000000h+(LA&0xFFFFF000)>>10d)&0fffff000h+PO 見上敘述
pAddr=(int *)((int)PageTableEntry+((lAddress&0xFFFFF000)>>10));
if((*pAddr)&1)
return ((*pAddr) &0xFFFFF000) |(lAddress&0x00000FFF);
return 0;
}
那麼又如何逆向實現物理地址轉換成線性地址呢?雖然其間沒有任何關係,但因為知道頁目錄與頁表的具體位置,可使用直接在這個範圍中搜尋的方法。理論上這個範圍最大為1024*1024*4(4M),但由於很多頁目錄項當前均不存在於實體記憶體中,所以實際上搜尋範圍小得多。這也導致一個問題,有可能導致藍屏(核心態訪問不存在的地址)。所以以下給出的代碼我檢查了頁目錄中每一項的P位。
// 物理地址->線性地址
// 相當於SoftICE中Phys命令
// 搜尋所有有效頁表尋找指定物理地址
// 有可能多個線性地址同時指向同一個物理地址
// 此函數若未輸出任何結果表明當前還沒有線性地址映射至此物理地址中
void PhysicalAddressToLinearAddress(ULONG pAddress)
{
unsigned int *pAddr;
unsigned int *PageDirectoryEntry=(unsigned int *)0xC0300000;
unsigned int *PageTableEntry=(unsigned int *)0xC0000000;
int i,j;
DbgPrint("/n");
for(i=0;i<1024;i++)
if((PageDirectoryEntry[i]&0xFFFFF000)&&(PageDirectoryEntry[i]&0x00000001))
for(j=0;j<1024;j++){
pAddr=(int *)((int)PageTableEntry+i*4096+j*4);
if((*pAddr)&0x00000001)
if(((*pAddr)&0xFFFFF000)==(pAddress&0xFFFFF000))
DbgPrint("%08X/n",
((i*4*1024*1024+j*4*1024)&0xFFFFF000)|(pAddress&0x00000FFF));
}
}
上面兩個程式段涉及到2G-4G範圍(線性地址C0000000以上)的記憶體訪問,普通使用者態的程式無法實現。在Windows中請使用裝置驅動程式以使其在核心態正確執行。
那麼Windows又是如何利用分頁機制以高效、合理的利用好有限的實體記憶體呢?Jeffrey Richter的經典著作<<Programming Applications for Microsoft Windows,Fourth Edition>>全面闡述了Windows的記憶體管理機制,相關原理請具體參閱此書!下面我列出同一程式(mspaint.exe)的兩個運行執行個體的線性地址與物理地址的對應關係,以說明Windows的記憶體分頁機制。
// 下面是mspaint.exe裝入記憶體後各段的地址(兩個同時啟動並執行mspaint.exe進程的映射地址一致)
:map32 mspaint
Owner Obj Name Obj# Address Size Type
mspaint .text 0001 001B:01001000 0003A500 CODE RO
mspaint .data 0002 0023:0103C000 00002670 IDATA RW
mspaint .rsrc 0003 0023:0103F000 000116C8 IDATA RO
-------------
|
|_邏輯地址
// mspaint.exe第一個運行執行個體的線性地址與物理地址的對應關係
Linear Address Range Physical Address Range Attributes
-------------------- ---------------------- ----------
00010000 - 00010FFF 03A8B000 - 03A8BFFF 047
00020000 - 00020FFF 03BCC000 - 03BCCFFF 047
0006D000 - 0006DFFF 018BC000 - 018BCFFF 047
. . .
. . .
. . .
//mspaint.exe第一個執行個體的.text段
01001000 - 01001FFF 00596000 - 00596FFF 005
01002000 - 01002FFF 03F97000 - 03F97FFF 005
01003000 - 01003FFF 03D58000 - 03D58FFF 005
. . .
. . .
. . .
//mspaint.exe第一個執行個體的.data段
0103C000 - 0103CFFF 0225F000 - 0225FFFF 047
0103D000 - 0103DFFF 03620000 - 03620FFF 047
0103E000 - 0103EFFF 03C1E000 - 03C1EFFF 047
. . .
. . .
. . .
//mspaint.exe第一個執行個體的.rsrc段
0103F000 - 0103FFFF 01652000 - 01652FFF 025
01040000 - 01040FFF 02653000 - 02653FFF 005
01041000 - 01041FFF 003D4000 - 003D4FFF 005
. . .
. . .
. . .
//mspaint.exe第一個執行個體的頁目錄的表
C0300000 - C0300FFF 030FD000 - 030FDFFF 063
C0301000 - C0301FFF 017FE000 - 017FEFFF 063
C0303000 - C0303FFF 0141F000 - 0141FFFF 163
. . .
. . .
. . .
FFD0F000 - FFD0FFFF 000FF000 - 000FFFFF 023
FFDF0000 - FFDF0FFF 0026A000 - 0026AFFF 163
FFDFF000 - FFDFFFFF 00269000 - 00269FFF 163
// mspaint.exe第二個運行執行個體的線性地址與物理地址的對應關係
Linear Address Range Physical Address Range Attributes
-------------------- ---------------------- ----------
00010000 - 00010FFF 03A6A000 - 03A6AFFF 047
00020000 - 00020FFF 0352B000 - 0352BFFF 067
0006D000 - 0006DFFF 03413000 - 03413FFF 047
. . .
. . .
. . .
//mspaint.exe第二個執行個體的.text段
01001000 - 01001FFF 00596000 - 00596FFF 005
01002000 - 01002FFF 03F97000 - 03F97FFF 005
01003000 - 01003FFF 03D58000 - 03D58FFF 005
. . .
. . .
. . .
//mspaint.exe第二個執行個體的.data段
0103C000 - 0103CFFF 030DF000 - 030DFFFF 047
0103D000 - 0103DFFF 009A0000 - 009A0FFF 047
0103E000 - 0103EFFF 02089000 - 02089FFF 047
. . .
. . .
. . .
//mspaint.exe第二個執行個體的.rsrc段
0103F000 - 0103FFFF 01652000 - 01652FFF 005
01040000 - 01040FFF 02653000 - 02653FFF 005
01041000 - 01041FFF 003D4000 - 003D4FFF 005
. . .
. . .
. . .
//mspaint.exe第二個執行個體的頁目錄的表
C0300000 - C0300FFF 037C9000 - 037C9FFF 063
C0301000 - C0301FFF 02F8A000 - 02F8AFFF 063
C0303000 - C0303FFF 0141F000 - 0141FFFF 163
. . .
. . .
. . .
FFD0F000 - FFD0FFFF 000FF000 - 000FFFFF 023
FFDF0000 - FFDF0FFF 0026A000 - 0026AFFF 163
FFDFF000 - FFDFFFFF 00269000 - 00269FFF 163
上面列表直接從mspaint.exe的兩個同時啟動並執行執行個體的頁目錄與頁表中取得。實際上你只要理解物理地址與線性地址的相互轉換,稍微修改一下上面所給的兩個程式碼片段便能得到上面的資訊(32位x86平台Windows 2000 Server Build 2195的某一時刻所取,我的機器實體記憶體為64M,即040000000H位元組)。
Windows中每個進程均有其私人的線性地址,線上性地址的前2G(使用者空間,Windows 2000 Server的ntoskrnl.exe的MmHighestUserAddress指出使用者空間最大值7FFEFFFFH),上例中列出的兩個執行個體同時啟動並執行mspaint.exe的.text段與.rsrc段均指向同一物理地址空間,而.data段指向不同的物理空間。這是由於不同段的作用與性質決定,不難理解。在後2G(核心空間,Windows 2000 Server的ntoskrnl.exe的MmSystemRangeStart指出其線性地址的開始位置),大部分物理空間均由兩個執行個體共用,實際上不同程式所有啟動並執行常式均共用這2G,當然頁目錄(C0300000h)與頁表(C0000000h)等其它比較特殊的操作除外,上例中基本可以看出這個規則。當然頁目錄與頁表也有項目是指向同一物理地區,這樣才能實現進程間共用實體記憶體,如上面mspaint.exe的兩個執行個體的頁目錄中C0303000-C0303FFF所映射的線性地址00800000-00BFFFFF(4M)指向同一物理地區。
這隻是討論通常情況下Windows如何在程式間高效的使用記憶體,實際上Windows賦予頁很多機制,如Copy On Write等,使.text段等需要時也可以指向不同的物理地址,典型情況是使用使用者態的調試器(Microsoft Visual C++所附的IDE調試環境等)對應用程式進行調試。當然Windows也提供讓資料共用同一實體記憶體地區的方法,即使用Microsoft連接器(link)的section開關,賦予特定的段共用(S)屬性。
在<<淺析Windows NT/2000環境切換>>(Nsfocus Magazine 12)中我曾詳細的介紹了Windows NT/2000環境切換後頁目錄基址(CR3)切換代碼,那麼Windows NT/2000如何為應用程式從頭分配頁目錄與頁表呢?因為只有在建立新進程時才牽涉到頁目錄與頁表的分配,所以還是讓我們看看Kernel32.dll中的CreateProcessW代碼吧(CreateProcessA間接調用CreateProcessW).
可以簡單的將流程用代碼如下顯示:
KERNEL32!CreateProcessW
.
.(一些錯誤常式如進程檔案是否存在,核心對象安全性檢查等過程)
.
;開啟檔案
001B:77E7DDD2 CALL [ntdll!NtOpenFile]
.
.(主要是一些參數的壓棧代碼)
.
;為可執行檔分配虛擬位址
001B:77E7DE0A CALL [ntdll!NtCreateSection]
.
.
.
;關閉檔案
001B:77E7DE1E CALL [ntdll!NtClose]
.
.
.
;調用NtCreateProcess建立進程
001B:77E7DF83 CALL [ntdll!NtCreateProcess]
.
.
.
其實上面ntdll.dll的四個過程在Windows 2000 Server Build 2195中分別是Service ID為64h、2bh、18h與29h的System Service。關於System Service可參閱<<再談Windows NT/2000內部資料結構>>(Nsfocus Magazine 11)。
我們繼續看看NtCreateProcess處理流程:
:u ntdll!NtCreateProcess //使用者態,就是常說的Native API
ntdll!NtCreateProcess
001B:77F92D2C MOV EAX,00000029
001B:77F92D31 LEA EDX,[ESP+04]
001B:77F92D35 INT 2E //使用中斷門進入核心態
001B:77F92D37 RET 0020
:ntcall
Service table address: 804704D8 Number of services:000000F8
.
.
.
0029 0008:804AD948 params=08 ntoskrnl!SeUnlockSubjectContext+0514
|
|_ID為29h的System Service(NtCreateProcess)的入口地址
.
.
.
:u 8:804ad948
0008:804AD948 55 PUSH EBP
0008:804AD949 8BEC MOV EBP,ESP
0008:804AD94B 6AFF PUSH FF
0008:804AD94D 6890354080 PUSH 80403590
0008:804AD952 682CCC4580 PUSH ntoskrnl!_except_handler3
0008:804AD957 64A100000000 MOV EAX,FS:[00000000]
0008:804AD95D 50 PUSH EAX
0008:804AD95E 64892500000000 MOV FS:[00000000],ESP
.
.
.
;EBP-30中此時存放建立進程的KPEB,以下幾句實現對KPEB後的0A2H*4(648)位元組清零
0008:804ADAF5 B9A2000000 MOV ECX,000000A2
0008:804ADAFA 33C0 XOR EAX,EAX
0008:804ADAFC 8B7DD0 MOV EDI,[EBP-30]
0008:804ADAFF F3AB REPZ STOSD
.
.
.
0008:804AD5E7 55 PUSH EBP
0008:804AD5E8 8BEC MOV EBP,ESP
;KPEB與進程Context分別作為這個過程的第一與第四個參數傳入(ebp+8與ebp+14h)
0008:804AD5EA 8B4508 MOV EAX,[EBP+08]
0008:804AD5ED 8D4808 LEA ECX,[EAX+08]
0008:804AD5F0 C60003 MOV BYTE PTR [EAX],03
0008:804AD5F3 89480C MOV [EAX+0C],ECX
0008:804AD5F6 C640021B MOV BYTE PTR [EAX+02],1B
0008:804AD5FA 8909 MOV [ECX],ECX
0008:804AD5FC 8A4D0C MOV CL,[EBP+0C]
0008:804AD5FF 884862 MOV [EAX+62],CL
0008:804AD602 8B4D10 MOV ECX,[EBP+10]
0008:804AD605 89485C MOV [EAX+5C],ECX
0008:804AD608 8A4D18 MOV CL,[EBP+18]
0008:804AD60B 884864 MOV [EAX+64],CL
0008:804AD60E 8B4D14 MOV ECX,[EBP+14]
0008:804AD611 8B11 MOV EDX,[ECX]
;EDX存放進程Context(即頁目錄的物理地址)
;至於進程Context的演算法,由於不僅與ntoskrnl.exe中的幾個變數有關,還與執行環境息息相關,感興趣的自己用SoftICE跟跟
;將進程Context放入建立的KPEB中
0008:804AD613 895018 MOV [EAX+18],EDX ;18H是進程Context相對KPEB的位移
.
.
.
;以下實現將建立的進程的KPEB插入系統KPEB的雙向鏈表
0008:804ADD22 A184A14680 MOV EAX,[8046A184]
/*
8046A180用以下兩條SoftICE命令輸出結果就可以很容易理解
(@8046a180)-a0
FE4E1D60 4266532192 (-28435104) "﨨`"
@PsInitialSystemProcess //顯示System進程的KPEB
FE4E1D60 4266532192 (-28435104) "﨨`"
即實現將新KPEB鏈插入已有鏈表尾
在插入KPEB後,系統就可以根據上面提供的頁目錄對進程進行調度(即新進程擁有新的私人的進程空間)
*/
0008:804ADD27 8B4DD0 MOV ECX,[EBP-30]
0008:804ADD2A C781A000000080A14680 MOV DWORD PTR [ECX+000000A0],8046A180
0008:804ADD34 8B4DD0 MOV ECX,[EBP-30]
0008:804ADD37 8981A4000000 MOV [ECX+000000A4],EAX
0008:804ADD3D 8B4DD0 MOV ECX,[EBP-30]
0008:804ADD40 81C1A0000000 ADD ECX,000000A0 ;是不是可以找出鏈狀結構相對KPEB的位移呢?
0008:804ADD46 8908 MOV [EAX],ECX
0008:804ADD48 8B45D0 MOV EAX,[EBP-30]
0008:804ADD4B 05A0000000 ADD EAX,000000A0
0008:804ADD50 A384A14680 MOV [8046A184],EAX
為了更直觀,上面的代碼我只是按照系統執行流程列出(相對於實際的磁碟存放順序).其實系統在建立進程初,首先用ObCreateObject建立section核心對象(section對象並不分配實體記憶體,Windows 2000 DDK Documentation中有詳細介紹),然後才有牽涉到頁目錄與頁表,自己在分析代碼時要特別注意,還有可以使用IDA對ntoskrnl.exe進程分析,畢竟IDA對代碼流程提供的比較清晰。至於以上所提的進程Context,系統KPEB雙向鏈表等請參閱<<淺析Windows NT/2000環境切換>>,在那我已經進行了比較詳細的說明。
在深入分析Windows的有關記憶體操作的API(如VirtualAllocEx,CreateFileMapping,HeapAlloc等等)後,還可以發現很多其它方面重要的資訊。如跟蹤ntoskrnl!NtCreateSection(Windows在裝入可執行檔與CreateFileMapping等都最終調用此函數)可發現Copy On Write等機制是如何?的等等.這些都留著你自己去找找了。Windows 2000支援多個分頁檔(命名為Pagefile.sys),這牽涉到原型PTE(Sofice中用ProtoPTE表示),分頁檔案使用的PTE等等很多的機制,要分析之,還要對Windows 2000的層次驅動程式中的FSD有很好的理解.簡單的說即文中只討論到PTE中的P位為1的情況。好了,還是那句話,以上分析,如有錯誤,還望指點(tsu00@263.net)!
參考資料:
1.Jeffrey Richter
<<Programming Applications for Microsoft Windows,Fourth Edition>>
2.Intel Corp<<Intel Architecture Software Developer's Manual,Volume 3>>
3.<<Undocumented Windows NT>>附帶源碼
4.Windows 2000 DDK Documentation