菜单

kaoqi
kaoqi
发布于 2026-08-05 / 7 阅读
0
0

内存初始化(一)

内存在上电后,会由固件引导加载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)


评论