arm64_linux启动流程分析05_配置内核启动的临时页表

这篇内容讲讲页表的配置, 为了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

/*
* 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

在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_dirswapper_pg_dir的大小.

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

通过对宏的观察, 我们了解到idmap_pg_dirswapper_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_dirswapper_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
/*
* x7保存level3页表的entry的flags, 表明是普通内存,
* 是一个block的entry, 后面细说
*/
mov x7, SWAPPER_MM_MMUFLAGS

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

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

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
/* 这里PGDIR_SHIFT是39, PTRS_PER_PGD是512 */
create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2
/* 使用4K页时SWAPPER_PGTABLE_LEVELS为3 */
#if SWAPPER_PGTABLE_LEVELS > 3
create_table_entry \tbl, \virt, PUD_SHIFT, PTRS_PER_PUD, \tmp1, \tmp2
#endif
#if SWAPPER_PGTABLE_LEVELS > 2
/* SWAPPER_TABLE_SHIFT是30, PTRS_PER_PTE是512 */
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
/* 下面两条指令取出虚拟地址(virt)的[shift+9:shift], 作为index */
lsr \tmp1, \virt, #\shift
and \tmp1, \tmp1, #\ptrs - 1 // table index
/*
* 下面两条指令计算出这一级页表对应virt的entry的值, 第一条指令计算entry指向的下一级
* 页表的物理地址, 第二条指令指定当前entry是PMD_TYPE_TABLE, 也就是表示当前entry
* 指向的仍然是一个页目录, 具体看arm的architecture reference manual.
*/
add \tmp2, \tbl, #PAGE_SIZE
orr \tmp2, \tmp2, #PMD_TYPE_TABLE // address of next table and entry type
/*
* 使用之前计算的index来得到virt对应的entry的位置(tbl + index * 8byte), 然后把
* 页表entry存到那个位置
*/
str \tmp2, [\tbl, \tmp1, lsl #3]
/* tbl指向下一级页表, 方便下一次计算 */
add \tbl, \tbl, #PAGE_SIZE // next level table page
.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
/* SWAPPER_BLOCK_SHIFT是21, 把物理地址右移21bit, 剩下的就是entry中的地址 */
lsr \phys, \phys, #SWAPPER_BLOCK_SHIFT
/* 这两条指令取出start的[29:21]作为level2页表的索引, 存在start中 */
lsr \start, \start, #SWAPPER_BLOCK_SHIFT
and \start, \start, #PTRS_PER_PTE - 1 // table index
/*
* phys = flags | (phys << 21), 很明显, 就是构建一条level2页表entry,
* entry将虚拟地址start(前面两条指令计算之前的值)转换成物理地址phys(计算前的值)
*/
orr \phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT // table entry
/* 将end也计算成一个index, 方便后面的循环建立页表 */
lsr \end, \end, #SWAPPER_BLOCK_SHIFT
and \end, \end, #PTRS_PER_PTE - 1 // table end index
/* 将之前构建的页表entry存到level2页表对应的位置(由[29:21]索引) */
9999: str \phys, [\tbl, \start, lsl #3] // store the entry
/*
* 索引每次加1, 页表entry的物理地址每次加2M, 这样就能计算出下一条entry的内容
* 和存放路径了
*/
add \start, \start, #1 // next entry
add \phys, \phys, #SWAPPER_BLOCK_SIZE // next block
/* 循环创建参数中start到end的地址的映射 */
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 // 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

后面的code参照前面的分析就很好理解了, 创建swapper的页表, 然后用dmb sy完成同步, 最后清空缓存.

这里由一个需要注意低地方, 跟之前说的KASLR有关. add x5, x5, x23在x5中保存_text的虚拟地址之后, 又加了x23, 这个x23就是之前保存的kaslr区域的大小. 也就是说把KEREL运行的虚拟地址进行了随机化.