ARM64启动过程分析

本文深入剖析ARM64架构下的启动流程,涵盖启动约定、内核启动步骤、页表创建、MMU开启等关键环节,并简要介绍启动过程中的其他功能实现。

arm64 启动过程分析

参考:DDI0487F_b_armv8_arm.pdf

1 启动约定(boot protocol)

arm64 启动涉及到的实际内容较为复杂也多,这里分析主要在主流程上,使用的 Linux 版本为 linux-5.0。

在系统启动过程中,首先由 bootloader 执行一系列操作,并最终将控制权交由 kernel。

这里的 bootloader 对于服务器常见是的 bios/uefi 加载,对于嵌入式典型的则是 u-boot,当然也可能是 Hypervisor 和 secure monitor,或者可能只是准备最小引导环境的少量指令。

不论 bootloader 是什么,arm64 linux 在引导阶段对 bootloader 提出以下要求:

  1. 设置并初始化RAM(必须)
  2. 准备好合适的设备树文件到 RAM 中,并提供 dtb 首地址给 kernel(必须)
  3. 解压内核镜像(可选)
  4. 将控制权交由kernel(必须)

在将控制权交由 kernel 时 Image 头部自身包含 64-byte header 信息,如下:

  u32 code0;			/* Executable code */
  u32 code1;			/* Executable code */
  u64 text_offset;		/* Image load offset, little endian */
  u64 image_size;		/* Effective Image size, little endian */
  u64 flags;			/* kernel flags, little endian */
  u64 res2	= 0;		/* reserved */
  u64 res3	= 0;		/* reserved */
  u64 res4	= 0;		/* reserved */
  u32 magic	= 0x644d5241;	/* Magic number, little endian, "ARM\x64" */
  u32 res5;			/* reserved (used for PE COFF offset) */
  1. code0/code1指向stext段,kernel执行代码的开始。
  2. 当支持EFI格式启动时,code0/code1将会被跳过,res5将是PE header偏移。
  3. flags 字段是小端的 64bit 组合,包含如下信息:
BIT 0:kernel字节序。1是BE,0是LE。
BIT 1-2:kernel页大小。
0 - 未指定
1 - 4K
2 - 16K
3 - 64K
BIT 3:kernel物理位置
0 - 2MB对齐应尽可能接近RAM低地址底部,因为后面内存不能通过线性映射访问。
1 - 2MB对齐可以在物理内存的任意位置。

在第四步控制权交给 kernel 时,对 CPU 状态,cache 也有以下要求:

  1. 主CPU通用寄存器设置
    x0 = 设备树首地址的物理地址
    x1 = 0
    x2 = 0
    x3 = 0
  2. CPU模式
    所有中断都必须在 PSTATE 中被屏蔽,DAIF(debug,SError,IRQ 和 FIQ)
    CPU 必须处于 EL2(为了访问虚拟化拓展,建议使用 EL2)或者非安全EL1中。
  3. Cache,MMUs
    MMU 必须是关闭状态。
    Icache 可以是关也可以是开。
    Dcache 必须是关闭,这是为了保证加载的内核镜像的地址范围是clean to Poc的。

内核中对上述描述的注释:

The requirements are:   
  MMU = off, D-cache = off, I-cache = on or off,
  x0 = physical address to the FDT blob.
  1. 另一个未说明但实际需要保证的要求:
    在所有CPU上,CNTFRQ必须设置好,CNTVOFF必须是关闭。

完成上述步骤即可将 cpu 控制权交由内核。

2 内核启动第一步

linux arm64 启动代码位于arch/arm64/kernel/head.S,入口代码如下:

	/*
	 * The following callee saved general purpose registers are used on the
	 * primary lowlevel boot path:
	 *
	 *  Register   Scope                      Purpose
	 *  x21        stext() .. start_kernel()  FDT pointer passed at boot in x0
	 *  x23        stext() .. start_kernel()  physical misalignment/KASLR offset
	 *  x28        __create_page_tables()     callee preserved temp register
	 *  x19/x20    __primary_switch()         callee preserved temp registers
	 */
ENTRY(stext)
	bl	preserve_boot_args
	bl	el2_setup			// Drop to EL1, w0=cpu_boot_mode
	adrp	x23, __PHYS_OFFSET
	and	x23, x23, MIN_KIMG_ALIGN - 1	// KASLR offset, defaults to 0
	bl	set_cpu_boot_mode_flag
	bl	__create_page_tables
	/*
	 * 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.
	 */
	bl	__cpu_setup			// initialise processor
	b	__primary_switch
