目录
[二、页表基地址寄存器(TTBR------Translation Table Base Register)](#二、页表基地址寄存器(TTBR——Translation Table Base Register))
[3.2、TCR_EL1(Translation Control Register)](#3.2、TCR_EL1(Translation Control Register))
[3.3、MAIR_EL1(Memory Attribute Indirection Register)](#3.3、MAIR_EL1(Memory Attribute Indirection Register))
[3.4、SCTLR_EL1(System Control Register)](#3.4、SCTLR_EL1(System Control Register))
[四、xv6 启动阶段代码](#四、xv6 启动阶段代码)
引言:
本文分2部分,前半部分主要介绍armv8中MMU相关的一些背景知识,后半部分 分析xv6 aarch64中主核启动过程中enable MMU的实现细节。
一、armv8页表
1)页表类型
armv8中,支持2级页表、3级页表、4级页表(level0 ~ level3),以4级页表为例,分别有以下几个类型的页表构成:
- PGD:page global directory (VA's bit[47] ~ bit[39]),页全局目录;
- PUD:page upper directory(VA's bit[38] ~ bit[30]),页上级目录;
- PMD:page middle directory(VA's bit[29] ~ bit[21]),页中间目录;
- PTE:page table entry(VA's bit[29] ~ bit[21]),页中间目录;

以上4种类型的页表,大小都为4KB,每个页表中包含512个页表项,记录下一级页表地址、物理页地址。
可能有些小伙伴会好奇:为什么每个页表由512个页表项组成,而不是1024个或其他个数的页表项组成?
其实这个问题也很好理解,一个页的大小为4096字节,每个页表项都是一个个指针,指针的大小为8字节,所以页表能够容纳的页表项个数是512个(4096 ÷ 8 = 512)。
若不考虑block mapping的话,PGD、PUD、PMD 这3类页表中记录的内容,都是下一级页表的物理基地址、以及一些页表属性,PTE页表中的页表项记录的是真正物理内存的4K对齐的物理地址、以及一些页表属性;
2)页表描述符(页表项)
页表描述符(Translation table descriptor)分为两大类型:
(1)**Table descriptor format:**此类描述符指向下一级页表的物理地址,且此类页表描述符只有Upper attributes;
(2)**Block/Page descriptor format:**此类描述符指向VA对应PA所在物理页地址,此类描述符有Upper attribute、Lower attribute;
|----------------------|------------------------------------------|
| Table descriptor | Upper attribute;只存在于 level 0、1、2 级页表 |
| Block descriptor | Upper/Lower attribute;只存在于 level 1、2 级页表 |
| Page descriptor | Upper/Lower attribute;只存在于 level 3 级页表 |
在ARMv8的页表描述符中,不仅包含目标物理地址,还包含一大块用于描述该内存区域属性的字段,这些属性控制着内存区域的访问行为、缓存策略等。
为了结构清晰,这些属性被分成两组:
**- Upper attribute:**位于描述符的[63:50]位;
**- Lower attribute:**位于描述符的[16:2]位;

2.1)Translation descriptor format(bit[0]、bit[1])
页表中每个描述符的类型、是否有效,由页表描述符的 bit[0]、bit[1] 决定:

2.2)UXN or XN(bit[54])
系统仅有一级特权时,代表XN(Execute-never);系统支持EL0/EL1两级特权时,代表UXN(Unprivileged execute-never);置1代表用户态不可执行;内核加载ELF时,为用户态程序的Data段内存区域设置该字段时,可以控制用户态数据段不可执行;
2.3)nG(bit[11])
若nG为0,代表这块内存区域是全局的,任何进程都可以访问;
若nG为1,代表内存区域是本地的,只能由当前进程访问(ASID);
页表项属性中的nG字段(non-global)用来设置对应TLB的类型。TLB的表项分成全局的、进程特有的。当设置nG为1时,表示这个页面对应的TLB表项是进程特有的;当为0时,表示这个TLB表项是全局的。
2.4)AF(bit[10])
Access Flag,当第一次访问页面时,硬件会自动设置这个访问位;Descriptors with AF set to zero can never be cached in a TLB;
当DBM为1,stage1的AP2为1时,可以等同于writeable。
2.5)SH(bit[9:8])
Shareability attribute规定了内存的Shareability,即:是否是Shareable的,以及是Inner Shareable 还是 Outer Shareable。该字段的配置,会影响硬件 MESI 协议起作用的范围。
对于同一Cluster内部,是Inner Shareable;对于两个Cluster之间,是Outer Shareable的;若这个字段配置错误,会导致缓存一致性出问题,比如对于Outer Shareable的物理内存相应页表想没有配置为Outer Shareable,那么对该物理内存的修改,外设的cache中的内存可能不会得到更新,造成缓存不一致的问题;

2.6)AP(bit[7:6])
Access permission,该字段控制CPU对页面的访问权限,具体如下所示:

2.7)NS(bit[5])
Non-secure,该字段用于标识物理内存是属于Non-Secure(REE侧访问)的还是Secure的(TEE侧访问);
2.8)AttrIndex[2:0](bit[4:2])
该字段是作为MAIR中的索引,用于查看具体的内存属性,MAIR_ELn寄存器分为8段,每一段用于描述不同的内存属性。AttrIndex字段可以控制对应内存区域是否使能cache等行为!
Memory type(normal or device)信息不是直接填写在页表项中的,是记录在页表项中AttrIndex指向的MAIR_ELn寄存器中的。
**注:**memory type信息不是直接编码记录在table entry中,但是TLB entry中会记录memory type信息。因此MAIR_ELn寄存器内容的修改,需要执行 ISB instruction barrier 和 TLB invalidate operation这两个操作后,才能确保可见!
3)页表翻译过程
当CPU要访问一个虚拟地址VA上的内容、且该内容没有缓存在cache时,会进行地址翻译获取VA对应的PA,最终真正去访问物理地址上的内容。获取物理地址PA的过程就是由MMU硬件根据OS设置好的页表去自行翻译查找的。
整个页表翻译的过程如下(以4级页表为例):

