ARM64 MMU和系统上电时Linux页表映射过程

参考文献:

  1. 窝窝博客:内存初始化代码分析(一):identity mapping和kernel image mapping

  2. 《奔跑吧Linux内核》

  3. 《ARM Cortex-A Series Programmer’s Guide for ARMv8-A》

1、MMU

        CPU通过地址来访问内存中的单元,地址有虚拟地址和物理地址之分,如果CPU没有MMU(Memory Management Unit,内存管理单元),或者有MMU但没有启用,CPU核在取指令或访问内存时发出的地址将直接传到CPU芯片的外部地址引脚上,直接被内存芯片(以下称为物理内存,以便与虚拟内存区分)接收,这称为物理地址(Physical Address,以下简称PA)。

        如果CPU启用了MMU,CPU核发出的地址将被MMU截获,从CPU到MMU的地址称为虚拟地址(Virtual Address,以下简称VA),而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将虚拟地址映射成物理地址,如下图所示

        MMU工作的过程就是查询页表的过程,但是页表是放置在内存当中的,如果每次虚拟地址到物理地址的转换都需要到内存中去查询的话,效率就很低,所以在MMU当中有专门的一块buffer,存放最近用到的页表,这块区域也就是页表的cache,被称为Translation Lookaside Buffer (TLB)。

2 页表映射过程

2.1 内核虚拟地址与用户空间虚拟地址

        在ARM64中,地址线由32bit变为64bit,但是64bit并不是全用到了,最大支持48位物理寻址,最大可寻找256T的物理地址空间,对于目前的应用来讲完全足够了。

在Linux 4.x中,

虚拟地址的最大宽度可配置,最大为48bit,还可以有36bit,39bit,42bit,47bit

[arch/arm64/Kconfig]

config ARM64_VA_BITS
        int
        default 36 if ARM64_VA_BITS_36
        default 39 if ARM64_VA_BITS_39
        default 42 if ARM64_VA_BITS_42
        default 47 if ARM64_VA_BITS_47
        default 48 if ARM64_VA_BITS_48

对于地址线宽度为48bit,

用户空间范围:0x0000-0000-0000-0000-0000----0x0000-ffff-ffff-ffff

内核空间范围:0xffff-0000-0000-0000----0xffff-ffff-ffff-ffff

对于地址线宽度39bit

用户空间范围:0x0000-0000-0000-0000----0x0000-007f-ffff-ffff

内核空间范围:0xffff-ff800-0000-0000---0xffff-ffff-ffff-ffff

        Linux内存空间布局与地址映射的粒度和地址映射的层级有关。基于ARMv8-A架构的处理器支持的页面大小可以是4KB、16KB、或者64KB。映射的层级可以是2/3/4级。下面是映射的层级和所对应的页大小和虚拟地址宽度。

[arch/arm64/Kconfig]

config PGTABLE_LEVELS
        int
        default 2 if ARM64_16K_PAGES && ARM64_VA_BITS_36
        default 2 if ARM64_64K_PAGES && ARM64_VA_BITS_42
        default 3 if ARM64_64K_PAGES && ARM64_VA_BITS_48
        default 3 if ARM64_4K_PAGES && ARM64_VA_BITS_39
        default 3 if ARM64_16K_PAGES && ARM64_VA_BITS_47
        default 4 if !ARM64_64K_PAGES && ARM64_VA_BITS_48

2.2 虚拟地址到物理地址的转换过程

我们来看虚拟地址48bit,4k页大小的转换过程

                                                                                       图1

1、[63:48]指示页表的基地址寄存器,如果全为1,表示这个地址是内核空间地址,页表基地址寄存器是TTBR1_EL1;如果全为0,表示这个地址是用户空间地址,页表基地址寄存器是TTBR0_EL1

2、TTBR寄存器保存了第0级页表寄存器(Linux中称为PGD,Page Global Directory),0级页表寄存器有512个页表项,每个页表项8个字节,所以PGD大小是4K,正好一页大小。通过虚拟地址的[47:39]作为索引查找相应的表项,表项中存储有下一级页表的基地址。

3、1级页表在Linux中称为PUD,同样512个页表项,PUD大小也是4K。通过虚拟地址的[38:30]作为索引查找相应的表项,表项中存储有下一级页表的基地址.

