1.概述
一個進程的虛擬地址空間是4GB,它的物理大小並不是真的4GB,如果真的是4GB,那運行中的所有進程都加起來佔用的空間是一個天文數字,事實上系統會對這4GB的地址空間進行轉換,然後映射到物理內存中。換句話說虛擬地址本身並不是真實存在的。
將虛擬地址轉換爲物理地址依賴的規則就是分頁機制。
物理地址也並不是內存條地址,物理地址需要再進行一層轉換才能找到真正的內存條地址。
我們只要會轉換到物理地址就夠用了。
1.1 邏輯地址
假如有指令:
MOV eax,dword ptr ds:[0x123456]
0x123456就是邏輯地址
1.2 線性地址
線性地址是對於虛擬空間來說的,還是上面的例子,ds.Base + 0x123456就是線性地址
1.3 物理地址
通過線性地址可以轉換爲物理地址。
1.4 分頁模式
分頁模式分爲10-10-12和2-9-9-12,前者最多可以支持4GB內存,後者在32位操作系統下可以支持16GB內存,64位操作系統下可以支持128G內存,後者也叫PAE分頁。
在Windows XP下,找到C盤下的boot.ini文件,將裏面的noexecute改爲execute即可將PAE分頁改爲10-10-12分頁。
2. 10-10-12模式
2.1 10-10-12模式概述
一個32位的虛擬地址會被解釋爲三個部分:
- 頁目錄索引(PDT,10位)
- 頁表索引(PTT,10位)
- 字節索引(12位)
系統中有一個CR3寄存器,它存儲了頁目錄索引的地址,頁目錄佔4KB,裏面的成員佔4個字節,因此有1024個成員,PDT裏面的成員被稱作PDE,PDE裏面保存的是一個32位的物理頁地址。
每個PDE又指向一個頁表索引,頁表索引裏面的成員被稱作PTE,PTE可以沒有物理頁(線性地址0一般就沒有),多個PTE也可以指向同一個物理頁。
10-10-12結構的第1個10是PDT中的索引,第2個10是PTT中的索引,最後的12是指物理頁上的偏移,所以簡單來說轉換成物理地址只需要將10-10-12的結構分別在PDT和PTT中找到對應的位置再加上物理頁的偏移就是最終的物理地址了。
(上面的圖其實不是很準確,不過爲了便於理解先以這個圖爲準吧,其實頁目錄表也是頁表,只不過它比較特殊,後面會介紹)
2.2 線性地址轉物理地址實驗
本實驗是在10-10-12模式下進行的,因此要將PAE關閉,PAE就是2-9-9-12模式,可以簡單地理解它爲增強型的分頁模式。
bcdedit /set pae forcedisable
重新打開PAE模式
bcdedit /set pae forceenable
1.找到數據在內存中的線性地址
- 新建一個.txt文件,文件中 有字符串“hello,world”,並保持.txt文件處於打開狀態。
- 使用CE附加到notepad.exe
- 在Text欄中搜“hello,world”,找到可以找到多個跟它有關的地址,通過修改.txt文件中的字符串最終篩選出真正的內存地址0x000AA8A0,注意勾選上Unicode以及Value Type的類型,如下圖所示:
在進程中查找字符串還有一方法:
- 在WinDbg中執行!process 0 0 notepad.exe
- 繼續執行 .process 9050c4e0,切換到記事本進程中
- 執行s -u 0x00000000 L0x01000000 "hello,world"搜索內存中的字符串
- 結果可能會有多個,但是隻有一個是正確的線性地址,可以使用du 0x28e090指令查看Unicode字符串。
2.分解線性地址
000AA8A0轉成二進制是32位,剛好可以按照10-10-12的格式分解成:
0000 0000 00
0010 1010 10
1000 1010 0000
3.根據分解後的線性地址找到物理地址
-
首先在WinDbg中使用!process 0 0,在一堆進程中找到記事本進程,然後可以找到它的CR3寄存器0x146a0000,這個地址就是PDT的基址,它保存的是物理地址,所以下面的指令都是以!開頭的。
-
10-10-12格式的第1個10部分是0000 0000 00,這個值指向了它在PDT中的下標,所以使用!dd 146a0000+0找到對應的PDE。
-
10-10-12格式的第2個10部分是0010 1010 10,上面找到的PDE是0x14dc0067,因此通過使用!dd 14dc0000+aa*4找到對應的PTE,爲什麼不是14dc0067而是14dc0000?因爲PDE的最後12位是屬性,下面PTE的末尾12位同樣也是屬性,PDE的屬性&PTE屬性=物理頁的屬性。
PDE和PTE的屬性如下:
P:1可用,0不可用
R/W:R/W=0只讀,R/W=1可讀可寫
U/S:0特權用戶,1普通用戶
A:是否被訪問,訪問置1
D:是否被寫過,寫過置1
P/S:PageSize,只對PDE有意義,1直接指向物理頁無PTE,低22位是頁內偏移,頁大小爲4MB,俗稱爲“大頁”
PAT:只對PTE有意義
G:CR3改變時,TLB會立即刷新,但不會刷新PDE/PTE的G位爲1的頁,一般高2G線性地址G爲1,因爲高2G是系統通用的,刷新影響效率
PWT、PCD、PAT在下篇介紹PAE時的時候會解釋。
0010 1010 10對應aa,乘以4的原因是每個元素佔4個字節
- 找到的PTE是0x13fdd067,最後再加上物理頁偏移1000 1010 0000(轉換成十六進制是0x8a0)就是最終的物理地址了,使用!dd 13fdd000+8a0。
- 使用上面的指令還看不出是字符串“hello,world”,使用!db 13fdd000+8a0就可以看到字符串了
2.3 頁目錄表基址
系統要保證一個線性地址有效,必須提前將這個線性地址對應的PDE和PTE填充,那麼系統是怎麼填充PDE和PTE的呢?
PDE和PTE都是物理頁,我們在程序中是無法直接訪問物理頁的,我們要訪問任何東西必須要通過線性地址。
系統通過0xC0300000這個線性地址找到的物理頁就是頁目錄表,這個物理頁比較特殊,它是頁目錄表PDT,但是本身也是頁表,即PTT,換句話說,其實所謂的PDT表本身也是一個PTT表。0xC030000指向了PDT,所以系統可以通過它填充PDE,PTE是怎麼填充的呢?看下一節頁表基址
2.4 頁表基址
系統通過0xC0000000訪問第一個PTT,0xC0001000指向第二個PTT。
一個4GB的進程中只能有一個PDT(上面說過,其實PDT是一種特殊的PTT),但是有1024個PTT,每個PTT都佔4KB。
總結一下:
- 整個頁表佔4M地址空間,從0xC0000000到0xC03FFFFF,頁表有1024個成員,每個成員都是一個PTT,佔4KB。
- 在這1024個成員中,有一個PTT比較特殊,它就是PDT。
真正的頁表是這樣的:
訪問頁目錄表的公式:
0xC0300000 + PDI * 4(PDI是頁目錄表的索引)
訪問頁表的公式:
0xC0000000 + PDI * 4096 + PTI * 4(PDI * 4096是因爲每個PTT的大小是4K,PTI是頁表的索引,PTI*4,每個PTE佔4個字節)