[Linux kernel] [ARM64] boot 流程梳理
一、启动汇编代码部分
0. 链接文件找代码段入口 – _text
arch/arm64/kernel/vmlinux.lds.S
ENTRY(_text)
. = KIMAGE_VADDR;.head.text : {_text = .;HEAD_TEXT}.text : ALIGN(SEGMENT_ALIGN) { /* Real text segment */_stext = .; /* Text and read-only data */IRQENTRY_TEXTSOFTIRQENTRY_TEXTENTRY_TEXTTEXT_TEXTSCHED_TEXTLOCK_TEXTKPROBES_TEXTHYPERVISOR_TEXT*(.gnu.warning)}. = ALIGN(SEGMENT_ALIGN);_etext = .; /* End of text section */
include/asm-generic/vmlinux.lds.h
/* Section used for early init (in .S files) */
#define HEAD_TEXT KEEP(*(.head.text))
/** If padding is applied before .head.text, virt<->phys conversions will fail.*/
ASSERT(_text == KIMAGE_VADDR, "HEAD is misaligned") // 校验内核入口地址是否正确ASSERT(swapper_pg_dir - reserved_pg_dir == RESERVED_SWAPPER_OFFSET,"RESERVED_SWAPPER_OFFSET is wrong!")#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
ASSERT(swapper_pg_dir - tramp_pg_dir == TRAMP_SWAPPER_OFFSET,"TRAMP_SWAPPER_OFFSET is wrong!")
#endif
1. Kernel startup entry point – __HEAD
include/linux/init.h
#define __HEAD .section ".head.text","ax"
arch/arm64/kernel/head.S
/** Kernel startup entry point.* ---------------------------** The requirements are:* MMU = off, D-cache = off, I-cache = on or off,* x0 = physical address to the FDT blob.** Note that the callee-saved registers are used for storing variables* that are useful before the MMU is enabled. The allocations are described* in the entry routines.*/__HEAD/** DO NOT MODIFY. Image header expected by Linux boot-loaders.*/efi_signature_nop // special NOP to identity as PE/COFF executableb primary_entry // branch to kernel start, magic.quad 0 // Image load offset from start of RAM, little-endianle64sym _kernel_size_le // Effective size of kernel image, little-endianle64sym _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.section ".idmap.text","a"
这段代码是内核启动的入口点。它定义了一些要求和操作。首先,对于内核启动入口点,需要满足以下条件:MMU(内存管理单元)处于关闭状态。
D-cache(数据缓存)处于关闭状态。
I-cache(指令缓存)可以在启动时处于打开或关闭状态。
寄存器 x0 中存储了 FDT(平台设备树) blob 的物理地址。
注释中提到,被调用保存的寄存器用于存储在启用 MMU 之前有用的变量。这些分配在入口例程中进行描述。代码中的一些关键部分包括:efi_signature_nop:标识为 PE/COFF 可执行文件的特殊 NOP(无操作码)。
b primary_entry:跳转到内核的起始地址,开始内核启动的魔术操作。
.quad 指令:用于定义保留的、未使用的空间。
.ascii 指令:用于定义特定的魔数标识符。
.long 指令:指定到 PE 头的偏移量。
最后,代码进入了 .idmap.text 段,该段是一个特别的代码段,用于创建 ID 映射。
2. primary_entry.
/** The following callee saved general purpose registers are used on the* primary lowlevel boot path:** Register Scope Purpose* x19 primary_entry() .. start_kernel() whether we entered with the MMU on* x20 primary_entry() .. __primary_switch() CPU boot mode* x21 primary_entry() .. start_kernel() FDT pointer passed at boot in x0* x22 create_idmap() .. start_kernel() ID map VA of the DT blob* x23 primary_entry() .. start_kernel() physical misalignment/KASLR offset* x24 __primary_switch() linear map KASLR seed* x25 primary_entry() .. start_kernel() supported VA size* x28 create_idmap() callee preserved temp register*/
这段注释列出了在主要的低级引导路径中使用的一些被调用保存的通用寄存器。每个寄存器都有其作用和使用范围。x19:从 primary_entry() 到 start_kernel(),用于判断是否开启了 MMU(内存管理单元)。
x20:从 primary_entry() 到 __primary_switch(),用于指示 CPU 的引导模式。
x21:从 primary_entry() 到 start_kernel(),用于传递启动时在 x0 中传递的 FDT(平台设备树)指针。
x22:从 create_idmap() 到 start_kernel(),用于存储设备树 blob 的 ID 映射虚拟地址。
x23:从 primary_entry() 到 start_kernel(),用于存储物理对齐错误或 KASLR(内核地址空间布局随机化)偏移量。
x24:在 __primary_switch() 中使用,用于线性映射的 KASLR 种子。
x25:从 primary_entry() 到 start_kernel(),用于存储支持的虚拟地址大小。
x28:在 create_idmap() 中使用,作为被调用保存的临时寄存器。
这些寄存器在引导过程中具有特定的功能和用途,并且由于它们是被调用保存的寄存器,必须在函数调用之间进行保存和恢复,以保证数据的正确性和稳定性。
下边代码片段中的cbz/adrp/adr_l/blr四条指令(宏)的用法可查阅如下wiki:
ARM64 指令用法整理
SYM_CODE_START(primary_entry)bl record_mmu_statebl preserve_boot_argsbl create_idmap/** If we entered with the MMU and caches on, clean the ID mapped part* of the primary boot code to the PoC so we can safely execute it with* the MMU off.*//* 这段代码是用于在MMU(内存管理单元)和缓存打开的情况下,清除主引导代码的ID映射部分,以确保在关闭MMU时能够安全地执行它。这段注释描述了以下操作:如果进入此代码段时MMU和缓存已经打开,那么需要将主引导代码的ID映射部分清除,以确保后续在关闭MMU时不会出现映射错误。具体操作可以是将主引导代码所在的地址范围与物理内存进行映射的关系解除,以使得在关闭MMU时不会访问到错误的内存地址。 */cbz x19, 0fadrp x0, __idmap_text_startadr_l x1, __idmap_text_endadr_l x2, dcache_clean_pocblr x2
0: mov x0, x19bl init_kernel_el // w0=cpu_boot_modemov x20, x0/** The following calls CPU setup code, see arch/arm64/mm/proc.S for* details.* On return, the CPU will be ready for the MMU to be turned on and* the TCR will have been set.*/
#if VA_BITS > 48mrs_s x0, SYS_ID_AA64MMFR2_EL1tst x0, #0xf << ID_AA64MMFR2_EL1_VARange_SHIFTmov x0, #VA_BITSmov x25, #VA_BITS_MINcsel x25, x25, x0, eqmov x0, x25
#endifbl __cpu_setup // initialise processorb __primary_switch
SYM_CODE_END(primary_entry)
这段代码是内核启动的入口点。首先,它调用了一些函数:record_mmu_state:记录 MMU 状态的函数。
preserve_boot_args:保留引导参数的函数。
create_idmap:创建 ID 映射的函数。
接下来,如果在启动时 MMU 和缓存已经打开,则会清除主要启动代码区域的 ID 映射部分,以便可以在关闭 MMU 的情况下安全地执行它。然后,代码调用 init_kernel_el 函数进行内核初始化,将 cpu_boot_mode 作为参数传递给该函数。返回值存储在寄存器 x20 中。接下来是一些 CPU 设置代码的调用,详细信息可以在 arch/arm64/mm/proc.S 文件中找到。执行这些代码后,CPU 将准备好打开 MMU,并且 TCR 已经被设置好。最后,代码跳转到 __primary_switch 处,表示内核启动的转换。
下面是对给定代码的逐句详细讲解:
SYM_CODE_START(primary_entry)
这是一个符号宏,表示代码的起始点。bl record_mmu_statebl preserve_boot_argsbl create_idmap
这里调用了三个函数,分别是 record_mmu_state、preserve_boot_args 和 create_idmap。cbz x19, 0fadrp x0, __idmap_text_startadr_l x1, __idmap_text_endadr_l x2, dcache_clean_pocblr x2
0: mov x0, x19bl init_kernel_el // w0=cpu_boot_modemov x20, x0
这段代码首先检查寄存器 x19 是否为零,如果是(MMU && cache on的情况下,清除主引导代码的ID映射部分,以确保在关闭MMU时能够安全地执行它。),则跳转到标签 0(即跳过一段代码)。如果 x19 不为零,则执行以下指令:使用 adrp 指令将 __idmap_text_start 的地址所在的页基址保存在 x0。
使用 adr_l 指令将 __idmap_text_end 的地址保存在 x1。
使用 adr_l 指令将 dcache_clean_poc 的地址保存在 x2。
使用 blr 指令调用寄存器 x2 中的函数,清洁 ID 映射的部分代码。
接着,执行标签 0 处的代码:
将寄存器 x19 的值移动到寄存器 x0 中。
调用 init_kernel_el 函数,将 x0 的值(即 cpu_boot_mode)作为参数传递,并将返回值移动到寄存器 x20 中。
#if VA_BITS > 48mrs_s x0, SYS_ID_AA64MMFR2_EL1tst x0, #0xf << ID_AA64MMFR2_EL1_VARange_SHIFTmov x0, #VA_BITSmov x25, #VA_BITS_MINcsel x25, x25, x0, eqmov x0, x25
#endif
这是一个条件编译块,根据定义的宏 VA_BITS 的值来决定是否编译这段代码。如果 VA_BITS 大于 48,则执行以下指令:使用 mrs_s 指令将系统寄存器 SYS_ID_AA64MMFR2_EL1 的值放入寄存器 x0。
使用 tst 指令测试寄存器 x0 是否与 0xf << ID_AA64MMFR2_EL1_VARange_SHIFT 进行按位与操作的结果为零。
将寄存器 x0 的值设置为 VA_BITS(将虚拟地址位数设置为 VA_BITS)。
将寄存器 x25 设置为 VA_BITS_MIN(虚拟地址位数的最小值)。
根据上一步测试的结果,如果相等,则将寄存器 x0 的值复制到寄存器 x25 中。
将寄存器 x0 的值设置为寄存器 x25。bl __cpu_setup // initialise processorb __primary_switch
这里调用了 __cpu_setup 函数进行处理器初始化。然后,使用无条件分支指令 b 跳转到 __primary_switch,进入主要的切换处理。
3. __primary_switch
SYM_FUNC_START_LOCAL(__primary_switch)adrp x1, reserved_pg_diradrp x2, init_idmap_pg_dirbl __enable_mmu
#ifdef CONFIG_RELOCATABLEadrp x23, KERNEL_STARTand x23, x23, MIN_KIMG_ALIGN - 1
#ifdef CONFIG_RANDOMIZE_BASEmov x0, x22adrp x1, init_pg_endmov sp, x1mov x29, xzrbl __pi_kaslr_early_initand x24, x0, #SZ_2M - 1 // capture memstart offset seedbic x0, x0, #SZ_2M - 1orr x23, x23, x0 // record kernel offset
#endif
#endifbl clear_page_tablesbl create_kernel_mappingadrp x1, init_pg_dirload_ttbr1 x1, x1, x2
#ifdef CONFIG_RELOCATABLEbl __relocate_kernel
#endifldr x8, =__primary_switchedadrp x0, KERNEL_START // __pa(KERNEL_START)br x8
SYM_FUNC_END(__primary_switch)
这段代码是内核启动的转换点。首先,代码使用 adrp 指令加载保留页目录表的地址到寄存器 x1 中,加载初始 ID 映射页目录表的地址到寄存器 x2 中。然后,调用 __enable_mmu 函数启用 MMU。接下来,根据配置进行一些处理:如果设置了可重定位选项 CONFIG_RELOCATABLE,则代码使用 adrp 指令加载内核起始地址到寄存器 x23 中,并将其与 MIN_KIMG_ALIGN - 1 进行与运算。如果启用了随机化基址 CONFIG_RANDOMIZE_BASE,则会执行一些随机化初始化的操作,并将结果存储在寄存器 x23 中。
如果没有设置可重定位选项,代码直接跳过这些处理。
接下来调用 clear_page_tables 函数清除页表,并调用 create_kernel_mapping 函数创建内核映射。然后,使用 adrp 指令加载初始页目录表的地址到寄存器 x1 中,并通过 load_ttbr1 指令将其加载到 TTBR1 寄存器中。如果配置中启用了可重定位选项 CONFIG_RELOCATABLE,代码还会调用 __relocate_kernel 函数进行内核重定位。最后,代码使用 ldr 指令加载 __primary_switched 标签的地址到寄存器 x8 中,使用 adrp 指令加载内核起始地址的物理地址 __pa(KERNEL_START) 到寄存器 x0 中,然后通过 br 指令跳转到 x8 中的地址。这段代码主要是进行 MMU 相关的初始化和设置,并在适当的时机进行内核重定位。
4. __primary_switched
/** The following fragment of code is executed with the MMU enabled.** x0 = __pa(KERNEL_START)*/
SYM_FUNC_START_LOCAL(__primary_switched)adr_l x4, init_taskinit_cpu_task x4, x5, x6adr_l x8, vectors // load VBAR_EL1 with virtualmsr vbar_el1, x8 // vector table addressisbstp x29, x30, [sp, #-16]!mov x29, spstr_l x21, __fdt_pointer, x5 // Save FDT pointerldr_l x4, kimage_vaddr // Save the offset betweensub x4, x4, x0 // the kernel virtual andstr_l x4, kimage_voffset, x5 // physical mappingsmov x0, x20bl set_cpu_boot_mode_flag// Clear BSSadr_l x0, __bss_startmov x1, xzradr_l x2, __bss_stopsub x2, x2, x0bl __pi_memsetdsb ishst // Make zero page visible to PTW#if VA_BITS > 48adr_l x8, vabits_actual // Set this early so KASAN early initstr x25, [x8] // ... observes the correct valuedc civac, x8 // Make visible to booting secondaries
#endif#ifdef CONFIG_RANDOMIZE_BASEadrp x5, memstart_offset_seed // Save KASLR linear map seedstrh w24, [x5, :lo12:memstart_offset_seed]
#endif
#if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)bl kasan_early_init
#endifmov x0, x21 // pass FDT address in x0bl early_fdt_map // Try mapping the FDT earlymov x0, x20 // pass the full boot statusbl init_feature_override // Parse cpu feature overrides
#ifdef CONFIG_UNWIND_PATCH_PAC_INTO_SCSbl scs_patch_vmlinux
#endifmov x0, x20bl finalise_el2 // Prefer VHE if possibleldp x29, x30, [sp], #16bl start_kernelASM_BUG()
SYM_FUNC_END(__primary_switched)
这段代码在启用了MMU的情况下执行。首先,代码将 init_task 的地址加载到寄存器 x4 中,并调用 init_cpu_task 函数,传递寄存器 x4、x5 和 x6 作为参数进行初始化。然后,代码使用 adr_l 指令加载 vectors 标签的地址到寄存器 x8 中,并将 vbar_el1 寄存器设置为寄存器 x8 中的向量表地址。接着执行 isb 指令以确保指令序列的正确执行顺序。接下来,代码保存寄存器 x29 和 x30 到栈中,并将当前栈指针保存到寄存器 x29 中。接着,代码将寄存器 x21 的值存储到 __fdt_pointer 中,用于保存 FDT(设备树)的指针。代码通过 ldr_l 指令加载 kimage_vaddr 标签的地址到寄存器 x4 中,并将其与寄存器 x0 中的 KERNEL_START 物理地址相减,得到内核虚拟地址和物理地址之间的偏移量。然后,代码将偏移量存储到 kimage_voffset 中。代码将寄存器 x20 的值存储到寄存器 x0 中,并调用 set_cpu_boot_mode_flag 函数,设置 CPU 启动模式标志。接下来,代码将 __bss_start 的地址加载到寄存器 x0 中,将寄存器 xzr(零寄存器)的值加载到寄存器 x1 中,将 __bss_stop 的地址加载到寄存器 x2 中,然后通过调用 __pi_memset 函数清零 BSS 段。然后,代码执行 dsb ishst 指令,确保零页面(zero page)对页表行为(PTW)可见。接下来,如果虚拟地址位数大于48位,代码将 vabits_actual 标签的地址加载到寄存器 x8 中,并将寄存器 x25 的值存储到 [x8] 中。随后,代码执行 dc civac, x8 指令,使更新后的值对启动的次要处理器可见。如果配置中启用了随机化基址 CONFIG_RANDOMIZE_BASE,代码将 memstart_offset_seed 的地址加载到寄存器 x5 中,并将寄存器 w24 的低位字保存到 [x5, :lo12:memstart_offset_seed] 中。接下来,如果配置中启用了 KASAN(内核地址边界检查器)或者 KASAN 软件标签,代码将调用 kasan_early_init 函数进行早期初始化。然后,代码将寄存器 x21 的值加载到寄存器 x0 中,并调用 early_fdt_map 函数尝试早期映射 FDT。之后,代码将寄存器 x20 的值加载到寄存器 x0 中,并调用 init_feature_override 函数,解析 CPU 功能覆盖。如果配置中启用了针对 SCS(系统控制寄存器)修补 VMLinux,代码将调用 scs_patch_vmlinux 函数。接下来,代码将寄存器 x20 的值加载到寄存器 x0 中,并调用 finalise_el2 函数,尽可能优先选择 VHE(虚拟化扩展模式)。最后,代码从栈中恢复寄存器 x29 和 x30 的值,并调用 start_kernel 函数启动内核。如果执行到这里,表示存在错误,代码将触发一个错误(ASM_BUG)。
3.1 load_ttbr1
arch/arm64/include/asm/assembler.h
/** load_ttbr1 - install @pgtbl as a TTBR1 page table* pgtbl preserved* tmp1/tmp2 clobbered, either may overlap with pgtbl*/.macro load_ttbr1, pgtbl, tmp1, tmp2phys_to_ttbr \tmp1, \pgtbloffset_ttbr1 \tmp1, \tmp2msr ttbr1_el1, \tmp1isb.endm
/** Offset ttbr1 to allow for 48-bit kernel VAs set with 52-bit PTRS_PER_PGD.* orr is used as it can cover the immediate value (and is idempotent).* In future this may be nop'ed out when dealing with 52-bit kernel VAs.* ttbr: Value of ttbr to set, modified.*/.macro offset_ttbr1, ttbr, tmp
#ifdef CONFIG_ARM64_VA_BITS_52mrs_s \tmp, SYS_ID_AA64MMFR2_EL1and \tmp, \tmp, #(0xf << ID_AA64MMFR2_EL1_VARange_SHIFT)cbnz \tmp, .Lskipoffs_\@orr \ttbr, \ttbr, #TTBR1_BADDR_4852_OFFSET
.Lskipoffs_\@ :
#endif.endm
/** Arrange a physical address in a TTBR register, taking care of 52-bit* addresses.** phys: physical address, preserved* ttbr: returns the TTBR value*/.macro phys_to_ttbr, ttbr, phys
#ifdef CONFIG_ARM64_PA_BITS_52orr \ttbr, \phys, \phys, lsr #46and \ttbr, \ttbr, #TTBR_BADDR_MASK_52
#elsemov \ttbr, \phys
#endif.endm
/*
将物理地址整理到 TTBR 寄存器中,注意处理52位的地址。
phys: 物理地址,保持不变
ttbr: 返回 TTBR 的值 */
这段代码定义了一个宏 phys_to_ttbr,用于将物理地址转换为 TTBR 寄存器的值。根据不同的配置选项,它执行不同的操作。首先,代码检查是否定义了 CONFIG_ARM64_PA_BITS_52 这个配置选项(可能是在配置文件中定义的宏)。如果定义了,表示支持使用 52 位的物理地址。如果支持 52 位物理地址,则执行以下操作:使用逻辑或指令 orr,将 phys 和右移46位后的 phys 相加,结果保存到寄存器 ttbr 中。这样做可以将低 46 位和高 6 位合并成 52 位的物理地址。
使用逻辑与指令 and,将 ttbr 和 TTBR_BADDR_MASK_52 相与,以保留 TTBR 的基地址字段(BADDR)的低 52 位。
如果不支持 52 位物理地址,则执行以下操作:使用移动指令 mov,直接将 phys 的值复制到寄存器 ttbr 中。
最后,宏定义结束(.endm)。总之,这段代码用于根据配置选项将物理地址转换为 TTBR 寄存器的值。当支持 52 位物理地址时,通过对物理地址进行位运算和掩码操作来获得 TTBR 值。当不支持 52 位物理地址时,直接将物理地址复制到 TTBR 寄存器。具体的应用场景和上下文需要查看代码的其他部分来确定。
orr \ttbr, \phys, \phys, lsr #46这句为啥要这样运算呢,为啥不直接把phy直接保存到ttbr中呢
orr \ttbr, \phys, \phys, lsr #46 这句代码的目的是将物理地址的高 6 位和低 46 位合并成一个 52 位的地址,并将结果保存到 TTBR 寄存器中。这里之所以需要进行逻辑或运算(orr)的原因是为了合并两个物理地址的值。首先,使用逻辑右移指令(lsr)将 phys 的值向右移动 46 位,得到高 6 位的值。然后,将两个 phys 值进行逻辑或运算,将高 6 位和低 46 位合并在一起,形成一个完整的 52 位物理地址。这样做的原因可能是因为在某些系统中,物理地址的高 6 位可能包含一些特殊的标识位或者其他有意义的信息。因此,为了在 TTBR 寄存器中保留这些信息,需要将两个物理地址进行合并。如果直接将 phys 的值复制到 ttbr 中,将丢失掉高 6 位的信息,可能导致错误的行为或结果。因此,这段代码选择使用逻辑或运算将两个物理地址合并成一个 52 位地址,并将合并的结果保存到 ttbr 寄存器中。
二、启动C代码部分
1. start_kernel
参考:
基于aarch64分析kernel源码 三:启动代码分析
Linux页表 - - 启动过程临时页表创建过程