1. 内核启动入口

根据Makefile对KBUILD_LDS的定义,链接vmlinux使用的连接脚本为 arch/$(SRCARCH)/kernel/vmlinux.lds,实际上这个脚本是在编译过程中生成的。从链接脚本 arch/arm64/kernel/vmlinux.lds可以查到,程序的入口为 _text,镜像起始位置存放的是 .head.text段生成的指令。

 1OUTPUT_ARCH(aarch64)
 2ENTRY(_text)
 3
 4SECTIONS
 5{
 6 . = ((((((-(((1)) << ((((48))) - 1)))) + (0x08000000))) + (0x08000000)));
 7 .head.text : {
 8  _text = .;
 9  KEEP(*(.head.text))
10 }
11 ...
12}

也就是说,内核镜像的起始位置是 .head.text段生成的指令。

搜索 .head.text,可以找到 include/linux/init.h__HEAD定义 .section ".head.text","ax"

1/* For assembly routines */
2#define __HEAD      .section    ".head.text","ax"
3#define __INIT      .section    ".init.text","ax"
4#define __FINIT     .previous

再看一下 arch/arm64/kernel/vmlinux.lds是怎么生成的,编译日志中,会有 LDS arch/arm64/kernel/vmlinux.ldsscripts/Makefile.build中可以看到是对 arch/arm64/kernel/vmlinux.lds.S进行预处理得到了最终的链接脚本。

1# Linker scripts preprocessor (.lds.S -> .lds)
2# ---------------------------------------------------------------------------
3quiet_cmd_cpp_lds_S = LDS     $@
4      cmd_cpp_lds_S = $(CPP) $(cpp_flags) -P -U$(ARCH) \
5                             -D__ASSEMBLY__ -DLINKER_SCRIPT -o $@ $<
6
7$(obj)/%.lds: $(src)/%.lds.S FORCE
8        $(call if_changed_dep,cpp_lds_S)

通过搜索 __HEAD,可以看到程序起始代码位于 arch/arm64/kernel/head.S

2. head.S总览

head.S可以分为三段,分别为 .head.text.init.text.idmap.text。各个段包含的符号可以通过objdump或nm查看。

arch/arm64/boot/head.S开始的注释简单说明了内核启动的条件。更详细的内容可以查看内核文档 Documentation/arm64/booting.rstDocumentation/translations/zh_CN/arm64/booting.txt

 1/*
 2 * Kernel startup entry point.
 3 * ---------------------------
 4 *
 5 * The requirements are:
 6 *   MMU = off, D-cache = off, I-cache = on or off,
 7 *   x0 = physical address to the FDT blob.
 8 *
 9 * Note that the callee-saved registers are used for storing variables
10 * that are useful before the MMU is enabled. The allocations are described
11 * in the entry routines.
12 */

2.1. .head.text

顾名思义,这部分为头部内容,根据是否使能 CONFIG_EFI而有所不同,在未定义 CONFIG_EFI的情况下,实际工作只是跳转到 primary_entry

 1    __HEAD
 2    /*
 3     * DO NOT MODIFY. Image header expected by Linux boot-loaders.
 4     */
 5    efi_signature_nop			// special NOP to identity as PE/COFF executable
 6    b	primary_entry			// branch to kernel start, magic
 7    .quad	0				// Image load offset from start of RAM, little-endian
 8    le64sym	_kernel_size_le			// Effective size of kernel image, little-endian
 9    le64sym	_kernel_flags_le		// Informative flags, little-endian
10    .quad	0				// reserved
11    .quad	0				// reserved
12    .quad	0				// reserved
13    .ascii	ARM64_IMAGE_MAGIC		// Magic number
14    .long	.Lpe_header_offset		// Offset to the PE header.
15
16    __EFI_PE_HEADER

2.2. .init.text

1primary_entry
2preserve_boot_args
3clear_page_tables
4remap_region
5create_idmap
6create_kernel_mapping
7__primary_switched

2.3. .idmap.text

用于恒等映射,需要编译为位置无关码。

 1init_kernel_el
 2init_el1
 3init_el2
 4set_cpu_boot_mode_flag
 5secondary_holding_pen
 6secondary_entry
 7secondary_startup
 8__secondary_switched
 9__secondary_too_slow
10__enable_mmu
11__cpu_secondary_check52bitva
12__no_granule_support
13__primary_switch

3. 概览:从入口到start_kernel

从入口到start_kernel的部分主要是汇编代码,后续的很多子系统都会依赖这部分代码做的初始化。

