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