ENDPROC(stext)

可以看到通过 b stext 进入 stext 段后,一共可以分为六个部分:

  1. preserve_boot_args boot 参数的保存
  2. el2_setup 异常级别的切换以及不同级别的部分控制寄存器设置
  3. boot 级别的标记保存,后续会使用相关变量来判断启动级别来做一些不同的初始化
  4. __create_page_tables 配置和初始化启动阶段的页表,包括 idmap_pg_dir 和 init_pg_dir
  5. __cpu_setup 对整个系统的工作的相关寄存器进行初始化及配置,包括控制寄存器,TCR 寄存器等
  6. __primary_switch 剩余的所有初始化,包括开启 mmu,设置 sp,异常向量表,地址重定向,地址随机化等,最后进入 start_kernel
2.1 preserve_boot_args
/*
 * Preserve the arguments passed by the bootloader in x0 .. x3
 */
preserve_boot_args:
	mov	x21, x0				// 将fdt首地址暂时存放至x21,释放x0用于其他使用

	adr_l	x0, boot_args			// x0保存boot_args变量地址
	stp	x21, x1, [x0]			// 这两步将x21,x1,x2,x3依次保存至boot_args中
	stp	x2, x3, [x0, #16]

	dmb	sy				// needed before dc ivac with
						// MMU off

	mov	x1, #0x20			// x0和x1是传递给__inval_cache_range的参数
	b	__inval_dcache_area		// tail call
ENDPROC(preserve_boot_args)

由于 MMU=off,D-cache=off,因此写入boot_args变量的操作都是 no cache 的,直接写入 sram 中。为了安全起见(也许 bootloader 中打开了 D-cache 并操作了boot_args这段memory,从而在各个级别的data cacheunified cache有了一些旧的,没有意义的数据),需要将boot_args变量对应的 cache line 设置为无效。在调用__inval_cache_range之前,x0是boot_args这段 memory 的首地址,x1 是末尾的地址(boot_args变量长度是4x8byte=32byte,也就是 0x20 了)。
为何要保存x0~x3这四个寄存器呢?因为 ARM64 boot protocol 对启动时候的x0~x3这四个寄存器有严格的限制:x0 是 dtb 的物理地址,x1~x3 必须是 0(非零值是保留将来使用)。在后续setup_arch函数执行的时候会访问boot_args并进行校验。

还有一个小细节是如何访问boot_args这个符号的,这个符号是一个虚拟地址,但是,现在没有建立好页表,也没有打开 MMU,如何访问它呢?这是通过adr_l这个宏来完成的。这个宏实际上是通过adrp这个汇编指令完成,通过该指令可以将符号地址变成运行时地址(通过PC relative offset形式),因此,当运行的 MMU OFF mode 下,通过adrp指令可以获取符号的物理地址。不过adrp是 page 对齐的(adrp 中的 p 就是 page 的意思),boot_args这个符号当然不会是page size对齐的,因此不能直接使用adrp,而是使用adr_l(通过计算页内偏移再在这个地址上加上偏移实现)这个宏进行处理。

这里使用dmb sy指令,在 armv8 手册中说明:除了dc zva外,所有指定地址的数据缓存指令都可以按照任意顺序执行,在任何 device 属性地址,或者不可缓存的普通内存属性必须在指令之间执行dmb或者dsb保证顺序执行。

2.2 el2_setup

根据上面描述知道,cpu 此时必须处于 EL2 或者 EL1,这一段将会完成 cpu 对虚拟拓展和基本系统控制的设定,并最终将 cpu 退回至 el1(如果开启 VHE 并且处理器支持虚拟化拓展,那么 kernel 将不会切换回 EL1,而是保持 EL2 运行,以便为 KVM 提供更好服务),此部分代码较长,分成四段。
第一段如下:

/*
 * If we're fortunate enough to boot at EL2, ensure that the world is
 * sane before dropping to EL1.
 *
 * Returns either BOOT_CPU_MODE_EL1 or BOOT_CPU_MODE_EL2 in w0 if
 * booted in EL1 or EL2 respectively.
 */
ENTRY(el2_setup)
	msr	SPsel, #1			// We want to use SP_EL{1,2} --(1)
	mrs	x0, CurrentEL 		
	cmp	x0, #CurrentEL_EL2 ------- 判断当前cpu是否处于el2
	b.eq	1f ------------------- 如果是处于el2则跳转至往后标号1:处执行
	mov_q	x0, (SCTLR_EL1_RES1 | ENDIAN_SET_EL1) 
	msr	sctlr_el1, x0 -------------------------------------(2)
	mov	w0, #BOOT_CPU_MODE_EL1		// This cpu booted in EL1
	isb
	ret

1:	mov_q	x0, (SCTLR_EL2_RES1 | ENDIAN_SET_EL2) ----------(3)
	msr	sctlr_el2, x0

#ifdef CONFIG_ARM64_VHE
	/*
	 * Check for VHE being present. For the rest of the EL2 setup,
	 * x2 being non-zero indicates that we do have VHE, and that the
	 * kernel is intended to run at EL2.
	 */
	mrs	x2, id_aa64mmfr1_el1 --------------------------------(4)
	ubfx	x2, x2, #8, #4
#else
	mov	x2, xzr -----------这里使用xzr而不是#0,其实是armv8架构pipe上的一种性能优化手段
#endif

	/* Hyp configuration. */
	mov_q	x0, HCR_HOST_NVHE_FLAGS
	cbz	x2, set_hcr -----------------------------------------(5)
	mov_q	x0, HCR_HOST_VHE_FLAGS
set_hcr:
	msr	hcr_el2, x0
	isb

(1)设置SPsel bit0 为 1,允许使用sp_elx寄存器,否则只能使用sp_el0
(2)当 cpu 处于 el1 时则无法配置虚拟化拓展相关内容则只需配置sctlr_el1后,并设置 x0 为BOOT_CPU_MODE_EL1 后返回。

首先看看sctlr_el1寄存器定义(具体 bit 含义不贴出):

SCTLR_EL1, System Control Register (EL1)
Provides top level control of the system, including its memory system, at EL1 and EL0.

sctlr_el1控制着整个系统行为。

SCTLR_EL1_RES1宏定义如下:(arch/arm64/include/asm/sysreg.h)

#define SCTLR_EL1_RES1	((_BITUL(11)) | (_BITUL(20)) | (_BITUL(22)) | (_BITUL(28)) | \
			 (_BITUL(29)))

