系列文章 : [6.1810] 跟著 MIT 6.1810 學習基礎作業系統觀念

每一個 user process 都有自己的 page table,並且當 xv6-riscv 進行 context-switch 的時候,satp 也會跟著切換 page table。
上圖可以看到一個 process 的 user address space。會從 0 開始,並到 MAXVA ( 0x4000000000 ) 結束,但實際上,真正會映射到 physical address 會是其中的一小部分。
一個 U-mode process 的 address space 會包含幾個部分
使用權限來管理 user process 的 address sapce 是很普遍的技巧。
假如 text of the program 被賦予了 PTE_W 權限,那一個 process 可以意外地修改自己的 program。一個簡單的範例是,假如我們意外取用了一個 null pointer,並且對這個指標所指向的地方賦值,那我們就會修改到 address 0 的程式,而不是發出 page fault。
為了避免這個情旺, xv6-riscv 不給 text section PTE_W 權限,而硬體會拒絕執行這個 store 並發出一個 page fault。xv6-riscv 的 kernel 會結束這個 process,並印出資訊,讓 developer 可以分析這個問題。
相似地,存放著資料的區段 ( data section, stack, heap ) 則不會給予執行權限 ( PTE_X ),這讓 user program 沒辦法讓 pc value 跳到存放資料的地方,並把這些資料視為 instructions 來執行。這可以避免類似 shellcode 的攻擊。
這些方式都可以讓攻擊者更難發起攻擊。
為了抵禦 stack overflowing,xv6-riscv 會在 stack 的 page 後面,擺放一個不可以取用的 page 來當作 guard page。藉由清除 PTE_U flag,可以讓這個 page 不可以在 U-mode 被使用。當有讓何人使用這個 page,不論是有意還是無意,硬體都會發出 page fault exception。
而真實世界的作業系統,可能會在發生這個 page fault 的時候,自動 allocate 更多記憶體給這個 process 的 stack。
在這裡,我們看到幾的很好的 page table 的使用範例
PTE_U ),這個 page 存放了 trampoline code。{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/vm.c#L179 }
使用 kalloc 要一個 page,這個 page 會作為該 user process 的 root page table。
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/vm.c#L192-L193 }
這個 function 可以在 pagetable 上,以 va 為起點,解除 npages 個 page 的 va→pa 映射關係。
// Remove npages of mappings starting from va. va must be
// page-aligned. It's OK if the mappings don't exist.
// Optionally free the physical memory.
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
if((va % PGSIZE) != 0)
panic("uvmunmap: not aligned");
for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
// 因為這邊在使用 `walk` function 的時候,alloc 參數是 0
// 所以遇到任何 pte 不存在的情況,不會 alloc 一個 page table 給它
// 而是會直接 return 0
// 這一步表示在 walk 的途中,就遇到 pte 不存在的情況
if((pte = walk(pagetable, a, 0)) == 0) // leaf page table entry allocated?
continue;
// 這一步代表有 leaf page table ( level 0 page table ),但是 leaf pte 不存在
if((*pte & PTE_V) == 0) // has physical page been allocated?
continue;
// 假如 do_free == true
// 會順便把這個 va 映射到的 pa 所代表的 page 給 free 掉
// 把這個 page 還給 kernel/kalloc.c/kmem
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
// 假如 pte 存在的話,就將這個 va → pa 的映射關係拿掉
*pte = 0;
}
}
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/vm.c#L192-L193 }
這個 function 可以在 pagetable 上,從 va==oldsz 一路配置記憶體到 va==newsz,並且這些記憶體的權限會是 xperm。
因為 xv6-riscv 預期執行檔的 .text 跟 .data 會從 0 開始往上增長 ( 可以參考 xv6-boot : ch3.6 Process address space 章節的 memory layout 圖示 ),所以 kexec 在使用這個 function 的時候也是從 0 開始配置 va → pa 的映射。
Q: 為什麼在 kernel/exec.c/kexec 不先遍尋 ELF 的各個 section,最後只需要呼叫一次的 uvmalloc ?
// Allocate PTEs and physical memory to grow a process from oldsz to
// newsz, which need not be page aligned. Returns new size or 0 on error.
uint64
uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm)
{
char *mem;
uint64 a;
if(newsz < oldsz)
return oldsz;
oldsz = PGROUNDUP(oldsz);
for(a = oldsz; a < newsz; a += PGSIZE){
mem = kalloc();
if(mem == 0){
// 假如在配置記憶體的過程中,發覺記憶體不足,
// 會再用 `uvmdealloc` 退回所有的記憶體配置
uvmdealloc(pagetable, a, oldsz);
return 0;
}
memset(mem, 0, PGSIZE);
if(mappages(pagetable, a, PGSIZE, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
kfree(mem);
uvmdealloc(pagetable, a, oldsz);
return 0;
}
}
return newsz;
}
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/vm.c#L246-L247 }
uint64
uvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
這個 function 會在 pagetable 上,把 newsz ~ oldsz 這個區間的 va →pa 的映射關係給解除。
預期 newsz < oldsz
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/vm.c#L262-L263 }
// Recursively free page-table pages.
// All leaf mappings must already have been removed.
void
freewalk(pagetable_t pagetable)
不是 leaf PTE
not leaf PTE 以及所有的 page table,並將所有 page table 的 physical memory page 用 kfree 回收。{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/vm.c#L282C1-L283C42 }
void
uvmfree(pagetable_t pagetable, uint64 sz)
leaf PTE
non-leaf PTE
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/vm.c#L290-L297 }
這個 function 會複製一整個 page table,以及 page table 所指向的所有 physical memory。
// Given a parent process's page table, copy
// its memory into a child's page table.
// Copies both the page table and the
// physical memory.
// returns 0 on success, -1 on failure.
// frees any allocated pages on failure.
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
char *mem;
for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0)
continue; // page table entry hasn't been allocated
if((*pte & PTE_V) == 0)
continue; // physical page hasn't been allocated
// 這個 pte 是 leaf pte
// allocate a page,並且將 leaf pte 指向的 physical memory
// 的資料複製過去
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
if((mem = kalloc()) == 0)
goto err;
memmove(mem, (char*)pa, PGSIZE);
// mappages 會為新的 root page table 配置新的 page table
// 以及相對應的 pte
if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
kfree(mem);
goto err;
}
}
return 0;
err:
// uvmunmap 只會釋放 leaf pte 的資料
// 並且釋放 leaf pte 所指向的 physical memory
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/vm.c#L327 }
// mark a PTE invalid for user access.
// used by exec for the user stack guard page.
void
uvmclear(pagetable_t pagetable, uint64 va)
針對特定的 pagetable,特定的 va,讓這個 va 沒辦法在 U-mode 使用。
可以用來製作 user stack 的 guard page。
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/vm.c#L121 }
這個 function 可以查找特定 page table 上的 virtual address,會對應到哪一個 physical address。要注意,這個 function 只能查到該 va 為 U-mode 可碰觸的情況。
// Look up a virtual address, return the physical address,
// or 0 if not mapped.
// Can only be used to look up user pages.
uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
pte_t *pte;
uint64 pa;
if(va >= MAXVA)
return 0;
// 透過 `walk` 嘗試拿到 leaf PTE
pte = walk(pagetable, va, 0);
if(pte == 0)
return 0;
if((*pte & PTE_V) == 0)
return 0;
// 該 leaf PTE 必須為 U-mode 可碰觸
if((*pte & PTE_U) == 0)
return 0;
// 拿出該 pte 指向的 physical memory
pa = PTE2PA(*pte);
return pa;
}
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/vm.c#L475-L476 }
int
ismapped(pagetable_t pagetable, uint64 va)
查看這個 va 有沒有映射到 pa。
kernel/vm.c/vmfault
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/vm.c#L448-L454 }
// allocate and map user memory if process is referencing a page
// that was lazily allocated in sys_sbrk().
// returns 0 if va is invalid or already mapped, or if
// out of physical memory, and physical address if successful.
uint64
vmfault(pagetable_t pagetable, uint64 va, int read)
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/vm.c#L343 }
把資料從 kernel space ( S-mode ) 搬到 user space ( U-mode )。
整理一下會用到的 function
leaf PTE。這個 function 的流程
Q: 可能不需要執行一次 walkaddr 拿 physical address,然後再執行一次 walk 拿 pte 嗎 ?
應該只需要執行一次 walk 就該能一次拿到 pte 跟 physical address 了。
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/vm.c#L377-L381 }
// Copy from user to kernel.
// Copy len bytes to dst from virtual address srcva in a given page table.
// Return 0 on success, -1 on error.
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
這個 function 的流程
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/vm.c#L405-L410 }
// Copy a null-terminated string from user to kernel.
// Copy bytes to dst from virtual address srcva in a given page table,
// until a '\0', or max.
// Return 0 on success, -1 on error.
int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
這個 function 跟 copyin 很像,差別在於,當遇到 \x00 的時候就停止搬運資料。
kvm 開頭的 function
uvm 開頭的 function
kvm* 也不是 uvm* 的 function