实验八 内存管理
分页内存管理是内存管理的基本方法之一。本实验的目的在于全面理解分页式内存管理的基本方法以及访问页表,完成地址转换等的方法。
Armv8的地址转换
ARM Cortex-A Series Programmer’s Guide for ARMv8-A 中提到:For EL0 and EL1, there are two translation tables. TTBR0_EL1 provides translations for the bottom of Virtual Address space, which is typically application space and TTBR1_EL1 covers the top of Virtual Address space, typically kernel space. This split means that the OS mappings do not have to be replicated in the translation tables of each task. 即TTBR0指向整个虚拟空间下半部分通常用于应用程序的空间,TTBR1指向虚拟空间的上半部分通常用于内核的空间。其中TTBR0除了在EL1中存在外,也在EL2 and EL3中存在,但TTBR1只在EL1中存在 [1]。
TTBR0_ELn 和 TTBR1_ELn 是页表基地址寄存器 [2],地址转换的过程如下所示 [3]。
In a simple address translation involving only one level of look-up. It assumes we are using a 64KB granule with a 42-bit Virtual Address. The MMU translates a Virtual Address as follows:
If VA[63:42] = 1 then TTBR1 is used for the base address for the first page table. When VA[63:42] = 0, TTBR0 is used for the base address for the first page table.
The page table contains 8192 64-bit page table entries, and is indexed using VA[41:29]. The MMU reads the pertinent level 2 page table entry from the table.
The MMU checks the page table entry for validity and whether or not the requested memory access is allowed. Assuming it is valid, the memory access is allowed.
In the above Figure, the page table entry refers to a 512MB page (it is a block descriptor).
Bits [47:29] are taken from this page table entry and form bits [47:29] of the Physical Address.
Because we have a 512MB page, bits [28:0] of the VA are taken to form PA[28:0]. See Effect of granule sizes on translation tables
The full PA[47:0] is returned, along with additional information from the page table entry.
In practice, such a simple translation process severely limits how finely you can divide up your address space. Instead of using only this first-level translation table, a first-level table entry can also point to a second-level page table.
使用identity mapping映射
虚拟地址的转换很容易出错也很难调试,所以我们从最简单方式开始,即identity mapping,所谓identity mapping就是将虚拟地址映射到相同的物理地址。
在start.s中启用identity mapping,
1.globl _start
2.extern LD_STACK_PTR
3
4.section ".text.boot"
5_start:
6
7 ldr x30, =LD_STACK_PTR
8 mov sp, x30
9
10 // Initialize exceptions
11 ldr x0, =exception_vector_table
12 msr vbar_el1, x0
13 isb
14_setup_mmu:
15 // Initialize translation table control registers
16 ldr x0, =TCR_EL1_VALUE
17 msr tcr_el1, x0
18 ldr x0, =MAIR_EL1_VALUE
19 msr mair_el1, x0
20_setup_pagetable:
21 // 因为采用的36位地址空间,所以是level1 page table
22 ldr x1, =LD_TTBR0_BASE
23 msr ttbr0_el1, x1 //页表基地址TTBR0
24 ldr x2, =LD_TTBR1_BASE
25 msr ttbr1_el1, x2 //页表基地址TTBR1
26
27 // entries of level1 page table
28 // 虚拟地址空间的下半部分做identity mapping
29 // 第一项 虚拟地址0 - 1g,根据virt的定义为flash和外设,参见virt.c
30 ldr x3, =0x0 //
31 lsr x4, x3, #30 // divide kernel start address by 1G
32 lsl x5, x4, #30 // multiply by 1G, and keep table index in x0
33 ldr x6, =PERIPHERALS_ATTR
34 orr x5, x5, x6 // add flags
35 str x5, [x1], #8 //
36 // 第二项 虚拟地址1g - 2g,内存部分
37 ldr x3, =_start //
38 lsr x4, x3, #30 // divide kernel start address by 1G
39 lsl x5, x4, #30 // multiply by 1G, and keep table index in x0
40 ldr x6, =IDENTITY_MAP_ATTR
41 orr x5, x5, x6 // add flags
42 str x5, [x1], #8
43
44_enable_mmu:
45 // Enable the MMU.
46 mrs x0, sctlr_el1
47 orr x0, x0, #0x1
48 msr sctlr_el1, x0
49 dsb sy //Programmer’s Guide for ARMv8-A chapter13.2 Barriers
50 isb
51
52_start_main:
53 bl not_main
54
55
56
57.equ PSCI_SYSTEM_OFF, 0x84000008
58.globl system_off
59system_off:
60 ldr x0, =PSCI_SYSTEM_OFF
61 hvc #0
62
63
64.equ TCR_EL1_VALUE, 0x1B55C351C // ---------------------------------------------
65// IPS | b001 << 32 | 36bits address space - 64GB
66// TG1 | b10 << 30 | 4KB granule size for TTBR1_EL1
67// SH1 | b11 << 28 | 页表所在memory: Inner shareable
68// ORGN1 | b01 << 26 | 页表所在memory: Normal, Outer Wr.Back Rd.alloc Wr.alloc Cacheble
69// IRGN1 | b01 << 24 | 页表所在memory: Normal, Inner Wr.Back Rd.alloc Wr.alloc Cacheble
70// EPD | b0 << 23 | Perform translation table walk using TTBR1_EL1
71// A1 | b1 << 22 | TTBR1_EL1.ASID defined the ASID
72// T1SZ | b011100 << 16 | Memory region 2^(64-28) -> 0xffffffexxxxxxxxx
73// TG0 | b00 << 14 | 4KB granule size
74// SH0 | b11 << 12 | 页表所在memory: Inner Sharebale
75// ORGN0 | b01 << 10 | 页表所在memory: Normal, Outer Wr.Back Rd.alloc Wr.alloc Cacheble
76// IRGN0 | b01 << 8 | 页表所在memory: Normal, Inner Wr.Back Rd.alloc Wr.alloc Cacheble
77// EPD0 | b0 << 7 | Perform translation table walk using TTBR0_EL1
78// 0 | b0 << 6 | Zero field (reserve)
79// T0SZ | b011100 << 0 | Memory region 2^(64-28)
80
81.equ MAIR_EL1_VALUE, 0xFF440C0400// ---------------------------------------------
82// INDX MAIR
83// DEVICE_nGnRnE b000(0) b00000000
84// DEVICE_nGnRE b001(1) b00000100
85// DEVICE_GRE b010(2) b00001100
86// NORMAL_NC b011(3) b01000100
87// NORMAL b100(4) b11111111
88
89.equ PERIPHERALS_ATTR, 0x60000000000601 // -------------------------------------
90// UXN | b1 << 54 | Unprivileged eXecute Never
91// PXN | b1 << 53 | Privileged eXecute Never
92// AF | b1 << 10 | Access Flag
93// SH | b10 << 8 | Outer shareable
94// AP | b01 << 6 | R/W, EL0 access denied
95// NS | b0 << 5 | Security bit (EL3 and Secure EL1 only)
96// INDX | b000 << 2 | Attribute index in MAIR_ELn,参见MAIR_EL1_VALUE
97// ENTRY | b01 << 0 | Block entry
98
99.equ IDENTITY_MAP_ATTR, 0x40000000000711 // ------------------------------------
100// UXN | b1 << 54 | Unprivileged eXecute Never
101// PXN | b0 << 53 | Privileged eXecute Never
102// AF | b1 << 10 | Access Flag
103// SH | b11 << 8 | Inner shareable
104// AP | b00 << 6 | R/W, EL0 access denied
105// NS | b0 << 5 | Security bit (EL3 and Secure EL1 only)
106// INDX | b100 << 2 | Attribute index in MAIR_ELn,参见MAIR_EL1_VALUE
107// ENTRY | b01 << 0 | Block entry
备注
ldr x0, =TCR_EL1_VALUE
用于将一个立即数载入x0,但arm是rsic的机器,不可能支持所有范围内的立即数,实际上这是一条伪指令。
mair_el1
是内存属性间接寄存器,他的作用是预先定义好属性,然后通过索引来访问这些预定义的属性。
_setup_pagetable (line 20)
构建页表。简单起见,我们采用1G的块,36位的虚拟地址空间。首先设置好寄存器 ttbr0_el1 和 ttbr1_el1 ,如前所述,为页表基地址寄存器。在AArch64中,每个表项占8个字节 [4] ,line 30 - 32, line 37 - 39 将偏移部分置0仅保留索引, line 33 - 34, line 40 - 41 将属性附加到索引上形成完整的表项值。最后存入 [x1] ,即 ttbr0_el1 所指向的位置,同时x1加8字节(因为每个表项大小为8字节)。
line 37 表示 虚拟地址空间1g - 2g部分(因为是第2项,每项1g)映射到 _start
所在物理内存的1g空间处。
_enable_mmu
启用MMU。
汇编语法可以参考 GNU ARM Assembler Quick Reference [5] 和 Arm Architecture Reference Manual Armv8 (Chapter C3 A64 Instruction Set Overview) [6]
关于rust内联汇编的相关知识可以参考 Inline assembly [7] 和 内联汇编中Clobbers的用途到底是什么 [8]
在上面的代码中,我们使用了LD_TTBR0_BASE和LD_TTBR1_BASE两个符号,这需要在链接脚本中定义。
LD_TTBR0_BASE = .; /*页表*/
. = . + 0x1000;
LD_TTBR1_BASE = .;
. = . + 0x1000;
再次运行内核,检查其是否能正常工作。
偏移映射与页面共享
自行实验:将虚拟地址2g - 3g处映射到物理地址 0 - 1g,从而对 0x89000000 地址的写入将通过 pl011 串口输出,因为此时 0x89000000 映射到了物理地址 pl011@9000000 的地方。
非identity mapping映射
identity mapping 毕竟过于简单,在实际的系统上并不实用,但也不是完全没有用途。如arm规定在启用地址映射时最好采用identity mapping。参见 这里 (D5.2.3 Controlling address translation stages)。
If the PA of the software that enables or disables a particular stage of address translation differs from its VA, speculative instruction fetching can cause complications. Arm strongly recommends that the PA and VA of any software that enables or disables a stage of address translation are identical if that stage of translation controls translations that apply to the software currently being executed.
下面我们将内核放置到整个虚拟空间上半部分,正如大部分的操作系统的选择一样。
在start.s中设置ttbr1_el1所指向的页表。
// 虚拟地址空间的上半部分处理
// 第一项 虚拟地址空间上半部分的首个1g映射到物理地址空间的0~1G,根据virt的定义为flash和外设,参见virt.c
ldr x3, =0x0 //
lsr x4, x3, #30 // divide kernel start address by 1G
lsl x5, x4, #30 // multiply by 1G, and keep table index in x0
ldr x6, =PERIPHERALS_ATTR
orr x5, x5, x6 // add flags
str x5, [x2], #8 //
// 第二项, 映射到内存(首先简单地实现块级映射,没有问题了再进一步将其映射到页表)
// ldr x3, =0x40010000 //
// lsr x4, x3, #30 // divide kernel start address by 1G
// lsl x5, x4, #30 // multiply by 1G, and keep table index in x0
// ldr x6, =KERNEL_ATTR
// orr x5, x5, x6 // add flags
// str x5, [x2], #8 //
// 第二项,映射到页表(可以先实验上面块级映射,如果没问题再进一步将其映射到页表)
ldr x3, =LD_TTBR1_L2TBL
ldr x4, =0xFFFFF000
and x5, x3, x4 // NSTable=0 APTable=0 XNTable=0 PXNTable=0.
orr x5, x5, 0x3 //Valid page table entry
str x5, [x2], #8 //TTBR1
// entries of level2 page table,内核总共16M,参见aarch64-qemu.ld文件
ldr x3, =LD_TTBR1_L2TBL
mov x4, #8 // 8个二级页表项
ldr x5, =KERNEL_ATTR //内核属性,可读写,可执行
ldr x7, =0x1
add x5, x5, x7, lsl #30 //物理地址在1G开始的位置
ldr x6, =0x00200000 // 每次增加2M
_build_2nd_pgtbl:
str x5, [x3], #8 // 填入内容到页表项
add x5, x5, x6 // 下一项的地址增加2M
subs x4, x4, #1 // 项数减少1
bne _build_2nd_pgtbl
.equ KERNEL_ATTR, 0x40000000000711 // -------------------------------------
// UXN | b1 << 54 | Unprivileged eXecute Never
// PXN | b0 << 53 | Privileged eXecute Never
// AF | b1 << 10 | Access Flag
// SH | b11 << 8 | Inner shareable
// AP | b00 << 6 | R/W, EL0 access denied
// NS | b0 << 5 | Security bit (EL3 and Secure EL1 only)
// INDX | b100 << 2 | Attribute index in MAIR_ELn,参见MAIR_EL1_VALUE
// ENTRY | b01 << 0 | Block entry
重要的是我们需要通过链接脚本来重新安排程序的组织,这需要一些技巧。
__KERN_VMA_BASE = 0xfffffff000000000;
__PHY_DRAM_START_ADDR = 0x40000000;
__PHY_START_LOAD_ADDR = 0x40010000;
ENTRY(__PHY_START_LOAD_ADDR)
SECTIONS
{
. = __KERN_VMA_BASE + __PHY_START_LOAD_ADDR;
.text.boot : AT(__PHY_START_LOAD_ADDR) { KEEP(*(.text.boot)) } /*ADDR(.text.boot) - __KERN_VMA_BASE*/
.text : /*AT(ADDR(.text) - __KERN_VMA_BASE)*/ {
*(.text*)
}
. = ALIGN(0x1000);
LD_RODATA_BASE = .;
.rodata : /*AT(ADDR(.rodata) - __KERN_VMA_BASE)*/ { *(.rodata*) }
. = ALIGN(0x1000);
LD_DATA_BASE = .;
.data : /*AT(ADDR(.data) - __KERN_VMA_BASE)*/ { *(.data*) }
. = ALIGN(0x1000);
LD_BSS_BASE = .;
.bss : /*AT(ADDR(.bss) - __KERN_VMA_BASE)*/ { *(.bss*)
. = ALIGN(4096); /* align to page size */
. += (4096 * 100); /* 栈的大小 */
stack_top = .;
LD_STACK_PTR = .;
}
. = ALIGN(0x1000);
.pt : /*AT(ADDR(.pt) - __KERN_VMA_BASE)*/ /* 页表 */
{
. = ALIGN(4096); /* align to page size */
LD_TTBR0_BASE = . - __KERN_VMA_BASE; /*页表*/
. = . + 0x1000;
LD_TTBR1_BASE = . - __KERN_VMA_BASE;
. = . + 0x1000;
LD_TTBR0_L2TBL = . - __KERN_VMA_BASE; /*二级页表*/
. = . + 0x1000;
LD_TTBR1_L2TBL = . - __KERN_VMA_BASE;
. = . + 0x1000;
}
. = . + 0x1000;
LD_KERNEL_END = . - __KERN_VMA_BASE;
}
最后我们需要将内核中的地址修改成虚拟地址,如:
// interrupts.rs
//const GICD_BASE: u64 = 0x08000000;
//const GICC_BASE: u64 = 0x08010000;
// ==>
const GICD_BASE: u64 = 0xfffffff000000000 + 0x08000000;
const GICC_BASE: u64 = 0xfffffff000000000 + 0x08010000;
// pl011.rs
//pub const PL011REGS: *mut PL011Regs = (0x0900_0000) as *mut PL011Regs;
// ==>
pub const PL011REGS: *mut PL011Regs = (0xfffffff000000000u64 + 0x0900_0000) as *mut PL011Regs;
// pl061.rs
//pub const PL061REGS: *mut PL061Regs = (0x903_0000) as *mut PL061Regs;
// ==>
pub const PL061REGS: *mut PL061Regs = (0xfffffff000000000u64 + 0x903_0000) as *mut PL061Regs;
再次运行内核,检查其是否能正常工作。