展示代码
/*
* Map the kernel image (starting with PHYS_OFFSET).
*/
adrp x0, init_pg_dir
mov_q x5, KIMAGE_VADDR // compile time __va(_text)
add x5, x5, x23 // add KASLR displacement
mov x4, PTRS_PER_PGD
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)
map_memory x0, x1, x5, x6, x7, x3, x4, x10, x11, x12, x13, x14
/*
* Since the page tables have been populated with non-cacheable
* accesses (MMU disabled), invalidate those tables again to
* remove any speculatively loaded cache lines.
*/
dmb sy
adrp x0, idmap_pg_dir
adrp x1, idmap_pg_end
sub x1, x1, x0
bl __inval_dcache_area
adrp x0, init_pg_dir
adrp x1, init_pg_end
sub x1, x1, x0
bl __inval_dcache_area
ret x28
SYM_FUNC_END(__create_page_tables)
分析代码
依依不舍也好,如坐针毡也好,无论我们是否喜欢函数 __create_page_tables,天下没有不散的宴席,下面到了函数 __create_page_tables 收尾的部分了。
刚才我们分析了将 __idmap_text_start 到 __idmap_text_end 做了恒等映射。
下面我们要将 _text 到 _end 也要做映射,当然,这就不是恒等映射了,我们重点分析差异部分。
第1行到第3行是注释,已经提示我们映射从宏 PHYS_OFFSET 开始,PHYS_OFFSET 这个宏后续我们再讲,目前只在注释里面体套
第4行 将 init_pg_dir 的物理地址存放在 x0
第5行宏 KIMAGE_VADDR 表示内核映像的虚拟起始地址,我们分析一下其值为多少
#define KIMAGE_VADDR (MODULES_END)
继续分析 MODULES_END
#define MODULES_END (MODULES_VADDR + MODULES_VSIZE)
涉及到两个宏 MODULES_VADDR 和 MODULES_VSIZE
#define MODULES_VADDR (BPF_JIT_REGION_END)
#define MODULES_VSIZE (SZ_128M)
而 SZ_128M 直接通关,BPF_JIT_REGION_END 需要继续
#define SZ_128M 0x08000000
#define BPF_JIT_REGION_END (BPF_JIT_REGION_START + BPF_JIT_REGION_SIZE)
BPF_JIT_REGION_SIZE 直接得到答案,BPF_JIT_REGION_START 需要继续
#define BPF_JIT_REGION_START (KASAN_SHADOW_END)
#define BPF_JIT_REGION_SIZE (SZ_128M)
这时,KASAN_SHADOW_END 走到了岔路
/*
* Generic and tag-based KASAN require 1/8th and 1/16th of the kernel virtual
* address space for the shadow region respectively. They can bloat the stack
* significantly, so double the (minimum) stack size when they are in use.
*/
#ifdef CONFIG_KASAN
#define KASAN_SHADOW_OFFSET _AC(CONFIG_KASAN_SHADOW_OFFSET, UL)
#define KASAN_SHADOW_END ((UL(1) << (64 - KASAN_SHADOW_SCALE_SHIFT)) \
+ KASAN_SHADOW_OFFSET)
#define KASAN_THREAD_SHIFT 1
#else
#define KASAN_THREAD_SHIFT 0
#define KASAN_SHADOW_END (_PAGE_END(VA_BITS_MIN))
#endif /* CONFIG_KASAN */
根据配置文件,当前没有使能 KASAN
$ cat .config | grep CONFIG_KASAN
# CONFIG_KASAN is not set
需要继续分析宏 VA_BITS_MIN 和 _PAGE_END
#if VA_BITS > 48
#define VA_BITS_MIN (48)
#else
#define VA_BITS_MIN (VA_BITS)
#endif
而 VA_BITS 的值我们之前讨论过,为48
另外一个宏 _PAGE_END 的定义如下
#define _PAGE_END(va) (-(UL(1) << ((va) - 1)))
我们计算 KIMAGE_VADDR 的值
KIMAGE_VADDR
= (-(UL(1) << ((48) - 1))) + 128M + 128M
= (-(UL(1) << ((48) - 1))) + 0x08000000 + 0x08000000
= (-(UL(1) << 47)) + 0x08000000 + 0x08000000
= -0b0000000000000000100000000000000000000000000000000000000000000000 + 0x08000000 + 0x08000000
= 0b1111111111111111011111111111111111111111111111111111111111111111 + 1 + 0x08000000 + 0x08000000
= 0xffff800000000000 + 0x08000000 + 0x08000000
= 0xffff800010000000
在链接脚本arch/arm64/kernel/vmlinux.lds.S里面
. = KIMAGE_VADDR;
.head.text : {
_text = .;
HEAD_TEXT
}
这说明 _text 和 KIMAGE_VADDR 的值相等,而链接脚本 . = KIMAGE_VADDR 的值也是根据我们上述的推导过程计算出来的,详见如下博客
慢慢欣赏arm64内核启动1 链接脚本.head.text部分的解析_linux arm64 链接脚本-CSDN博客
我们查找 System.map,确认内核的起始虚拟地址,也就是链接地址正确
$ cat System.map | grep _text
ffff800010000000 T _text
这证明了我们的计算正确
所以第5行的含义是将 ffff800010000000 存放在 x5
第6行将 x5 和 x23 相加存放在 x5 里,
那么 x23 里面存放的是什么呢,在 head.S 函数 el2_setup 执行完毕返回后有两句代码没有分析
adrp x23, __PHYS_OFFSET
and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0
其中,__PHYS_OFFSET 的定义如下
$ grep -rn __PHYS_OFFSET arch/arm64/
arch/arm64/kernel/head.S:39:#define __PHYS_OFFSET KERNEL_START
而 KERNEL_START 的定义如下
$ grep -rnw KERNEL_START arch/arm64/
arch/arm64/include/asm/memory.h:67:#define KERNEL_START _text
而在本章节我们计算了 _text 的值为 ffff800010000000
所以 x23 一开始保存了 内核的物理地址所在的物理页的基地址
对于 MIN_KIMG_ALIGN,
$ grep -rnw MIN_KIMG_ALIGN arch/arm64/
arch/arm64/include/asm/boot.h:18:#define MIN_KIMG_ALIGN SZ_2M
通过 and 的运算,x23 的值应该为0
所以当 KASLR 没有使能,第6行没有用途
第7行将全局页表的页表项数目 PTRS_PER_PGD 存放在 x4 里面,关于 PTRS_PER_PGD 我们之前分析过
第8行将 _end 的物理地址所在的页表基地址存放在 x6
第9行将_text 的物理地址所在的页表基地址存放在x3
第10行将计算内核镜像的大小
第11行计算内核虚拟地址的结束地址,存放在 x6,计算方式为
x6 = x6 + x5
= 内核镜像的大小 + 内核虚拟起始地址
第13行按照链接地址建立内核镜像的三级页表
x0 表示全局页表的物理地址
x1 表示下一级页表的物理地址,该参数是在函数内部计算,并不通过入参传入,前提是假设3级页表都是连续的。
x5 和 x6 分别代表内核的虚拟地址的起始地址和终止地址
x7 代表物理页的属性
x3 表示内核起始地址的物理地址
x4 表示全局页表可以容纳多少个表项,目前是 512
x10 和 x11 分别表示调用该函数内部的时候,用来存放各级页表的页表项的起始索引和终止索引,并不通过入参传入,所以并不需要在调用前赋初值
x12 表示临时变量
x13 表示各级页表的页表项的数目,该值在函数内部计算,并不通过入参传入,所以并不需要在调用前赋初值
x14 也是临时变量
函数 map_memory 调用完毕后按照内核编译链接形成的地址的三级页表建立完毕
第22行到第30行表示刷新页表的数据cache
第32行函数返回,对应的返回地址 保存在 x28