[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_TEXT
			SOFTIRQENTRY_TEXT
			ENTRY_TEXT
			TEXT_TEXT
			SCHED_TEXT
			LOCK_TEXT
			KPROBES_TEXT
			HYPERVISOR_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 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

	.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_state
	bl	preserve_boot_args
	bl	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, 0f
	adrp	x0, __idmap_text_start
	adr_l	x1, __idmap_text_end
	adr_l	x2, dcache_clean_poc
	blr	x2
0:	mov	x0, x19
	bl	init_kernel_el			// w0=cpu_boot_mode
	mov	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 > 48
	mrs_s	x0, SYS_ID_AA64MMFR2_EL1
	tst	x0, #0xf << ID_AA64MMFR2_EL1_VARange_SHIFT
	mov	x0, #VA_BITS
	mov	x25, #VA_BITS_MIN
	csel	x25, x25, x0, eq
	mov	x0, x25
#endif
	bl	__cpu_setup			// initialise processor
	b	__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_state
	bl	preserve_boot_args
	bl	create_idmap
这里调用了三个函数,分别是 record_mmu_state、preserve_boot_args 和 create_idmap。

	cbz	x19, 0f
	adrp	x0, __idmap_text_start
	adr_l	x1, __idmap_text_end
	adr_l	x2, dcache_clean_poc
	blr	x2
0:	mov	x0, x19
	bl	init_kernel_el			// w0=cpu_boot_mode
	mov	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 > 48
	mrs_s	x0, SYS_ID_AA64MMFR2_EL1
	tst	x0, #0xf << ID_AA64MMFR2_EL1_VARange_SHIFT
	mov	x0, #VA_BITS
	mov	x25, #VA_BITS_MIN
	csel	x25, x25, x0, eq
	mov	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 processor
	b	__primary_switch
这里调用了 __cpu_setup 函数进行处理器初始化。然后,使用无条件分支指令 b 跳转到 __primary_switch,进入主要的切换处理。

3. __primary_switch

SYM_FUNC_START_LOCAL(__primary_switch)
	adrp	x1, reserved_pg_dir
	adrp	x2, init_idmap_pg_dir
	bl	__enable_mmu
#ifdef CONFIG_RELOCATABLE
	adrp	x23, KERNEL_START
	and	x23, x23, MIN_KIMG_ALIGN - 1
#ifdef CONFIG_RANDOMIZE_BASE
	mov	x0, x22
	adrp	x1, init_pg_end
	mov	sp, x1
	mov	x29, xzr
	bl	__pi_kaslr_early_init
	and	x24, x0, #SZ_2M - 1		// capture memstart offset seed
	bic	x0, x0, #SZ_2M - 1
	orr	x23, x23, x0			// record kernel offset
#endif
#endif
	bl	clear_page_tables
	bl	create_kernel_mapping

	adrp	x1, init_pg_dir
	load_ttbr1 x1, x1, x2
#ifdef CONFIG_RELOCATABLE
	bl	__relocate_kernel
#endif
	ldr	x8, =__primary_switched
	adrp	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_task
	init_cpu_task x4, x5, x6

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

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

	str_l	x21, __fdt_pointer, x5		// Save FDT pointer

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

	mov	x0, x20
	bl	set_cpu_boot_mode_flag

	// Clear BSS
	adr_l	x0, __bss_start
	mov	x1, xzr
	adr_l	x2, __bss_stop
	sub	x2, x2, x0
	bl	__pi_memset
	dsb	ishst				// Make zero page visible to PTW

#if VA_BITS > 48
	adr_l	x8, vabits_actual		// Set this early so KASAN early init
	str	x25, [x8]			// ... observes the correct value
	dc	civac, x8			// Make visible to booting secondaries
#endif

#ifdef CONFIG_RANDOMIZE_BASE
	adrp	x5, memstart_offset_seed	// Save KASLR linear map seed
	strh	w24, [x5, :lo12:memstart_offset_seed]
#endif
#if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)
	bl	kasan_early_init
#endif
	mov	x0, x21				// pass FDT address in x0
	bl	early_fdt_map			// Try mapping the FDT early
	mov	x0, x20				// pass the full boot status
	bl	init_feature_override		// Parse cpu feature overrides
#ifdef CONFIG_UNWIND_PATCH_PAC_INTO_SCS
	bl	scs_patch_vmlinux
#endif
	mov	x0, x20
	bl	finalise_el2			// Prefer VHE if possible
	ldp	x29, x30, [sp], #16
	bl	start_kernel
	ASM_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, tmp2
	phys_to_ttbr	\tmp1, \pgtbl
	offset_ttbr1 	\tmp1, \tmp2
	msr		ttbr1_el1, \tmp1
	isb
	.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_52
	mrs_s	\tmp, SYS_ID_AA64MMFR2_EL1
	and	\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_52
	orr	\ttbr, \phys, \phys, lsr #46
	and	\ttbr, \ttbr, #TTBR_BADDR_MASK_52
#else
	mov	\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页表 - - 启动过程临时页表创建过程

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值