**step1:**根据VA的最高位,选取对应的页表寄存器,获取pgd页表所在地址;
**step2 ~ step8:**根据VA中对应位,获取在页表中的偏移位置,接着根据页表中记录的页表项内容获取下一级页表的基地址;
**step9 ~ step10:**获取真是物理页基地址(4K对齐),并根据VA中的低12位与物理地址拼接获得最终VA对应的PA地址
二、页表基地址寄存器(TTBR------Translation Table Base Register)
armv8中,MMU负责 "VA ---> PA" 的转换,在进行地址翻译的时候,首先需要获取PGD页表的地址,然后开始地址翻译。
pgd页表的地址,记录在页表基地址寄存器中,由内核设置好每一级页表的内容后,将pgd的物理地址设置到页表基地址寄存器中。
EL1中有2个页表基地址寄存器:ttbr0_el1、ttbr1_el1,分别指向 "用户态"、"内核态"的PGD页表。
更准确的说法是:地址翻译时,虚拟地址的最高位用于标识地址的属性,其中最高位用于区分选取 ttbr0_el1、还是选取 ttbr1_el1作为页表基地址(仅适用于EL0、EL1):
- 虚拟地址最高位为0: 选取ttbr0_el1中记录的pgd进行翻译,表示该地址属于用户空间;
- 虚拟地址最高位为1: 选取ttbr1_el1中记录的pgd进行翻译,表示该地址属于内核空间;

地址空间布局:

三、一致性页表、使能MMU
3.1、一致性页表
当SoC上电后,前期BL1、BL2、BL31等初始化完毕跳转到uboot,并由uboot跳转到内核的那一刻,MMU通常情况下是关闭的。内核入口的汇编代码,一般是直接在物理内存上运行的,不需要MMU进行地址转换。
为了运行效率(cache的使能前提,需要MMU enable)等考虑,在一定阶段,需要开启MMU,因此在开启MMU之前,需要先准备好页表。
在armv8架构上,一般内核在开启MMU之前,会准备好两套页表:
- 一致性页表:PA == VA(VA 最高位为0)
2)正常映射内核虚拟地址的页表:VA == 属于内核区域的高地址虚拟地址区域(VA 最高位为1)

