引导程序加载完成后,会执行 arch/arm64/kernel/head.s 中的汇编代码。
😁_head 段
/** 内核启动入口点.* ---------------------------** 要求:* MMU = off, D-cache = off, I-cache = on or off,* x0 = physical address to the FDT blob.** 这段代码的位置是独立的, 所以你可以通过下面的语句进行调用* __pa(PAGE_OFFSET + TEXT_OFFSET).*/__HEAD_head:#ifdef CONFIG_EFI/** 这个 add 指令除了生成 UEFI 需要的 MZ 标志位外没有其他作用.*/add x13, x18, #0x16b stext#elseb stext // branch to kernel start, magic.long 0 // reserved#endif
这段代码主要用于开启对 UEFI 的支持;UEFI(Unified Extensible Firmware Interface)是统一的可扩展固件接口,用于取代BIOS。
😂_stext 段
/** 下面的被调用者保存的通用寄存器用于主低级别引导:** Register Scope Purpose* x21 stext() .. start_kernel() 在 x0 引导中传递的 FDT 指针* x23 stext() .. start_kernel() 物理偏移/KASLR offset* x28 __create_page_tables() 被调用者保存的临时寄存器* x19/x20 __primary_switch() 被调用者保存的临时寄存器*/ENTRY(stext)bl preserve_boot_args // 把引导程序传递的 4 个参数保存在全局数组 boot_args 中bl el2_setup // 判断是否需要将异常级别降低到 EL1adrp x23, __PHYS_OFFSET // 保存物理内核物理偏移and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR 偏移, 默认是 0bl set_cpu_boot_mode_flag // 更具异常级别设置 CPU 运行模式bl __create_page_tables // 创建页表映射/** 下面的 CPU 启动代码的细节可以查看 arch/arm64/mm/proc.S* 执行完成时, CPU 将会开启 MMU和 TCR.*/bl __cpu_setup // 初始化处理器/** 为主处理器开启内存管理单元, 搭建 C 语言执行环境, 进入 C 语言部分的入口函数 start_kernel.*/b __primary_switchENDPROC(stext)
🍕el2_setup 函数
el2_setup 函数的主要工作如下:
- 进入内核的时候,ARM64 处理器的异常级别可能是 1 或者 2
- 如果异常级别是 1,那么在异常级别 1 执行内核
如果异常级别是 2,那么根据处理器是否支持 虚拟化宿主扩展(Virtualization Host Extensions, VHE),决定是否需要降级到异常级别1
- 如果处理器支持虚拟化宿主扩展,那么在异常级别 2 执行内核
如果处理器不支持虚拟化宿主扩展,那么降级到异常级别 1,在异常级别 1 执行内核
🍔 __create_page_tables 函数
```c /*
- 设置初始化页表. 我们仅仅设置提供内核运行的最基本的页表数量.
- 需要完成下面的工作:
- 创建恒等映射, 开启 MMU (低地址, TTBR0)
- 启动 MMU 之后直接跳转到内核开始的数个 MB */ __create_page_tables: mov x28, lr
/*
- 使 idmap 和 swapper 页表无效,以避免收回潜在的脏缓存. */ adrp x0, idmap_pg_dir adrp x1, swapper_pg_dir + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE bl __inval_cache_range
/*
- 清空 idmap 和 swapper 页表. */ adrp x0, idmap_pg_dir adrp x6, swapper_pg_dir + 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 cmp x0, x6 b.lo 1b
mov x7, SWAPPER_MM_MMUFLAGS
/*
- 创建恒等映射. */ adrp x0, idmap_pg_dir adrp x3, idmap_text_start // pa(__idmap_text_start)
ifndef CONFIG_ARM64_VA_BITS_48
define EXTRA_SHIFT (PGDIR_SHIFT + PAGE_SHIFT - 3)
define EXTRA_PTRS (1 << (48 - EXTRA_SHIFT))
/** If VA_BITS < 48, it may be too small to allow for an ID mapping to be* created that covers system RAM if that is located sufficiently high* in the physical address space. So for the ID map, use an extended* virtual range in that case, by configuring an additional translation* level.* First, we have to verify our assumption that the current value of* VA_BITS was chosen such that all translation levels are fully* utilised, and that lowering T0SZ will always result in an additional* translation level to be configured.*/
if VA_BITS != EXTRA_SHIFT
error “Mismatch between VA_BITS and page size/number of translation levels”
endif
/** 计算 TCR_EL1.T0SZ 的最大允许值, 以便可以映射整个ID映射区域.* 由于 T0SZ == (64 - #bits used), 这个数字恰好等于* __idmap_text_end 的物理地址中前导零的数量.*/adrp x5, __idmap_text_endclz x5, x5cmp x5, TCR_T0SZ(VA_BITS) // default T0SZ small enough?b.ge 1f // .. then skip additional leveladr_l x6, idmap_t0szstr x5, [x6]dmb sydc ivac, x6 // Invalidate potentially stale cache linecreate_table_entry x0, x3, EXTRA_SHIFT, EXTRA_PTRS, x5, x6
1:
endif
create_pgd_entry x0, x3, x5, x6mov x5, x3 // __pa(__idmap_text_start)adr_l x6, __idmap_text_end // __pa(__idmap_text_end)create_block_map x0, x7, x3, x5, x6/** 映射内核镜像 (从 PHYS_OFFSET 开始).*/adrp x0, swapper_pg_dirmov_q x5, KIMAGE_VADDR + TEXT_OFFSET // compile time __va(_text)add x5, x5, x23 // add KASLR displacementcreate_pgd_entry x0, x5, x3, x6adrp x6, _end // runtime __pa(_end)adrp x3, _text // runtime __pa(_text)sub x6, x6, x3 // _end - _textadd x6, x6, x5 // runtime __va(_end)create_block_map x0, x7, x3, x5, x6/** 由于页表中填充了不可缓存的访问 (禁用了MMU),* 再次使 idmap 和 swapper 页表无效,以删除任何加载的缓存线.*/adrp x0, idmap_pg_diradrp x1, swapper_pg_dir + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZEdmb sybl __inval_cache_rangeret x28
ENDPROC(__create_page_tables)
__create_page_tables 函数的主要工作如下:1. 创建恒等映射(identity mapping)1. 为内核镜像创建映射恒等映射的特点是虚拟地址和物理地址相同,是为了在开启处理器的内存管理单元的一瞬间能够平滑过渡。<br />函数 __enable_mmu 负责开启内存管理单元,内核把函数 __enable_mmu 附近的代码放在恒等映射代码节(.idmap.text)里面,恒等映射代码节的起始地址存放在全局变量 __idmap_text_start 中,结束地址存放在全局变量 __idmap_text_end 中。<br />恒等映射是为恒等映射代码节创建的映射,idmap_pg_dir 是恒等映射的页全局目录(即第一级页表)的起始地址。<br />在内核的页表中为内核镜像创建映射,内核镜像的起始地址是 _text,结束地址是 _end,swapper_pg_dir 是内核的页全局目录的起始地址。<a name="7kEF5"></a>## 🍟__primary_switch 函数__primary_switch 函数的主要执行流程如下:1. 调用函数 __enable_mmu 以开启内存管理单元1. 调用函数 __primary_switched<a name="VnZSc"></a>### 🚗__enable_mmu 函数__enable_mmu 函数的主要执行流程如下:1. 把转换表基准寄存器 0(TTBR0_EL1)设置为恒等映射的页全局目录的起始物理地址1. 把转换表基准寄存器 1(TTBR1_EL1)设置为内核的页全局目录的起始物理地址1. 设置系统控制寄存器(SCTLR_EL1),开启内存管理单元,以后执行程序时内存管理单元将会把虚拟地址转换成物理地址<a name="zQj0k"></a>### 🚓__primary_switched 函数```c/** 下面的代码需要在 MMU 开启后执行.** x0 = __PHYS_OFFSET*/__primary_switched:adrp x4, init_thread_unionadd sp, x4, #THREAD_SIZE // 将栈指针指向 0 号线程顶部adr_l x5, init_taskmsr sp_el0, x5 // 保存线程信息adr_l x8, vectors // 将 VBAR_EL1 设置为虚拟msr vbar_el1, x8 // 向量表的初始地址isbstp xzr, x30, [sp, #-16]!mov x29, spstr_l x21, __fdt_pointer, x5 // 保存 FDT 指针ldr_l x4, kimage_vaddr // 保存内核虚拟地址sub x4, x4, x0 // 与实际地址之间str_l x4, kimage_voffset, x5 // 的差值// Clear BSSadr_l x0, __bss_startmov x1, xzradr_l x2, __bss_stopsub x2, x2, x0bl __pi_memsetdsb ishst // 用 0 初始化内核的未初始化数据段/** 下面的这些宏配置不是特别懂*/#ifdef CONFIG_KASANbl kasan_early_init#endif#ifdef CONFIG_RANDOMIZE_BASEtst x23, ~(MIN_KIMG_ALIGN - 1) // already running randomized?b.ne 0fmov x0, x21 // pass FDT address in x0mov x1, x23 // pass modulo offset in x1bl kaslr_early_init // parse FDT for KASLR optionscbz x0, 0f // KASLR disabled? just proceedorr x23, x23, x0 // record KASLR offsetldp x29, x30, [sp], #16 // we must enable KASLR, returnret // to __primary_switch()0:#endifb start_kernel // 调用 C 语言函数 start_kernelENDPROC(__primary_switched)
__primary_switched 函数的执行流程如下
- 把当前异常级别的栈指针寄存器设置为 0 号线程内核栈的顶部(init_thread_union +THREAD_SIZE)
- 把异常级别 0 的栈指针寄存器(SP_EL0)设置为 0 号线程的结构体 thread_info 的地址(init_task.thread_info)
- 把向量基准地址寄存器(VBAR_EL1)设置为异常向量表的起始地址(vectors)
- 计算内核镜像的起始虚拟地址(kimage_vaddr)和物理地址的差值,保存在全局变量 kimage_voffset 中
- 用 0 初始化内核的未初始化数据段
- 调用 C 语言函数 start_kernel
