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

对照第二章第一节建立的虚拟地址空间,重建后的虚拟地址空间有几个主要的变化。
-
SBI 被从映射中去除了。SBI 相当于上一级的 bootloader,我们的内核必须通过 SBI 提供的 sbi_ecall 来调用它的功能,不应该看到 SBI 在地址空间中的映射区间。
-
内核本身的各个段,按照段的特点属性,进行精细化的映射。例如代码段 .text 的权限是只读和执行,而数据段 .data 就是读写。
-
内核自身空间之上的所有空闲区间,作为内核的堆空间,堆空间的上限是物理内存最高地址,上一章已经通过解析 dtb 获得。
-
建立对设备地址区间的映射,主要是 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 的地址范围典型值,验证。