引入一致性页表的原因:
在开启MMU的瞬间,由于CPU流水线的原因,可能先前预加载的指令还未执行,这些指令内存地址仍是PA,在使能MMU的那一瞬间,这些地址会需要经过MMU翻译,但若页表是以内核链接地址(高16位全1)建立的,那么这些PA在MMU翻译时就会报错!
为了解决上述问题,在开启MMU之前,会额外建立一份**"VA==PA"** 的页表,并且将页表基地址存放到 TTBR0_EL1中,这样在开启MMU的瞬间,就算流水线中执行/访问了一些物理地址上的指令或内存,也同样能够正常进行MMU翻译动作!(注: 由于PA的高16位为0,因此将VA设置为PA后,在MMU翻译时,会以TTBR0_EL1寄存器中的页表进行翻译!)
在使能MMU之前,除了配置页表寄存器(TTBR)之外,还需要配置其他几个寄存器:TCR、MAIR;用于设置页面大小、寻址大小、页表级别、内存属性等等。
3.2、TCR_EL1(Translation Control Register) 
- 页面粒度(TG0)
TG0字段控制页面大小,常见设置为4KB;

- 地址空间大小(T0SZ、T1SZ)
决定 用户态、内核态的虚拟地址范围(VA),例如T0SZ=25表示用户态39位VA;


- 内存属性(ORGN0、IRGN0)


- 页表级别
TCR_EL1支持xv6的3级页表,起始级别由T0SZ和TG0决定(例如,T0SZ=25,TG0=4KB,3级页表)。
- ASID相关配置
AS字段决定了TTBR寄存器中,高8位、还是高16位存放ASID的信息;A1字段决定ASID字段保存在TTBR0_EL1还是TTBR1_EL1中;


3.3、MAIR_EL1(Memory Attribute Indirection Register)
mair_el1 寄存器,用于定义虚拟地址空间中不同区域的内存属性(memory type、Cacheability)。例如某个区域可以被标记为可读写、可执行、缓存等属性。ARM提供了页表项AttrIdx字段、MAIR_ELx来支持内存类型、内存Cacheability属性的配置。
只要"MAIR_ELx配置的内存属性所处下标" 与 "页表项AttrIdx字段所代表的下标" 是一致的就行!


3.4、SCTLR_EL1(System Control Register)
SCTLR_EL1是一个系统寄存器,控制系统的种种行为,MMU、D-Cache、I-Cache的使能就是通过该寄存器中的指定字段来配置的;
- MMU使能

- I-Cache使能

- D-Cache使能

四、xv6 启动阶段代码
1)内核入口 _entry
内核入口定义在 kernel.ld 链接脚本中,通过readelf查看编译出来的 kernel elf 文件可见:内核的编译地址(即:虚拟地址)是0xffffff8040000000,这个也是通过链接脚本中指定的。