这些 BIT 为预留位并且默认为 1。

ENDIAN_SET_EL1宏定义如下:(arch/arm64/include/asm/sysreg.h)

#ifdef CONFIG_CPU_BIG_ENDIAN
#define ENDIAN_SET_EL1		(SCTLR_EL1_E0E | SCTLR_ELx_EE)
#define ENDIAN_CLEAR_EL1	0
#else
#define ENDIAN_SET_EL1		0
#define ENDIAN_CLEAR_EL1	(SCTLR_EL1_E0E | SCTLR_ELx_EE)
#endif

根据配置上述两个标志控制着系统大小端字节序。

综上可以知道当 cpu 处于 el1 阶段时则只配置 cpu 字节序后则返回。

(3)同样的,当cpu处于 el2 时将 el2,el1,el0 配置为小端字节序。

(4)当配置了支持虚拟化拓展时,首先通过id_aa64mmfr1_el1寄存的VH字段获悉 cpu 是否支持Virtualization Host Extensions。

并将结果写入x2,如果没有配置则默认x2 = 0表示不支持此功能。

这里ubfx意思是从x2寄存器的第8bit开始提取4个bit数据并将结果写入x2。此字段对应VH feild。1表示cpu支持,0表示不支持。

(5)根据从id_aa64mmfr1_el1获取到的 cpu 是否对 Virtualization Host Extensions 提供支持来设置hcr_el2系统寄存器,该寄存器主要提供虚拟化控制配置以及陷入el2设置(具体 bit 不贴出)
HCR_HOST_NVHE_FLAGS 宏定义如下:(arch/arm64/include/asm/kvm_arm.h)

#define HCR_HOST_NVHE_FLAGS (HCR_RW | HCR_API | HCR_APK)

#define HCR_RW_SHIFT	31
#define HCR_RW		(UL(1) << HCR_RW_SHIFT) //设置1,el1 执行状态为 aarch64,el0执行状态由 PSTATE 值决定

#define HCR_API		(UL(1) << 41) //设置1,身份认证相关指令不会陷入el2

#define HCR_APK		(UL(1) << 40) //同上,认证相关’KEY‘值不会陷入el2

