这篇内容讲讲页表的配置, 为了kernel运行速度加快, 我们需要启动cache, 启动cache需要先启动MMU让CPU运行到虚拟地址上, 那么我们就需要启动一个能覆盖KERNEL内存区域的页表.
本篇内容假设在您对MMU有一定的了解的基础上来讲述的.
我们假设使用4K页来管理内存, 同时虚拟地址使用48位地址
我们将__create_page_tables
函数分成几段来讲解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 __create_page_tables: mov x28, lr adrp x0, idmap_pg_dir ldr x1, =(IDMAP_DIR_SIZE + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE) bl __inval_dcache_area 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
在vmlinux.lds.S中
1 2 3 4 5 . = ALIGN(PAGE_SIZE); idmap_pg_dir = .; . += IDMAP_DIR_SIZE; swapper_pg_dir = .; . += SWAPPER_DIR_SIZE;
所以idmap_pg_dir
是在bss段后面且是PAGE_SIZE
对齐的. x0保存idmap_pg_dir
当前所在的物理地址. x1保存idmap_pg_dir
和swapper_pg_dir
的大小.
1 2 #define SWAPPER_DIR_SIZE (SWAPPER_PGTABLE_LEVELS * PAGE_SIZE) #define IDMAP_DIR_SIZE (IDMAP_PGTABLE_LEVELS * PAGE_SIZE)
通过对宏的观察, 我们了解到idmap_pg_dir
和swapper_pg_dir
都是3个PAGE_SIZE(12K), 我们知道使用4K页加48bit虚拟地址需要4级页表才能满足, 每级页表都是一个PAGE
level0 [47:39] 512个entry, 每个8byte, 一共4K
level1 [38:30] 512个entry, 每个8byte, 一共4K
level2 [29:21] 512个entry, 每个8byte, 一共4K
level3 [20:13] 512个entry, 每个8byte, 一共4K
剩下的11:0地址用level3的内容拼接得到最终的物理地址.
而这里idmap_pg_dir
和swapper_pg_dir
都只有3个PAGE是如何来映射的呢? 其实这里KERNEL在早期的临时页表为了节省内存, 并没有使用标准的4级映射, 而是使用的MMU中的block来直接描述2M(使用低21bit)内存区, 而不是用entry来描述4K页, 从而节省了level3的页表.
也就是说上面的level2页表中, 每条entry指向的不是level3页表, 而是一个2M的内存区.
上面的代码把这里的6个page全部清零, 因此所有的entry都成了invalid.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 mov x7, SWAPPER_MM_MMUFLAGS adrp x0, idmap_pg_dir adrp x3, __idmap_text_start create_pgd_entry x0, x3, x5, x6 mov x5, x3 adr_l x6, __idmap_text_end create_block_map x0, x7, x3, x5, x6
x0保存idmap_pg_dir
的物理地址. x3保存__idmap_text_start
的物理地址.
1 2 3 4 . = ALIGN(SZ_4K); \ VMLINUX_SYMBOL(__idmap_text_start) = .; \ *(.idmap.text) \ VMLINUX_SYMBOL(__idmap_text_end) = .;
所以x3保存的是.idmap.text
段所在的物理地址. 这个段是head.S文件的后半部分, 这部份code是CPU和MMU从关闭到开启的过程中执行的code, 说道这里你应该明白了, idmap_pg_dir
对应的页表是用来将与KERNEL所在物理地址相等的虚拟地址映射到相同的物理地址. 从而保证开启MMU时, 不会发生无法获取页表的情况. 而swapper_pg_dir
如其名是swapper进程运行所需的页表, 是内核初始化过程所用的页表.
另外ARM64有TTBR0, TTBR1(Translation Table Base Register)分别用来指示内核空间和用户空间页表所在的物理地址, 而在这个时候, TTBR0不是用来指示用户空间地址, 而是用来指示与物理地址相等的虚拟地址所用的页表. 所以TTBR0里面是.idmap.text
的物理地址, TTBR1里面是swapper_pg_dir
的物理地址.
1 2 3 4 5 6 7 8 9 10 11 12 .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
create_pgd_entry
宏用来创建level0和和level1的页表. 虚拟地址的[47:39]在level0页表中进行索引, 索引到的entry指向level1的页表, 这里level1的页表就是level0页表的下一个PAGE. 对应的[38:30]在level1页表中进行索引, 索引的entry指向level2的页表. 也就是再下一个PAGE. 这里来分析下create_table_entry
宏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .macro create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2 lsr \tmp1, \virt, #\shift and \tmp1, \tmp1, #\ptrs - 1 add \tmp2, \tbl, #PAGE_SIZE orr \tmp2, \tmp2, #PMD_TYPE_TABLE str \tmp2, [\tbl, \tmp1, lsl #3 ] add \tbl, \tbl, #PAGE_SIZE .endm
level2的页表由create_block_map
来配置.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 .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 orr \phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT lsr \end, \end, #SWAPPER_BLOCK_SHIFT and \end, \end, #PTRS_PER_PTE - 1 9999 : str \phys, [\tbl, \start, lsl #3 ] add \start, \start, #1 add \phys, \phys, #SWAPPER_BLOCK_SIZE cmp \start, \end b.ls 9999b .endm
综合上面的注释, 这里就是按照armv8 MMU的block条目来创建从start到end虚拟地址空间的映射, 每一个条目映射2M的地址空间. 这里把前面提到的x7保存的flags细说一下.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #define SWAPPER_MM_MMUFLAGS (PMD_ATTRINDX(MT_NORMAL) | SWAPPER_PMD_FLAGS) #define MT_NORMAL 4 #define PMD_ATTRINDX(t) (_AT(pmdval_t, (t)) << 2) #define SWAPPER_PMD_FLAGS (PMD_TYPE_SECT | PMD_SECT_AF | PMD_SECT_S) #define PMD_TYPE_SECT (_AT(pmdval_t, 1) << 0) #define PMD_SECT_AF (_AT(pmdval_t, 1) << 10) #define PMD_SECT_S (_AT(pmdval_t, 3) << 8) typedef u64 pmdval_t ;#ifdef __ASSEMBLY__ #define _AC(X,Y) X #define _AT(T,X) X #else #define __AC(X,Y) (X##Y) #define _AC(X,Y) __AC(X,Y) #define _AT(T,X) ((T)(X)) #endif
具体每个字段的含义可以查询arm手册来看, 大致来说, 这个flags表明, 这条entry指向的是一个2M的block, 这个block是一段普通的内存(不是device memory, 下篇内容我们还会继续说), 是已经访问过的, 是inner sharable的. 另外一些没有设定的位置为0也有一些含义, 如表示code是可执行的, 访问权限是EL0 RO, EL1 RW, 具体看arm的architecture reference manual.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 adrp x0, swapper_pg_dir mov_q x5, KIMAGE_VADDR + TEXT_OFFSET add x5, x5, x23 create_pgd_entry x0, x5, x3, x6 adrp x6, _end adrp x3, _text sub x6, x6, x3 add x6, x6, x5 create_block_map x0, x7, x3, x5, x6 adrp x0, idmap_pg_dir ldr x1, =(IDMAP_DIR_SIZE + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE) dmb sy bl __inval_dcache_area ret x28
后面的code参照前面的分析就很好理解了, 创建swapper的页表, 然后用dmb sy
完成同步, 最后清空缓存.
这里由一个需要注意低地方, 跟之前说的KASLR有关. add x5, x5, x23
在x5中保存_text的虚拟地址之后, 又加了x23, 这个x23就是之前保存的kaslr区域的大小. 也就是说把KEREL运行的虚拟地址进行了随机化.