Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Page Table

PageTable defined in awkernel_lib/src/paging.rs is a trait that provides a way to abstract page tables. It is defined as follows.

#![allow(unused)]
fn main() {
pub trait PageTable<F, FA, E>
where
    F: Frame,
    FA: FrameAllocator<F, E>,
{
    /// Map `virt_addr` to `phy_addr` with `flag`.
    ///
    /// # Safety
    ///
    /// - virt_addr and phy_addr must be aligned to page size.
    unsafe fn map_to(
        &mut self,
        virt_addr: VirtAddr,
        phy_addr: PhyAddr,
        flags: Flags,
        page_allocator: &mut FA,
    ) -> Result<(), E>;
}
}

map_to method of PageTable is used to specify a page frame allocator, which allocates physical pages for the page table, when mapping pages. It is typically used when initializing the kernel's page tables or initializing device drivers.

Frame defined in awkernel_lib/src/paging.rs is a trait to represent a physical page frame. It is defined as follows.

#![allow(unused)]
fn main() {
pub trait Frame {
    fn start_address(&self) -> PhyAddr;
    fn set_address(&mut self, addr: PhyAddr);
    fn size(&self) -> usize;
}
}

FrameAllocator defined in awkernel_lib/src/paging.rs is a trait to allocate physical pages. It is used by PageTable as described above.

Implementation

x86_64

For x86_64, the PageTable structure is defined in awkernel_lib/src/arch/x86_64/page_table.rs as follows.

#![allow(unused)]
fn main() {
pub struct PageTable<'a> {
    offset_page_table: &'a mut OffsetPageTable<'static>,
}
}

The PageTable structure implements the PageTable:awkernel_lib/src/paging.rs trait as follows.

#![allow(unused)]
fn main() {
impl<'a> crate::paging::PageTable<super::page_allocator::Frame, VecPageAllocator, &'static str>
    for PageTable<'a>
{
    unsafe fn map_to(
        &mut self,
        virt_addr: crate::addr::virt_addr::VirtAddr,
        phy_addr: crate::addr::phy_addr::PhyAddr,
        flags: crate::paging::Flags,
        page_allocator: &mut VecPageAllocator,
    ) -> Result<(), &'static str> {
        let flags = flags_to_x86_flags(flags);

        let page = Page::containing_address(VirtAddr::new(virt_addr.as_usize() as u64));
        let frame =
            PhysFrame::<Size4KiB>::containing_address(PhysAddr::new(phy_addr.as_usize() as u64));

        match self
            .offset_page_table
            .map_to(page, frame, flags, page_allocator)
        {
            Ok(flusher) => {
                flusher.flush();
                Ok(())
            }
            Err(_) => Err("Failed to map page"),
        }
    }
}
}

AArch64

For AArch64, the PageTable structure is defined in awkernel_lib/src/arch/aarch64/page_table.rs as follows.

#![allow(unused)]
fn main() {
/// - 3 transition levels
/// - 4KiB page
/// - up to 512GiB memory
pub struct PageTable {
    root: PageTableEntry,
}
}

The PageTable structure implements the PageTable:awkernel_lib/src/paging.rs trait as follows.

#![allow(unused)]
fn main() {
impl crate::paging::PageTable<Page, PageAllocator<Page>, &'static str> for PageTable {
    unsafe fn map_to(
        &mut self,
        virt_addr: VirtAddr,
        phy_addr: PhyAddr,
        flags: crate::paging::Flags,
        page_allocator: &mut PageAllocator<Page>,
    ) -> Result<(), &'static str> {
        let mut f = FLAG_L3_AF | 0b11;

        if !flags.execute {
            f |= FLAG_L3_XN | FLAG_L3_PXN;
        }

        if flags.write {
            f |= FLAG_L3_SH_RW_N;
        } else {
            f |= FLAG_L3_SH_R_N;
        }

        match (flags.device, flags.cache) {
            (true, true) => f |= FLAG_L3_ATTR_MEM | FLAG_L3_OSH,
            (true, false) => f |= FLAG_L3_ATTR_DEV | FLAG_L3_OSH,
            (false, true) => f |= FLAG_L3_ATTR_MEM | FLAG_L3_ISH,
            (false, false) => f |= FLAG_L3_NS | FLAG_L3_ISH,
        }

        self.map_to_aarch64(virt_addr, phy_addr, f, page_allocator)
    }
}
}

RISC-V 32-bit (RV32)

For RV32, the PageTable structure is defined in awkernel_lib/src/arch/rv32/page_table.rs. It implements the Sv32 translation scheme: 32-bit virtual addresses, 4 KiB pages and a 2-level page table. Each page table entry (PTE) holds a physical page number (PPN) and the standard RISC-V flag bits.