2)内核启动
当CPU跳转到内核入口后,会根据 mpidr_el1 寄存器判断当前CPU是主核还是从核,跳转到不同代码label处执行,主核会跳转到entry label处,并将BSS段清0;
cpp
_entry:
mrs x1, mpidr_el1
and x1, x1, #0x3
cbz x1, entry // primary
b entryothers // secondary
entry:
// clear .bss
adrp x1, bss_start
ldr w2, =bss_size
1:
cbz w2, 2f
str xzr, [x1], #8
sub w2, w2, #1
b 1b
2:
3)设置一致性页表、内核页表
xv6 开启MMU之前,会通过汇编设置两套页表:
- 一致性页表(l1entrypgt)
VA映射范围:[ 0x40000000, PA(end) ]
PA映射范围:[ 0x40000000, PA(end) ]
映射方式:block块映射(2M)、二级页表
- 内核页表(l1kpgt)
VA映射范围:[ 0xffffff8040000000, PA(end) ]
PA映射范围:[ 0x40000000, PA(end) ]
映射方式:block块映射(2M)、二级页表
cpp
// set up entry pagetable
//
// Phase 1.
// map the kernel code identically.
// map [0x40000000,PA(end)) to [0x40000000,PA(end))
// memory type is normal
//
// Phase 2.
// map the kernel code.
// map [0xffffff8040000000,VA(end)) to [0x40000000,PA(end))
// memory type is normal.
// Phase 1
// map [0x40000000,PA(end)) to [0x40000000,PA(end))
adrp x0, l2entrypgt
mov x1, #0x40000000
ldr x2, =V2P_WO(end)-1
lsr x3, x1, #PXSHIFT(2)
and x3, x3, #PXMASK // PX(2, x1)
lsr x4, x2, #PXSHIFT(2)
and x4, x4, #PXMASK // PX(2, x2)
mov x5, #(PTE_AF | PTE_INDX(AI_NORMAL_NC_IDX) | PTE_VALID) // entry attr
orr x6, x1, x5 // block entry
l2epgt_loop:
str x6, [x0, x3, lsl #3] // l2entrypgt[l2idx] = block entry
add x3, x3, #1 // next index
add x6, x6, #0x200000 // next block, block size is 2MB
cmp x3, x4
b.ls l2epgt_loop // if start va idx <= end va idx
adrp x0, l1entrypgt
lsr x3, x1, #PXSHIFT(1)
and x3, x3, #PXMASK // start va level1 index
mov x4, #(PTE_TABLE | PTE_VALID) // entry attr
adrp x5, l2entrypgt
orr x6, x4, x5 // table entry
str x6, [x0, x3, lsl #3] // l1entrypgt[l1idx] = table entry
// Phase 2
// map [0xffffff8040000000,VA(end)) to [0x40000000,PA(end))
adrp x0, l2kpgt
mov x1, #0x40000000 // start pa
ldr x2, =V2P_WO(end)-1 // end pa
mov x3, #KERNBASE
add x4, x1, x3 // start va
add x5, x2, x3 // end va
lsr x6, x4, #PXSHIFT(2)
and x6, x6, #PXMASK // x6 = PX(2,x4)
lsr x7, x5, #PXSHIFT(2)
and x7, x7, #PXMASK // x7 = PX(2,x5)
mov x8, #(PTE_AF | PTE_INDX(AI_NORMAL_NC_IDX) | PTE_VALID) // entry attr
orr x9, x1, x8 // block entry
l2kpgt_loop:
str x9, [x0, x6, lsl #3] // l2entrypgt[l2idx] = block entry
add x6, x6, #1 // next index
add x9, x9, #0x200000 // next block, block size is 2MB
cmp x6, x7
b.ls l2kpgt_loop // if start va idx <= end va idx
adrp x0, l1kpgt
lsr x5, x4, #PXSHIFT(1)
and x5, x5, #PXMASK // x5 = PX(1,x4)
mov x6, #(PTE_TABLE | PTE_VALID) // entry attr
adrp x7, l2kpgt
orr x8, x6, x7 // table entry
str x8, [x0, x5, lsl #3] // l1kpgt[l1idx] = table entry
上述两套页表所在的内存,是内核data段的一块全局数组:
cpp
__attribute__((aligned(PGSIZE))) pte_t l1entrypgt[512];
__attribute__((aligned(PGSIZE))) pte_t l2entrypgt[512];
__attribute__((aligned(PGSIZE))) pte_t l1kpgt[512];
__attribute__((aligned(PGSIZE))) pte_t l2kpgt[512];
4)开启MMU,并跳转到内核编译地址(虚拟地址)
在准备好2套页表后,分别将它们设置到 TTBR0_EL1、TTBR1_EL1中,并且配置好TCR_EL1、MAIR_EL1寄存器内容,采用39位虚拟地址大小、4K页面粒度;
随后,通过设置sctlr_el1的bit[0],将其置1,来使能MMU。
值得注意的是: ldr x1, =_start 这条汇编语句,其作用是获取_start的链接地址(即:内核的虚拟地址),用于开启MMU后,通过 br x1 跳转到内核虚拟地址继续运行!这也是armv8中,内核开启MMU后,地址跳转的常用方式。
cpp
entryothers: // secondary CPU starts here
// load pagetable
adrp x0, l1entrypgt
adrp x1, l1kpgt
msr ttbr0_el1, x0 <<<<< 设置页表寄存器
msr ttbr1_el1, x1
// setup tcr
ldr x0, =(TCR_T0SZ(25)|TCR_T1SZ(25)|TCR_TG0(0)|TCR_TG1(2)|TCR_IPS(0))
msr tcr_el1, x0
// setup mair
ldr x1, =((MT_DEVICE_nGnRnE<<(8*AI_DEVICE_nGnRnE_IDX)) | (MT_NORMAL_NC<<(8*AI_NORMAL_NC_IDX)))
msr mair_el1, x1
isb
ldr x1, =_start // x1 = VA(_start)
// enable paging
mrs x0, sctlr_el1
orr x0, x0, #1
msr sctlr_el1, x0 <<<<< 开启MMU <<<<<
br x1 // jump to higher address (0xffffff8000000000~)
五、总结
本文借助xv6 aarch64的代码,介绍了 armv8 中内核启动阶段的MMU使能方式,以及页表、页表寄存器、一致性页表等相关基础概念。更多细节内容,感兴趣的小伙伴可参考官方文档、及linux内核启动阶段相关代码。