聊聊SOC启动(十) 内核启动先导知识
本文基于以下软硬件假定:
架构:AARCH64
内核版本:5.14.0-rc5
1 问题引出
经过漫漫征途终于进入内核大门了,现在内核将愉快地从第一条指令开始执行。但在开始内核之旅前,还是有必要再看下系统进入内核之前的状态。我们知道uboot的最后一步是把内核拷贝到内存,并将cpu设置为如下状态:
(1)MMU处于关闭状态
(2)数据cache处于关闭状态,指令cache可以是关闭或者打开的
(3)将dtb的地址存放到x0寄存器中
(4)通过armv8_switch_to_el2函数跳转到内核入口地址执行
由于刚进入内核时页表还没有建立,此时系统运行在实模式,且ARM8数据cache的开启需要依赖于MMU,因此显然在启动内核前需要关闭MMU和数据cache。
我们知道armv8一共有四个异常等级,正常情况下内核应该运行在EL1,但是由于ARMv8支持虚拟化。对于type 2 hypervisor,其guest OS运行在EL1,若host OS也运行在EL1,则其架构如下:

此时host os与guest os都运行在EL1,而hypervisor作为host os的一部分运行在EL2,由于host os与hypervisor运行在不同的异常等级,它们之间需要通过异常进行交互。这需要异常等级切换,以及上下文的保存和恢复,显然会带来比较大的开销。为了提高虚拟化效率,arm在armv8.1之后的架构增加了对vhe的支持,以允许host os运行在EL2。此时系统架构将变为以下方式:

由于host os与hypervisor都运行在EL2,因此减少了上述开销。但是host os本身与普通os并没有两样,默认被设计为工作于EL1中,如其通过sctlr_el1访问系统控制寄存器,通过vbar_el1访问向量表基地址寄存器等。若host os运行在EL2,则需要将这些寄存器的访问操作重定向到其对应的xxx_el2寄存器,arm架构在硬件层面提供了寄存器重定向特性,但是os也需要做相应的适配,如使能HCR_EL2.E2H以开启vhe支持,使能HCR_EL2.TGE以将本来被路由到EL1的异常路由到EL2等。此后,host os就可以马照跑,舞照跳,所有的操作与运行于EL1时相同,根本无需关心自己实际上是运行于哪个EL
uboot加载内核时会将内核拷贝到内存的低地址处(如0x40000000),然后直接跳转到该物理地址处运行。但实际上程序运行依赖于链接脚本中定义的虚拟地址,若代码不是地址无关的,则加载地址必须要与链接地址一致才能正确运行。我们再来看一下arm64架构链接脚本中内核起始地址的定义(arch/arm64/kernel/vmlinu.lds.S):
OUTPUT_ARCH(aarch64)
ENTRY(_text)
SECTIONS
. = KIMAGE_VADDR;
.head.text : {
_text = .;
HEAD_TEXT
}
即内核入口函数_text的链接地址为KIMAGE_VADDR,从下图内核虚拟地址空间布局可以看到,该地址为0xffff80000fffffff,显然与uboot的加载地址不同。因此为了内核能正常启动,其开头部分的代码必须要支持位置无关特性。

由于位置无关代码不能直接访问全局变量的地址,除非编译阶段指定了pie选项,否则在编码方面会有诸多限制。因此内核启动后自然希望能尽快切换到正常执行模式,这就需要在启动早期就为内核运行所需的代码段、栈、数据段等部分内存建立初始化页表,从而使其运行地址与链接地址匹配。
2 内核执行的异常等级
2.1 内核启动时的异常等级
除了必须支持EL0和EL1以外,arm可以灵活地配置是否支持EL2和EL3。为了讨论方便,我们假定讨论的系统支持所有EL0 – EL3异常等级,且启动流程为bl1 bl2 bl31 bl32 uboot linux。以下为其典型流程图:

Arm规定cpu以系统支持的最高异常等级开始启动,因此bl1运行于EL3,Bl2根据需求可运行于S-EL1或EL3,BL31需要执行secure monitor功能,故只能运行于EL3。BL32主要用于支持trust os,其必须执行在S-EL1和S-EL0下
2.1.1 Uboot的执行异常等级
由于uboot(bl33)由bl31启动,因此其异常等级也由bl31确定。我们以qemu平台为例,atf获取bl33异常等级的流程如下:

