匠心 x64 結構體系下的記憶體定址
在閱讀NewBluePill原始碼的時候,看記憶體的那一塊簡直頭疼,全是x64下的定址,之前根本就沒有接觸過x64的記憶體定址上的內容,看的暈頭轉向,決定先把x64下的定址給弄明白了再回過頭來看NewBluePill的原始碼,然後在網上一頓找,居然沒有找到關於x64定址的部落格或者文章,簡直痛苦啊,終於把x64的定址問題弄清楚了,總結出來分享一下學習歷程。
0x01 x64定址簡介
在保護模式,CPU發出的線性地址,記憶體管理單元(MMU),根據當前CR3暫存器所指向的頁表實體地址將該線性地址翻譯成實體地址進行記憶體訪問,該過程稱為地址翻譯。
在x64體系結構中,線性地址的結構如圖
在x64體系中只實現了48位的virtual address,高16位被用作符號擴充套件,這高16位要麼全是0,要麼全是1。
不同於x86體系結構,每級頁表定址長度變成9位,由於在x64體系結構中,普通頁大小仍為4KB,然而資料卻表示64位長,因此一個4KB頁在x64體系結構下只能包含512項內容,所以為了保證頁對齊和以頁為單位的頁表內容換入換出,在x64下每級頁表定址部分長度定位9位。
為了正確翻譯x64的線性地址,其頁表也從x86的2級變成了4級,翻譯過程如圖所示,在x64體系結構中,每級頁表包含512項(2^9)下級目錄的指標,該指標稱為頁表項,描述了儲存下級
- PML4T(Page Map Level4 Table)及表內的PML4E結構,每個表為4K,內含512個PML4E結構,每個8位元組
- PDPT (Page Directory Pointer Table)及表內的PDPTE結構,每個表4K,內含512個PDPTE結構,每個8位元組
- PDT (Page Directory Table) 及表內的PDE結構,每個表4K,內含512個PDE結構,每個8位元組
- PT(Page Table)及表內額PTE結構,每個表4K,內含512個PTE結構,每個8位元組。
每個table entry 的結構都是8個位元組64位寬,而virtual address中每個索引值都是9位,因此每個table都是512 x 8 = 4K位元組。
0x02 頁轉換模型
X64,準確的說應該是IA32e paging 模型提供了三種頁轉換模型,
① 4K頁面的轉換表結構;
② 2M 頁面的轉換結構;
③ 1G頁面的轉換結構;
在64位模式下,處理器將48的虛擬地址轉化為實體地址,在相容模式下,轉化32位的虛擬地址。
三種模型都是物理頁幀的基地址加上頁偏移得到實體地址,不同只是在於頁幀的大小劃分不同:
①4K頁面: 使用PML4T,PDPT,PDT和PT 四級頁轉化表結構;
②2M頁面:使用PML4T,PDPT 和PDT三級頁轉化表結構;
③1G 頁面:使用PML4T和PDPT二級頁錶轉化結構。
而在這裡我們主要討論的是4K頁面大小的定址方式,因為在個人計算機上,普遍都是4K
頁面定址,其他的方式也主要就是頁面大小的差異。
0x03 最大實體地址
在Intel中使用MAXPHYADDR來表示最大的實體地址,我們可以通過CPUID的指令來獲得處理支援的最大實體地址,然而這已經不在此次的討論範圍之內,我們需要知道的只是:
當MAXPHYADDR 為36位,在Intel平臺的桌面處理器上普遍實現了36位的最高實體地址值,也就是我們普通的個人計算機,可定址64G空間;
當MAXPHYADDR 為40位,在Inter的伺服器產品和AMD 的平臺上普遍實現40位的最高實體地址,可定址達1TB;
當MAXPHYADDR為52位,這是x64體系結構描述最高實現值,目前尚未有處理器實現。
而對下級表的實體地址的儲存4K頁面定址遵循如下規則:
① 當MAXPHYADDR為52位時,上一級table entry的12~51位提供下一級table物理基地址的高40位,低12位補零,達到基地址在4K邊界對齊;
② 當MAXPHYADDR為40位時,上一級table entry的12~39位提供下一級table物理基地址的高28位,此時40~51是保留位,必須置0,低12位補零,達到基地址在4K邊界對齊;
③ 當MAXPHYADDR為36位時,上一級table entry的12~35位提供下一級table物理基地址的高24位,此時36~51是保留位,必須置0,低12位補零,達到基地址在4K邊界對齊。
0x04 實際轉化
l CR3
當CR4.PCIDE = 0時,CR3的結構如圖,
CR3可以使用64位寬,但是它表示的PML4T的物理基地址同樣受到之前所說的MAXPHYADDR的約束,圖示的只是理想的MAXPHYADDR為52位時的情況。
而當CR4.PCIDE = 1的時:
R3的低12位提供一個PCID值,用來定義當前Process Context ID.
當對CR3進行更新時,CR3第63位決定是否需要處理器的TLB和paging-struct cache,這不在我們此次談論的範圍之內。
l PML4E
接著再看PML4E的結構,如圖:
PML4E並沒有PS標誌位,因此第7位是保留的,而PML4E提供的PDPT的物理基地址也受之前的MAXPHYADDR規則的約束。
l PDPTE
然後就是PDPTE結構:
由於新增了1G 頁面,因此在PDPTE結構裡將控制1G的頁面轉化,由PDPTE.PS標誌位進行轉換,如圖:
當PDPTE.PS=1,也就是PDPTE的第7位為1時,PDPTE將提供1G的物理頁面地址;當PDPTE.PS=0,也就是PDPTE的第7位為0時,使用非1G的頁面,將提供下一級的PDT的物理基地址,同樣受MAXPHYADDR規則的約束。
1G頁面下的PDPTE 的結構解析如下:
同樣地,PDPTE提供的1G頁面的實體地址也遵守MAXPHYADDR的規則,1G頁面的地址低30將補0,意味著1G邊界上對齊。
4K和2M頁面下的PDPTE結構解析如下:
將提供下一級PDT的物理基地址,同樣也遵循MAXPHYADDR規則,那麼再根據PDE.PS再決定是使用2M頁面還是4K頁面。
l PDE
PDE的結構和PDPTE類似,也是用PS(第7位)表示是使用2M的頁面還是4K 的頁面,下面是2M 頁面的PDE結構解析:
同樣對於頁面的物理基地址也遵循MAXPHYAD原則。
接下來是4K 頁面的 PDE 結構解析:
也遵循MAXPHYADDR 規則。
l PTE
PTE的結構解析:
同樣遵循MAXPHYADDR規則。
0x05 實際例子
上面寫了很多都是原理性的東西,可能看完之後對於x64還沒有很清晰的認識,我們以一個很簡單的例子來加深對於x64結構體系的定址的認識。
#include "stdafx.h" #include <Windows.h> int _tmain(int argc, _TCHAR* argv[]) { char szName[20] = "HelloWorld"; printf("szName:%x\n",szName); getchar(); return 0; }
很簡單的一個程式,就是打印出szName的虛擬地址,執行結果如下:
我們接下來要做的就是將0x2ffde8這個虛擬地址轉換成實體地址,在物理頁上找到我們的”HelloWorld”。
0x2ffde8 ===> 轉換二進位制:
000000000 000 0000 00 00 0000 001 0 1111 1111 1101 1110 1000
0 0 1 0xff 0xde8
PML4E索引 PDPTE索引 PDE索引 PTE索引 頁內偏移
目標程序的DirBase為0x7d838000,根據我們之前學習的定址方式,應該是按照MAXPHYADDR為36位的規則,即上一級table entry的12~35位提供下一級table物理基地址的高24位,此時36~51是保留位,必須置0,低12位補零。
因為PML4E的索引為0,所以我們的目標PML4E項的值為0x02b00000~7d274867,
12~35位為0x07d274,低12位補零,則:
PDPTE的索引也為0,目標PDPTE項的值為 0x03000000~7d737867,
PS位(第7位)為0,12~35位為 0x 07d737 ,低12位補零,則:
因為PDE的索引為1,所以我們要加上8,目標PDE項的值為 :0x01500000~7d7bb867
PS位(第7位)為0,12~35位為 0x07d7bb,低12位補零,則:
PTE的索引為0xff,所以要加上0xff*8,得到目標PTE項的值為:0x89a00000~7d084867
12~35位為 0x07d084,低12補零,得到頁面物理基地址,再加上頁面偏移,我們是0xde8,則:
終於在物理頁上看到了我們熟悉的“HelloWorld”。