今天先往一頁實體記憶體寫入 0x12345678,再從虛擬位址 0x40000000 讀到同一個值。
接著透過虛擬位址改成 0xabcdef01,原本的實體頁也會看見更新。
我們沒有複製兩份資料,而是讓兩個位址指向同一個儲存位置。
這個別名映射(Alias Mapping)實驗,是把「程式使用的位址」和「RAM 的位置」拆開的第一步。
《Operating System Concepts》第 10 版第 9.3.1 節,將邏輯記憶體的 page 對應到實體記憶體的 frame。
程式使用虛擬位址(Virtual Address,VA),硬體依分頁表找到實體位址(Physical Address,PA)。
頁內偏移保持不變,改變的是頁面對應的實體起點。
Day 09 的配置器負責取得 frame,今天的映射函式則決定如何讓 CPU 透過另一個 VA 找到它。
這次先在單一核心位址空間驗證映射。
行程間的隔離還需要不同的頁表與權限安排,不能只因為啟用了分頁,就宣稱所有行程已經隔離。
本系列沿用《OS in 1,000 Lines》的〈分頁表〉,使用 RV32 的 Sv32 模式。
對 4 KiB 頁面而言,一個 VA 拆成以下三部分:
| 欄位 | 位元 | 用途 |
|---|---|---|
| VPN[1] | 31~22 | 根頁表索引 |
| VPN[0] | 21~12 | 第二層頁表索引 |
| Offset | 11~0 | 頁內偏移 |
VPN 是 Virtual Page Number,虛擬頁號。
每層有 1024 個分頁表項(Page Table Entry,PTE),每個 PTE 為四個 bytes,一層剛好放進一頁。
Sv32 也能使用較大的葉節點,本系列的最小 helper 先只處理兩層查詢與 4 KiB 頁面。
此外,範例的 PA 使用 32-bit 整數,限定本次 QEMU 的 RAM 範圍,不涵蓋 Sv32 規格所有可表達的實體位址。
在 30-days-os-kernel/examples/tiny-kernel/ 執行:
make DAY=14 run
完整邏輯位於 vm.c,vm_demo() 先準備根頁表與一頁資料:
pte_t *root = vm_kernel();
uint32_t *page = alloc_pages(1);
KASSERT(page);
page[0] = 0x12345678;
vm_map(root, 0x40000000, (uint32_t) page, PTE_R | PTE_W);
vm_enable(root);
vm_kernel() 先建立核心繼續執行需要的映射,vm_map() 再加入實驗用 VA。
如果根頁表裡還沒有對應的第二層頁表,就從 Day 09 的配置器再取一頁。
建立 PTE 時,先把 PA 轉成實體頁號,再放進 PTE 的 PPN 欄位:
table[j] = ((pa >> 12) << 10) | flags | PTE_V | PTE_A | PTE_D;
V 表示有效,R/W/X 控制讀寫與執行,A/D 則與頁面的存取和修改狀態有關。
為了先把基本轉譯跑通,本實驗預先設好 A/D,不利用它們追蹤頁面歷史。
當 satp 切到 Sv32,接下來取指令與存取堆疊也會經過轉譯。
因此核心程式、資料、啟動堆疊與頁池都建立 VA 等於 PA 的 identity mapping。
在同一份頁表中,核心程式碼是 RX,唯讀資料是 R,其餘必要資料與堆疊是 RW。
這些映射都沒有 U 位元,後續進入 U-mode 時,使用者程式不能直接透過核心的高位址別名存取它們。
vm_enable() 把根頁表的實體頁號放入 satp,並以 sfence.vma 同步位址轉譯狀態。
此處固定單核心,尚未處理其他 hart 上的 TLB 同步。
volatile uint32_t *alias = (void *) 0x40000000;
KASSERT(*alias == 0x12345678);
*alias = 0xabcdef01;
KASSERT(page[0] == 0xabcdef01);
第一個檢查驗證讀取對應,第二個檢查驗證寫入對應。
如果兩者都通過,Console 會出現:
alias read/write=ok satp=0x...
VA=0x40000000 vpn1=256 vpn0=0 offset=0
L1=0x... L0=0x... PA=0x...
experiment complete
satp 的最高位元應是 1,代表 RV32 的 Sv32,其他位元中的根頁號則依本次配置位置而定。
實體頁位址會隨核心大小改變,請比較映射關係,勿把本機 PA 當成所有人的固定答案。
先確認核心目前的 pc 與 sp 都在頁表映射範圍內,再看根 PTE 指向的是實體頁表地址還是誤填的 VA。
若進到 handler,Instruction Page Fault、Load Page Fault 與 Store Page Fault 分別反映不同存取失敗。
我們的 handler 會印出 scause、sepc 與 stval,保留判斷入口。
本次頁表供整個實驗存活到關機,不會在仍啟用時釋放。
後續若切換或回收頁表,還需要管理誰仍在使用它,不能直接把 backing pages 交回配置器。
本篇主要檔案是 vm.c、lab.h 與 kernel.ld,建議 commit 訊息:
day14: enable sv32 virtual memory
Day 15 會手動走過兩層 PTE,確認 CPU 背後依據的查詢關係,也會測試頁內 offset 與沒有映射的位址。