xv6 源码精读(二)开启MMU、一致性映射页表

目录

一、armv8页表

1)页表类型

2)页表描述符(页表项)

3)页表翻译过程

[二、页表基地址寄存器(TTBR------Translation Table Base Register)](#二、页表基地址寄存器(TTBR——Translation Table Base Register))

三、一致性页表、使能MMU

3.1、一致性页表

[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之前,会准备好两套页表:

  1. 一致性页表: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内核启动阶段相关代码。

相关推荐
雪碧聊技术3 小时前
Linux命令过关挑战
linux·运维·数据库
liulilittle3 小时前
在 Android Shell 终端上直接运行 OPENPPP2 网关路由配置指南
android·linux·开发语言·网络·c++·编程语言·通信
GoodTimeGGB3 小时前
轻量服务器Lighthouse + 1Panel + Halo,三步打造你的专属网站
服务器·1panel·lighthouse·建站·halo
ayaya_mana4 小时前
CentOS 7 安装指定内核版本与切换内核版本
linux·运维·centos
uncle_ll4 小时前
Sherpa 语音识别工具链安装指南(Linux CPU 版)
linux·nlp·语音识别·tts·sherpa
你什么冠军?4 小时前
云计算与服务器概述
运维·服务器·云计算
UNbuff_04 小时前
Linux top 命令使用说明
linux·运维·服务器
---学无止境---5 小时前
Linux中dcache和inode缓存回收函数的实现
linux
Мартин.5 小时前
[Meachines] [Hard] Pollution MyBB+Redis_session+PHP-Filter+PHP-FPM+prototype
linux