查看6.1和6.6的修改,git diff v6.1 v6.6 -- arch/arm64/kernel/head.S,启动文件arch/arm64/kernel/head.S流程前后对比如下:

3.1. linux-6.6

 1_text()                                 /// 内核启动入口
 2\-- primary_entry()
 3    +-- record_mmu_state()              /// 记录当前MMU状态,保存到x19
 4    +-- preserve_boot_args()            /// 保存x0~x3到boot_args[0~3]
 5    +-- create_idmap()                  /// 建立恒等映射init_idmap_pg_dir和内核镜像映射init_pg_dir的页表
 6    +-- dcache_clean_poc()              /// 清空.idmap.text段数据缓存
 7    +-- init_kernel_el()                /// 根据内核运行异常等级进行配置,返回启动模式
 8    |   +-- init_el1()                  /// 通常情况下从EL1启动内核
 9    |   \-- init_el2()                  /// 从EL2启动内核,用于开启VHE(Virtualization Host Extensions)
10    +-- __cpu_setup()                   /// 为开启MMU做的CPU初始化
11    \-- __primary_switch()
12        +-- __enable_mmu()              /// 开启MMU,将init_idmap_pg_dir加载到ttbr0,reserved_pg_dir加载到ttbr1
13        +-- clear_page_tables()         /// 清空init_pg_dir
14        +-- create_kernel_mapping()     /// 填充init_pg_dir
15        +-- load_ttbr1()                /// 将init_pg_dir加载到ttbr1
16        \-- __primary_switched()        /// 初始化init_task栈,设置VBAR_EL1,保存FDT地址,计算kimage_voffset,清空bss段
17            +-- set_cpu_boot_mode_flag()/// 设置__boot_cpu_mode变量
18            +-- early_fdt_map()
19            |   +-- early_fixmap_init() /// 尝试建立fixmap的页表,可能失败,后边init_feature_override会用到
20            |   \-- fixmap_remap_fdt()  /// 如果成功建立fixmap页表,将fdt映射到fixmap的FIX_FDT区域
21            +-- init_feature_override() /// 根据BootLoader传入的bootargs,对一些参数的改写
22            +-- finalise_el2()          /// Prefer VHE if possible
23            \-- start_kernel()          /// 跳转到start_kernel执行
24                +-- setup_machine_fdt()
25                +-- arm64_memblock_init()
26                \-- paging_init()

3.2. linux-6.1

 1+-- _text()                                 /// 内核启动入口
 2    \-- primary_entry()
 3        +-- preserve_boot_args()            /// 保存x0~x3到boot_args[0~3]
 4        +-- init_kernel_el()                /// 根据内核运行异常等级进行配置,返回启动模式
 5        |   +-- init_el1()                  /// 通常情况下从EL1启动内核
 6        |   \-- init_el2()                  /// 从EL2启动内核,用于开启VHE(Virtualization Host Extensions)
 7        +-- create_idmap()                  /// 建立恒等映射init_idmap_pg_dir和内核镜像映射init_pg_dir的页表
 8        +-- __cpu_setup()                   /// 为开启MMU做的CPU初始化
 9        \-- __primary_switch()
10            +-- __enable_mmu()              /// 开启MMU,将init_idmap_pg_dir加载到ttbr0,reserved_pg_dir加载到ttbr1
11            +-- clear_page_tables()         /// 清空init_pg_dir
12            +-- create_kernel_mapping()     /// 填充init_pg_dir
13            +-- load_ttbr1()                /// 将init_pg_dir加载到ttbr1
14            \-- __primary_switched()        /// 初始化init_task栈,设置VBAR_EL1,保存FDT地址,计算kimage_voffset,清空bss段
15                +-- set_cpu_boot_mode_flag()/// 设置__boot_cpu_mode变量
16                +-- early_fdt_map()
17                |   +-- early_fixmap_init() /// 尝试建立fixmap的页表,可能失败,后边init_feature_override会用到
18                |   \-- fixmap_remap_fdt()  /// 如果成功建立fixmap页表,将fdt映射到fixmap的FIX_FDT区域
19                +-- init_feature_override() /// 根据BootLoader传入的bootargs,对一些参数的改写
20                +-- finalise_el2()          /// Prefer VHE if possible
21                \-- start_kernel()          /// 跳转到start_kernel执行

如下代码分析来自linux-6.1。

4. MMU开启之前:primary_entry

