同一個小程式的頁表中,有程式碼、堆疊,也有使用者不能直接存取的 trampoline 與 Trap Frame 映射。
今天不只印出頁表根指標,而是把三層查詢一路展開,讓每筆映射的用途可以被檢查。
Day 15 用兩個 10-bit 索引走 Sv32,現在改成三個 9-bit 索引,最後仍是 12-bit 頁內 offset。
每一層有 512 筆、每筆 8 bytes,剛好占一個 4 KiB 頁面。
walk() 負責找到對應的 leaf PTE 位置,必要時配置中間頁表。mappages() 則建立 VA 到 PA 的映射與權限,兩者都不表示應用程式當下就能任意存取那一頁。
還必須看 U/R/W/X 位元,以及目前 CPU 使用哪份頁表。
在 xv6 checkout 的 kernel/vm.c 檔案末端加入:
static void
obs_walk(pagetable_t table, int level, uint64 base)
{
for (int i = 0; i < 512; i++) {
pte_t pte = table[i];
if ((pte & PTE_V) == 0)
continue;
uint64 va = base | ((uint64)i << (12 + 9 * level));
uint64 pa = PTE2PA(pte);
int leaf = (pte & (PTE_R | PTE_W | PTE_X)) != 0;
printk("L%d va=%p pte=%p pa=%p %s flags=%lx\n",
level, (void *)va, (void *)pte, (void *)pa,
leaf ? "leaf" : "table", (uint64)PTE_FLAGS(pte));
if (!leaf && level > 0)
obs_walk((pagetable_t)pa, level - 1, va);
}
}
void
obs_vmprint(pagetable_t table)
{
printk("pagetable=%p\n", (void *)table);
obs_walk(table, 2, 0);
}
在 kernel/defs.h 加入宣告:
void obs_vmprint(pagetable_t);
這份 viewer 只讀取本版本 xv6 建立的使用者頁表,使用低半部 VA,不負責一般 Sv39 高位址的符號擴展或不可信頁表驗證。
它看到 leaf 就停止遞迴,不能把 leaf 指向的資料頁誤當成下一層頁表。
現在觀察的是 xv6 的 4 KiB 映射,不把支援列出中間層 leaf 描述成已完成 superpage 管理。
在 kernel/exec.c 成功路徑中,找到 proc_freepagetable(oldpagetable, oldsz);,緊接其後加入:
if (strcmp(p->name, "echo") == 0)
obs_vmprint(p->pagetable);
這個時點已安裝新頁表、入口與使用者堆疊,不是在 allocproc() 還沒載入使用者程式的階段。
只針對 echo 印出,避免每次 exec 都淹沒終端機。
目前行程正在執行自己的 exec,沒有其他執行緒同時修改這份位址空間。
在 30-days-os-kernel/examples/xv6-riscv/ 執行:
make TOOLPREFIX=riscv64-linux-gnu- CPUS=1 qemu
於 xv6 Shell 輸入 echo vm-ready。
先看到頁表各層,再看到字串,PA 以自己的環境為準。
退出 QEMU 後,在同一目錄檢查 ELF 的 LOAD segments:
riscv64-linux-gnu-readelf -l user/_echo
將 segment 的虛擬範圍與 R/W/E 權限對照 leaf,不要只按印出的順序猜用途。exec.c 還會加入堆疊與 guard page,因此不是所有 leaf 都直接來自 ELF。
本版本用 uvmclear() 清除 guard page 的 U 位元。
因此 viewer 可能仍看見有效的 leaf,只是使用者模式不允許存取。
這與 Day 16 的「完全不映射 guard page」有不同的呈現,兩者不能用同一個 PTE_V == 0 判準。
另外,trampoline 與 Trap Frame 位於高處,但仍在 xv6 使用的 VA 範圍內。
它們不是一般使用者可讀寫的資料頁,請檢查 U 位元,而不是看到它們出現在 user page table 就認定使用者有權限。
p->sz 描述使用者記憶體大小的界線,不是已配置實體頁的總和。
特殊高位址映射、guard page 與 lazy allocation 都會讓兩者不同。
因此最後 Observer 的欄位會叫 size_bytes,不標成 RSS 或實際 RAM 消耗。
本日修改 vm.c、defs.h 與 exec.c,建議 commit:
day25: add an xv6 Sv39 page-table viewer
下一篇回到 user/kernel 邊界,沿著一次 getpid() 把 Trap 與 syscall 串起來。