第三节 启用分页 - 进入虚拟地址空间
上一节中我们已经实现了组件 page_table,本节就基于它来启用分页,让内核从物理地址空间进入到虚拟地址空间。
两步完成地址空间的切换:
- 第一步:恒等映射保证虚拟空间与物理空间有一个相等范围的地址空间映射(0x80000000~0xC0000000)。切换前后地址范围不变,但地址空间其实已经从物理空间切换到虚拟空间;
- 第二步:给指令指针寄存器 pc,栈寄存器 sp 等加偏移,在图中该偏移是 0xffff_ffc0_0000_0000,即 PHYS_VIRT_OFFSET。通过在虚拟空间内地址平移,我们就完成了最终的地址映射。
以下扩展组件 axhal,在 RiscV64 架构下实现一个 paging 的模块,用于处理与分页管理相关的功能。
- 新增 paging 模块,在其中初始化启动阶段的页表:
// axhal/src/riscv64/paging.rs
use riscv::register::satp;
use axconfig::SIZE_1G;
use page_table::{PageTable, PAGE_KERNEL_RWX, phys_pfn};
extern "C" {
fn boot_page_table();
}
pub unsafe fn init_boot_page_table() {
let mut pt: PageTable = PageTable::init(boot_page_table as usize, 0);
let _ = pt.map(0x8000_0000, 0x8000_0000, SIZE_1G, SIZE_1G, PAGE_KERNEL_RWX);
let _ = pt.map(0xffff_ffc0_8000_0000, 0x8000_0000, SIZE_1G, SIZE_1G, PAGE_KERNEL_RWX);
}
第 10~14 行:函数 init_boot_page_table 调用上一节 page_table 组件的功能来初始化页表,其中第 12 行建立恒等映射,第 13 行则是建立了最终的地址映射关系。这两行为上面所述的两步切换,进行了页表方面的准备。
- 继续在 paging 模块中,增加启用分页的功能函数 init_mmu:
// axhal/src/riscv64/paging.rs
pub unsafe fn init_mmu() {
let page_table_root = boot_page_table as usize;
satp::set(satp::Mode::Sv39, 0, phys_pfn(page_table_root));
riscv::asm::sfence_vma_all();
}
第 4 行:针对 satp 寄存器,进行了两项配置,指定 Sv39 分页模式和指定根页表的物理页帧号;
第 5 行:刷新 TLB,确保此后 MMU 使用最新的页表。
- 注意要把 paging 模块加到 riscv64 模块下。
// axhal/src/riscv64.rs
mod paging;
- 调整内核启动过程,插入初始化页表和启用分页的过程,修改后 _start() 的实现如下:
// axhal/src/riscv64/boot.rs
use crate::riscv64::paging;
#[no_mangle]
#[link_section = ".text.boot"]
unsafe extern "C" fn _start() -> ! {
// a0 = hartid
// a1 = dtb
core::arch::asm!("
mv s0, a0 // save hartid
mv s1, a1 // save DTB pointer
la a3, _sbss
la a4, _ebss
ble a4, a3, 2f
1:
sd zero, (a3)
add a3, a3, 8
blt a3, a4, 1b
2:
la sp, boot_stack_top // setup boot stack
call {init_boot_page_table} // setup boot page table
call {init_mmu} // enabel MMU
li s2, {phys_virt_offset} // fix up virtual high address
add sp, sp, s2 // readjust stack address
mv a0, s0 // restore hartid
mv a1, s1 // restore DTB pointer
la a2, {entry}
add a2, a2, s2 // readjust rust_entry address
jalr a2 // call rust_entry(hartid, dtb)
j .",
init_boot_page_table = sym paging::init_boot_page_table,
init_mmu = sym paging::init_mmu,
phys_virt_offset = const axconfig::PHYS_VIRT_OFFSET,
entry = sym super::rust_entry,
options(noreturn),
)
}
第 21 行:调用 init_boot_page_table 初始化页表;
第 22 行:通过 init_mmu 启用 MMU 的分页机制;
第 23 行:在 s2 寄存器存放 PHYS_VIRT_OFFSET,后面用于调整栈指针与指令寄存器偏移;
第 24行:调整栈指针寄存器 sp,指向最终的虚拟地址位置;
第 27~29 行:第 27 行以相对寻址的方式去取得 rust_entry 地址,实际上该地址仍是之前的物理地址,在第 28 行加偏移调整后,才是最终的虚拟地址,第 29 行跳转后完成指令寄存器 PC 中运行地址的最终切换。
在上述工作之外,还有两项新增的工作:
第 10~11 行:通过 s0 和 s1 分别对 a0 和 a1 进行保存:a0 和 a1 保持着当前 CPU 的 ID 和 DTB 指针,保存是要防止后面的函数调用破坏它们。
第 25~26 行:从 s0 和 s1 寄存器中恢复 a0 和 a1,紧跟着再调用 rust_entry 时,它们会作为前两个参数传入 CPU_ID 和 DTB 指针。
- 修改 linker.lds,把 BASE_ADDRESS 从 0x80200000 改成 0xffffffc080200000。在分页机制启用的条件下,我们要求内核 Image 中的各种符号地址都以虚拟地址为准,确保将来符号地址寻址的正确性。
执行 make clean & make run
,可以看到内核在完成地址空间切换后,启动成功了。但是单从屏幕输出,我们看不出区别,下面利用 qemu.log 确认我们的工作成果。
我们知道当前版本的内核调用 rust_entry 时,已经进入到虚拟地址空间。所以先查找 rust_entry 的符号地址:
riscv64-unknown-elf-objdump -t ./target/riscv64gc-unknown-none-elf/release/axorigin | grep rust_entry
输出结果:
ffffffc0802000a6 l F .text 0000000000000070 _ZN5axhal7riscv6410rust_entry17hde9b663f0cccf0f7E
发现它对应的地址是 0xffffffc0802000a6(同学们的实验环境中该值可能不同),然后进一步在 qemu.log 查找该地址:
----------------
IN:
Priv: 1; Virt: 0
0x000000008020008a: 12000073 sfence.vma zero,zero
0x000000008020008e: 8082 ret
----------------
IN:
Priv: 1; Virt: 0
0x000000008020003a: 597d addi s2,zero,-1
0x000000008020003c: 191a slli s2,s2,38
0x000000008020003e: 914a add sp,sp,s2
0x0000000080200040: 8522 mv a0,s0
0x0000000080200042: 85a6 mv a1,s1
0x0000000080200044: 00000617 auipc a2,0 # 0x80200044
0x0000000080200048: 06260613 addi a2,a2,98
0x000000008020004c: 964a add a2,a2,s2
0x000000008020004e: 9602 jalr ra,a2,0
----------------
IN:
Priv: 1; Virt: 0
0xffffffc0802000a6: 711d addi sp,sp,-96
0xffffffc0802000a8: ec86 sd ra,88(sp)
注意第 23 行,0xffffffc0802000a6 是 rust_entry 地址,基于前面的工作,当进入 rust_entry 时,此时的运行地址相对之前发生了明显变化,证明内核已经从物理地址空间切换到了虚拟地址空间。