在内核启动入口直接跳转到primary_entry,这是MMU开启之前所有函数的总流程。

 1    /*
 2     * The following callee saved general purpose registers are used on the
 3     * primary lowlevel boot path:
 4     *
 5     *  Register   Scope                      Purpose
 6     *  x20        primary_entry() .. __primary_switch()    CPU boot mode
 7     *  x21        primary_entry() .. start_kernel()        FDT pointer passed at boot in x0
 8     *  x22        create_idmap() .. start_kernel()         ID map VA of the DT blob
 9     *  x23        primary_entry() .. start_kernel()        physical misalignment/KASLR offset
10     *  x24        __primary_switch()                       linear map KASLR seed
11     *  x25        primary_entry() .. start_kernel()        supported VA size
12     *  x28        create_idmap()                           callee preserved temp register
13     */
14SYM_CODE_START(primary_entry)
15    bl	preserve_boot_args  /// 保存启动参数到boot_args数组
16    bl	init_kernel_el      /// 返回时,w0=cpu_boot_mode
17    mov	x20, x0             /// 保存cpu_boot_mode到x20
18    bl	create_idmap
19
20    /*
21     * The following calls CPU setup code, see arch/arm64/mm/proc.S for
22     * details.
23     * On return, the CPU will be ready for the MMU to be turned on and
24     * the TCR will have been set.
25     */
26#if VA_BITS > 48
27    mrs_s	x0, SYS_ID_AA64MMFR2_EL1
28    tst	x0, #0xf << ID_AA64MMFR2_LVA_SHIFT
29    mov	x0, #VA_BITS
30    mov	x25, #VA_BITS_MIN
31    csel	x25, x25, x0, eq
32    mov	x0, x25
33#endif
34    bl	__cpu_setup         /// 为开启MMU做的CPU初始化
35    b	__primary_switch
36SYM_CODE_END(primary_entry)

4.1. preserve_boot_args

主要工作是将FDT的基地址保存到x21寄存器,将启动参数(x0~x3)保存到boot_args数组。

 1/*
 2 * Preserve the arguments passed by the bootloader in x0 .. x3
 3 */
 4SYM_CODE_START_LOCAL(preserve_boot_args)
 5    mov	x21, x0			/// x21=FDT,x0是uboot传入的第一个参数,记录fdt的基地址,将x0的值保存到x21寄存器备份
 6
 7    adr_l	x0, boot_args       /// 读取boot_args变量的当前地址到x0,此时MMU处于关闭状态,访问的是物理地址
 8    stp	x21, x1, [x0]       /// record the contents of, x0 .. x3 at kernel entry
 9    stp	x2, x3, [x0, #16]   /// 将x0~x3保存到boot_args[0~3]
10
11    dmb	sy                  /// 确保stp指令完成
12
13    add	x1, x0, #0x20       /// 4 x 8 bytes,boot_args数组的大小
14    b	dcache_inval_poc    /// 使boot_args[]数组对应的高速缓存失效
15SYM_CODE_END(preserve_boot_args)

adr_l是在 arch/arm64/include/asm/assembler.h定义的一个宏,用于操作当前PC值前后4G范围内存。

dmb sy在全系统高速缓冲范围内做一次内存屏障,保证前面的stp指令运行顺序正确,保证stp在调用dcache_inval_poc前完成。 dcache_inval_poc传入参数为boot_args数组的起始和结束地址,函数的作用是使boot_args数组对应的高速缓存失效,并清除这些缓存。

 1/*
 2 *	dcache_inval_poc(start, end)
 3 *
 4 * 	Ensure that any D-cache lines for the interval [start, end)
 5 * 	are invalidated. Any partial lines at the ends of the interval are
 6 *	also cleaned to PoC to prevent data loss.
 7 *
 8 *	- start   - kernel start address of region
 9 *	- end     - kernel end address of region
10 */

4.2. init_kernel_el

判断启动的模式是EL2还是非安全模式的EL1,并进行相关级别的系统配置(ARMv8中EL2是hypervisor模式,EL1是标准的内核模式),然后使用w0返回启动模式(BOOT_CPU_MODE_EL1或BOOT_CPU_MODE_EL2)。

1#define BOOT_CPU_MODE_EL1	(0xe11)
2#define BOOT_CPU_MODE_EL2	(0xe12)
3
4/* Current Exception Level values, as contained in CurrentEL */
5#define CurrentEL_EL1		(1 << 2)
6#define CurrentEL_EL2		(2 << 2)
 1/*
 2 * Starting from EL2 or EL1, configure the CPU to execute at the highest
 3 * reachable EL supported by the kernel in a chosen default state. If dropping
 4 * from EL2 to EL1, configure EL2 before configuring EL1.
 5 *
 6 * Since we cannot always rely on ERET synchronizing writes to sysregs (e.g. if
 7 * SCTLR_ELx.EOS is clear), we place an ISB prior to ERET.
 8 *
 9 * Returns either BOOT_CPU_MODE_EL1 or BOOT_CPU_MODE_EL2 in x0 if
10 * booted in EL1 or EL2 respectively, with the top 32 bits containing
11 * potential context flags. These flags are *not* stored in __boot_cpu_mode.
12 */
13SYM_FUNC_START(init_kernel_el)
14    mrs	x0, CurrentEL           /// 读取当前EL等级
15    cmp	x0, #CurrentEL_EL2      /// 如果是EL2则跳转到init_el2,否则继续向下执行init_el1的代码
16    b.eq	init_el2
17
18SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)
19    ...
20    eret
21SYM_INNER_LABEL(init_el2, SYM_L_LOCAL)
22    ...
23    eret
24SYM_FUNC_END(init_kernel_el)

