高效內(nèi)存管理:x86-64架構(gòu)中的分頁(yè)機(jī)制
在 x86-64 架構(gòu)的世界里,內(nèi)存分頁(yè)機(jī)制扮演著舉足輕重的角色,它就像是一座橋梁,連接著虛擬地址與物理地址。簡(jiǎn)單來(lái)說(shuō),內(nèi)存分頁(yè)機(jī)制就是將線性地址(也就是虛擬地址)切分成一個(gè)個(gè)固定大小的頁(yè),并把這些頁(yè)映射為物理地址的機(jī)制 。
這種機(jī)制并非隨時(shí)可用,它有一個(gè)前提條件,那就是必須在保護(hù)模式下才能發(fā)揮作用(CR0.PE =1 時(shí)進(jìn)入保護(hù)模式)。當(dāng)分頁(yè)機(jī)制開(kāi)啟后,從應(yīng)用程序的視角來(lái)看,它所看到的是一個(gè)線性地址(虛擬地址)空間,而不是實(shí)際的物理地址。這就好比我們?cè)谕嬉粓?chǎng)虛擬現(xiàn)實(shí)游戲,游戲中的角色看到的各種場(chǎng)景和物品的位置都是虛擬的,而背后對(duì)應(yīng)的真實(shí)物理位置,角色是感知不到的。
為了更深入地理解分頁(yè)機(jī)制,我們先來(lái)看看它的開(kāi)啟與關(guān)閉條件。在保護(hù)模式下,是否開(kāi)啟分頁(yè)是由 CR0 寄存器的 PG 位(第 31 位)決定的。當(dāng) CR0.PG =0 時(shí),分頁(yè)機(jī)制未開(kāi)啟,此時(shí)線性地址等同于物理地址,就好像游戲里的虛擬位置和現(xiàn)實(shí)中的物理位置是一一對(duì)應(yīng)的,沒(méi)有任何轉(zhuǎn)換。而當(dāng) CR0.PG =1 時(shí),分頁(yè)機(jī)制開(kāi)啟,線性地址需要通過(guò)分頁(yè)單元的轉(zhuǎn)換才能形成物理地址,這就開(kāi)啟了虛擬地址與物理地址之間復(fù)雜而精妙的映射關(guān)系。
內(nèi)存分頁(yè)還提供了許多優(yōu)勢(shì),包括:
- 虛擬化:每個(gè)進(jìn)程都有自己獨(dú)立的地址空間,提高了安全性和隔離性。
- 內(nèi)存共享:多個(gè)進(jìn)程可以共享同一物理頁(yè)面,節(jié)省內(nèi)存資源。
- 惰性加載:只有當(dāng)程序需要訪問(wèn)某個(gè)頁(yè)面時(shí)才將其加載到內(nèi)存中,減少了初始化時(shí)間和內(nèi)存占用。
- 內(nèi)存保護(hù):通過(guò)將頁(yè)面標(biāo)記為只讀或不可執(zhí)行,可以提供對(duì)代碼和數(shù)據(jù)的保護(hù)。
一、內(nèi)存分頁(yè)的作用
內(nèi)存分頁(yè)是一種操作系統(tǒng)和硬件協(xié)同工作的機(jī)制,用于將物理內(nèi)存分割成固定大小的頁(yè)面(通常為4KB)并將虛擬內(nèi)存空間映射到這些頁(yè)面上。內(nèi)存分頁(yè)的主要作用包括:
- 虛擬內(nèi)存管理: 內(nèi)存分頁(yè)允許操作系統(tǒng)將進(jìn)程的虛擬地址空間映射到物理內(nèi)存中的不同頁(yè)面上,從而實(shí)現(xiàn)了虛擬內(nèi)存管理。這使得每個(gè)進(jìn)程能夠擁有獨(dú)立的地址空間,提高了內(nèi)存的利用率和安全性。
- 內(nèi)存保護(hù): 通過(guò)頁(yè)表中的權(quán)限位可以對(duì)頁(yè)面進(jìn)行保護(hù),例如只讀、讀寫(xiě)、執(zhí)行等權(quán)限設(shè)置。這樣可以保護(hù)操作系統(tǒng)和進(jìn)程之間的內(nèi)存隔離,防止非法訪問(wèn)或修改內(nèi)存數(shù)據(jù)。
- 內(nèi)存共享: 內(nèi)存分頁(yè)也支持不同進(jìn)程之間的內(nèi)存共享。多個(gè)進(jìn)程可以將同一個(gè)物理頁(yè)面映射到各自的虛擬地址空間中,從而實(shí)現(xiàn)共享內(nèi)存的目的。
- 內(nèi)存管理: 通過(guò)內(nèi)存分頁(yè),操作系統(tǒng)可以更靈活地管理物理內(nèi)存,如內(nèi)存的分配、回收、頁(yè)面置換(換出到磁盤、換入到內(nèi)存)、內(nèi)存壓縮等操作。
- 減少外部碎片: 內(nèi)存分頁(yè)可以將物理內(nèi)存劃分為固定大小的頁(yè)面,減少了外部碎片的產(chǎn)生,提高了內(nèi)存的利用效率。
1.1一級(jí)頁(yè)表
分頁(yè)機(jī)制是在分段機(jī)制的基礎(chǔ)之上的,分段機(jī)制獲取的地址就是之前我們用選擇子選擇到的全局描述符里面的段基址+EIP中的段內(nèi)偏移地址,這兩個(gè)地址相加可以獲得實(shí)際的物理地址,在我們沒(méi)有進(jìn)行內(nèi)存分頁(yè)之前。
如果打開(kāi)了分頁(yè)機(jī)制,段部件輸出的線性地址就不再等同于物理地址了,我們稱之為虛擬地址,它是邏輯上的,是假的,不應(yīng)該被送上地址總線。CPU必須要拿到物理地址才行,此虛擬地址對(duì)應(yīng)的物理地址需要在頁(yè)表中查找,這項(xiàng)查找工作是由頁(yè)部件自動(dòng)完成的。
我們直接舉個(gè)例子講述一級(jí)頁(yè)表的工作方式,結(jié)合我們上節(jié)講的GDT,假設(shè)選擇子選擇出來(lái)的段基址為0,偏移地址為0x1234。
圖片
1.2二級(jí)頁(yè)表
一級(jí)頁(yè)表我們只是舉個(gè)例子,用來(lái)說(shuō)明頁(yè)表的操作,但實(shí)際我們用的是二級(jí)頁(yè)表,因?yàn)橐患?jí)頁(yè)表有些問(wèn)題:
- 一級(jí)頁(yè)表中最多可容納1M(1048576)個(gè)頁(yè)表項(xiàng),每個(gè)頁(yè)表項(xiàng)是4字節(jié),如果頁(yè)表項(xiàng)全滿的話,便是4MB大小
- 一級(jí)頁(yè)表中所有頁(yè)表項(xiàng)必須要提前建好,原因是操作系統(tǒng)要占用4GB虛擬地址空間的高1GB,用戶進(jìn)程要占用低3GB
- 每個(gè)進(jìn)程都有自己的頁(yè)表,進(jìn)程一多,光是頁(yè)表占用的空間就很可觀了。
歸根結(jié)底,我們要解決的是:不要一次性地將全部頁(yè)表項(xiàng)建好,需要時(shí)動(dòng)態(tài)創(chuàng)建頁(yè)表項(xiàng)。
所以我們多套一層,多一個(gè)頁(yè)目錄項(xiàng):
圖片
每個(gè)進(jìn)程都有自己的頁(yè)表,這樣的話每個(gè)進(jìn)程中相同的虛擬地址可以映射到不同的物理地址中,這樣的話就實(shí)現(xiàn)了進(jìn)程與進(jìn)程之間內(nèi)存的隔離,順便也解決了碎片化的問(wèn)題。
圖片
1.3頁(yè)表項(xiàng)和頁(yè)目錄項(xiàng)
圖片
- P,Present,意為存在位。若為1表示該頁(yè)存在于物理內(nèi)存中,若為0表示該表不在物理內(nèi)存中。
- RW,Read/Write,意為讀寫(xiě)位。若為1表示可讀可寫(xiě),若為0表示可讀不可寫(xiě)。
- US,User/Supervisor,意為普通用戶/超級(jí)用戶位。若為1時(shí),任意級(jí)別都可以訪問(wèn)。為0,只允許特權(quán)級(jí)別為0、1、2的程序訪問(wèn)。
- PWT,Page-level Write-Through,意為頁(yè)級(jí)通寫(xiě)位,也稱頁(yè)級(jí)寫(xiě)透位。若為1表示此項(xiàng)采用通寫(xiě)方式,本位用來(lái)間接決定是否用此方式改善該頁(yè)的訪問(wèn)效率。這里直接置為0就可以。
- PCD,Page-level Cache Disable,意為頁(yè)級(jí)高速緩存禁止位,置為0。
- A,Accessed,意為訪問(wèn)位。若為1表示該頁(yè)被CPU訪問(wèn)過(guò)啦。是用來(lái)在內(nèi)存不足時(shí)與將不常用的內(nèi)存置換到硬盤中。
- D,Dirty,意為臟頁(yè)位。當(dāng)CPU對(duì)一個(gè)頁(yè)面執(zhí)行寫(xiě)操作時(shí),就會(huì)設(shè)置對(duì)應(yīng)頁(yè)表項(xiàng)的D位為1。
- PAT,Page Attribute Table,意為頁(yè)屬性表位,置0。
- G, Global,意為全局位,為1表示是全局頁(yè),為0表示不是全局頁(yè)。若為全局頁(yè),該頁(yè)將在高速緩存TLB中一直保存,無(wú)需繁瑣的置換過(guò)程。
- AV L,意為Available位,即保留位。
頁(yè)表同描述符表一樣,是個(gè)內(nèi)存中的數(shù)據(jù)結(jié)構(gòu),處理器要使用它們,必須要知道它們的物理地址,所以頁(yè)表也有個(gè)專門的寄存器來(lái)存儲(chǔ)其地址。這就是控制寄存器cr3。控制寄存器cr3用于存儲(chǔ)頁(yè)表物理地址,所以cr3寄存器又稱為頁(yè)目錄基址寄存器(Page Directory Base Register,PDBR)。
圖片
由于頁(yè)目錄表所在的地址要求在一個(gè)自然頁(yè)內(nèi),即頁(yè)目錄的起始地址是4KB的整數(shù)倍,低12位地址全是0。所以,只要在cr3寄存器的第31~12位中寫(xiě)入物理地址的高20位就行了。PWT位和PCD位在介紹頁(yè)表項(xiàng)時(shí)說(shuō)過(guò)了,它們用于設(shè)置高速緩存相關(guān)的特性,在此將其置為0即可。
二、分頁(yè)機(jī)制的開(kāi)啟與關(guān)閉
開(kāi)啟內(nèi)存分頁(yè)機(jī)制分為三步:
1、準(zhǔn)備好頁(yè)目錄以及頁(yè)表
2、在cr3寄存器的第31~12位中寫(xiě)入頁(yè)目錄物理地址的高20位
3、寄存器cr0的PG位置1。(其中cr0寄存器的各個(gè)位在進(jìn)入保護(hù)模式時(shí)有講)
分頁(yè)只能在保護(hù)模式(CR0.PE = 1)下使用。在保護(hù)模式下,是否開(kāi)啟分頁(yè),由 CR0. PG 位(位 31)決定:
- 當(dāng) CR0.PG = 0 時(shí),未開(kāi)啟分頁(yè),線性地址等同于物理地址;
- 當(dāng) CR0.PG = 1 時(shí),開(kāi)啟分頁(yè)。
我們可以看代碼了,loader.s添加了一下代碼:
; os/src/boot/loader.s
; 下面就是保護(hù)模式下的程序了
[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp,LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax
mov byte [gs:320], 'M'
mov byte [gs:322], 'A'
mov byte [gs:324], 'I'
mov byte [gs:326], 'N'
call setup_page ; 創(chuàng)建頁(yè)目錄及頁(yè)表并初始化頁(yè)內(nèi)存位圖
;要將描述符表地址及偏移量寫(xiě)入內(nèi)存gdt_ptr,一會(huì)用新地址重新加載
sgdt [gdt_ptr] ; 存儲(chǔ)到原來(lái)gdt的位置
;將gdt描述符中視頻段描述符中的段基址+0xc0000000
mov ebx, [gdt_ptr + 2]
or dword [ebx + 0x18 + 4], 0xc0000000 ;視頻段是第3個(gè)段描述符,每個(gè)描述符是8字節(jié),故0x18。
;段描述符的高4字節(jié)的最高位是段基址的31~24位
;將gdt的基址加上0xc0000000使其成為內(nèi)核所在的高地址
add dword [gdt_ptr + 2], 0xc0000000
add esp, 0xc0000000 ; 將棧指針同樣映射到內(nèi)核地址
; 把頁(yè)目錄地址賦給cr3
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax
; 打開(kāi)cr0的pg位(第31位)
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
;在開(kāi)啟分頁(yè)后,用gdt新的地址重新加載
lgdt [gdt_ptr] ; 重新加載
mov byte [gs:320], 'V' ;視頻段段基址已經(jīng)被更新,用字符v表示virtual addr
mov byte [gs:322], 'i' ;視頻段段基址已經(jīng)被更新,用字符v表示virtual addr
mov byte [gs:324], 'r' ;視頻段段基址已經(jīng)被更新,用字符v表示virtual addr
mov byte [gs:326], 't' ;視頻段段基址已經(jīng)被更新,用字符v表示virtual addr
mov byte [gs:328], 'u' ;視頻段段基址已經(jīng)被更新,用字符v表示virtual addr
mov byte [gs:330], 'a' ;視頻段段基址已經(jīng)被更新,用字符v表示virtual addr
mov byte [gs:332], 'l' ;視頻段段基址已經(jīng)被更新,用字符v表示virtual addr
jmp $
setup_page: ; 創(chuàng)建頁(yè)目錄及頁(yè)表
mov ecx, 4096
mov esi, 0
.clear_page_dir: ; 清理頁(yè)目錄空間
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir
.create_pde: ; 創(chuàng)建頁(yè)目錄
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x1000 ; 此時(shí)eax為第一個(gè)頁(yè)表的位置及屬性,屬性全為0
mov ebx, eax ; 此處為ebx賦值,是為.create_pte做準(zhǔn)備,ebx為基址。
; 下面將頁(yè)目錄項(xiàng)0和0xc00都存為第一個(gè)頁(yè)表的地址,
; 一個(gè)頁(yè)表可表示4MB內(nèi)存,這樣0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的頁(yè)表,
; 這是為將地址映射為內(nèi)核地址做準(zhǔn)備
or eax, PG_US_U | PG_RW_W | PG_P ; 頁(yè)目錄項(xiàng)的屬性RW和P位為1,US為1,表示用戶屬性,所有特權(quán)級(jí)別都可以訪問(wèn).
mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 第1個(gè)目錄項(xiàng),在頁(yè)目錄表中的第1個(gè)目錄項(xiàng)寫(xiě)入第一個(gè)頁(yè)表的位置(0x101000)及屬性(7)
mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; 一個(gè)頁(yè)表項(xiàng)占用4字節(jié),0xc00表示第768個(gè)頁(yè)表占用的目錄項(xiàng),0xc00以上的目錄項(xiàng)用于內(nèi)核空間,
; 也就是頁(yè)表的0xc0000000~0xffffffff共計(jì)1G屬于內(nèi)核,0x0~0xbfffffff共計(jì)3G屬于用戶進(jìn)程.
sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最后一個(gè)目錄項(xiàng)指向頁(yè)目錄表自己的地址
;下面創(chuàng)建第一個(gè)頁(yè)表PTE,其地址為0x101000,也就是1MB+4KB的位置,需要映射前1MB內(nèi)存
mov ecx, 256 ; 1M低端內(nèi)存 / 每頁(yè)大小4k = 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P ; 屬性為7,US=1,RW=1,P=1
.create_pte:
mov [ebx+esi*4],edx ; 此時(shí)的ebx已經(jīng)在上面成為了第一個(gè)頁(yè)表的地址,edx地址為0,屬性為7
add edx,4096 ; edx+4KB地址
inc esi ; 循環(huán)256次
loop .create_pte
;創(chuàng)建內(nèi)核其它頁(yè)表的PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000 ; 此時(shí)eax為第二個(gè)頁(yè)表的位置
or eax, PG_US_U | PG_RW_W | PG_P ; 頁(yè)目錄項(xiàng)的屬性為7
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254 ; 范圍為第769~1022的所有目錄項(xiàng)數(shù)量
mov esi, 769
.create_kernel_pde:
mov [ebx+esi*4], eax
inc esi
add eax, 0x1000
loop .create_kernel_pde
ret
boot.inc 添加了如下的宏定義:
PAGE_DIR_TABLE_POS equ 0x100000
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 10b
PG_US_S equ 000b
PG_US_U equ 100b
我們可以畫(huà)個(gè)圖,看一下現(xiàn)在的內(nèi)存中的頁(yè)目錄和頁(yè)表是怎么回事:
圖片
其實(shí)有兩個(gè)頁(yè)目錄項(xiàng)指向了第一個(gè)頁(yè)表,第一個(gè)頁(yè)目錄以及第768個(gè)頁(yè)目錄,第768個(gè)頁(yè)目錄意味著虛擬地址為 1100_0000_00 開(kāi)頭的地址,這一部分指向了第一個(gè)PTE,第一個(gè)PTE首先包含了1024項(xiàng),但是只有前256項(xiàng)被用到,這個(gè)地址范圍是 0000_0000_00 ~ 0100_0000_00 ,是虛擬地址的中10位,最后十二位就是在相應(yīng)內(nèi)存塊的位置。
這部分作用就是將物理地址 0x00000~0xfffff 映射到虛擬地址 0xc0000000 ~ 0xc00fffff,樣我們的內(nèi)核代碼就放在物理地址1MB以下的位置即可。最后看一下成果:
圖片
最后一個(gè)頁(yè)目錄是指向了自己,這也為修改頁(yè)目錄表埋下了機(jī)會(huì),否則內(nèi)存虛擬化后,無(wú)法通過(guò)直接訪問(wèn)物理地址來(lái)訪問(wèn)內(nèi)存,頁(yè)目錄表也不在虛擬內(nèi)存可以訪問(wèn)的空間內(nèi),那么這個(gè)表相當(dāng)于直接丟失了,無(wú)法訪問(wèn)。
可以觀察到有三個(gè)奇怪的地址映射,這就是最后一個(gè)頁(yè)目錄指向自己導(dǎo)致的:
0xffc00000-0xffc00fff -> 0x000000101000-0x000000101fff
0xfff00000-0xffffefff -> 0x000000101000-0x0000001fffff
0xfffff000-0xffffffff -> 0x000000100000-0x000000100fff
1、若虛擬地址的高十位為 11_1111_1111 ,那么索引為當(dāng)前的頁(yè)目錄表,所以,當(dāng)前的頁(yè)目錄表就被當(dāng)做了頁(yè)表。
2、若虛擬地址的中十位為 11_1111_1111 ,那么索引為當(dāng)前的頁(yè)目錄表(被當(dāng)做頁(yè)表)的最后一項(xiàng),指向的還是當(dāng)前的頁(yè)目錄表,再配合虛擬地址的后12位就可以修改頁(yè)目錄表了,這就是 0xfffff000-0xffffffff -> 0x000000100000-0x000000100fff 這個(gè)地址映射的由來(lái)。
3、若虛擬地址的中十位為 00_0000_0000, 那么索引為當(dāng)前的頁(yè)目錄表(被當(dāng)做頁(yè)表)的第一項(xiàng),指向的是第一項(xiàng)的PTE頁(yè)表,再配合虛擬地址的后12位就可以修改第一個(gè)頁(yè)表了,這就是 ,0xffc00000-0xffc00fff -> 0x000000101000-0x000000101fff 這個(gè)地址映射的由來(lái)。
4、若虛擬地址的中十位為 11_0000_0000 ~ 11_1111_1111, 那么索引為當(dāng)前的頁(yè)目錄表(被當(dāng)做頁(yè)表)的第768項(xiàng)到1024項(xiàng),指向的是第768項(xiàng)到1024項(xiàng)的PTE頁(yè)表,再配合虛擬地址的后12位就可以修改這些頁(yè)表了,這就是 ,0xfff00000-0xffffefff -> 0x000000101000-0x0000001fffff 這個(gè)地址映射的由來(lái)。
拿到了這個(gè)虛擬地址,我們就可以直接訪問(wèn)這塊內(nèi)存對(duì)PDE與PTE進(jìn)行修改。
三、四種分頁(yè)模式
Intel-64 處理器支持 4 種分頁(yè)模式:
- 32-bit paging: CR4.PAE = 0
- PAE paging: CR4.PAE = 1, IA32_EFER.LME = 0
- 4-level paging: CR4.PAE = 1, IA32_EFER.LME = 1, CR4.LA57 = 0
- 5-level paging: CR4.PAE = 1, IA32_EFER.LME = 1, CR4.LA57 = 1
處理器當(dāng)前處于哪種分頁(yè)模式,由 CR4.PAE, CR4.LA57 以及 IA32_EFER.LME 聯(lián)合決定:
- 如果 CR4.PAE = 0, 使用的是 32位分頁(yè)模式。
- 如果 CR4.PAE = 1 且 IA32_EFER.LME = 0,使用的是 PAE 分頁(yè)模式
- 如果 CR4.PAE = 1, IA32_EFER.LME = 1 且 CR4.LA57 = 0,使用的是 4 級(jí)分頁(yè)模式。
- 如果 CR4.PAE = 1, IA32_EFER.LME = 1 且 CR4.LA57 = 1,使用的是 5 級(jí)分頁(yè)模式。
這些標(biāo)志位的說(shuō)明如下所示:
圖片
3.1 32-bit模式
32-bit分頁(yè)模式是分頁(yè)開(kāi)啟后的默認(rèn)模式,CR0.PG被置位后默認(rèn)進(jìn)入32-bit的分頁(yè)模式。該模式下只支持32位的線性地址作為輸入,并將其轉(zhuǎn)化為32位的物理地址。任意時(shí)刻只有4G的線性空間可以被訪問(wèn)。
(1)層級(jí)結(jié)構(gòu)
32-bit分頁(yè)模式通過(guò)三級(jí)索引實(shí)現(xiàn)線性地址到物理地址的映射,如下圖:
圖片
32-bit分頁(yè)模式下,CR3存放的是頁(yè)目錄的物理地址,地址轉(zhuǎn)換時(shí)首先通過(guò)CR3查找到頁(yè)目錄,再?gòu)捻?yè)目錄中查找頁(yè)表的物理地址,最后從頁(yè)表中查找目標(biāo)頁(yè)面的物理地址。線性地址轉(zhuǎn)換時(shí)定位頁(yè)表項(xiàng)的過(guò)程為CR3 -> PDE -> PTE -> Page Physical Address。
(2)PTE/PDE
線性地址低12位的最大尋址能力是4K,通過(guò)它可以從一個(gè)頁(yè)面內(nèi)找到任意地址的內(nèi)存;中間10位用來(lái)在一個(gè)擁有2^10=1024個(gè)條目的頁(yè)表中查詢目標(biāo)項(xiàng),由于頁(yè)表存放的單位也是頁(yè),因此一個(gè)頁(yè)面如果作為頁(yè)表,其每個(gè)條目的長(zhǎng)度是4K / 1024 = 4Byte = 32bit。頁(yè)表項(xiàng)PTE(Page Table Entry)的格式如下:
圖片
因?yàn)镻TE指向的是物理頁(yè)的地址,物理頁(yè)總是4K(或者大于4K)對(duì)齊的,所以物理頁(yè)地址的低12位總是為0。因此PTE不需要將低12位記錄下來(lái),而是利用這12位做其它事情,比如描述所映射的物理頁(yè),Intel就是這么設(shè)計(jì)的,PTE的低12位存放描述物理頁(yè)的元數(shù)據(jù)。
這些信息包括物理頁(yè)是否被分配(bit0 = Present),頁(yè)是否可寫(xiě)(bit1 = R/W),用戶特權(quán)級(jí)是否可以訪問(wèn)此頁(yè)(bit2 = U/S),頁(yè)cache是否打開(kāi)(bit4 = PCD),是否為臟頁(yè)(bit6 = Dirty),用戶程序是否具有對(duì)該頁(yè)的訪問(wèn)權(quán)限等。
線性地址的高10位用來(lái)在一個(gè)擁有2^10 = 1024個(gè)條目的頁(yè)目錄中查詢目標(biāo)項(xiàng),頁(yè)目錄的的每一項(xiàng)存放的是頁(yè)表地址,頁(yè)目錄的存放單位也是頁(yè),一個(gè)頁(yè)面如果作為頁(yè)目錄,其每個(gè)條目的長(zhǎng)度是4K/1024 = 4Byte = 32bit。頁(yè)目錄項(xiàng)PDE(Page Directory Entry)的格式如下:
圖片
(3)CR3
32-bit分頁(yè)模式下CR3存放的頁(yè)目錄表物理地址,格式如下:
圖片
3.2 PAE 分頁(yè)
PAE(Physical Adress Extend)即物理地址擴(kuò)展。顧名思義,它是對(duì)物理地址的擴(kuò)展,擴(kuò)展什么呢?擴(kuò)展32-bit模式下只能尋址4G內(nèi)存空間的限制,它可以將32位的線性地址轉(zhuǎn)化位最高52位的物理地址,可以尋址大于4G的地址空間。
怎么做到的?PAE分頁(yè)模式的頁(yè)表結(jié)構(gòu)變了,頁(yè)表項(xiàng)長(zhǎng)度增加變成了8字節(jié)(64bit),PAE模式的開(kāi)啟需要CR0.PG和CR4.PAE兩個(gè)標(biāo)志位同時(shí)打開(kāi)。
32-bit模式
32-bit分頁(yè)模式是分頁(yè)開(kāi)啟后的默認(rèn)模式,CR0.PG被置位后默認(rèn)進(jìn)入32-bit的分頁(yè)模式。該模式下只支持32位的線性地址作為輸入,并將其轉(zhuǎn)化為32位的物理地址。任意時(shí)刻只有4G的線性空間可以被訪問(wèn)。
(1)層級(jí)結(jié)構(gòu)
32-bit分頁(yè)模式通過(guò)三級(jí)索引實(shí)現(xiàn)線性地址到物理地址的映射,如下圖:
圖片
32-bit分頁(yè)模式下,CR3存放的是頁(yè)目錄的物理地址,地址轉(zhuǎn)換時(shí)首先通過(guò)CR3查找到頁(yè)目錄,再?gòu)捻?yè)目錄中查找頁(yè)表的物理地址,最后從頁(yè)表中查找目標(biāo)頁(yè)面的物理地址。線性地址轉(zhuǎn)換時(shí)定位頁(yè)表項(xiàng)的過(guò)程為CR3 -> PDE -> PTE -> Page Physical Address。
(2)PTE/PDE
線性地址低12位的最大尋址能力是4K,通過(guò)它可以從一個(gè)頁(yè)面內(nèi)找到任意地址的內(nèi)存;中間10位用來(lái)在一個(gè)擁有2^10=1024個(gè)條目的頁(yè)表中查詢目標(biāo)項(xiàng),由于頁(yè)表存放的單位也是頁(yè),因此一個(gè)頁(yè)面如果作為頁(yè)表,其每個(gè)條目的長(zhǎng)度是4K / 1024 = 4Byte = 32bit。頁(yè)表項(xiàng)PTE(Page Table Entry)的格式如下:
圖片
因?yàn)镻TE指向的是物理頁(yè)的地址,物理頁(yè)總是4K(或者大于4K)對(duì)齊的,所以物理頁(yè)地址的低12位總是為0。因此PTE不需要將低12位記錄下來(lái),而是利用這12位做其它事情,比如描述所映射的物理頁(yè),Intel就是這么設(shè)計(jì)的,PTE的低12位存放描述物理頁(yè)的元數(shù)據(jù)。
這些信息包括物理頁(yè)是否被分配(bit0 = Present),頁(yè)是否可寫(xiě)(bit1 = R/W),用戶特權(quán)級(jí)是否可以訪問(wèn)此頁(yè)(bit2 = U/S),頁(yè)cache是否打開(kāi)(bit4 = PCD),是否為臟頁(yè)(bit6 = Dirty),用戶程序是否具有對(duì)該頁(yè)的訪問(wèn)權(quán)限等。
線性地址的高10位用來(lái)在一個(gè)擁有2^10 = 1024個(gè)條目的頁(yè)目錄中查詢目標(biāo)項(xiàng),頁(yè)目錄的的每一項(xiàng)存放的是頁(yè)表地址,頁(yè)目錄的存放單位也是頁(yè),一個(gè)頁(yè)面如果作為頁(yè)目錄,其每個(gè)條目的長(zhǎng)度是4K/1024 = 4Byte = 32bit。頁(yè)目錄項(xiàng)PDE(Page Directory Entry)的格式如下:
圖片
(3)CR3
32-bit分頁(yè)模式下CR3存放的頁(yè)目錄表物理地址,格式如下:
圖片
PAE分頁(yè)模式下存放頁(yè)目錄物理地址的寄存器不再是CR3,而是該模式下特有4個(gè)PDPTE寄存器,4個(gè)寄存器從PDPT(page-directory-pointer table)中加載值,該表的物理地址存放到了CR3中。
在PAE模式下,只要CR3的值有改變,就會(huì)同步更新4個(gè)PDPTE寄存器,當(dāng)發(fā)生地址轉(zhuǎn)換時(shí)CPU可以直接從PDPTE寄存其中讀取頁(yè)目錄的物理地址,4個(gè)PDPTE寄存器在地址轉(zhuǎn)換中起到的作用和32-bit模式下CR3的作用相同。
圖片
對(duì)比32-bit分頁(yè)模式,PAE分頁(yè)模式多了一級(jí)索引,物理地址轉(zhuǎn)換過(guò)程變?yōu)镃R3 -> PDPTE -> PDE -> PTE -> Page Physical Address。這里要注意,CR3 -> PDPTE的過(guò)程并非發(fā)生在地址轉(zhuǎn)換流程中,而是在CR3寄存器變化的時(shí)候。所以從地址轉(zhuǎn)換效率上說(shuō),32-bit模式和PAE模式都只有三次查詢動(dòng)作,效率接近。PAE模式通過(guò)多維護(hù)4個(gè)PDPTE寄存器實(shí)現(xiàn)了物理地址的擴(kuò)展。
(4)PTE/PDE
PAE模式頁(yè)大小我們也分析4K的情況,同32-bit模式一樣,通過(guò)低12位可以從一個(gè)頁(yè)面內(nèi)找到任意地址的內(nèi)存。因此線性低12位用作查找頁(yè)面內(nèi)的偏移。
線性地址的12 ~ 20位,這9位用來(lái)在頁(yè)表中索引目標(biāo)頁(yè)表項(xiàng),9位地址可以索引2 ^ 9 = 512個(gè)條目,如果一張頁(yè)表只存放512個(gè)條目,那么每個(gè)條目的長(zhǎng)度為 2 ^ 12 / 2 ^ 9 = 2 ^ 3 = 8byte = 64bit。有足夠的寬度來(lái)存放物理地址,而32-bit模式下,最多只有32bit來(lái)存放物理地址,這是PAE能夠擴(kuò)展物理地址尋址能力的關(guān)鍵。PTE格式如下:
圖片
PTE的低12位仍然用于描述內(nèi)存頁(yè)的元數(shù)據(jù),剩下的52bit中除最高位以外,其余可以全部用作存放頁(yè)的物理地址。因此PTE模式下的頁(yè)表,理論上可以存放51bit的物理地址。Intel在具體實(shí)現(xiàn)上,頁(yè)表中頁(yè)的物理地址取決于最大物理地址位寬MAXPHYADDR,該值是cpu所支持的最大地址寬度。
很明顯32-bit分頁(yè)模式下,MAXPHYADDR為32,PAE分頁(yè)模式下通常可以配置的值為36,40,52這三個(gè),PTE中存放頁(yè)面物理地址的區(qū)間可以表示為12 ~ MAXPHYADDR
線性地址的21 ~ 29位,這9位用來(lái)在頁(yè)目錄表中索引目標(biāo)頁(yè)目錄表項(xiàng),9位地址可以索引 2 ^ 9 = 512個(gè)條目,同PTE類似,PDE存放的是PTE的物理地址,這個(gè)物理地址也是4K對(duì)齊,存放頁(yè)表物理地址的區(qū)間可以表示為12 ~ MAXPHYADDR,PDE格式如下:
圖片
(5)PDPTE
PAE模式下線性地址的最高兩位用于索引4個(gè)PDPTE寄存器,每個(gè)PDPTE(page-directory-pointer table entry)寄存器都存放了一個(gè)頁(yè)目錄表的地址,指向一張頁(yè)目錄表。PDPTE寄存器的初始值來(lái)自于頁(yè)目錄指針表(page-directory-pointer table),在PAE模式開(kāi)啟之前,用戶軟件就需要在內(nèi)存中準(zhǔn)備這樣一張表,然后將其地址取出,存放到CR3寄存器中。
存放的MOV指令會(huì)觸發(fā)加載動(dòng)作,將內(nèi)存中表的內(nèi)容加載到寄存器中。這樣PDPTE寄存器就替代了CR3的作用,CPU每次進(jìn)行地址轉(zhuǎn)換時(shí),不再通過(guò)CR3尋找頁(yè)目錄表的地址,而是通過(guò)PDPTE寄存器尋找地址。而PDPTE有4個(gè),用哪個(gè)呢?它的索引就是線性地址的最高兩位。PDPTE格式如下:
圖片
PDPTE的格式和PDE/PTE的格式類似,低12位都用于存放描述內(nèi)存區(qū)域的元數(shù)據(jù),12 ~ MAXPHYADDR
用于存放頁(yè)目錄表的物理地址。
(6)CR3
PAE模式下CR3存放的不再是頁(yè)目錄表的物理地址,而是頁(yè)目錄指針表的物理地址,格式如下:
圖片
由于PDPT包含4個(gè)PDPTE,所以總長(zhǎng)度是4 * 8 = 32字節(jié),PDPT的物理地址32字節(jié)對(duì)齊,低5位始終為0。CR3寄存器的低5位可以復(fù)用,但這里暫時(shí)沒(méi)有定義,因此忽略低5位。除去低5位,CR3的剩余27用于存放頁(yè)目錄指針表的物理地址。
3.3 4 級(jí)分頁(yè)
4-level模式,顧名思義,地址轉(zhuǎn)換需要通過(guò)4級(jí)層層索引才能實(shí)現(xiàn)。4-level與32-bit和PAE相比,最大的不同就是它在地址轉(zhuǎn)換時(shí)多了一級(jí),32-bit和PAE可以看做是2-level的地址轉(zhuǎn)換(因?yàn)閮?nèi)存里面存放了兩張表)。4-level將48位線性地址轉(zhuǎn)化成52位物理地址,因此4-level分頁(yè)模式必須在64位cpu上才能支持。
4-level分頁(yè)模式多出的一級(jí)索引叫做PML4(Page Map Level 4),它存放的是頁(yè)目錄指針表的地址,4-level模式下的地址轉(zhuǎn)換過(guò)程CR3 -> PML4E -> PDPTE -> PDE -> PTE。
4-level模式開(kāi)啟需要CR0.PG,CR4.PAE和IA32_EFER.LME三個(gè)標(biāo)志位同時(shí)打開(kāi)。
(1)層級(jí)結(jié)構(gòu)
4-level分頁(yè)模式增加了PML4結(jié)構(gòu),層級(jí)結(jié)構(gòu)如下:
圖片
(2)PTE/PDE
4-level模式下的線性地址,頁(yè)面大小仍然以4K為例,低12位用作一個(gè)物理頁(yè)面的內(nèi)部地址索引,線性地址余下的部分被分成了4部分,每個(gè)部分都占用9位,用做對(duì)應(yīng)表的索引,可以計(jì)算出,每個(gè)部分對(duì)應(yīng)的表?xiàng)l目都是2 ^ 9 = 512個(gè),如果頁(yè)面是4K,那么每個(gè)條目的長(zhǎng)度為2 ^ 12 / 2 ^ 9 = 2 ^ 3 = 8字節(jié),每個(gè)條目都是64bit。
線性地址中用來(lái)索引PTE/PDE的域都是9位,格式如下:
圖片
(3)PDPTE
4-level分頁(yè)模式下,頁(yè)目錄指針表被存放到了內(nèi)存中,其內(nèi)容并沒(méi)有加載到4個(gè)寄存器上,這一點(diǎn)和PAE模式不同,線性地址中同樣用9位來(lái)索引PDPTE,其格式如下:
圖片
(4)CR3
4-level分頁(yè)模式下CR3存放的是PML4表的物理地址:
圖片
不同的分頁(yè)模式,其支持的線性地址寬度、物理地址寬度和頁(yè)大小也是同的,其對(duì)應(yīng)關(guān)系如下:
圖片
不同型號(hào)的處理器,所支持的物理地址和線性地址寬度也不相同,處理器提供了cpuid
指令來(lái)查詢 CPU 信息。Linux 系統(tǒng)下,有個(gè)同名的 shell 命令(需要單獨(dú)安裝),可用來(lái)查看當(dāng)前處理器信息,包括所支持的地址寬度。在我的 Ubuntu 虛擬機(jī)上,使用 cpuid
命令,查看結(jié)果如下:
$ cpuid|grep address
physical address extensions = true
maximum physical address bits = 0x27 (39)
maximum linear (virtual) address bits = 0x30 (48)
可以看到,該 CPU 支持最大 39 位物理地址,以及最大 48 位虛擬地址。
32 位分頁(yè)和 PAE 分頁(yè)只能在 32 位保護(hù)模式(IA32_EFER.LME = 0)下使用,只能轉(zhuǎn)換 32 位的線性地址。 本文不會(huì)對(duì)這兩種分頁(yè)模式進(jìn)行討論。
相對(duì)的,4 級(jí)和 5 級(jí)分頁(yè),只能在 IA-32e 模式(IA32_EFER.LME = 1)下使用。IA-32e 模式有兩種子模式:
兼容模式。這種子模式下,只使用 32 位的線性地址;4 級(jí)和 5 級(jí)分頁(yè)把線性地址中的位 63:32 全部當(dāng)做 0 來(lái)看待。
64 位模式。這種子模式下,能夠使用 64 位的線性地址。但由于 4 級(jí)分頁(yè)只支持 48 位線性地址(5 級(jí)分頁(yè)支持 57 位),所以 4 級(jí)分頁(yè)線性地址的 63:47 位,5 級(jí)分頁(yè)線性地址的 63:57 位,均未使用。在 64 位模式下,處理器要求線性地址必須是 canonical 的,即這些冗余位應(yīng)該是一致的,要么全是 0,要么全是 1。
3.4 5 級(jí)分頁(yè)
5 級(jí)分頁(yè)模式是 x86-64 架構(gòu)分頁(yè)模式中的 “新成員”,當(dāng) CR4.PAE = 1、IA32_EFER.LME = 1 且 CR4.LA57 = 1 時(shí),它便被啟用。它支持 57 位線性地址,進(jìn)一步擴(kuò)大了虛擬地址空間的范圍,就像給超大型大樓又增加了更多的樓層和房間,每個(gè)房間的編號(hào)變成了 57 位(線性地址) 。
在物理地址方面,它同樣最大支持 52 位。頁(yè)大小與 4 級(jí)分頁(yè)相同,有 4KB、2MB 和 1GB。5 級(jí)分頁(yè)模式的出現(xiàn),主要是為了滿足對(duì)內(nèi)存需求極高的應(yīng)用場(chǎng)景,比如一些大型的云計(jì)算平臺(tái)、大規(guī)模的數(shù)據(jù)處理中心等。在這些場(chǎng)景中,大量的虛擬機(jī)同時(shí)運(yùn)行,每個(gè)虛擬機(jī)都需要大量的內(nèi)存資源,5 級(jí)分頁(yè)模式能夠更有效地管理這些內(nèi)存,提高系統(tǒng)的整體性能和穩(wěn)定性。
四、層級(jí)分頁(yè)結(jié)構(gòu)解析
上述 4 種分頁(yè)模式,都使用了層級(jí)分頁(yè)結(jié)構(gòu)。每個(gè)頁(yè)結(jié)構(gòu)的大小為 4096 字節(jié),由多個(gè)項(xiàng)組成。在 32 位分頁(yè)模式下,每一項(xiàng)大小為 4 字節(jié)(32位),每個(gè)頁(yè)結(jié)構(gòu)包含 1024 項(xiàng);在 4 級(jí) 或 5 級(jí)分頁(yè)模式下,每一項(xiàng)大小為 8 字節(jié)(64位),每個(gè)頁(yè)結(jié)構(gòu)包含 512 項(xiàng)。PAE 分頁(yè)模式中有個(gè)例外情況,使用了大小為 32 個(gè)字節(jié)的頁(yè)結(jié)構(gòu),該頁(yè)結(jié)構(gòu)由 4 個(gè) 8 字節(jié)(64 位)的項(xiàng)組成。
從功能上來(lái)說(shuō),線性地址可分為 2 個(gè)部分。線性地址的高位部分(稱為頁(yè)號(hào),page number),用來(lái)識(shí)別一系列頁(yè)結(jié)構(gòu)項(xiàng)。這些項(xiàng)中的最后一個(gè),用來(lái)標(biāo)識(shí)線性地址轉(zhuǎn)換后的內(nèi)存區(qū)域的物理地址(稱為頁(yè)幀,page frame)。線性地址的低位部分(稱為頁(yè)偏移量, page offset),標(biāo)識(shí)了線性地址轉(zhuǎn)換后的內(nèi)存區(qū)域內(nèi)的特定地址。
總的來(lái)說(shuō),線性地址的高位部分,決定了物理地址的高位部分;線性地址的低位部分,決定了物理地址的低位部分。而頁(yè)的大小,決定了頁(yè)號(hào)(page number)和頁(yè)偏移量(page offset)的邊界。
每一個(gè)頁(yè)結(jié)構(gòu)項(xiàng)都包含一個(gè)物理地址,該物理地址要么是另一個(gè)頁(yè)結(jié)構(gòu)項(xiàng)的地址,要么是一個(gè)頁(yè)幀的地址。對(duì)于第一種情況,我們說(shuō)該頁(yè)結(jié)構(gòu)項(xiàng)引用了另一個(gè)頁(yè)結(jié)構(gòu)項(xiàng);對(duì)于后者,我們說(shuō)該頁(yè)結(jié)構(gòu)項(xiàng)映射了一個(gè)頁(yè)。
不論哪種分頁(yè)模式,第一個(gè)頁(yè)結(jié)構(gòu)(根頁(yè)結(jié)構(gòu))的物理地址都會(huì)被保存在 CR3 寄存器中。然后,使用以下迭代過(guò)程來(lái)進(jìn)行線性地址轉(zhuǎn)換:使用線性地址的一部分(剛開(kāi)始時(shí)使用最高位部分)定位到頁(yè)結(jié)構(gòu)(剛開(kāi)始時(shí)使用保存在 CR3 寄存器中的地址)中的一項(xiàng)。如果該項(xiàng)又引用了另一個(gè)頁(yè)結(jié)構(gòu)項(xiàng),那么使用被引用項(xiàng)和線性地址的剩余部分,繼續(xù)該過(guò)程。如果該項(xiàng)映射到了一個(gè)頁(yè),那么轉(zhuǎn)換過(guò)程完成:該項(xiàng)包含的物理地址即為頁(yè)幀,線性地址的剩余部分就是頁(yè)內(nèi)偏移量。
4 級(jí) 和 5 級(jí)分頁(yè)模式下(以 4KB 頁(yè)為例),轉(zhuǎn)換過(guò)程概述如下:
- 在 4 級(jí)分頁(yè)下,每個(gè)頁(yè)結(jié)構(gòu)由 512 (2^9)項(xiàng)組成,每次轉(zhuǎn)換使用 48 位線性地址中的 9 位。位 47:39 標(biāo)識(shí)了第一個(gè)頁(yè)結(jié)構(gòu)項(xiàng),位 38:30 標(biāo)識(shí)了第二個(gè);位 29:21 標(biāo)識(shí)了第三個(gè);位 20:12 標(biāo)識(shí)了第四個(gè)。注意,最后一個(gè)頁(yè)結(jié)構(gòu)項(xiàng)標(biāo)識(shí)了頁(yè)幀。
- 5 級(jí)分頁(yè)跟 4 級(jí)類似,只不過(guò) 5 級(jí)分頁(yè)的線性地址是 57 位的。位 56:48 標(biāo)識(shí)了第一個(gè)頁(yè)結(jié)構(gòu)項(xiàng),剩余位用于 4 級(jí)分頁(yè)。
上述示例中,最后一個(gè)頁(yè)結(jié)構(gòu)項(xiàng)映射了一個(gè) 4KB 的頁(yè),線性地址的低 12 位作為頁(yè)內(nèi)偏移。但情況并非總是如此,因?yàn)槌?4KB 大小的頁(yè),處理器還支持其它尺寸的頁(yè)。比如,在 4 級(jí) 和 5 級(jí)分頁(yè)下,支持 4KB、2MB 及 1GB 大小的頁(yè)。頁(yè)結(jié)構(gòu)項(xiàng)的 PS (page size)位,決定了該項(xiàng)是否映射到頁(yè),同時(shí)也決定了頁(yè)的大小:
- 如果線性地址剩余的位數(shù)超過(guò) 12,則參考當(dāng)前頁(yè)結(jié)構(gòu)項(xiàng)的位 7(PS — page size)。如果該位為 0,該項(xiàng)引用了另一個(gè)頁(yè)結(jié)構(gòu);如果為 1,該項(xiàng)映射了頁(yè)。
- 如果線性地址只剩余 12 位,當(dāng)前頁(yè)結(jié)構(gòu)項(xiàng)總是映射到一個(gè)頁(yè)(位 7 被用作其它用途)。
在轉(zhuǎn)換過(guò)程中,每一層頁(yè)結(jié)構(gòu)被賦予不同的名稱。下表提供了不同頁(yè)結(jié)構(gòu)的名稱。同時(shí)也提供了其物理地址的來(lái)源(CR3 或 不同的頁(yè)結(jié)構(gòu)項(xiàng))、線性地址中用來(lái)選擇頁(yè)結(jié)構(gòu)項(xiàng)的位、該項(xiàng)是否以及如何映射到一個(gè)頁(yè)。
4.1轉(zhuǎn)換過(guò)程
(1)先看一張intel手冊(cè)上的4-level paging4KB大小的頁(yè)的轉(zhuǎn)換圖:
圖片
(2)CR3寄存器介紹
CR3寄存器又叫頁(yè)目錄基址寄存器(Page Directory Base Register, PDGR), CR3中存放著當(dāng)前任務(wù)頁(yè)表目錄的物理地址.
(2)內(nèi)核中線性地址和物理地址轉(zhuǎn)換宏
下面代碼使用內(nèi)核中定義的宏打印物理地址,代碼中的宏選自linux5.4.34arch/x86/include/asm/page.harch/x86/include/asm/page_64.h
#include <stdio.h>
#define __AC(X,Y) (X##Y)
#define _AC(X,Y) __AC(X,Y)
#define __PAGE_OFFSET_BASE_L4 _AC(0xffff888000000000, UL)
#define __PAGE_OFFSET __PAGE_OFFSET_BASE_L4
#define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
#define __START_KERNEL_map _AC(0xffffffff80000000, UL)
// __va宏是將物理地址轉(zhuǎn)換成線性地址,直接等于物理地址 + 0xffff888000000000
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
static inline unsigned long __phys_addr_nodebug(unsigned long x)
{
unsigned long y = x - __START_KERNEL_map;
/* use the carry flag to determine if x was < __START_KERNEL_map */
// 筆者電腦上phys_base為0
x = y + ((x > y) ? 0 /* phys_base */ : (__START_KERNEL_map - PAGE_OFFSET));
return x;
}
#define __phys_addr(x) __phys_addr_nodebug(x)
#define __phys_addr_symbol(x) \
((unsigned long)(x) - __START_KERNEL_map + 0 /* phys_base */) // phys_base為0
#define __phys_reloc_hide(x) (x)
// __pa宏是將線性地址轉(zhuǎn)換成物理地址
#define __pa(x) __phys_addr((unsigned long)(x))
// ___pa_symbol宏也是將線性地址轉(zhuǎn)換成物理地址,轉(zhuǎn)換以0xffffffff8開(kāi)頭的在vmlinux.lds.S中定義的符號(hào)
#define __pa_symbol(x) \
__phys_addr_symbol(__phys_reloc_hide((unsigned long)(x)))
int main() {
printf("address: 0x%lx\n", __pa(0xffff88800220a000UL));
printf("address: 0x%lx\n", __pa(0xffffffff8220a000UL));
printf("address: 0x%lx\n", __pa_symbol(0xffffffff8220a000UL));
}
上面代碼輸出如下,所以問(wèn)題1和問(wèn)題2的答案為0x220a000,下面講解怎么轉(zhuǎn)換成0x220a000
address: 0x220a000
address: 0x220a000
address: 0x220a000
(4)各級(jí)頁(yè)目錄索引
下面代碼使用內(nèi)核中定義的宏和索引函數(shù)打印各級(jí)頁(yè)目錄索引,代碼中的宏和索引函數(shù)選自linux5.4.34 arch/x86/include/asm/pgtable.h
#include <stdio.h>
#define PAGE_SHIFT 12
#define PGDIR_SHIFT 39
#define PTRS_PER_PGD 512
#define PUD_SHIFT 30
#define PTRS_PER_PUD 512
#define PMD_SHIFT 21
#define PTRS_PER_PMD 512
#define PTRS_PER_PTE 512
#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
static inline unsigned long pud_index(unsigned long address)
{
return (address >> PUD_SHIFT) & (PTRS_PER_PUD - 1);
}
static inline unsigned long pmd_index(unsigned long address)
{
return (address >> PMD_SHIFT) & (PTRS_PER_PMD - 1);
}
static inline unsigned long pte_index(unsigned long address)
{
return (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
}
void printIndex(unsigned long address) {
printf("address: 0x%lx\n", address);
printf("pgd_index: %ld\n", pgd_index(address)); // 對(duì)應(yīng)第一節(jié)圖中的PML4
printf("pud_index: %ld\n", pud_index(address)); // 對(duì)應(yīng)第一節(jié)圖中的Directory Ptr
printf("pmd_index: %ld\n", pmd_index(address)); // 對(duì)應(yīng)第一節(jié)圖中的Directory
printf("pte_index: %ld\n", pte_index(address)); // 對(duì)應(yīng)第一節(jié)圖中的Table
}
int main() {
printIndex(0xffffffff8220a000);
printIndex(0xffff88800220a000);
}
代碼輸出如下所示:
address: 0xffffffff8220a000
pgd_index: 511
pud_index: 510
pmd_index: 17
pte_index: 10
address: 0xffff88800220a000
pgd_index: 273
pud_index: 0
pmd_index: 17
pte_index: 10
(5)在內(nèi)核中添加代碼打印頁(yè)表
在內(nèi)核代碼文件arch/x86/mm/init.c
中添加代碼。
// 添加打印頁(yè)表代碼開(kāi)始
void printPTETable(unsigned long parent) {
unsigned long* pte = (unsigned long*)((parent & PTE_PFN_MASK) + PAGE_OFFSET);
int i = 0;
printk("------pte: 0x%lx\n", pte);
while (i < PTRS_PER_PTE /* 512 */) {
unsigned long entry = *(pte + i);
if (entry) {
printk("--------index: %d pysical address: 0x%lx\n", i, entry);
}
i++;
}
}
void printPMDTable(unsigned long parent) {
unsigned long* pmd = (unsigned long*)((parent & PTE_PFN_MASK) + PAGE_OFFSET);
int i = 0;
printk("----pmd: 0x%lx\n", pmd);
while (i < PTRS_PER_PMD /* 512 */) {
unsigned long entry = *(pmd + i);
if (entry) {
printk("------index: %d pte entry: 0x%lx\n", i, entry);
if (entry >> 7 & 1) {
printk("--------pysical address: 0x%lx\n", entry);
} else {
printPTETable(entry);
}
}
i++;
}
}
void printPUDTable(unsigned long parent) {
unsigned long* pud = (unsigned long*)((parent & PTE_PFN_MASK) + PAGE_OFFSET);
int i = 0;
printk("--pud: 0x%lx\n", pud);
while (i < PTRS_PER_PUD /* 512 */) {
unsigned long entry = *(pud + i);
if (entry) {
printk("----index: %d pud entry: 0x%lx\n", i, entry);
if (entry >> 7 & 1) {
printk("----pysical address: 0x%lx\n", entry);
} else {
printPMDTable(entry);
}
}
i++;
}
}
void printPGDTable(void) {
// 讀取CR3寄存器,轉(zhuǎn)換成線性地址
unsigned long* pgd = (unsigned long*)(native_read_cr3_pa() + PAGE_OFFSET);
int i = 0;
printk("cr3 pgd: 0x%lx\n", pgd);
while (i < PTRS_PER_PGD /* 512 */) {
unsigned long entry = *(pgd + i);
if (entry) {
printk("--index: %d pgd entry: 0x%lx\n", i, entry);
printPUDTable(entry);
}
i++;
}
}
// 添加打印頁(yè)表代碼結(jié)束
void __init init_mem_mapping(void)
{
// ... 省略
printPGDTable();
load_cr3(swapper_pg_dir); // 切換CR3,切換前后打印
printPGDTable();
// ... 省略
}
下面截取部分輸出內(nèi)容,截取部分為上一節(jié)打印出來(lái)的索引對(duì)應(yīng)的物理地址
[ 0.000000] cr3 pgd: 0xffff88800269e000
[ 0.000000] --index: 273 pgd entry: 0x26a0063
[ 0.000000] --pud: 0xffff8880026a0000
[ 0.000000] ----index: 0 pud entry: 0x26a1063
[ 0.000000] ----pmd: 0xffff8880026a1000
省略
[ 0.000000] ------index: 17 pte entry: 0x80000000022000e3
[ 0.000000] --------pysical address: 0x80000000022000e3
省略
[ 0.000000] --index: 511 pgd entry: 0x220c067
[ 0.000000] --pud: 0xffff88800220c000
[ 0.000000] ----index: 510 pud entry: 0x220d063
[ 0.000000] ----pmd: 0xffff88800220d000
省略
[ 0.000000] ------index: 17 pte entry: 0x22001e3
[ 0.000000] --------pysical address: 0x22001e3
省略
切換CR3
[ 0.000000] cr3 pgd: 0xffff88800220a000
[ 0.000000] --index: 273 pgd entry: 0x2801067
[ 0.000000] --pud: 0xffff888002801000
[ 0.000000] ----index: 0 pud entry: 0x2802067
省略
[ 0.000000] ----pmd: 0xffff888002802000
[ 0.000000] ------index: 17 pte entry: 0x80000000022001e3
[ 0.000000] --------pysical address: 0x80000000022001e3
省略
[ 0.000000] --index: 511 pgd entry: 0x220c067
[ 0.000000] --pud: 0xffff88800220c000
[ 0.000000] ----index: 510 pud entry: 0x220d063
[ 0.000000] ----pmd: 0xffff88800220d000
省略
[ 0.000000] ------index: 17 pte entry: 0x22001e3
[ 0.000000] --------pysical address: 0x22001e3
省略
可以看出最后的物理地址為0x80000000022000e3, 0x22001e3, 0x80000000022001e3。
其中0x80000000022000e3中的8即第63位設(shè)置為1表示這快內(nèi)存是不可執(zhí)行的。
其中0x0e3和0x1e3即低12位表示內(nèi)存的FLAG,具體含義我們先不討論。
所以通過(guò)頁(yè)表找出來(lái)的物理內(nèi)存地址為0x2200000,這里有個(gè)問(wèn)題,只轉(zhuǎn)化了3次就結(jié)束了,和第一節(jié)圖中轉(zhuǎn)換了4次不一樣,這里我迷惑了很久,搜了很多資料沒(méi)找到答案,最終在intel手冊(cè)里找到了答案。
4.2異常
正常情況下,當(dāng)識(shí)別出頁(yè)幀時(shí),轉(zhuǎn)換過(guò)程就完成了。但是,當(dāng)轉(zhuǎn)換過(guò)程遇到標(biāo)識(shí)了”不存在“(P 位為 0)的頁(yè)結(jié)構(gòu)時(shí),或者修改了保留位,轉(zhuǎn)換過(guò)程就會(huì)提前中止,并觸發(fā) page-fault 異常。
4級(jí)和5級(jí)頁(yè)表中的保留位如下所示:
- 位 51:MAXPHYADDR 被保留
- PML5E 或 PML4E 中的 PS 標(biāo)志位被保留
- 如果處理器不支持 1-GByte 的頁(yè),PDPTE 中的 PS 標(biāo)志位被保留
- 如果 PDPTE 中的 PS 標(biāo)志為 1,該項(xiàng)中的 29:13 位被保留
- 如果 PDE 中的 PS 標(biāo)志為 1,該項(xiàng)中的 20:13 位被保留
- 如果 IA32_EFER.NXE = 0,XD 標(biāo)志位(第 63 位)被保留
五、實(shí)際應(yīng)用與案例分析
為了更直觀地感受不同處理器對(duì)分頁(yè)模式的支持情況,我們可以通過(guò)實(shí)際案例來(lái)進(jìn)行分析。在 Linux 系統(tǒng)下,有一個(gè)非常實(shí)用的工具 ——cpuid 命令(需要單獨(dú)安裝),它就像是一個(gè) CPU 信息的探測(cè)器,能夠幫助我們查看當(dāng)前處理器的各種信息,包括所支持的地址寬度。
在我的 Ubuntu 虛擬機(jī)上,我迫不及待地使用 cpuid 命令來(lái)一探究竟。當(dāng)我在終端中輸入cpuid|grep address后,得到了如下結(jié)果:
physical address extensions = true
maximum physical address bits = 0x27 (39)
maximum linear (virtual) address bits = 0x30 (48)
從這個(gè)結(jié)果中,我們可以清晰地看到,該 CPU 支持最大 39 位物理地址,以及最大 48 位虛擬地址 。這就像是了解到了一臺(tái)電腦的內(nèi)存 “容量” 和 “規(guī)格”,讓我們對(duì)它的內(nèi)存管理能力有了一個(gè)基本的認(rèn)識(shí)。根據(jù)前面介紹的分頁(yè)模式與地址寬度的對(duì)應(yīng)關(guān)系,我們可以推斷出這臺(tái)虛擬機(jī)的 CPU 支持 4 級(jí)分頁(yè)模式,因?yàn)?4 級(jí)分頁(yè)模式支持 48 位線性地址,與我們查詢到的結(jié)果相匹配。這就好比我們根據(jù)一個(gè)人的身高、體重等特征,判斷出他適合參加哪種體育項(xiàng)目一樣。
再來(lái)看 Linux 內(nèi)核中用戶空間和內(nèi)核空間的虛擬地址范圍,以 4 級(jí)分頁(yè)的 Linux 內(nèi)核為例 ,用戶空間的虛擬地址范圍是 0x0000000000000000 - 0x00007fffffffffff(47 位) ,這個(gè)范圍就像是一個(gè)大型商場(chǎng)的顧客購(gòu)物區(qū),每個(gè)顧客(應(yīng)用程序)都在這個(gè)區(qū)域內(nèi)活動(dòng),進(jìn)行各種數(shù)據(jù)的處理和操作。
而內(nèi)核使用的虛擬地址范圍是 0xffff800000000000 - 0xffffffffffffffff(47 位) ,這就如同商場(chǎng)的管理區(qū)域,負(fù)責(zé)整個(gè)商場(chǎng)的運(yùn)營(yíng)和維護(hù),管理著各種資源和權(quán)限。這兩個(gè)范圍都是滿足 canonical 類型地址要求的,中間的空洞部分,就像是商場(chǎng)中暫時(shí)未開(kāi)放的區(qū)域,不滿足 canonical 地址要求,所以未被使用。這種合理的地址范圍劃分,保證了 Linux 內(nèi)核在內(nèi)存管理上的高效和穩(wěn)定,就像一個(gè)井然有序的商場(chǎng),每個(gè)區(qū)域都發(fā)揮著自己的作用,共同保障著整個(gè)系統(tǒng)的正常運(yùn)轉(zhuǎn)。