- 打开
MMU
。 - 使用简单的方案:静态的
64 KiB
转换表。 - 为了教学目的,我们将数据写入重新映射的
UART
,并对其他所有内容进行identity map
。
虚拟内存是一个非常复杂但重要且强大的主题。在本教程中,我们从简单易懂的方式开始,
通过打开MMU
,使用静态转换表和一次性进行identity-map
(除了为教育目的而重新映射的UART
之外;在下一个教程中,这将被取消)。
在这一点上,我们不会重新发明轮子并详细描述现代应用级处理器中分页的工作原理。 互联网上有很多关于这个主题的优秀资源,我们鼓励您阅读其中一些以获得对该主题的高层理解。
继续阅读本AArch64
特定的教程,我强烈建议您在此处停下来,首先阅读ARM Cortex-A Series Programmer's Guide for ARMv8-A的第12章
,
以便在继续之前获得所有所需的AArch64
特定知识。
已经阅读完第12章
了吗?做得好 👍!
- 通用的
kernel
部分:src/memory/mmu.rs
及其子模块提供了与体系结构无关的描述符类型, 用于组合一个高级数据结构,描述内核的虚拟内存布局:memory::mmu::KernelVirtualLayout
。 BSP
部分:src/bsp/raspberrypi/memory/mmu.rs
包含一个KernelVirtualLayout
的静态实例,并通过函数bsp::memory::mmu::virt_mem_layout()
使其可访问。aarch64
部分:src/_arch/aarch64/memory/mmu.rs
及其子模块包含实际的MMU
驱动程序。它使用64 KiB
粒度获取BSP
的高级KernelVirtualLayout
并进行映射。
在这个文件中提供的描述符类型是构建块,用于描述不同内存区域的属性。
例如,R/W
(读/写)、no-execute
(不执行)、cached/uncached
(缓存/非缓存)等等。
这些描述符与硬件MMU
的实际描述符无关。不同的BSP
可以使用这些类型来生成内核虚拟内存布局的高级描述。
真实硬件的实际MMU
驱动程序将使用这些类型作为输入。
通过这种方式,我们在BSP
和_arch
代码之间实现了清晰的抽象,这样可以在不需要调整另一个的情况下进行交换。
这个文件包含了一个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/aarch64
的MMU
代码使用,用于请求虚拟地址和转换的属性,该转换提供物理输出地址
(返回元组中的usize
)。该函数扫描包含查询地址的描述符,并返回第一个匹配的条目的相应结果。
如果找不到条目,则返回普通可缓存DRAM的默认属性和输入地址,从而告诉MMU
代码请求的地址应该是identity mapped
。
由于这种默认行为,不需要定义普通可缓存DRAM区域。
这些模块包含了AArch64
的MMU
驱动程序。粒度在这里被硬编码为(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::SIZE
是512 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
。最后一步还启用了数据和指令的缓存。
我们需要将code
段对齐到64 KiB
,这样它就不会与下一个需要读/写属性而不是读/执行属性的部分重叠。
. = ALIGN(PAGE_SIZE);
__code_end_exclusive = .;
这会增加二进制文件的大小,但考虑到与传统的4 KiB
粒度相比,它显著减少了静态分页条目的数量,这是一个小小的代价。
出于教育目的,定义了一个布局,允许通过两个不同的虚拟地址访问UART
- 由于我们对整个
Device MMIO
区域进行了身份映射,所以在MMU
打开后,可以通过断言其物理基地址 (0x3F20_1000
或0xFA20_1000
,取决于使用的是哪个RPi版本)来访问它。 - 此外,它还映射到第一个
512 MiB
中的最后一个64 KiB
槽位,使其可以通过基地址0x1FFF_1000
访问。
以下块图可视化了第二个映射的底层转换。
初始化代码再次是展示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
,不同的types
和constants
来提供类型安全的寄存器操作。
最后,此代码根据数据表将寄存器的前四个字节设置为特定值。查看生成的代码, 我们可以看到,尽管有所有的类型安全和抽象,但它可以归结为两条汇编指令:
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
请检查英文版本,这是最新的。