第二节 重建地址空间

上一节建立了对多级页表动态映射的支持,让我们具备了更强大的管理地址空间的能力,这一节就开始重建内核的地址空间。

地址空间重映射

对照第二章第一节建立的虚拟地址空间,重建后的虚拟地址空间有几个主要的变化。

  1. SBI 被从映射中去除了。SBI 相当于上一级的 bootloader,我们的内核必须通过 SBI 提供的 sbi_ecall 来调用它的功能,不应该看到 SBI 在地址空间中的映射区间。

  2. 内核本身的各个段,按照段的特点属性,进行精细化的映射。例如代码段 .text 的权限是只读和执行,而数据段 .data 就是读写。

  3. 内核自身空间之上的所有空闲区间,作为内核的堆空间,堆空间的上限是物理内存最高地址,上一章已经通过解析 dtb 获得。

  4. 建立对设备地址区间的映射,主要是 virtio-mmio 各地址区间,为后面内核进行设备的发现与管理做准备。

其中,上面第 1 条在重映射过程中忽略 SBI 占据区间即可;对第 2、第 3 和第 4,我们先定义好它们对应的一组地址区间,再进行重映射。

地址区间的数据结构 MemRegion 包括四个成员,分别是(物理)起始地址,长度,映射属性和显示名:

// axhal/src/riscv64/mem.rs
use axconfig::{virt_to_phys, align_up, align_down, PAGE_SIZE};
use page_table::{PAGE_KERNEL_RO, PAGE_KERNEL_RW, PAGE_KERNEL_RX};

#[derive(Debug)]
pub struct MemRegion {
    pub paddr: usize,
    pub size: usize,
    pub flags: usize,
    pub name: &'static str,
}

// axhal/src/riscv64.rs
pub mod mem;
mod paging;
pub use paging::write_page_table_root;

表示内核段的地址区间列表:

// axhal/src/riscv64/mem.rs
pub fn kernel_image_regions() -> impl Iterator<Item = MemRegion> {
    [
        MemRegion {
            paddr: virt_to_phys((_stext as usize).into()),
            size: _etext as usize - _stext as usize,
            flags: PAGE_KERNEL_RX,
            name: ".text",
        },
        MemRegion {
            paddr: virt_to_phys((_srodata as usize).into()),
            size: _erodata as usize - _srodata as usize,
            flags: PAGE_KERNEL_RO,
            name: ".rodata",
        },
        MemRegion {
            paddr: virt_to_phys((_sdata as usize).into()),
            size: _edata as usize - _sdata as usize,
            flags: PAGE_KERNEL_RW,
            name: ".data .tdata .tbss .percpu",
        },
        MemRegion {
            paddr: virt_to_phys(_skernel as usize) - 0x100000,
            size: 0x100000,
            flags: PAGE_KERNEL_RW,
            name: "early heap",
        },
        MemRegion {
            paddr: virt_to_phys((boot_stack as usize).into()),
            size: boot_stack_top as usize - boot_stack as usize,
            flags: PAGE_KERNEL_RW,
            name: "boot stack",
        },
        MemRegion {
            paddr: virt_to_phys((_sbss as usize).into()),
            size: _ebss as usize - _sbss as usize,
            flags: PAGE_KERNEL_RW,
            name: ".bss",
        },
    ]
    .into_iter()
}

extern "C" {
    fn _skernel();
    fn _stext();
    fn _etext();
    fn _srodata();
    fn _erodata();
    fn _sdata();
    fn _edata();
    fn _sbss();
    fn _ebss();
    fn _ekernel();
    fn boot_stack();
    fn boot_stack_top();
}

再看一下空闲内存区间:

// axhal/src/riscv64/mem.rs
pub fn free_regions(phys_mem_size: usize) -> impl Iterator<Item = MemRegion> {
    let start = align_up(virt_to_phys(_ekernel as usize), PAGE_SIZE);
    let size = _skernel as usize + phys_mem_size - _ekernel as usize;
    core::iter::once(MemRegion {
        paddr: start,
        size: align_down(size, PAGE_SIZE),
        flags: PAGE_KERNEL_RW,
        name: "free memory",
    })
}

空闲内存区间的开始地址是 _ekernel,即内核本身占用空间的上限;而计算该区间的上限需要通过 phys_mem_size - 从 dtb 中解析获得的当前平台物理内存的大小。

下面我们将在 axruntime 的启动过程中,调用 remap_kernel_memory 进行重映射,位置就在解析和展示 dtb 信息的后面。

// axruntime/src/lib.rs
use axconfig::{phys_to_virt, SIZE_2M};
use axhal::mem::{MemRegion, kernel_image_regions, free_regions};
use axsync::BootOnceCell;
use page_table::{PAGE_KERNEL_RW, PageTable};

pub extern "C" fn rust_main(hartid: usize, dtb: usize) -> ! {
    ... ...
    
    info!("Memory: {:#x}, size: {:#x}", dtb_info.memory_addr, dtb_info.memory_size);
    info!("Virtio_mmio[{}]:", dtb_info.mmio_regions.len());
    for r in &dtb_info.mmio_regions {
        info!("\t{:#x}, size: {:#x}", r.0, r.1);
    }

    info!("Initialize kernel page table...");
    remap_kernel_memory(dtb_info);

	... ...
}

fn remap_kernel_memory(dtb: DtbInfo) {
    let mmio_regions = dtb.mmio_regions.iter().map(|reg| MemRegion {
        paddr: reg.0.into(),
        size: reg.1,
        flags: PAGE_KERNEL_RW,
        name: "mmio",
    });

    let regions = kernel_image_regions()
        .chain(free_regions(dtb.memory_size))
        .chain(mmio_regions);

    let mut kernel_page_table = PageTable::alloc_table(0);
    for r in regions {
        let _ = kernel_page_table.map(
            phys_to_virt(r.paddr),
            r.paddr,
            r.size,
            SIZE_2M,
            r.flags,
        );
    }

    static KERNEL_PAGE_TABLE: BootOnceCell<PageTable> = BootOnceCell::new();
    KERNEL_PAGE_TABLE.init(kernel_page_table);
    unsafe {
        axhal::write_page_table_root(KERNEL_PAGE_TABLE.get().root_paddr())
    };
}

// axruntime/Cargo.toml
[dependencies]
axsync = { path = "../axsync" }
page_table = { path = "../page_table" }

第 47 行:重置根页表,启用新的地址空间。需要在 axhal 中实现这个 write_page_table_root 方法。

增加 write_page_table_root 方法,同时简化 init_mmu 方法。

// axhal/src/riscv64/paging.rs
pub unsafe fn init_mmu() {
    write_page_table_root(boot_page_table as usize);
}

pub unsafe fn write_page_table_root(pa: usize) {
    satp::set(satp::Mode::Sv39, 0, phys_pfn(pa));
    riscv::asm::sfence_vma_all();
}

至此,我们为内核建立了完整的地址空间。

执行 make run LOG=info 进行测试,依然正常显示,结果与之前相同,地址空间切换成功!

(待补充)附加实验:增加测试用例,访问 SBI 空间,写 text 范围,这些违规操作应当触发异常;读 mmio 的地址范围典型值,验证。