You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

13 KiB

教程10 - 虚拟内存第一部分:将所有内容进行身份映射!

tl;dr

  • 打开MMU
  • 使用简单的方案:静态的64 KiB转换表。
  • 为了教学目的,我们将数据写入重新映射的UART,并对其他所有内容进行identity map

目录

介绍

虚拟内存是一个非常复杂但重要且强大的主题。在本教程中,我们从简单易懂的方式开始, 通过打开MMU,使用静态转换表和一次性进行identity-map (除了为教育目的而重新映射的UART之外;在下一个教程中,这将被取消)。

MMU和分页理论

在这一点上,我们不会重新发明轮子并详细描述现代应用级处理器中分页的工作原理。 互联网上有很多关于这个主题的优秀资源,我们鼓励您阅读其中一些以获得对该主题的高层理解。

继续阅读本AArch64特定的教程,我强烈建议您在此处停下来,首先阅读ARM Cortex-A Series Programmer's Guide for ARMv8-A第12章 以便在继续之前获得所有所需的AArch64特定知识。

已经阅读完第12章了吗?做得好 👍!

方法

  1. 通用的kernel部分:src/memory/mmu.rs及其子模块提供了与体系结构无关的描述符类型, 用于组合一个高级数据结构,描述内核的虚拟内存布局:memory::mmu::KernelVirtualLayout
  2. BSP部分:src/bsp/raspberrypi/memory/mmu.rs包含一个KernelVirtualLayout的静态实例,并通过函数 bsp::memory::mmu::virt_mem_layout()使其可访问。
  3. aarch64部分:src/_arch/aarch64/memory/mmu.rs及其子模块包含实际的MMU驱动程序。它使用64 KiB粒度获取 BSP的高级KernelVirtualLayout并进行映射。

通用内核代码:memory/mmu.rs

在这个文件中提供的描述符类型是构建块,用于描述不同内存区域的属性。 例如,R/W(读/写)、no-execute(不执行)、cached/uncached(缓存/非缓存)等等。

这些描述符与硬件MMU的实际描述符无关。不同的BSP可以使用这些类型来生成内核虚拟内存布局的高级描述。 真实硬件的实际MMU驱动程序将使用这些类型作为输入。

通过这种方式,我们在BSP_arch代码之间实现了清晰的抽象,这样可以在不需要调整另一个的情况下进行交换。

BSP: bsp/raspberrypi/memory/mmu.rs

这个文件包含了一个KernelVirtualLayout的实例,用于存储先前提到的描述符。 将其放在BSP中是正确的位置,因为它具有目标板的内存映射知识。

策略是只描述不是普通的、可缓存的DRAM的区域。然而如果您希望也可以定义这些区域。 这里是一个设备MMIO区域的示例

TranslationDescriptor {
    name: "Device MMIO",
    virtual_range: mmio_range_inclusive,
    physical_range_translation: Translation::Identity,
    attribute_fields: AttributeFields {
        mem_attributes: MemAttributes::Device,
        acc_perms: AccessPermissions::ReadWrite,
        execute_never: true,
    },
},

KernelVirtualLayout本身实现了以下方法:

pub fn virt_addr_properties(
    &self,
    virt_addr: usize,
) -> Result<(usize, AttributeFields), &'static str>

它将被_arch/aarch64MMU代码使用,用于请求虚拟地址和转换的属性,该转换提供物理输出地址 (返回元组中的usize)。该函数扫描包含查询地址的描述符,并返回第一个匹配的条目的相应结果。 如果找不到条目则返回普通可缓存DRAM的默认属性和输入地址从而告诉MMU代码请求的地址应该是identity mapped

由于这种默认行为不需要定义普通可缓存DRAM区域。

