文章目录
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 提出以下要求:
- 设置并初始化RAM(必须)
- 准备好合适的设备树文件到 RAM 中,并提供 dtb 首地址给 kernel(必须)
- 解压内核镜像(可选)
- 将控制权交由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) */
- code0/code1指向stext段,kernel执行代码的开始。
- 当支持EFI格式启动时,code0/code1将会被跳过,res5将是PE header偏移。
- 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 也有以下要求:
- 主CPU通用寄存器设置
x0 = 设备树首地址的物理地址
x1 = 0
x2 = 0
x3 = 0 - CPU模式
所有中断都必须在 PSTATE 中被屏蔽,DAIF(debug,SError,IRQ 和 FIQ)
CPU 必须处于 EL2(为了访问虚拟化拓展,建议使用 EL2)或者非安全EL1中。 - 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.
- 另一个未说明但实际需要保证的要求:
在所有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 段后,一共可以分为六个部分:
- preserve_boot_args boot 参数的保存
- el2_setup 异常级别的切换以及不同级别的部分控制寄存器设置
- boot 级别的标记保存,后续会使用相关变量来判断启动级别来做一些不同的初始化
- __create_page_tables 配置和初始化启动阶段的页表,包括 idmap_pg_dir 和 init_pg_dir
- __cpu_setup 对整个系统的工作的相关寄存器进行初始化及配置,包括控制寄存器,TCR 寄存器等
- __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 cache和unified 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 |\

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

被折叠的 条评论
为什么被折叠?