4.2.1. init_el1

配置CPU的大小端模式,将启动模式BOOT_CPU_MODE_EL1写入w0,然后返回到primary_entry。

1#define INIT_SCTLR_EL1_MMU_OFF \
2    (ENDIAN_SET_EL1 | SCTLR_EL1_LSMAOE | SCTLR_EL1_nTLSMD | \
3     SCTLR_EL1_EIS  | SCTLR_EL1_TSCXT  | SCTLR_EL1_EOS)
4
5#define INIT_PSTATE_EL1 \
6    (PSR_D_BIT | PSR_A_BIT | PSR_I_BIT | PSR_F_BIT | PSR_MODE_EL1h)
1SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)
2    mov_q	x0, INIT_SCTLR_EL1_MMU_OFF      /// MMU关闭时,对sctlr_el1的赋值
3    msr	sctlr_el1, x0                       /// 配置CPU的大小端模式,EE域用来配置EL1,E0E域用来配置EL0
4    isb                                     /// 配置CPU大小端模式后,确保前面的指令都运行完成
5    mov_q	x0, INIT_PSTATE_EL1             ///
6    msr	spsr_el1, x0                        /// 将INIT_PSTATE_EL1写入spsr_el1
7    msr	elr_el1, lr                         /// 将返回地址写入elr_el1,lr是primary_entry中`bl init_kernel_el`的下一条指令地址
8    mov	w0, #BOOT_CPU_MODE_EL1              /// 记录启动模式
9    eret                                    /// 通过eret来使用ELR_ELx和SPSR_ELx来恢复PC和PSTATE

4.2.2. init_el2

vhe的全称是Virtualization Host Extension support。是armv8.1的新特性,其最终要就是支持type-2的hypervisors 这种扩展让kernel直接跑在el2上,这样可以减少host和guest之间share的寄存器,并减少overhead of virtualization 具体实现如下面的https://lwn.net/Articles/650524

 1SYM_INNER_LABEL(init_el2, SYM_L_LOCAL)
 2    mov_q	x0, HCR_HOST_NVHE_FLAGS
 3    msr	hcr_el2, x0
 4    isb
 5
 6    init_el2_state
 7
 8    /* Hypervisor stub */
 9    adr_l	x0, __hyp_stub_vectors
10    msr	vbar_el2, x0
11    isb
12
13    mov_q	x1, INIT_SCTLR_EL1_MMU_OFF
14
15    /*
16     * Fruity CPUs seem to have HCR_EL2.E2H set to RES1,
17     * making it impossible to start in nVHE mode. Is that
18     * compliant with the architecture? Absolutely not!
19     */
20    mrs	x0, hcr_el2
21    and	x0, x0, #HCR_E2H
22    cbz	x0, 1f
23
24    /* Set a sane SCTLR_EL1, the VHE way */
25    msr_s	SYS_SCTLR_EL12, x1
26    mov	x2, #BOOT_CPU_FLAG_E2H
27    b	2f
28
291:
30    msr	sctlr_el1, x1
31    mov	x2, xzr
322:
33    msr	elr_el2, lr
34    mov	w0, #BOOT_CPU_MODE_EL2
35    orr	x0, x0, x2
36    eret
37SYM_FUNC_END(init_kernel_el)

4.2.2.1. init_el2_state

 1/**
 2 * Initialize EL2 registers to sane values. This should be called early on all
 3 * cores that were booted in EL2. Note that everything gets initialised as
 4 * if VHE was not evailable. The kernel context will be upgraded to VHE
 5 * if possible later on in the boot process
 6 *
 7 * Regs: x0, x1 and x2 are clobbered.
 8 */
 9.macro init_el2_state