AArch64: _arch/aarch64/memory/*

这些模块包含了AArch64MMU驱动程序。粒度在这里被硬编码为(64 KiB页描述符)。

translation_table.rs中,有一个实际的转换表结构的定义,它对LVL2表的数量进行了泛化。 后者取决于目标板的内存大小。自然地,BSP了解目标板的这些细节,并通过常量 bsp::memory::mmu::KernelAddrSpace::SIZE提供大小信息。

translation_table.rs使用这些信息来计算所需的LVL2表的数量。由于在64 KiB配置中, 一个LVL2表可以覆盖512 MiB,所以只需要将KernelAddrSpace::SIZE除以512 MiB (有几个编译时检查确保KernelAddrSpace::SIZE512 MiB的倍数)。

最终的表类型被导出为KernelTranslationTable。以下是来自translation_table.rs的相关代码:

/// A table descriptor for 64 KiB aperture.
///
/// The output points to the next table.
#[derive(Copy, Clone)]
#[repr(C)]
struct TableDescriptor {
    value: u64,
}

/// A page descriptor with 64 KiB aperture.
///
/// The output points to physical memory.
#[derive(Copy, Clone)]
#[repr(C)]
struct PageDescriptor {
    value: u64,
}

const NUM_LVL2_TABLES: usize = bsp::memory::mmu::KernelAddrSpace::SIZE >> Granule512MiB::SHIFT;

//--------------------------------------------------------------------------------------------------
// Public Definitions
//--------------------------------------------------------------------------------------------------

/// Big monolithic struct for storing the translation tables. Individual levels must be 64 KiB
/// aligned, hence the "reverse" order of appearance.
#[repr(C)]
#[repr(align(65536))]
pub struct FixedSizeTranslationTable<const NUM_TABLES: usize> {
    /// Page descriptors, covering 64 KiB windows per entry.
    lvl3: [[PageDescriptor; 8192]; NUM_TABLES],

    /// Table descriptors, covering 512 MiB windows.
    lvl2: [TableDescriptor; NUM_TABLES],
}

/// A translation table type for the kernel space.
pub type KernelTranslationTable = FixedSizeTranslationTable<NUM_LVL2_TABLES>;

mmu.rs中,KernelTranslationTable用于创建内核表的最终实例:

//--------------------------------------------------------------------------------------------------
// Global instances
//--------------------------------------------------------------------------------------------------

/// The kernel translation tables.
static mut KERNEL_TABLES: KernelTranslationTable = KernelTranslationTable::new();

它们在MMU::init()期间通过调用KERNEL_TABLES.populate_tt_entries()进行填充, 该函数利用bsp::memory::mmu::virt_mem_layout().virt_addr_properties()和一系列实用函数,将内核通用描述符转换为 AArch64 MMU硬件所需的实际64 bit整数条目,用于填充转换表数组。

一个值得注意的事情是,每个页描述符都有一个索引(AttrIndex),它索引到MAIR_EL1寄存器, 该寄存器保存了有关相应页面的缓存属性的信息。我们目前定义了普通可缓存内存和设备内存(不被缓存)。

impl MemoryManagementUnit {
    /// Setup function for the MAIR_EL1 register.
    fn set_up_mair(&self) {
        // Define the memory types being mapped.
        MAIR_EL1.write(
            // Attribute 1 - Cacheable normal DRAM.
            MAIR_EL1::Attr1_Normal_Outer::WriteBack_NonTransient_ReadWriteAlloc +
        MAIR_EL1::Attr1_Normal_Inner::WriteBack_NonTransient_ReadWriteAlloc +

        // Attribute 0 - Device.
        MAIR_EL1::Attr0_Device::nonGathering_nonReordering_EarlyWriteAck,
        );
    }

然后,Translation Table Base Register 0 - EL1使用lvl2表的基地址进行设置,同时配置Translation Control Register - EL1

// Set the "Translation Table Base Register".
TTBR0_EL1.set_baddr(KERNEL_TABLES.phys_base_address());

self.configure_translation_control();

最后,通过System Control Register - EL1打开MMU。最后一步还启用了数据和指令的缓存。

kernel.ld

我们需要将code段对齐到64 KiB,这样它就不会与下一个需要读/写属性而不是读/执行属性的部分重叠。

. = ALIGN(PAGE_SIZE);
__code_end_exclusive = .;

这会增加二进制文件的大小,但考虑到与传统的4 KiB粒度相比,它显著减少了静态分页条目的数量,这是一个小小的代价。

地址转换示例

出于教育目的,定义了一个布局,允许通过两个不同的虚拟地址访问UART

  • 由于我们对整个Device MMIO区域进行了身份映射,所以在MMU打开后,可以通过断言其物理基地址 0x3F20_10000xFA20_1000取决于使用的是哪个RPi版本来访问它。
  • 此外,它还映射到第一个512 MiB中的最后一个64 KiB槽位,使其可以通过基地址0x1FFF_1000访问。

以下块图可视化了第二个映射的底层转换。

使用64KiB页描述符进行地址转换

Page Tables 64KiB

零成本抽象

初始化代码再次是展示Rust零成本抽象在嵌入式编程中巨大潜力的一个很好的例子[1][2]。

让我们再次看一下使用aarch64-cpucrate设置MAIR_EL1寄存器的代码片段:

/// Setup function for the MAIR_EL1 register.
fn set_up_mair(&self) {
    // Define the memory types being mapped.
    MAIR_EL1.write(
        // Attribute 1 - Cacheable normal DRAM.
        MAIR_EL1::Attr1_Normal_Outer::WriteBack_NonTransient_ReadWriteAlloc +
    MAIR_EL1::Attr1_Normal_Inner::WriteBack_NonTransient_ReadWriteAlloc +

    // Attribute 0 - Device.
    MAIR_EL1::Attr0_Device::nonGathering_nonReordering_EarlyWriteAck,
    );
}

这段代码具有超强的表达能力,它利用traits,不同的typesconstants来提供类型安全的寄存器操作。

最后,此代码根据数据表将寄存器的前四个字节设置为特定值。查看生成的代码, 我们可以看到,尽管有所有的类型安全和抽象,但它可以归结为两条汇编指令:

   800a8:       529fe089        mov     w9, #0xff04                     // #65284
   800ac:       d518a209        msr     mair_el1, x9

测试

打开虚拟内存现在是我们在内核初始化过程中要做的第一件事:

unsafe fn kernel_init() -> ! {
    use memory::mmu::interface::MMU;

    if let Err(string) = memory::mmu::mmu().enable_mmu_and_caching() {
        panic!("MMU: {}", string);
    }

稍后在引导过程中,可以观察到有关映射的打印:

$ make chainboot
[...]
Minipush 1.0

[MP] ⏳ Waiting for /dev/ttyUSB0
[MP] ✅ Serial connected
[MP] 🔌 Please power the target now

 __  __ _      _ _                 _
|  \/  (_)_ _ (_) |   ___  __ _ __| |
| |\/| | | ' \| | |__/ _ \/ _` / _` |
|_|  |_|_|_||_|_|____\___/\__,_\__,_|

           Raspberry Pi 3

[ML] Requesting binary
[MP] ⏩ Pushing 64 KiB =========================================🦀 100% 0 KiB/s Time: 00:00:00
[ML] Loaded! Executing the payload now

[    0.811167] mingo version 0.10.0
[    0.811374] Booting on: Raspberry Pi 3
[    0.811829] MMU online. Special regions:
[    0.812306]       0x00080000 - 0x0008ffff |  64 KiB | C   RO PX  | Kernel code and RO data
[    0.813324]       0x1fff0000 - 0x1fffffff |  64 KiB | Dev RW PXN | Remapped Device MMIO
[    0.814310]       0x3f000000 - 0x4000ffff |  17 MiB | Dev RW PXN | Device MMIO
[    0.815198] Current privilege level: EL1
[    0.815675] Exception handling state:
[    0.816119]       Debug:  Masked
[    0.816509]       SError: Masked
[    0.816899]       IRQ:    Masked
[    0.817289]       FIQ:    Masked
[    0.817679] Architectural timer resolution: 52 ns
[    0.818253] Drivers loaded:
[    0.818589]       1. BCM PL011 UART
[    0.819011]       2. BCM GPIO
[    0.819369] Timer test, spinning for 1 second
[     !!!    ] Writing through the remapped UART at 0x1FFF_1000
[    1.820409] Echoing input now

相比之前的变化diff

请检查英文版本,这是最新的。