#![allow(unused)]
fn main() {
bitflags! {
    /// PTE Flags for RISC-V Sv32 page table
    pub struct Flags: u8 {
        const V = 1 << 0; // Valid
        const R = 1 << 1; // Readable
        const W = 1 << 2; // Writable
        const X = 1 << 3; // Executable
        const U = 1 << 4; // User-accessible
        const G = 1 << 5; // Global
        const A = 1 << 6; // Accessed
        const D = 1 << 7; // Dirty
    }
}

pub struct PageTable {
    root_ppn: PhysPageNum,
    frames: Vec<FrameTracker>,
}
}

The root page table is allocated from the physical frame allocator on PageTable::new, and every intermediate table allocated while walking the tree is tracked in frames so that it is kept alive for the lifetime of the address space. The virtual page number is split into two 10-bit indices (VirtPageNum::indexes), one per level of the Sv32 table.

The PageTable structure implements the PageTable:awkernel_lib/src/paging.rs trait as follows. The generic Flags are translated into RISC-V PTE flags: entries are always made valid, accessed and readable (V | A | R); writable mappings also set the dirty bit (W | D), and executable mappings set X.

#![allow(unused)]
fn main() {
impl crate::paging::PageTable<Page, RV32PageAllocator, &'static str> for PageTable {
    unsafe fn map_to(
        &mut self,
        virt_addr: VirtAddr,
        phy_addr: PhyAddr,
        flags: crate::paging::Flags,
        _page_allocator: &mut RV32PageAllocator,
    ) -> Result<(), &'static str> {
        let vpn = VirtPageNum::from(virt_addr);
        let ppn = PhysPageNum::from(phy_addr);

        let mut rv_flags = Flags::V | Flags::A; // Always valid and accessed
        if flags.write {
            rv_flags |= Flags::W | Flags::D; // Writable and dirty
        }
        rv_flags |= Flags::R; // Always readable
        if flags.execute {
            rv_flags |= Flags::X;
        }

        if self.map(vpn, ppn, rv_flags) {
            Ok(())
        } else {
            Err("Mapping failed")
        }
    }
}
}

Translation is enabled by writing the satp (Supervisor Address Translation and Protection) register. For Sv32 the token method returns the register value with MODE = 1 (Sv32) in bit 31 and the root table's PPN in the low bits.

#![allow(unused)]
fn main() {
pub fn token(&self) -> usize {
    (1usize << 31)    // MODE = 1 (Sv32 paging mode)
    | self.root_ppn.0 // PPN of the root page table
}
}

RISC-V 64-bit (RV64)

For RV64, the PageTable structure is defined in awkernel_lib/src/arch/rv64/page_table.rs. It implements the Sv39 translation scheme: 39-bit virtual addresses, 4 KiB pages and a 3-level page table. The PTE layout and Flags definition are identical to RV32; the differences are the number of levels and the satp encoding.

#![allow(unused)]
fn main() {
pub struct PageTable {
    root_ppn: PhysPageNum,
    frames: Vec<FrameTracker>,
}
}

The virtual page number is split into three 9-bit indices (VirtPageNum::indexes), walked by find_pte / find_pte_create; the leaf PTE is reached at level index 2.

The map_to implementation is the same as RV32 — generic Flags are mapped to the RISC-V PTE flags (V | A | R, plus W | D for writable and X for executable pages):

#![allow(unused)]
fn main() {
impl crate::paging::PageTable<Page, RV64PageAllocator, &'static str> for PageTable {
    unsafe fn map_to(
        &mut self,
        virt_addr: VirtAddr,
        phy_addr: PhyAddr,
        flags: crate::paging::Flags,
        _page_allocator: &mut RV64PageAllocator,
    ) -> Result<(), &'static str> {
        let vpn = VirtPageNum::from(virt_addr);
        let ppn = PhysPageNum::from(phy_addr);

        let mut rv_flags = Flags::V | Flags::A;
        if flags.write {
            rv_flags |= Flags::W | Flags::D;
        }
        rv_flags |= Flags::R;
        if flags.execute {
            rv_flags |= Flags::X;
        }

        if self.map(vpn, ppn, rv_flags) {
            Ok(())
        } else {
            Err("Mapping failed")
        }
    }
}
}

For Sv39 the token method encodes MODE = 8 (Sv39) in bits 63-60 and the root PPN in the low 44 bits:

#![allow(unused)]
fn main() {
pub fn token(&self) -> usize {
    (8usize << 60)    // MODE = 8 (Sv39 paging mode)
    | self.root_ppn.0 // PPN of the root page table
}
}

The kernel address space is built in awkernel_lib/src/arch/rv64/vm.rs: new_kernel maps the kernel sections (.text, .rodata, .data, .bss) and an identity mapping for available RAM, and activate installs the table by writing satp and flushing the TLB:

#![allow(unused)]
fn main() {
pub fn activate(&self) {
    let satp = self.page_table.token();
    unsafe {
        asm!("csrw satp, {}", in(reg) satp);
        asm!("sfence.vma");
    }
}
}