第三节 启用分页 - 进入虚拟地址空间

上一节中我们已经实现了组件 page_table,本节就基于它来启用分页,让内核从物理地址空间进入到虚拟地址空间。

恒等映射空间切换

两步完成地址空间的切换:

  • 第一步:恒等映射保证虚拟空间与物理空间有一个相等范围的地址空间映射(0x80000000~0xC0000000)。切换前后地址范围不变,但地址空间其实已经从物理空间切换到虚拟空间;
  • 第二步:给指令指针寄存器 pc,栈寄存器 sp 等加偏移,在图中该偏移是 0xffff_ffc0_0000_0000,即 PHYS_VIRT_OFFSET。通过在虚拟空间内地址平移,我们就完成了最终的地址映射。

以下扩展组件 axhal,在 RiscV64 架构下实现一个 paging 的模块,用于处理与分页管理相关的功能。

  1. 新增 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 行则是建立了最终的地址映射关系。这两行为上面所述的两步切换,进行了页表方面的准备。

  1. 继续在 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 使用最新的页表。

  1. 注意要把 paging 模块加到 riscv64 模块下。
// axhal/src/riscv64.rs
mod paging;
  1. 调整内核启动过程,插入初始化页表和启用分页的过程,修改后 _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 指针。

  1. 修改 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 时,此时的运行地址相对之前发生了明显变化,证明内核已经从物理地址空间切换到了虚拟地址空间。