10    __init_el2_sctlr
11    __init_el2_timers
12    __init_el2_debug
13    __init_el2_lor
14    __init_el2_stage2
15    __init_el2_gicv3
16    __init_el2_hstr
17    __init_el2_nvhe_idregs
18    __init_el2_nvhe_cptr
19    __init_el2_fgt
20    __init_el2_nvhe_prepare_eret
21.endm

4.3. create_idmap

填充init_idmap_pg_dir和init_pg_dir页表,这个时候的映射是以块为单位的,每个块大小为2M。在开启MMU时,init_idmap_pg_dir会被加载到ttbr0。

init_idmap_pg_dir用于恒等映射,就是虚拟地址和物理地址相同的映射。linux-6.1的init_idmap_pg_dir替代了早期版本的idmap_pg_dir。之前版本的idmap_pg_dir只会映射idmap.text段,而init_idmap_pg_dir会映射整个内核镜像,在内核镜像之后,还会映射FDT,所以init_idmap_pg_dir映射的空间会比内核镜像大一些。 create_idmap首先将整个区间(包含内核镜像和FDT)映射为RX属性,再将init_pg_dir~init_pg_end重新映射为RW属性,最后把FDT以RW属性映射到内核镜像之后。

4.4. __cpu_setup

代码位于 arch/arm64/mm/proc.S。是为开启MMU而初始化处理器相关的代码,配置MMU,配置访问权限,内存地址划分等。 在虚拟地址小于48bit时,调用此函数前,x0记录虚拟地址位数。函数返回时x0记录了SCTLR_EL1要写入的值,最后传给__enable_mmu。

 1/*
 2 *	__cpu_setup
 3 *
 4 *	Initialise the processor for turning the MMU on.
 5 *
 6 * Input:
 7 *	x0 - actual number of VA bits (ignored unless VA_BITS > 48)
 8 * Output:
 9 *	Return in x0 the value of the SCTLR_EL1 register.
10 */

5. 开启MMU:__primary_switch

__primary_switch表示重要的切换,这个非常重要的切换就是开启MMU。开启MMU(__enable_mmu)前,CPU访问的是物理地址,__enable_mmu之后,CPU访问的是虚拟地址。这个虚拟地址最高位为0,使用的是TTBR0,而此时TTBR0执行的页表是init_idmap_pg_dir。而reserved_pg_dir这个时候还没有填充。

 1SYM_FUNC_START_LOCAL(__primary_switch)
 2    adrp	x1, reserved_pg_dir     /// 传递给__enable_mmu的参数,用于配置ttbr1
 3    adrp	x2, init_idmap_pg_dir   /// 传递给__enable_mmu的参数,用于配置ttbr0
 4    bl	__enable_mmu                /// 开启MMU
 5#ifdef CONFIG_RELOCATABLE
 6    adrp	x23, KERNEL_START
 7    and	x23, x23, MIN_KIMG_ALIGN - 1
 8#ifdef CONFIG_RANDOMIZE_BASE
 9    mov	x0, x22
10    adrp	x1, init_pg_end
11    mov	sp, x1
12    mov	x29, xzr
13    /// kaslr_early_init@arch/arm64/kernel/pi/kaslr_early.c
14    /// OBJCOPYFLAGS := --prefix-symbols=__pi_
15    bl	__pi_kaslr_early_init
16    and	x24, x0, #SZ_2M - 1		// capture memstart offset seed
17    bic	x0, x0, #SZ_2M - 1
18    orr	x23, x23, x0			// record kernel offset
19#endif
20#endif
21    /// 清空init_pg_dir
22    bl	clear_page_tables
23    /// 填充init_pg_dir,以块映射的方式映射内核镜像
24    bl	create_kernel_mapping
25
26    adrp	x1, init_pg_dir
27    load_ttbr1 x1, x1, x2           /// 加载init_pg_dir到ttbr1,为跳转到新的地址做准备
28#ifdef CONFIG_RELOCATABLE
29    bl	__relocate_kernel
30#endif
31    /// __primary_switched编译时的地址赋值给x8
32    /// 如果使能了CONFIG_RELOCATABLE,__relocate_kernel会给__primary_switched加上一个偏移
33    ldr	x8, =__primary_switched
34    /// 运行时的物理地址
35    adrp	x0, KERNEL_START		// __pa(KERNEL_START)
36    /// 跳转到__primary_switched虚拟地址运行
37    br	x8
38SYM_FUNC_END(__primary_switch)

