在 AArch64 體系結構中,將內存地址分為物理地址和虛擬地址,物理地址是實際訪問內存的地址,而虛擬地址是程序指令中使用的地址。虛擬地址通過 MMU 來轉換成物理地址。
因為 Rustable 需要支持一定數量的用戶進程的并發,所以每個進程的頁表需要經常切換,但內核的頁表甚少切換,所以 Armv8 提供了 TTBR0_EL1 和 TTBR1_EL1 兩個頁表地址來給我們實現 內核地址空間和用戶地址空間的隔離。如果頁表是 TTBR0 ,則表示是用戶地址,其虛擬地址的高位全是 0 。如果頁表為 TTBR1 ,則表示是內核地址,其虛擬地址的高位全是 1 。
我們需要把上述兩塊地址空間映射到 40 位的物理地址。
[圖]
因為我們使用 4KB 為物理頁的大小,所以硬件要使用 4 級頁表。虛擬地址中有 48 位需要用來轉換,每級頁表使用 9 位,所以每級頁表有 512 項。最後 12 位用來選擇一個 4KB 頁中的地址。
VA bits [47:39] | VA bits [38:30] | VA bits [29:21] | VA bits [20:12] | VA bits [11:0] |
---|---|---|---|---|
第 0 級頁表 | 第 1 級頁表 | 第 2 級頁表 | 第 3 級頁表 | 頁的 offset |
指向第一級頁表 | 指向第二級頁表,1GB block 的基地址 | 指向第三級頁表,2MB block 的基地址 | 每個頁表項是一個 4KB block 的基地址 | 即 PA[11:0] |
树莓派启动后,kernel8.img 中的二进制代码会被加载到物理内存 0x80000
位置上。此时,MMU 处于关闭状态,访存指令访问的地址即为内存的物理地址。为了将内核与用户程序在内存中隔离开来,我们需要将内核加载至高地址空间( 0xffffff0000800000
),并确保用户程序无权读写内存中的内核部分。
kernel8.img 显然无法被直接加载至物理地址 0xFFFFFF0000080000
,因为物理内存远没有这么大;所以我們通过在 bootloader 中设置页表並打開 MMU,建立起高地址空间到低地址空间的线性映射。然後從串口中接受 kernel 並把它寫到 0xFFFFFF0000080000
上。
於是我們在 bootloader 的 init.S
中,在切換完特權級 EL1 後,加入以下代碼,跳到一段負責填寫頁表的 Rust 代碼。並使能指能的 cache。
vm:
bl vm_init
mrs x0, sctlr_el1
orr x0, x0, #(1 << 12)
msr sctlr_el1, x0 // enable instruction cache
我們把頁表放在 0x1000000
的物理地址,在 vm_init
中,首先分配一個頁給頁表,然後使用 boot_map_segment
函數來建立起高地址和低地址的線性映射,並填寫頁表。 boot_map_segment
函數的實現基本與 ucore 中的一致。
pub extern "C" fn vm_init() {
let mut binary_end = 0x1000000;
unsafe { FREEMEM = align_up(binary_end as usize, PGSIZE); }
let mut pgdir = boot_alloc(PGSIZE, true).expect("out of memory");
let n = align_up(MAXPA, PGSIZE);
boot_map_segment(pgdir, 0, n, 0, ATTRINDX_NORMAL);
boot_map_segment(pgdir, n, n, n, ATTRINDX_DEVICE);
}
填寫完頁表後回到 bootloader,開始使能 MMU。
首先設置 TTBR0 和 TTBR1 頁表的地址,即把 TTBR0_EL1
和 TTBR1_EL1
寄存器設為 0x1000000
。
el1_mmu_activate:
ldr x0, =0x04cc
msr mair_el1, x0
isb
// Translation table base address
ldr x1, =0x01000000
msr ttbr0_el1, x1
msr ttbr1_el1, x1
isb
初始化 TCR_EL1
寄存器的值,即 Translation Control Register, EL1:
mrs x2, tcr_el1
ldr x3, =0x70040ffbf
bic x2, x2, x3
TCR_EL1
寄存器的各個位設置如下
- bits [34:32] = 010: Intermediate Physical Address Size. 40-bit, 1TByte.
- bits [31] = 1: Reseverd
- bits [30] = 0: TTBR1_EL1 的粒度為 4KB
- bits [29:28] = 11: Shareability attribute for memory associated with translation table walks using TTBR1. Inner Shareable.
- bits [27:26] = 11: Outer cacheability attribute for memory associated with translation table walks using TTBR1. Normal memory, Outer Write-Back no Write-Allocate Cacheable.
- bits [25:24] = 11: Inner cacheability attribute for memory associated with translation table walks using TTBR1. Normal memory, Inner Write-Back no Write-Allocate Cacheable.
- bits [21:16] = 011000: bSize offset of the memory region addressed by TTBR1
- bits [13:12] = 11: Shareability attribute for memory associated with translation table walks using TTBR0. Inner Shareable.
- bits [11:10] = 11: Outer cacheability attribute for memory associated with translation table walks using TTBR0. Normal memory, Outer Write-Back no Write-Allocate Cacheable.
- bits [9:8] = 11: Inner cacheability attribute for memory associated with translation table walks using TTBR0. Normal memory, Inner Write-Back no Write-Allocate Cacheable.
- bits [5:0] = 011000: Size offset of the memory region addressed by TTBR0.
ldr x3, =0x2bf183f18
orr x2, x2, x3
msr tcr_el1, x2
isb
設置 SCTLR_EL1
寄存器,即 System Control Register, EL1
Write permission implies Execute Never (XN). You can use this bit to require all memory regions with write permissions are treated as XN. The WXN bit is permitted to be cached in a TLB.
ldr x5, =kmain
mrs x3, sctlr_el1
ldr x4, =0x80000
bic x3, x3, x4
SCTLR_EL1
寄存器的各位設置如下:
- bits [12] : Instruction caches enabled.
- bits [2] : Data and unified caches enabled.
- bits [0] : EL1 and EL0 stage 1 MMU enabled.
ldr x4, =0x1005
orr x3, x3, x4
msr sctlr_el1, x3
isb
br x5
至此,MMU 已開始,可以跳到 kernel 執行
go_kmain:
bl kmain
b 1b
根據上述的轉換格式就可以實現 get_pte
函數,給定頁表地址和虛擬地址,可以獲取對應的物理地址。
pub fn get_pte(pgdir_addr: *const usize, va: usize, create: bool) -> Result<*mut usize, AllocErr> {
// 第0級 => 第1級
let pgtable0_entry_ptr = pgdir_addr as *mut usize;
let mut pgtable1 = PTE_ADDR(unsafe { *pgtable0_entry_ptr }) + PT1X(va) * 8;
if (unsafe { *pgtable0_entry_ptr } & PTE_V) == 0 && create == true {
pgtable1 = alloc_page().expect("cannot alloc page") as usize;
unsafe { *pgtable0_entry_ptr = pgtable1 | PTE_V };
pgtable1 += PT1X(va) * 8;
}
// 第1級 => 第2級
let pgtable1_entry_ptr = pgtable1 as *mut usize;
同上
// 第2級 => 第3級
同上
Ok(pgtable3 as *mut usize)
}
page_insert
函數負責建立給定物理頁 page
和虛擬地址 va
的映射,並插入到給定的頁表 pgdir
中。
此函數首先嘗試通過 get_pte
獲得 va
對應的頁表項 pte
和對應的頁,
- 若該頁等於
page
則不變 - 若不等於,則調用
page_remove
把此頁移除
最後把頁表項 pte
設為 page
,並設置權限。因為頁表有所修改,需要更新 TLB。
pub fn page_insert(pgdir: *const usize, page: *mut Page, va: usize, perm: usize) -> Result<i32, i32>{
let PERM = perm | PTE_V | ATTRINDX_NORMAL | ATTRIB_SH_INNER_SHAREABLE | AF;
match get_pte(pgdir, va, true) {
Ok(pte) => {
(unsafe { &mut *page }).page_ref_inc();
if unsafe{ *pte & PTE_V != 0} {
if pa2page(PTE_ADDR(unsafe{*pte})) != page {
page_remove(pte);
} else {
(unsafe { &mut *page }).page_ref_dec();
}
}
unsafe{ *pte = PTE_ADDR(page2pa(page)) | PERM };
tlb_invalidate();
return Ok(0);
},
Err(_) => {
return Err(-1);
}
}
}
page_remove
函數是把 pte
對應的頁釋放掉。
根據頁表項計算出對應的頁,若此頁的 reference
減一後為 0 ,則把此頁釋放掉。
pub fn page_remove(pte: *mut usize) {
let pa = unsafe{ PTE_ADDR(*pte as usize) as *mut usize };
let page = pa2page(pa as usize);
if (unsafe { &mut *page }).page_ref_dec() <= 0 {
dealloc_page(pa as *mut u8);
}
unsafe { *pte = 0; }
tlb_invalidate();
}
pgdir_alloc_page
函數利用 alloc_page
和 page_insert
實現,分配頁並插入到給定頁表。
user_pgdir_alloc_page
函數主要利用 alloc_page_at
函數在用戶的 allocator
中分配虛擬頁,然後把虛擬頁地址利用 pgdir_alloc_page
為其分配物理頁,並插入到用戶的頁表中。
pub fn pgdir_alloc_page(pgdir: *const usize, va: usize, perm: usize) -> Result<*mut u8, AllocErr>
pub fn user_pgdir_alloc_page(allocator: &mut Allocator, pgdir: *const usize, va: usize, perm: usize) -> Result<*mut u8, AllocErr> {
//分配虛擬頁
alloc_page_at(allocator, va, pgdir).expect("alloc virtual page failed");
pgdir_alloc_page(pgdir, va, perm)
}
此函數被用於上述的 user_pgdir_alloc_page
函數
pub fn alloc_at(&mut self, va: usize, layout: Layout, pgdir: *const usize) -> Result<*mut u8, AllocErr> {
let npage = align_up(layout.size(), PGSIZE) / PGSIZE;
if npage as u32 > self.n_free {
return Err( AllocErr::Exhausted { request: layout } );
}
for i in self.free_list.iter_mut() {
找到包含va的空閒頁
}
match page {
Some(page) => {
let prev_npage = 空閒內存塊中前面剩的頁數
let next_npage = 空閒內存塊中後面剩的頁數
if next_npage > 0 { 把後面剩的空閒頁插回鏈表 }
if prev_npage > 0 { 原空閒頁的property設為prev_npage; } else { 刪除原空閒頁 }
把分配的頁設為Used
self.n_free -= npage as u32;
return Ok(self.page2addr(alloc_page) as *mut usize as * mut u8);
}
_ => {
switch_back();
Err( AllocErr::Exhausted { request: layout } )
}
}
}