以下为其实际获取流程的定义:
static inline uint64_t el_implemented(unsigned int el)
if (el > 3U) {
return EL_IMPL_NONE;
} else {
unsigned int shift = ID_AA64PFR0_EL1_SHIFT * el;
return (read_id_aa64pfr0_el1() >> shift) & ID_AA64PFR0_ELX_MASK; (1)
static uint32_t qemu_get_spsr_for_bl33_entry(void)
mode = (el_implemented(2) != EL_IMPL_NONE) ? MODE_EL2 : MODE_EL1; (2)
spsr = SPSR_64(mode, MODE_SP_ELX, DISABLE_ALL_EXCEPTIONS); (3)
}
(1)该函数通过过读取id_aa64pfr0_el1寄存器判断是否支持给定的异常等级,该寄存器的定义如下:

即判断寄存器中给定异常等级对应的字段是否被设置,若其被设置则cpu支持该异常等级,否则不支持
(2)若cpu支持EL2,则uboot从EL2启动,否则从EL1启动
(3)将启动模式设置到non secure上下文的spsr成员中,该上下文在退出bl31之前会被设置到实际的寄存器中
2.1.2 内核启动异常等级的确定
uboot本身作为firmware支持运行在EL1 – EL3的任一等级,但内核只能运行于EL 1或EL2。因此在进入内核之前,uboot需要根据实际情况切换到对应的异常等级。
加载完内核后,它会通过boot_jump_linux执行实际的切换流程,其中aarch64架构流程如下:
static void boot_jump_linux(bootm_headers_t *images, int flag)
#ifdef CONFIG_ARMV8_SWITCH_TO_EL1
printf("switch to EL1 AARCH64\n");
armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,
(u64)switch_to_el1, ES_TO_AARCH64); (1)
#else
if ((IH_ARCH_DEFAULT == IH_ARCH_ARM64) &&
(images->os.arch == IH_ARCH_ARM)) {
printf("switch to EL2 AARCH32\n");
armv8_switch_to_el2(0, (u64)gd->bd->bi_arch_number,
(u64)images->ft_addr, 0,
(u64)images->ep,
ES_TO_AARCH32); (2)
} else {
printf("switch to EL2 AARCH64\n");
armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,
images->ep,
ES_TO_AARCH64); (3)
#endif
}
armv8_to_el2会获取cpu的当前异常等级,并根据该值确定内核的运行等级。若配置了CONFIG_ARMV8_SWITCH_TO_EL1强制从EL1启动,则不管当前运行在那个EL下,都切换到EL1再启动内核。若当前在EL3下执行,则需要切换到EL2再启动内核。否则,内核将跟随uboot的EL。其关系可表示为下表:
当前异常等级 | CONFIG_ARMV8_SWITCH_TO_EL1的值 | 内核异常等级 |
EL1 | yes | EL1 |
EL1 | no | El1 |
EL2 | yes | EL1 |
EL2 | no | El2 |
EL3 | yes | EL1 |
EL3 | no | EL2 |
下面是其代码实现,不感兴趣的同学直接跳过即可:
(1)通过配置参数CONFIG_ARMV8_SWITCH_TO_EL1强制内核在EL1下运行。在该流程中armv8_switch_to_el2先跳转到switch_to_el1函数,然后由switch_to_el1执行实际的异常等级切换和内核启
(2)若os需要运行于aarch32状态,则传入对应参数
(3)若os需要运行于aarch64状态,则传入对应参数。由于现在的主流架构是aarch64,故后面涉及架构相关的代码我们都只关注aarch64相关的分支
armv8_switch_to_el2的流程如下:
ENTRY(armv8_switch_to_el2)
switch_el x6, 1f, 0f, 0f (a)
cmp x5, #ES_TO_AARCH64
b.eq 2f
bl armv8_el2_to_aarch32
br x4 (b)
1: armv8_switch_to_el2_m x4, x5, x6 (c)
ENDPROC(armv8_switch_to_el2)
它根据当前运行的异常等级,确定需要执行的分支。由于uboot可以执行在EL1、EL2或EL3下,因此这里对其分别执行不同的处理。通过下面switch_el的定义,可知当前运行异常等级不同时,其跳转分支分别如下:
当前异常等级 | 跳转标签 |
EL1 | 0 |
EL2 | 0 |
EL3 | 1 |
.macro switch_el, xreg, el3_label, el2_label, el1_label
mrs \xreg, CurrentEL
cmp \xreg, 0xc
b.eq \el3_label
cmp \xreg, 0x8
b.eq \el2_label
cmp \xreg, 0x4
b.eq \el1_label
.endm
(a)当前异常等级为EL3时,通过armv8_switch_to_el2_m切换到EL2并启动内核。当前异常等级为EL1或EL2,根据内核运行状态是aarch32还是aarch64,先切换cpu状态,然后跳转到参数给定的入口函数
(b)若未定义CONFIG_ARMV8_SWITCH_TO_EL1,则该函数会直接跳转到内核入口函数处启动内核,此时内核的异常等级与uboot当前运行的异常等级相同。若定义了CONFIG_ARMV8_SWITCH_TO_EL1,则会跳转到switch_to_el1接口,cpu先切换到EL1,然后再启动内核
(c)将异常等级由EL3切换到EL2,然后启动内核
2.2 内核运行时的异常等级
uboot可能以EL1或EL2方式启动内核,上一章的讨论中我们知道内核要工作在EL2需要vhe的支持。因此若内核以EL2启动,则必须要进一步处理以确定其是运行于El2的vhe模式,还是降级到EL1。以下是其代码实现:
SYM_FUNC_START(init_kernel_el)
mrs x0, CurrentEL
cmp x0, #CurrentEL_EL2
b.eq init_el2 (1)
SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)
mov_q x0, INIT_SCTLR_EL1_MMU_OFF
msr sctlr_el1, x0 (2)
mov_q x0, INIT_PSTATE_EL1
msr spsr_el1, x0
msr elr_el1, lr (3)
mov w0, #BOOT_CPU_MODE_EL1 (4)
eret (5)
SYM_INNER_LABEL(init_el2, SYM_L_LOCAL)
mov_q x0, HCR_HOST_NVHE_FLAGS
msr hcr_el2, x0 (6)
init_el2_state (7)
adr_l x0, __hyp_stub_vectors
msr vbar_el2, x0 (8)
mrs x0, hcr_el2
and x0, x0, #HCR_E2H
cbz x0, 1f (9)
mov_q x0, INIT_SCTLR_EL1_MMU_OFF
msr_s SYS_SCTLR_EL12, x0 (10)
mov x0, #INIT_PSTATE_EL2
msr spsr_el1, x0
adr x0, __cpu_stick_to_vhe
msr elr_el1, x0 (11)
mov_q x0, INIT_SCTLR_EL1_MMU_OFF
msr sctlr_el1, x0
msr elr_el2, lr
mov w0, #BOOT_CPU_MODE_EL2
__cpu_stick_to_vhe:
mov x0, #HVC_VHE_RESTART
hvc #0
mov x0, #BOOT_CPU_MODE_EL2
SYM_FUNC_END(init_kernel_el)
(1)获取当前运行的异常等级,若其为EL1则直接进入init_el1处理,否则进入init_el2
(2 - 5)用于初始化EL1的执行状态,如sctlr_el1、spsr_el1,并通过eret将spsr_el1的设置到PSATE中,以及执行函数返回
(6)初始化hcr_el2寄存器的值,该寄存器用于配置hypervisor的属性。如E2H字段用于开关对vhe的支持,TGE字段确定需要被路由到EL1的异常是否被路由到EL2等
(7)这个宏用于初始化el2的状态,其定义如下:
.macro init_el2_state
__init_el2_sctlr
__init_el2_timers
__init_el2_debug
__init_el2_lor
__init_el2_stage2
__init_el2_gicv3
__init_el2_hstr
__init_el2_nvhe_idregs
__init_el2_nvhe_cptr
__init_el2_nvhe_sve
__init_el2_fgt
__init_el2_nvhe_prepare_eret
.endm
这里我们重点看一下__init_el2_nvhe_prepare_eret,该宏会将spsr中的下一异常等级设置为EL1,即执行eret之后cpu将会切换到EL1状态。以下为该宏的定义:
.macro __init_el2_nvhe_prepare_eret
mov x0, #INIT_PSTATE_EL1
msr spsr_el2, x0
.endm