HCR_HOST_VHE_FLAGS宏定义如下:(arch/arm64/include/asm/kvm_arm.h)

#define HCR_HOST_VHE_FLAGS (HCR_RW | HCR_TGE | HCR_E2H)

#define HCR_TGE		(UL(1) << 27) //控制el0上异常捕捉相关,在el2安全状态激活时会对el0上某些指定捕捉并路由陷入到el2

#define HCR_E2H		(UL(1) << 34) //设置1,Host虚拟机操作系统运行在el2上被激活

最终根据x2的值判断是否支持虚拟化而设置hcr_el2系统寄存器,并同步指定执行。

第二段如下:

	/*
	 * Allow Non-secure EL1 and EL0 to access physical timer and counter.
	 * This is not necessary for VHE, since the host kernel runs in EL2,
	 * and EL0 accesses are configured in the later stage of boot process.
	 * Note that when HCR_EL2.E2H == 1, CNTHCTL_EL2 has the same bit layout
	 * as CNTKCTL_EL1, and CNTKCTL_EL1 accessing instructions are redefined
	 * to access CNTHCTL_EL2. This allows the kernel designed to run at EL1
	 * to transparently mess with the EL0 bits via CNTKCTL_EL1 access in
	 * EL2.
	 */
	cbnz	x2, 1f
	mrs	x0, cnthctl_el2
	orr	x0, x0, #3			// Enable EL1 physical timers
	msr	cnthctl_el2, x0 --------------------------------------------(1)
1:
	msr	cntvoff_el2, xzr		// 将虚拟计数counter清零保持与物理counter一致的计数值。

#ifdef CONFIG_ARM_GIC_V3 // 在允许cpu对gic v3直接访问时,配置cpu对gic v3的访问支持。
	/* GICv3 system register access */
	mrs	x0, id_aa64pfr0_el1
	ubfx	x0, x0, #24, #4 -----------------------------------------(2)
	cbz	x0, 3f // 不支持对gic v3 cpu接口访问则跳过对gic v3的配置。

	mrs_s	x0, SYS_ICC_SRE_EL2
	orr	x0, x0, #ICC_SRE_EL2_SRE	// Set ICC_SRE_EL2.SRE==1 启用el1和el2访问ICH_*和ICC_*寄存器支持。
	orr	x0, x0, #ICC_SRE_EL2_ENABLE	// Set ICC_SRE_EL2.Enable==1 非安全el1访问ICC_SRE_EL1不会陷入el2。
	msr_s	SYS_ICC_SRE_EL2, x0
	isb					// Make sure SRE is now set
	mrs_s	x0, SYS_ICC_SRE_EL2		// Read SRE back,
	tbz	x0, #0, 3f			// and check that it sticks 检查设置情况
	msr_s	SYS_ICH_HCR_EL2, xzr		// Reset ICC_HCR_EL2 to defaults 若未成功设置,则复位ICH_HCR_EL2为0。

3:
#endif

	/* Populate ID registers. */
	mrs	x0, midr_el1 // 提供PE的定义信息和设备id号。
	mrs	x1, mpidr_el1 // 提供PE多处理器表示ID和分组等信息。
	msr	vpidr_el2, x0
	msr	vmpidr_el2, x1 // 这里将midr_el1和mpidr_el1的信息写入到了虚拟配置里供虚拟化使用。

#ifdef CONFIG_COMPAT
	msr	hstr_el2, xzr			// Disable CP15 traps to EL2 当配置支持aarch32时,兼容aarch32访问cp15不会陷入el2。
#endif

(1)当不支持虚拟化相关功能时,配置cnthctl_el2系统寄存器低两位为1表示非安全模式下el1和el0支持访问physical timer registers和physical counter register。

当支持虚拟化相关功能时,则是对el0的physical timer registers和physical counter register 访问配置,这里没有设置。

(2)id_aa64pfr0_el1寄存器主要提供对 pe 实现特性的一些信息。ubfx 提取位是对 gic 支持的信息,为1表示支持系统寄存器在3.0/4.0版本的gic cpu接口访问。

第三段如下:

	/* EL2 debug */
	mrs	x1, id_aa64dfr0_el1		// Check ID_AA64DFR0_EL1 PMUVer 提供top level debug系统在aarch64的状态信息
	sbfx	x0, x1, #8, #4 // sbfx同理ubfx,u表示无符号,sbfx则是有符号位提供,此域提供对PMU支持情况信息
	cmp	x0, #1
	b.lt	4f				// Skip if no PMU present
	mrs	x0, pmcr_el0			// Disable debug access traps
	ubfx	x0, x0, #11, #5			// to EL2 and allow access to
