内存管理-启动代码

vmlinux.lds.S的链接信息

.head.text : {
        _text = .;
        HEAD_TEXT
}

启动代码是在HEAD_TEXT段中的

__HEAD
/*
 * DO NOT MODIFY. Image header expected by Linux boot-loaders.
 */
efi_signature_nop                        // special NOP to identity as PE/COFF executable
b        primary_entry                        // branch to kernel start, magic
.quad        0                                // Image load offset from start of RAM, little-endian
le64sym        _kernel_size_le                        // Effective size of kernel image, little-endian
le64sym        _kernel_flags_le                // Informative flags, little-endian
.quad        0                                // reserved
.quad        0                                // reserved
.quad        0                                // reserved
.ascii        ARM64_IMAGE_MAGIC                // Magic number
.long        .Lpe_header_offset                // Offset to the PE header.

__EFI_PE_HEADER

除了b primary_entry之外,后面那些字段主要都是给引导程序用来解析、校验和加载映像的元数据。

primary_entry

SYM_CODE_START(primary_entry)
    bl  record_mmu_state
    bl  preserve_boot_args

    adrp    x1, early_init_stack
    mov sp, x1
    mov x29, xzr
    adrp    x0, init_idmap_pg_dir
    mov x1, xzr
    bl  __pi_create_init_idmap

    cbnz    x19, 0f
    dmb     sy
    mov x1, x0              // end of used region
    adrp    x0, init_idmap_pg_dir
    adr_l   x2, dcache_inval_poc
    blr x2
    b   1f


0:  adrp    x0, __idmap_text_start
    adr_l   x1, __idmap_text_end
    adr_l   x2, dcache_clean_poc
    blr x2

1:  mov x0, x19
    bl  init_kernel_el          // w0=cpu_boot_mode
    mov x20, x0


     */
    bl  __cpu_setup         // initialise processor
    b   __primary_switch
SYM_CODE_END(primary_entry)
  • bl record_mmu_state
    将当前 MMU 和缓存状态保存到 record_mmu_state(),以便后面可能恢复。
  • bl preserve_boot_args
    保存从 boot-loader 传入的启动参数(传给内核命令行、设备树等)。
adrp    x1, early_init_stack
mov     sp, x1
mov     x29, xzr

把栈指针切换到早期初始化栈,清零FP指针(x29),建立一个干净的调用帧。

adrp    x0, init_idmap_pg_dir
mov     x1, xzr
bl      __pi_create_init_idmap

将 identity-map 页目录的基地址加载到 x0

  • bl __pi_create_init_idmap
    调用 create_init_idmap(),在这块预留的内存区域里搭建好一个“1:1 映射”的页表,用于在打开 MMU 之前访问内核的虚拟地址。

注:

为什么是 __pi_create_init_idmap 而不是 create_init_idmap

在arch\arm64\kernel\pi\Makefile中,有一段给符号添加prefix的代码:

$(obj)/%.pi.o: OBJCOPYFLAGS := --prefix-symbols=__pi_ \
			       --remove-section=.note.gnu.property
cbnz    x19, 0f
dmb     sy
mov     x1, x0          // x0 此时指向 idmap 末端地址
adrp    x0, init_idmap_pg_dir
adr_l   x2, dcache_inval_poc
blr     x2
b       1f