4、2级页表在Linux中称为PMD,同样512个页表项,PMD大小也是4K。通过虚拟地址的[29:21]作为索引查找相应的表项,表项中存储有下一级页表的基地址.

5、2级页表在Linux中称为PTE,同样512个页表项,PTE大小也是4K。通过虚拟地址的[29:21]作为索引查找相应的表项,表项中存储了最终物理地址的[47:12],与虚拟地址的[11:0]组合之后就找到了最终的物理地址。

对于虚拟地址为39bit,页大小4KB的情况,转换过程与上述过程类似,只是页表有原来的4级变为了3级,0级页表对应[38:30],1级页表对应[29:21],2级页表对应[20:12]。

下面来看看在Linux中页表中常用到一些数据定义

[arch/arm64/include/asm/pgtable-hwdef.h]

#define ARM64_HW_PGTABLE_LEVEL_SHIFT(n)	((PAGE_SHIFT - 3) * (4 - (n)) + 3)

#define PTRS_PER_PTE		(1 << (PAGE_SHIFT - 3))
/*
 * PMD_SHIFT determines the size a level 2 page table entry can map.
 */
#if CONFIG_PGTABLE_LEVELS > 2
#define PMD_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(2)
#define PMD_SIZE		(_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK		(~(PMD_SIZE-1))
#define PTRS_PER_PMD		PTRS_PER_PTE
#endif

/*
 * PUD_SHIFT determines the size a level 1 page table entry can map.
 */
#if CONFIG_PGTABLE_LEVELS > 3
#define PUD_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(1)
#define PUD_SIZE		(_AC(1, UL) << PUD_SHIFT)
#define PUD_MASK		(~(PUD_SIZE-1))
#define PTRS_PER_PUD		PTRS_PER_PTE
#endif

/*
 * PGDIR_SHIFT determines the size a top-level page table entry can map
 * (depending on the configuration, this level can be 0, 1 or 2).
 */
#define PGDIR_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(4 - CONFIG_PGTABLE_LEVELS)
#define PGDIR_SIZE		(_AC(1, UL) << PGDIR_SHIFT)
#define PGDIR_MASK		(~(PGDIR_SIZE-1))
#define PTRS_PER_PGD		(1 << (VA_BITS - PGDIR_SHIFT))

上面的数据表示的4级页表对应的偏移以及一个页表项所能覆盖内存范围大小。其中只有在页表层数大于3时,PUD页表才存在,只有在页表层数大于2时,PMU才存在。也就是说,

  1. 对于4级页表(如虚拟地址宽度48bit,页大小为4K),页表转换过程就是PGD--->PUD--->PMD--->PTR
  2. 对于3级页表(如虚拟地址宽度39bit,页大小为4K),PUD页表不存在,页表转换过程是PGD--->PMD--->PTR
  3. 对于2级页表(如虚拟地址宽度36bit,页大小为16K),PUD和PMD不存在,页表转换过程是PDG--->PTR

XXX_SHIFT表示的是该页表对应的虚拟地址偏移,该值与PAGE_SHIFT有关系。

XXX_SIZE表示的是该页表覆盖的地址空间大小,实际上也就是左移上面的XXX_SHIFT的大小。

PTRS_PER_XXX表示该页表有多少页表项。

3 页表的创建

       在第二章描述了如何通过页表查找物理地址的过程,那页表是如何完成映射的呢,或者说的直白一些,页表中的数据是什么时候、如何填充的呢?

3.1 创建启动阶段的页表

        在从bootloader到kernel的时候,MMU是关闭的,为了提高性能,加快初始化速度,我们必须某个阶段(越早越好)打开MMU和cache,而在此之前,我们必须要设定好页表。

        在初始化阶段,需要映射三段地址,

  1. identity mapping,这段映射的虚拟地址和物理是一样的,这是为了防止开启MMU后,无法获取页表。如下图System.map,可以看到__enable_mmu的代码是在idmap代码段当中的。实际上这一段地址是kernel image的一部分的,等到mmu打开之后,这一段地址自然是要映射到正确的虚拟地址上去的。也就是说identity会被映射两次。
  2. kernel image,代码要正常的执行,自然需要映射kernel
  3. blob memory对应的mapping

        初始阶段的页表页表定义在链接脚本中