__primary_switch的最后,使用 adrp x0, KERNEL_START记录了 _text的地址,也就是内核镜像起始的物理地址。

1#define KERNEL_START		_text
2#define KERNEL_END		_end

5.1. __enable_mmu

主要工作:

  1. 检查CPU是否支持软件设置的页面大小,如果不支持,CPU会在停止在这里。
  2. 将init_idmap_pg_dir和reserved_pg_dir分别加载到TTBR0_EL1和TTBR1_EL1。
  3. 开启MMU,并使本地icache失效。
 1/*
 2 * Enable the MMU.
 3 *
 4 *  x0  = SCTLR_EL1 value for turning on the MMU.
 5 *  x1  = TTBR1_EL1 value
 6 *  x2  = ID map root table address
 7 *
 8 * Returns to the caller via x30/lr. This requires the caller to be covered
 9 * by the .idmap.text section.
10 *
11 * Checks if the selected granule size is supported by the CPU.
12 * If it isn't, park the CPU
13 */
14SYM_FUNC_START(__enable_mmu)
15    mrs	x3, ID_AA64MMFR0_EL1
16    ubfx	x3, x3, #ID_AA64MMFR0_EL1_TGRAN_SHIFT, 4
17    cmp     x3, #ID_AA64MMFR0_EL1_TGRAN_SUPPORTED_MIN
18    b.lt    __no_granule_support
19    cmp     x3, #ID_AA64MMFR0_EL1_TGRAN_SUPPORTED_MAX
20    b.gt    __no_granule_support
21    phys_to_ttbr x2, x2
22    msr	ttbr0_el1, x2			// load TTBR0
23    load_ttbr1 x1, x1, x3
24
25    set_sctlr_el1	x0
26
27    ret
28SYM_FUNC_END(__enable_mmu)
1SYM_FUNC_START_LOCAL(__no_granule_support)
2    /* Indicate that this CPU can't boot and is stuck in the kernel */
3    update_early_cpu_boot_status \
4        CPU_STUCK_IN_KERNEL | CPU_STUCK_REASON_NO_GRAN, x1, x2
51:
6    wfe
7    wfi
8    b	1b
9SYM_FUNC_END(__no_granule_support)

5.2. clear_page_tables

create_idmap填充了init_pg_dir,这里对init_pg_dir进行清零。

memset(init_pg_dir, 0, init_pg_end - init_pg_dir)

 1SYM_FUNC_START_LOCAL(clear_page_tables)
 2    /*
 3     * Clear the init page tables.
 4     */
 5    adrp	x0, init_pg_dir /// 起始地址
 6    adrp	x1, init_pg_end /// 结束地址
 7    sub	x2, x1, x0
 8    mov	x1, xzr             /// xzr是一个特殊寄存器,读取时值为0
 9    b	__pi_memset			// tail call
10SYM_FUNC_END(clear_page_tables)

5.3. create_kernel_mapping

创建内核镜像映射,填充init_pg_dir,注意此时还是使用的块映射。

6. 开启MMU之后:__primary_switched

开启MMU之后,CPU访问的是虚拟地址。

  1. 准备0号进程内核栈
  2. VBAR_EL1中断向量表配置
  3. 计算kimage_voffset
  4. 保存CPU启动模式
  5. 清空BSS段
  6. 尝试fixmap映射
  7. 解析boot_args中CPU相关的一些特性,并修改默认值
  8. 跳转到start_kernel。
1/*
2 * The following fragment of code is executed with the MMU enabled.
3 *
4 *   x0 = __pa(KERNEL_START)
5 */
6SYM_FUNC_START_LOCAL(__primary_switched)

6.1. 初始化init_task栈空间

主要工作:

  1. 设置SP_EL0、SP_ELx、x29(FP)寄存器,配置init_task的栈
  2. 将per_cpu_offset写入TPIDR_ELx
1    adr_l	x4, init_task
2    init_cpu_task x4, x5, x6

6.1.1. init_cpu_task

先看一下涉及到的几个宏。arch/arm64/kernel/asm-offsets.c

1#define TSK_TI_CPU 16 /* offsetof(struct task_struct, thread_info.cpu) */
2#define TSK_STACK 32 /* offsetof(struct task_struct, stack) */
3#define S_STACKFRAME 304 /* offsetof(struct pt_regs, stackframe) */
4#define PT_REGS_SIZE 336 /* sizeof(struct pt_regs) */