0:  adrp    x0, __idmap_text_start
    adr_l   x1, __idmap_text_end
    adr_l   x2, dcache_clean_poc
    blr x2
  • cbnz x19, 0f:如果 cpu_boot_modex19)不为零(通常表示“二级启动”或“热启动”),跳到标签 0;否则走下面的路径。

    • 路径 A(cold boot,x19==0

      1. dmb sy:全局内存屏障,确保前面的内存映射修改生效。mov x1, x0:把 __pi_create_init_idmap 返回的“映射末端”地址放入 x1
      2. 重新 adrp x0, init_idmap_pg_dir 加载页目录基址。
      3. adr_l x2, dcache_inval_poc:取到缓存失效函数入口,
      4. blr x2:调用 dcache_inval_poc(pg_dir, end),对这段 mapping 区域做缓存失效(invalidate),保证后续执行能看到最新映射。
      5. b 1f:跳过标签 0。
    • 路径 B(warm boot,x19≠0

      1. adrp x0, __idmap_text_start / adr_l x1, __idmap_text_end:定位 idmap 文本区的开始和结束。
      2. adr_l x2, dcache_clean_poc:取到缓存清理函数入口。
      3. blr x2:调用 dcache_clean_poc(start, end),把这段代码清理到 PoC(Point-of-Coherency),确保二级启动时用的 idmap 代码能被所有核一致地看到。
  • 跳转到内核主入口
mov     x0, x19
bl      init_kernel_el       // w0 = cpu_boot_mode
mov     x20, x0              // 保存 init_kernel_el 的返回值(新 boot mode / status)
bl      __cpu_setup          // 对当前处理器做更进一步的初始化
b       __primary_switch     // 跳到 C 语言实现的主启动分支
  • init_kernel_el:早的汇编阶段完成了 EL1/EL2 路径的环境准备,确保后续的 C 代码能在正确的 Exception Level 和处理器状态下运行
  • __cpu_setup:在 EL1 早期阶段初始化内存管理单元(MMU)相关的寄存器,并屏蔽了用户态对高特权资源的直接访问,为内核后续的页面表创建、缓存配置和系统运行环境打下基础
  • __primary_switch:配置寄存器,开启MMU,跳转进入内核。

create_init_idmap

来详细分析一下create_init_idmap都干了什么。

asmlinkage u64 __init create_init_idmap(pgd_t *pg_dir, pteval_t clrmask)
{
	u64 ptep = (u64)pg_dir + PAGE_SIZE;
	pgprot_t text_prot = PAGE_KERNEL_ROX;
	pgprot_t data_prot = PAGE_KERNEL;

	pgprot_val(text_prot) &= ~clrmask;
	pgprot_val(data_prot) &= ~clrmask;

	map_range(&ptep, (u64)_stext, (u64)__initdata_begin, (u64)_stext,
		  text_prot, IDMAP_ROOT_LEVEL, (pte_t *)pg_dir, false, 0);
	map_range(&ptep, (u64)__initdata_begin, (u64)_end, (u64)__initdata_begin,
		  data_prot, IDMAP_ROOT_LEVEL, (pte_t *)pg_dir, false, 0);

	return ptep;
}
  • ptep(page table entry pointer)用来指向下一层页表(PUD/PTE)的起始地址。
  • map_range创建直接映射的页表

调用了两次 map_range,在同一套页表(同一个 pg_dir)里,对两个不同的虚拟地址区间分别做映射,目的是给代码段数据段设置不同的访问属性。

map_range

void __init map_range(u64 *pte, u64 start, u64 end, u64 pa, pgprot_t prot,
		      int level, pte_t *tbl, bool may_use_cont, u64 va_offset)
{
	u64 cmask = (level == 3) ? CONT_PTE_SIZE - 1 : U64_MAX;
	pteval_t protval = pgprot_val(prot) & ~PTE_TYPE_MASK;
	int lshift = (3 - level) * PTDESC_TABLE_SHIFT;
	u64 lmask = (PAGE_SIZE << lshift) - 1;

	start	&= PAGE_MASK;
	pa	&= PAGE_MASK;

	/* Advance tbl to the entry that covers start */
	tbl += (start >> (lshift + PAGE_SHIFT)) % PTRS_PER_PTE;

	/*
	 * Set the right block/page bits for this level unless we are
	 * clearing the mapping
	 */
	if (protval)
		protval |= (level == 2) ? PMD_TYPE_SECT : PTE_TYPE_PAGE;

	while (start < end) {
		u64 next = min((start | lmask) + 1, PAGE_ALIGN(end));

		if (level < 2 || (level == 2 && (start | next | pa) & lmask)) {
			/*
			 * This chunk needs a finer grained mapping. Create a
			 * table mapping if necessary and recurse.
			 */
			if (pte_none(*tbl)) {
				*tbl = __pte(__phys_to_pte_val(*pte) |
					     PMD_TYPE_TABLE | PMD_TABLE_UXN);
				*pte += PTRS_PER_PTE * sizeof(pte_t);
			}
			map_range(pte, start, next, pa, prot, level + 1,
				  (pte_t *)(__pte_to_phys(*tbl) + va_offset),
				  may_use_cont, va_offset);
		} else {
			/*
			 * Start a contiguous range if start and pa are
			 * suitably aligned
			 */
			if (((start | pa) & cmask) == 0 && may_use_cont)
				protval |= PTE_CONT;

			/*
			 * Clear the contiguous attribute if the remaining
			 * range does not cover a contiguous block
			 */
			if ((end & ~cmask) <= start)
				protval &= ~PTE_CONT;

			/* Put down a block or page mapping */
			*tbl = __pte(__phys_to_pte_val(pa) | protval);
		}
		pa += next - start;
		start = next;
		tbl++;
	}
}

1. 函数作用和参数含义

  • 功能:在给定的页表层级(level)上,为虚拟地址区间 [start, end) 建立映射到物理地址 pa,并设置访问属性 prot
  • 主要参数
    • u64 *pte:指向一个“分配器”页(或页表块)的起始指针,用于在需要时分配下一级页表页面。
    • u64 start, end:要映射的虚拟地址范围(半开区间)。
    • u64 pa:映射起始的物理地址。
    • pgprot_t prot:页属性(可读/写/执行等)。
    • int level:当前处理的页表层级(0 表示顶层,最大到 3)。
    • pte_t *tbl:当前层级的页表基址,用于写入映射条目。
    • bool may_use_cont:是否允许使用连续(contiguous)映射以减少表项数量。
    • u64 va_offset:页表物理地址到内核虚拟地址的偏移,用于把物理地址转换为内核可寻址指针。

2. 计算各种掩码

	u64 cmask = (level == 3) ? CONT_PTE_SIZE - 1 : U64_MAX;
	pteval_t protval = pgprot_val(prot) & ~PTE_TYPE_MASK;
	int lshift = (3 - level) * PTDESC_TABLE_SHIFT;
	u64 lmask = (PAGE_SIZE << lshift) - 1;
  • cmask 用于判断地址和物理地址是否对齐到可以做“连续”映射的粒度。只有在第 3 级(页级映射)才可能做连续页映射。
  • 去掉原有的类型位(PTE/PMD/etc),留出位置后面 OR 上正确的类型。
  • lmask 用来对齐当前层级能一次映射的最大连续范围。例如,level=2(PMD)时,lmask = (4KB << PTDESC_TABLE_SHIFT) -1 = (2MB)-1

3. while循环代码的作用

使用 **level < 2 || (level == 2 && (start | next | pa) & lmask)**判断

  1. PGD或者PUD递归调用map_range,填充下一级页表,如果是PMD但是没有按照2Mb对齐,也进入下一级页表
  2. PMD可以使用2Mb大页映射,或者到了PTE页表,增加start地址,填充当前页表的下一个页表项

注:

这里会把没有按照2M对齐的部分使用PTE映射,对齐的部分会使用2M的PMD大页映射。

__primary_switch

SYM_FUNC_START_LOCAL(__primary_switch)
	adrp	x1, reserved_pg_dir
	adrp	x2, init_idmap_pg_dir
	bl	__enable_mmu

	adrp	x1, early_init_stack
	mov	sp, x1
	mov	x29, xzr
	mov	x0, x20				// pass the full boot status
	mov	x1, x21				// pass the FDT
	bl	__pi_early_map_kernel		// Map and relocate the kernel

	ldr	x8, =__primary_switched
	adrp	x0, KERNEL_START		// __pa(KERNEL_START)
	br	x8
SYM_FUNC_END(__primary_switch)
  • __enable_mmu:主要的作用就是把ttbr0、ttbr1寄存复制,同时开启mmu
phys_to_ttbr   x2, x2
msr    ttbr0_el1, x2

load_ttbr1     x1, x1, x3

set_sctlr_el1  x0

这里会把init_idmap_pg_dir赋值给ttbr0,ttbr0这个页表寄存器是在虚拟地址高位为0的时候使用的。

启动阶段idmap是直接映射,高位地址都是0,所以需要用的ttbr0

  • __pi_early_map_kernelearly_map_kernel() 在 MMU 打开后立刻运行,负责清理早期内存、解析启动参数、调整寄存器设置,再把整个内核镜像映射到虚拟地址空间中。

    idmap_cpu_replace_ttbr1(init_pg_dir);
    

    设置ttbr1,后续切换到0xFFFF…的虚拟地址也可以正常工作。

  • __primary_switched

    1. 任务初始化:构造 CPU 对应的 task_struct(idle task)。
    2. 异常向量:设置 EL1 的中断/异常处理入口。
    3. 栈帧设置:构建函数帧,准备局部调用。
    4. 关键全局变量:保存 FDT 指针、内核镜像偏移。
    5. 运行时标志:记录启动模式、做 KASAN 和 VHE 收尾。
    6. 转入 C 端:最终调用 start_kernel(),完成高层初始化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值