4:
	csel	x3, xzr, x0, lt			// all PMU counters from EL1

	/* Statistical profiling */
	ubfx	x0, x1, #32, #4			// Check ID_AA64DFR0_EL1 PMSVer
	cbz	x0, 7f				// Skip if SPE not present
	cbnz	x2, 6f				// VHE?
	mrs_s	x4, SYS_PMBIDR_EL1		// If SPE available at EL2,
	and	x4, x4, #(1 << SYS_PMBIDR_EL1_P_SHIFT)
	cbnz	x4, 5f				// then permit sampling of physical
	mov	x4, #(1 << SYS_PMSCR_EL2_PCT_SHIFT | \
		      1 << SYS_PMSCR_EL2_PA_SHIFT)
	msr_s	SYS_PMSCR_EL2, x4		// addresses and physical counter
5:
	mov	x1, #(MDCR_EL2_E2PB_MASK << MDCR_EL2_E2PB_SHIFT)
	orr	x3, x3, x1			// If we don't have VHE, then
	b	7f				// use EL1&0 translation.
6:						// For VHE, use EL2 translation
	orr	x3, x3, #MDCR_EL2_TPMS		// and disable access from EL1
7:
	msr	mdcr_el2, x3			// Configure debug traps

	/* LORegions */
	mrs	x1, id_aa64mmfr1_el1
	ubfx	x0, x1, #ID_AA64MMFR1_LOR_SHIFT, 4
	cbz	x0, 1f
	msr_s	SYS_LORC_EL1, xzr
1:

	/* Stage-2 translation */
	msr	vttbr_el2, xzr // 将虚拟vttbr清空属于虚拟化注册功能组和内存虚拟化控制功能组相关内容。

	cbz	x2, install_el2_stub // 如果 支持了虚拟化则直接返回,后续在kvm配置虚拟化相关内容,并设置 cpu boot 在 el2 值保存在x0,后续 kernel 工作在 EL2。

	mov	w0, #BOOT_CPU_MODE_EL2		// This CPU booted in EL2
	isb
	ret

install_el2_stub:

el2_setup最后一段:

// 在不支持虚拟化拓展时,KVM 需要下面这段配置并切换回 el1,后续 KVM 功能通过 hvc 来访问。
install_el2_stub: // 这里最后在el2对el2和el1早期配置进行设置,并切换至el1。
	/*
	 * When VHE is not in use, early init of EL2 and EL1 needs to be
	 * done here.
	 * When VHE _is_ in use, EL1 will not be used in the host and
	 * requires no configuration, and all non-hyp-specific EL2 setup
	 * will be done via the _EL1 system register aliases in __cpu_setup.
	 */
	mov_q	x0, (SCTLR_EL1_RES1 | ENDIAN_SET_EL1)
	msr	sctlr_el1, x0 // 同处于el1一样,需要在el2时设置好sctlr_el1的值,主要配置大小端字节序。

	/* Coprocessor traps. */ //协处理器访问陷阱设置
	mov	x0, #0x33ff // 其中大部分位为预留值,主要配置CPACR,CPACR_EL1 SIMD访问时是否陷入el2,这里设置为都不陷入el2。
	msr	cptr_el2, x0			// Disable copro. traps to EL2

	/* SVE register access */ //可伸缩矢量拓展相关设置
	mrs	x1, id_aa64pfr0_el1
	ubfx	x1, x1, #ID_AA64PFR0_SVE_SHIFT, #4
	cbz	x1, 7f

	bic	x0, x0, #CPTR_EL2_TZ		// Also disable SVE traps
	msr	cptr_el2, x0			// Disable copro. traps to EL2
	isb
	mov	x1, #ZCR_ELx_LEN_MASK		// SVE: Enable full vector
	msr_s	SYS_ZCR_EL2, x1			// length for EL1.

	/* Hypervisor stub */
7:	adr_l	x0, __hyp_stub_vectors // __hyp_stub_vectors虚拟化管理异常向量表入口
	msr	vbar_el2, x0 // 将虚拟化管理异常向量表写入Vector Base Address Register el2

	/* spsr */
	mov	x0, #(PSR_F_BIT | PSR_I_BIT | PSR_A_BIT | PSR_D_BIT |\
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值