[arch/arm64/kernel/vmlinux.lds.S]

        BSS_SECTION(0, 0, 0)

        . = ALIGN(PAGE_SIZE);
        idmap_pg_dir = .;
        . += IDMAP_DIR_SIZE;
        swapper_pg_dir = .;
        . += SWAPPER_DIR_SIZE;

初始化时候的页表放在了BSS段之后。swapper_pd_dir指向PGD页表基地址。

来看看关于size的定义

[arch/arm64/include/asm/kernel-pgtable.h]

#if ARM64_SWAPPER_USES_SECTION_MAPS
#define SWAPPER_PGTABLE_LEVELS	(CONFIG_PGTABLE_LEVELS - 1)
#define IDMAP_PGTABLE_LEVELS	(ARM64_HW_PGTABLE_LEVELS(PHYS_MASK_SHIFT) - 1)
#else
#define SWAPPER_PGTABLE_LEVELS	(CONFIG_PGTABLE_LEVELS)
#define IDMAP_PGTABLE_LEVELS	(ARM64_HW_PGTABLE_LEVELS(PHYS_MASK_SHIFT))
#endif

#define SWAPPER_DIR_SIZE	(SWAPPER_PGTABLE_LEVELS * PAGE_SIZE)
#define IDMAP_DIR_SIZE		(IDMAP_PGTABLE_LEVELS * PAGE_SIZE)

ARM64_SWAPPER_USES_SECTION_MAPS这个宏定义是说明了swapper/idmap的映射是否使用section map。什么是section map呢?我们用一个实际的例子来描述。假设VA是48 bit,page size是4K,那么,在地址映射过程中,地址被分成9(level 0) + 9(level 1) + 9(level 2) + 9(level 3) + 12(page offset),对于kernel image这样的big block memory region,使用4K的page来mapping有点得不偿失,在这种情况下,可以考虑让level 2的Translation table entry指向一个2M 的memory region,而不是下一级的Translation table。所谓的section map就是指使用2M的为单位进行映射。当然,不是什么情况都是可以使用section map,对于kernel image,其起始地址是2M对齐的,因此block size是2M的情况下才OK,对于PAGE SIZE是16K,其Block descriptor指向了一个32M的内存块,PAGE SIZE是64K的时候,Block descriptor指向了一个512M的内存块,因此,只有4K page size的情况下,才可以启用section map.

下面来看看在head.S中的具体实现,下面的代码是create_table_entry,这个宏定义主要是用来创建一个中间level的translation table中的描述符。

[arch/arm64/kernel/head.S]