在task_pt_regs(current)->stackframe创建一个最终帧记录,这样unwinder就可以根据任务堆栈中的位置来识别任何任务的最终帧记录。保留整个pt_regs空间使用户任务和kthread保持一致性。

 1    /*
 2     * Initialize CPU registers with task-specific and cpu-specific context.
 3     *
 4     * Create a final frame record at task_pt_regs(current)->stackframe, so
 5     * that the unwinder can identify the final frame record of any task by
 6     * its location in the task stack. We reserve the entire pt_regs space
 7     * for consistency with user tasks and kthreads.
 8     */
 9    .macro	init_cpu_task tsk, tmp1, tmp2
10    msr	sp_el0, \tsk                    /// 将init_task的地址写入sp_el0,内核运行于EL2或EL1,内核中会使用sp_el0来作为current
11
12    ldr	\tmp1, [\tsk, #TSK_STACK]       /// 获取init_task的栈地址,offsetof(struct task_struct, stack)
13    add	sp, \tmp1, #THREAD_SIZE         /// 栈是由高地址向下生长的,所以SP_ELx要加上THREAD_SIZE,init_task的栈是静态分配的,它指向init_stack这是一个数组。
14    sub	sp, sp, #PT_REGS_SIZE           /// 为struct pt_regs留出空间
15
16    stp	xzr, xzr, [sp, #S_STACKFRAME]   /// 将struct pt_regs的u64 stackframe[2]清零
17    add	x29, sp, #S_STACKFRAME          /// x29(FP)指向栈中pt_regs的stackframe
18
19    scs_load \tsk                       	/// 此处为空操作,详细信息可以参考arch/Kconfig中的SHADOW_CALL_STACK
20
21    adr_l	\tmp1, __per_cpu_offset     	/// 读取__per_cpu_offset[NR_CPUS]数组基地址
22    ldr	w\tmp2, [\tsk, #TSK_TI_CPU]     /// offsetof(struct task_struct, thread_info.cpu)
23    ldr	\tmp1, [\tmp1, \tmp2, lsl #3]   /// tmp1 = __per_cpu_offset[init_task.cpu << 3],通常来说,bootcpu为0
24    set_this_cpu_offset \tmp1           	/// 将当前cpu的per_cpu变量的offset值写入TPIDR_ELx
25    .endm

几个寄存器的最终结果:

SP_EL0 = &init_task SP_ELx = init_task.stack + THREAD_SIZE - sizeof(struct pt_regs) x29(FP) = SP_ELx + S_STACKFRAME

6.2. 中断向量基地址寄存器配置

将中断向量表的起始虚拟地址写入到VBAR_EL1。

1    adr_l	x8, vectors			// load VBAR_EL1 with virtual
2    msr	vbar_el1, x8			// vector table address
3    isb

6.3. 备份fp和lr寄存器

此时sp的值为 init_task.stack + THREAD_SIZE - sizeof(struct pt_regs)。主要工作如下:

  1. 将x29(FP)和x30(LR)分别保存到 sp-16sp-8的地址上,然后 sp -= 16
  2. 将sp的值写入到x29(FP)

这是实现了ARM64函数调用标准规定的栈布局,为后续函数调用的入栈出栈做好了准备。

1    stp	x29, x30, [sp, #-16]!
2    mov	x29, sp

6.4. 保存设备树物理地址到__fdt_pointer

__fdt_pointer是一个全局变量,在后续进行FDT映射时需要用到。

1    str_l	x21, __fdt_pointer, x5		// Save FDT pointer

6.5. 计算kimage_voffset

arch/arm64/mm/mmu.c

1u64 kimage_vaddr __ro_after_init = (u64)&_text;
2EXPORT_SYMBOL(kimage_vaddr);
3
4u64 kimage_voffset __ro_after_init;
5EXPORT_SYMBOL(kimage_voffset);

kimage_voffset记录了内核镜像映射后的虚拟地址与内核镜像在内存中的物理地址之间的差值。kimage_vaddr记录了 _text的链接地址,也就是最终 _text的虚拟地址(如果使能了CONFIG_RELOCATE,此值会在运行过程中改变),x0作为传入参数记录了 _text的物理地址,相减即可得到 kimage_voffset

1    ldr_l	x4, kimage_vaddr		// Save the offset between
2    sub	x4, x4, x0			// the kernel virtual and
3    str_l	x4, kimage_voffset, x5		// physical mappings

6.6. set_cpu_boot_mode_flag

将启动时的特权级别保存到__boot_cpu_mode[2]全局数组。 早期版本的内核set_cpu_boot_mode_flag会在primary_entry中执行,set_cpu_boot_mode_flag会在ret之前调用 dmb sydc ivac, x1来保证指令执行顺序和使高速缓存失效。 6.1版本内核改到了在 __primary_switched中执行,等到清空bss段后,通过 dsb ishst完成高速缓存的失效操作。

 1/*
 2 * Sets the __boot_cpu_mode flag depending on the CPU boot mode passed
 3 * in w0. See arch/arm64/include/asm/virt.h for more info.
 4 */
 5SYM_FUNC_START_LOCAL(set_cpu_boot_mode_flag)
 6    adr_l	x1, __boot_cpu_mode     /// x1记录__boot_cpu_mode[]的起始地址
 7    cmp	w0, #BOOT_CPU_MODE_EL2      /// w0记录启动时的异常等级
 8    b.ne	1f                      /// 如果不是从EL2启动,则跳转到1处
 9    add	x1, x1, #4                  /// 如果是从EL2启动,地址指向__boot_cpu_mode[1]
10    /// 保存启动模式到x1指向的地址,如果是从EL1启动,地址指向__boot_cpu_mode[0]
111:	str	w0, [x1]			// Save CPU boot mode
12    ret
13SYM_FUNC_END(set_cpu_boot_mode_flag)

简化的C语言伪代码:

1    u32 *x1 = &__boot_cpu_mode[0];
2    if (w0 == BOOT_CPU_MODE_EL2) {
3        x1++;   // *x1 = &__boot_cpu_mode[1]
4    }
5    *x1 = w0;

最终结果与如下代码相同

1    if (w0 == BOOT_CPU_MODE_EL2) {
2        __boot_cpu_mode[1] = BOOT_CPU_MODE_EL2;
3    } else {
4        __boot_cpu_mode[0] = BOOT_CPU_MODE_EL1;
5    }

6.7. 清空BSS段

清空BSS后,要使高速缓存失效。

1    // Clear BSS
2    adr_l	x0, __bss_start /// 起始地址
3    mov	x1, xzr             /// 要写入的值,xzr是一个特殊的寄存器,值为64位的0
4    adr_l	x2, __bss_stop  /// 结束地址
5    sub	x2, x2, x0          /// size = __bss_stop - __bss_start
6    bl	__pi_memset         /// memset(x0, x1, x2)
7    dsb	ishst				// Make zero page visible to PTW

6.8. 一些其他工作

 1#if VA_BITS > 48
 2    adr_l	x8, vabits_actual		// Set this early so KASAN early init
 3    str	x25, [x8]			// ... observes the correct value
 4    dc	civac, x8			// Make visible to booting secondaries
 5#endif
 6
 7#ifdef CONFIG_RANDOMIZE_BASE
 8    adrp	x5, memstart_offset_seed	// Save KASLR linear map seed
 9    strh	w24, [x5, :lo12:memstart_offset_seed]
10#endif
11#if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)
12    bl	kasan_early_init
13#endif

6.9. early_fdt_map

尝试建立fixmap的页表,可能失败,后边init_feature_override会用到。 如果失败,会在setup_arch通过early_fixmap_init重新映射。

1    mov	x0, x21				// pass FDT address in x0
2    bl	early_fdt_map			// Try mapping the FDT early

6.10. init_feature_override

根据BootLoader传入的bootargs参数,对一些参数的改写。

1    mov	x0, x20				// pass the full boot status
2    bl	init_feature_override		// Parse cpu feature overrides

6.11. finalise_el2

1    mov	x0, x20
2    bl	finalise_el2			// Prefer VHE if possible

6.12. 跳转start_kernel

从栈中恢复x29(FP)和x30(LR),sp重新指向 init_task.stack + THREAD_SIZE - sizeof(struct pt_regs)

1    ldp	x29, x30, [sp], #16     /// 从栈中恢复x29(FP)和x30(LR),sp += 16
2    bl	start_kernel            /// 跳转到start_kernel
3    ASM_BUG()                   /// 如果start_kernel返回到这里说明出错了
4SYM_FUNC_END(__primary_switched)

7. 参考资料

  1. Documentation/arm64/booting.rst
  2. Documentation/translations/zh_CN/arm64/booting.txt
  3. Linux 内核启动分析-BugMan-ChinaUnix博客
  4. 中断管理基础学习笔记 - 5.1 ARM64底层中断处理
  5. Linux kernel ARM64 寄存器tpidr_el1 的用处
  6. arm64: Extract early FDT mapping from kaslr_early_init()
  7. kernel启动流程-head.S的执行_4.el2_setup_kernel5.10 内核流程