参考文献:
-
《奔跑吧Linux内核》
-
《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才存在。也就是说,
- 对于4级页表(如虚拟地址宽度48bit,页大小为4K),页表转换过程就是PGD--->PUD--->PMD--->PTR
- 对于3级页表(如虚拟地址宽度39bit,页大小为4K),PUD页表不存在,页表转换过程是PGD--->PMD--->PTR
- 对于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,而在此之前,我们必须要设定好页表。
在初始化阶段,需要映射三段地址,
- identity mapping,这段映射的虚拟地址和物理是一样的,这是为了防止开启MMU后,无法获取页表。如下图System.map,可以看到__enable_mmu的代码是在idmap代码段当中的。实际上这一段地址是kernel image的一部分的,等到mmu打开之后,这一段地址自然是要映射到正确的虚拟地址上去的。也就是说identity会被映射两次。
- kernel image,代码要正常的执行,自然需要映射kernel
- 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)