内存在上电后,会由固件引导加载kernel,并根据kernel的描述完成内存页表的准备,进而完成初始化。注意,这里的描述都是以kernel源代码的角度进行的。
具体来说,内存初始化分为基础页表准备、早期内存分配器初始化、核心页表初始化、伙伴系统初始化四个部分。
临时页表阶段
内核的加载本身由某种BootLoader进行(可以是UEFI固件,也可以是grub或其他可执行二进制命令的东西)。此时MMU这个硬件设备没有被启用,Bootloader实际上是将内核根据一个确定的物理内存地址、一个确定的DTB设备树、一个确定的可传入启动参数、一些可能的持续性服务,来将内核导入到一个具体的物理地址中(boot无法代替kernel进行内存MMU的初始化,MMU的初始化本身就时kernel完成的标志,它与虚拟地址的建立直接相关)。
当BootLoader完成内核的加载后,会执行一个跳转命令,将CPU的程序计数器直接指向内核入口代码的物理地址。具体内核的代码执行逻辑可以参考另一篇文章(一个head段到init段的逻辑)。注意,grub或者是什么其他的固件,在传递给kernel参数时只会传递具体的静态参数,不会提供一个类似UEFI固件中system table一样的可供持续调用的函数来给kernel。kernel除非启用efi stub进行短暂的内存搬运动作外,也同样不会使用到任何可供持续调用的服务。
具体来说,内核启动时会约定如下内容:
1、内核命令行参数:一串文本字符,会被放置在内存的某个约定位置,内核启动时会去读取并解析。
2、Initramfs/Initrd的位置:如果配置了初始内存文件系统,BootLoader会将其加载到内存,并告知内核这个文件系统的物理地址和大小。
3、硬件描述信息:DTB的物理地址,UEFI固件加载DTB物理地址到内存中后,由EFI工具遍历GUID确定DTB的物理地址,并通过寄存器把地址传递给内核(一般使用x0寄存器传递)。
4、内核入口与CPU状态:跳转到内核入口代码的物理地址,并将CPU装填设置为干净的状态(MMU关闭、中断关闭、寄存器设置正确)。
那么好,这里明确了内核最初是由BootLoader加载到物理地址中之后,接下来就可以搭建早期的临时页表了。
kernel的汇编代码(head.S)会创建一张临时页表,这张页表包含两个映射:
1、恒等映射,将开启MMU的那段代码的物理地址,映射到完全相同的虚拟地址,防止MMU开启瞬间程序跑飞。
2、内核镜像映射,将整个内核映射到它链接时指定的高端虚拟地址。
完成后,内核加载页表基址并开启MMU,正式进入虚拟地址世界。
具体来说,这个代码位于/arch/arm64/kernel/head.S文件中:
SYM_CODE_START(primary_entry)
bl preserve_boot_args // 这里进行参数的获取,如设备树指针、内核命令行
bl el2_setup // 切换异常级别
adrp x23, __PHYS_OFFSET // 获取__PHYS_OFFSET的内存页基地址,并将其加载到x23寄存器中
and x23, x23, MIN_KIMG_ALIGN - 1 // 这两行是用于计算内核地址空间布局随机化(KASLR)的物理偏移量,随机偏移量是一个大于等于0小于页表对齐颗粒度的值,由kernel之前的bootloader在执行内核加载搬运时添加随机量,此处是计算出这个随机量并保存在x23的寄存器中
bl set_cpu_boot_mode_flag // 设置启动模式标志
bl __create_page_tables // 创建初始的MMU页表映射
bl __cpu_setup // CPU初始化
b __primary_switch // 跳转到主切换代码
SYM_CODE_END(primary_entry)这里可以看到,汇编视角下临时页表在__create_page_tables这边进行,同样也在这个head.S文件里:
/* 这里简述了初始化页表要干啥 */
SYM_FUNC_START_LOCAL(__create_page_tables)
mov x28, lr
/* 将初始页表作废,其他页表作为内核镜像中的一部分 */
adrp x0, init_pg_dir
adrp x1, init_pg_end
sub x1, x1, x0
bl __inval_dcache_area // 一般将x0、x1寄存器中保存的数值作为函数变量传参时的参数,这里的含义是执行一个__inval_dcache_area的函数,这个函数是汇编器的一个底层函数,专门负责将指定地址范围的数据高速缓存(Data Cache)作废并无效化
/* 清理初始页表 */
adrp x0, init_pg_dir
adrp x1, init_pg_end
sub x1, x1, x0
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 // 将init_pg_dir到init_pg_end之间的临时页表内存全部清零,以循环写入4对zxr(零寄存器)方式
mov x7, SWAPPER_MM_MMUFLAGS // 准备页表标志位
/* 建立直接映射表 */
adrp x0, idmap_pg_dir
adrp x3, __idmap_text_start // 准备页表基址
#ifdef CONFIG_ARM64_VA_BITS_52 // 这里是52位的虚拟地址检测
mrs_s x6, SYS_ID_AA64MMFR2_EL1
and x6, x6, #(0xf << ID_AA64MMFR2_LVA_SHIFT)
mov x5, #52
cbnz x6, 1f
#endif
mov x5, #VA_BITS_MIN // 使用x5寄存器包含的虚拟地址位宽值
1:
adr_l x6, vabits_actual // 这个地址的旧数据副本全部失效,用于强制从主存重新加载
str x5, [x6]
dmb sy // 全系统内存屏障
dc ivac, x6 // 数据缓存无效化指令
/* 恒等映射 */
adrp x5, __idmap_text_end
clz x5, x5 // 将恒等映射代码段结束地址的高位部分加载到x5,clz计算x5中前导零的个数存回x5,为容纳该物理地址所需的最小T0SZ值
cmp x5, TCR_T0SZ(VA_BITS_MIN) // 与默认配置比较,检查是否足够大
b.ge 1f // 根据CPSR标志位来判断条件成立时跳转1f目标标签执行
adr_l x6, idmap_t0sz
str x5, [x6]
dmb sy
dc ivac, x6 // 重计算T0SZ值
#if (VA_BITS < 48)
#define EXTRA_SHIFT (PGDIR_SHIFT + PAGE_SHIFT - 3)
#define EXTRA_PTRS (1 << (PHYS_MASK_SHIFT - EXTRA_SHIFT))
/* ARM64的页表结构由VA_BITS(虚拟地址位宽)和PAGE_SIZE(页面大小)共同决定,4级页表可以覆盖256TB的空间(标准4级页表:PGD->PUD->PMD->PTE),虚拟地址位宽不够时就需要创建新的页表条目 */
#if VA_BITS != EXTRA_SHIFT
#error "Mismatch between VA_BITS and page size/number of translation levels"
#endif
mov x4, EXTRA_PTRS
create_table_entry x0, x3, EXTRA_SHIFT, x4, x5, x6 // 这里是虚拟地址位宽不够,使用额外页表条目数加载到x4
#else
/* 虚拟地址位宽=48时,直接使用4级标准页表 */
mov x4, #1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT)
str_l x4, idmap_ptrs_per_pgd, x5
#endif
1:
ldr_l x4, idmap_ptrs_per_pgd
mov x5, x3 // __pa(__idmap_text_start)
adr_l x6, __idmap_text_end // __pa(__idmap_text_end)
map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14 // 真正在内存中填写页表项,完成从__idmap_text_start到__idmap_text_end的恒等映射
/*
* 为内核镜像本身创建虚拟地址映射
*/
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 // 将内存镜像的物理内存_text到_end映射到其运行时的虚拟地址KIMAGE_VADDR+KASLR偏移上
/* 开启MMU之前执行的页表缓存无效化操作,确保刚刚创建好的恒等映射页表和临时内核页表能够被MMU硬件正确读取 */
dmb sy // 全局内存屏障
adrp x0, idmap_pg_dir
adrp x1, idmap_pg_end
sub x1, x1, x0
bl __inval_dcache_area // 调用__inval_dcache_area函数对idmap_pg_dir到idmap_pg_end的区域执行数据缓存无效化操作,将缓存中的脏数据写回主存,并丢弃所有的缓存行,确保后续对这片内存的读取都直接来自主存。
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)