/*
 * Macro to create a table entry to the next page.
 *
 *      tbl:    page table address
 *      virt:   virtual address
 *      shift:  #imm page table shift
 *      ptrs:   #imm pointers per table page
 *
 * Preserves:   virt
 * Corrupts:    tmp1, tmp2
 * Returns:     tbl -> next level table page address
 */
        .macro  create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2
        lsr     \tmp1, \virt, #\shift
        and     \tmp1, \tmp1, #\ptrs - 1        // table index
        add     \tmp2, \tbl, #PAGE_SIZE
        orr     \tmp2, \tmp2, #PMD_TYPE_TABLE   // address of next table and entry type
        str     \tmp2, [\tbl, \tmp1, lsl #3]
        add     \tbl, \tbl, #PAGE_SIZE          // next level table page
        .endm

1、参数tbl表示页表的地址,virt表示要映射的虚拟地址,shift表示这一级页表的在虚拟地址中的偏移,ptr表示这一级页表是几位的。tmp1和tmp2是两个临时变量。

2、代码的前两行取出虚拟地址中对应的本级页表的那几位。

3、第三行取出下一级页表的地址,初始阶段的页表(PGD/PUD/PMD/PTE)都是排列在一起的,每一个占用一个page。也就是说,如果create_table_entry当前操作的是PGD,那么tmp2这时候保存了下一个level的页表,也就是PUD了。

4、页表描述符的前两个bit表示该描述符是否有效,这里或上0x11,到这里,页表项的数据填充完毕

5、把页表项内容放到指定的页表项当中

6、结束的时候tbl会加上一个PAGE_SIZE,也就是tbl变成了下一级页表的地址

下面的代码是create_pgd_entry,名称有点迷惑性,这个宏的作用并不仅仅是创建pgd,除了最末级页表pte外,其他页表都会创建。

/*
 * Macro to populate the PGD (and possibily PUD) for the corresponding
 * block entry in the next level (tbl) for the given virtual address.
 *
 * Preserves:   tbl, next, virt
 * Corrupts:    tmp1, tmp2
 */
        .macro  create_pgd_entry, tbl, virt, tmp1, tmp2
        create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2
#if SWAPPER_PGTABLE_LEVELS > 3
        create_table_entry \tbl, \virt, PUD_SHIFT, PTRS_PER_PUD, \tmp1, \tmp2
#endif
#if SWAPPER_PGTABLE_LEVELS > 2
        create_table_entry \tbl, \virt, SWAPPER_TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2
#endif
        .endm

1、参数tbl表示页表的地址,virt表示要映射的虚拟地址。tmp1和tmp2是两个临时变量

2、第一行,create_table_entry前面已经介绍过了,就是填充该页表的页表项(只填充一个)。

3、SWAPPER_PGTABLE_LEVELS 这个前面已经介绍,对于页大小为4K的情况,SWAPPER_PGTABLE_LEVELS会比页表LEVEL小1,其他页大小这个值与页表LEVEL一致。

例子1:当虚拟地址是48个bit,4k page size,这时候page level等于4,映射关系是PGD(L0)--->PUD(L1)--->PMD(L2)--->Page table(L3)--->page,但是如果采用了section mapping(4k的page一定会采用section mapping),映射关系是PGD(L0)--->PUD(L1)--->PMD(L2)--->section。在create_pgd_entry函数中将创建PGD和PUD这两个中间level。

例子2:当虚拟地址是48个bit,16k page size(不能采用section mapping),这时候page level等于4,映射关系是PGD(L0)--->PUD(L1)--->PMD(L2)--->Page table(L3)--->page。在create_pgd_entry函数中将创建PGD、PUD和PMD这三个中间level。

例子3:当虚拟地址是39个bit,4k page size,这时候page level等于3,映射关系是PGD(L1)--->PMD(L2)--->Page table(L3)--->page。由于是4k page,因此采用section mapping,映射关系是PGD(L1)--->PMD(L2)--->section。在create_pgd_entry函数中将创建PGD这一个中间level。

下面的代码是create_block_map,通过前面的代码我们知道在4K页大小的情况下,会有section mapping,也就是对2M block大小进行映射。这种情况下该函数是对2M的block进行映射,其余情况下则是对相拥页大小进行映射。

    .macro    create_block_map, tbl, flags, phys, start, end 
    lsr    \phys, \phys, #SWAPPER_BLOCK_SHIFT 
    lsr    \start, \start, #SWAPPER_BLOCK_SHIFT 
    and    \start, \start, #PTRS_PER_PTE - 1    // table index 
    orr    \phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT    // table entry 
    lsr    \end, \end, #SWAPPER_BLOCK_SHIFT 
    and    \end, \end, #PTRS_PER_PTE - 1        // table end index 
9999:    str    \phys, [\tbl, \start, lsl #3]        // store the entry 
    add    \start, \start, #1            // next entry 
    add    \phys, \phys, #SWAPPER_BLOCK_SIZE        // next block 
    cmp    \start, \end 
    b.ls    9999b 
    .endm

1、参数tbl页表地址,参数flags表示当前页表项指示的是block还是page。phys表示要映射的物理地址的起始地址,start和end分表表示物理地址要映射到的虚拟地址的开始和结束。

2、前6行当中,phys和flag计算得到页表项的内容,通过start得到页表的index开始,通过end得到页表的计数。

3、9999循环映射从phys开始的地址映射到start---end的区域

上面这些子函数介绍完毕之后,下面来看看创建页表的全过程。

1、准备阶段

__create_page_tables:
        mov     x28, lr

        /*
         * Invalidate the idmap and swapper page tables to avoid potential
         * dirty cache lines being evicted.
         */
        adrp    x0, idmap_pg_dir
        ldr     x1, =(IDMAP_DIR_SIZE + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE)
        bl      __inval_dcache_area

        /*
         * Clear the idmap and swapper page tables.
         */
        adrp    x0, idmap_pg_dir
        ldr     x1, =(IDMAP_DIR_SIZE + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE)
1:      stp     xzr, xzr, [x0], #16
        stp     xzr, xzr, [x0], #16
        stp     xzr, xzr, [x0], #16
        stp     xzr, xzr, [x0], #16
        subs    x1, x1, #64
        b.ne    1b

        mov     x7, SWAPPER_MM_MMUFLAGS

分别获取identity mapping的页表的起始地址和swapper页表的结束地址,因为这两个页表是挨着分布的,所以直接相加就可以得到末尾地址。将这个页表invalid,为什么要调用__inval_cache_range来invalidate idmap_pg_dir和swapper_pg_dir对应页表空间的cache呢?根据boot protocol,代码执行到此,对于cache的要求是kernel image对应的那段空间的cache line是clean到PoC的,不过idmap_pg_dir和swapper_pg_dir对应页表空间不属于kernel image的一部分,因此其对应的cacheline很可能有一些旧的,无效的数据,必须要清理掉。

然后将两个页表的内容清零。将idmap和swapper页表内容设定为0是有意义的。实际上这些translation table中的大部分entry都是没有使用的,PGD和PUD都是只有一个entry是有用的,而PMD中有效的entry数目是和mapping的地 址size有关。将页表内容清零也就是意味着将页表中所有的描述符设定为invalid(描述符的bit 0指示是否有效,等于0表示无效描述符)

2、创建identity mapping

        /*
         * Create the identity mapping.
         */
        adrp    x0, idmap_pg_dir
        adrp    x3, __idmap_text_start          // __pa(__idmap_text_start)

#ifndef CONFIG_ARM64_VA_BITS_48
#define EXTRA_SHIFT     (PGDIR_SHIFT + PAGE_SHIFT - 3)
#define EXTRA_PTRS      (1 << (48 - EXTRA_SHIFT))

        /*
         * If VA_BITS < 48, it may be too small to allow for an ID mapping to be
         * created that covers system RAM if that is located sufficiently high
         * in the physical address space. So for the ID map, use an extended
         * virtual range in that case, by configuring an additional translation
         * level.
         * First, we have to verify our assumption that the current value of
         * VA_BITS was chosen such that all translation levels are fully
         * utilised, and that lowering T0SZ will always result in an additional
         * translation level to be configured.
         */
#if VA_BITS != EXTRA_SHIFT
#error "Mismatch between VA_BITS and page size/number of translation levels"
#endif

        /*
         * Calculate the maximum allowed value for TCR_EL1.T0SZ so that the
         * entire ID map region can be mapped. As T0SZ == (64 - #bits used),
         * this number conveniently equals the number of leading zeroes in
         * the physical address of __idmap_text_end.
         */
        adrp    x5, __idmap_text_end
        clz     x5, x5
        cmp     x5, TCR_T0SZ(VA_BITS)   // default T0SZ small enough?
        b.ge    1f                      // .. then skip additional level

        adr_l   x6, idmap_t0sz
        str     x5, [x6]
        dmb     sy
        dc      ivac, x6                // Invalidate potentially stale cache line
        create_table_entry x0, x3, EXTRA_SHIFT, EXTRA_PTRS, x5, x6
1:
#endif

        create_pgd_entry x0, x3, x5, x6
        mov     x5, x3                          // __pa(__idmap_text_start)
        adr_l   x6, __idmap_text_end            // __pa(__idmap_text_end)
        create_block_map x0, x7, x3, x5, x6

3、创建kernel image mapping

        /*
         * Map the kernel image (starting with PHYS_OFFSET).
         */
        adrp    x0, swapper_pg_dir
        mov_q   x5, KIMAGE_VADDR + TEXT_OFFSET  // compile time __va(_text)
        add     x5, x5, x23                     // add KASLR displacement
        create_pgd_entry x0, x5, x3, x6
        adrp    x6, _end                        // runtime __pa(_end)
        adrp    x3, _text                       // runtime __pa(_text)
        sub     x6, x6, x3                      // _end - _text
        add     x6, x6, x5                      // runtime __va(_end)
        create_block_map x0, x7, x3, x5, x6

        /*
         * Since the page tables have been populated with non-cacheable
         * accesses (MMU disabled), invalidate the idmap and swapper page
         * tables again to remove any speculatively loaded cache lines.
         */
        adrp    x0, idmap_pg_dir
        ldr     x1, =(IDMAP_DIR_SIZE + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE)
        dmb     sy
        bl      __inval_dcache_area

        ret     x28
ENDPROC(__create_page_tables)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值