一、前言
kernel的整个启动过程涉及的内容很多,不可能每一个细节都描述清楚,因此我打算针对部分和ARM64相关的启动步骤进行学习、整理,并方便后续查阅。本文实际上描述在系统启动最开始的时候,bootloader和kernel的交互以及kernel如何保存bootloader传递的参数并进行校验,此外,还有一些最基础的硬件初始化的内容。
本文中的source来自4.1.10内核,这是一个long term的版本,后续一段时间的文章都会基于这个long term版本进行。
二、bootloader和kernel的交互
系统启动过程中,linux kernel不是一个人在战斗,在kernel之前bootloader会进行下面的动作
1、初始化系统中的RAM并将RAM的信息告知kernel
2、准备好device tree blob的信息并将dtb的首地址告知kernel
3、解压内核(可选)
4、将控制权转交给内核。当然,bootloader和kernel的交互的时候需求如下:
MMU = off, D-cache = off, I-cache = on or off
x0 = physical address to the FDT blob
更详细的ARM64 boot protocol请参考Documentation/arm64/booting.txt文档。
三、参数的保存和校验
最开始的ARM64启动代码位于arch/arm64/kernel/head.S文件中,代码如下:
ENTRY(stext)
bl preserve_boot_args
bl el2_setup // Drop to EL1, w20=cpu_boot_mode
adrp x24, __PHYS_OFFSET
bl set_cpu_boot_mode_flagbl __vet_fdt
……
ENDPROC(stext)
1、preserve_boot_args
preserve_boot_args:
mov x21, x0------将x0的值暂存在x21寄存器中,后面要使用x0adr_l x0, boot_args---x0保存了boot_args变量的地址
stp x21, x1, [x0]----保存x0和x1的值到boot_args[0]和boot_args[1]
stp x2, x3, [x0, #16] ---保存x2和x3的值到boot_args[2]和boot_args[3]dmb sy---------full system data memory barrier
add x1, x0, #0x20----x0和x1是传递给__inval_cache_range的参数
b __inval_cache_range
ENDPROC(preserve_boot_args)
由于MMU = off, D-cache = off,因此写入boot_args变量的操作都是略过data cache的,直接写入了RAM中,为了安全起见(也许bootloader中打开了D-cache并操作了boot_args这段memory),将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这个宏进行处理,如果读者有兴趣可以自己看source code。
2、el2_setup
程序执行至此,CPU处于哪一个exception level呢?根据ARM64 boot protocol,CPU要么处于EL2(推荐)或者non-secure EL1。如果在EL1,情形有些类似过去arm处理器的感觉,处于EL2稍微复杂一些,需要对virtualisation extensions进行基本的设定,然后将cpu退回到EL1。代码太长了,我们分成两段来阅读,第一段如下:
ENTRY(el2_setup)
mrs x0, CurrentEL------------------------(1)
cmp x0, #CurrentEL_EL2------判断是否处于EL2
b.ne 1f--------------不是的话,跳到1f
mrs x0, sctlr_el2-------------------------(2)
CPU_BE( orr x0, x0, #(1 << 25) ) // Set the EE bit for EL2
CPU_LE( bic x0, x0, #(1 << 25) ) // Clear the EE bit for EL2
msr sctlr_el2, x0----写回sctlr_el2寄存器
b 2f
1: mrs x0, sctlr_el1-------------------------(3)
CPU_BE( orr x0, x0, #(3 << 24) ) // Set the EE and E0E bits for EL1
CPU_LE( bic x0, x0, #(3 << 24) ) // Clear the EE and E0E bits for EL1
msr sctlr_el1, x0
mov w20, #BOOT_CPU_MODE_EL1----w20寄存器保存了cpu启动时候的Eexception level
isb---------instruction memory barrier
ret2: mov x0, #(1 << 31) ------------------------(4)
msr hcr_el2, x0mrs x0, cnthctl_el2 -------------------------(5)
orr x0, x0, #3 // Enable EL1 physical timers
msr cnthctl_el2, x0
msr cntvoff_el2, xzr // Clear virtual offsetmrs x0, id_aa64pfr0_el1 -----------------------(6)
ubfx x0, x0, #24, #4 ----取出24 bit开始的4个bit的值并将该值赋给x0
cmp x0, #1
b.ne 3f -----不支持system register接口mrs_s x0, ICC_SRE_EL2
orr x0, x0, #ICC_SRE_EL2_SRE // Set ICC_SRE_EL2.SRE==1
orr x0, x0, #ICC_SRE_EL2_ENABLE // Set ICC_SRE_EL2.Enable==1
msr_s ICC_SRE_EL2, x0
isb // Make sure SRE is now set
msr_s ICH_HCR_EL2, xzr // Reset ICC_HCR_EL2 to defaults3: ……
(1)当前的exception level保存在PSTATE中,程序可以通过MRS或者MSR来访问PSTATE,当然需要传递一个Special-purpose register做为参数,CurrentEL就是获取PSTATE中current exception level域的特殊寄存器。
(2)sctlr_el2也是一个可以通过MRS/MSR指令访问的寄存器,当CPU处于EL2状态的时候,该寄存器可以控制整个系统的行为。当然,这里仅仅是设定EL2下的数据访问和地址翻译过程中的endianess配置,也就是EE bit[25]。根据配置,CPU_BE和CPU_LE包围的指令只会保留一行。对于little endian而言,实际上就是将sctlr_el2寄存器的EE(bit 25)设定为0。顺便说一下,这个bit不仅仅控制EL2数据访问的endianess以及EL2 stage 1的地址翻译过程中的endianess(当然,EL2只有stage 1),还可以控制EL1和EL0 stage 2地址翻译的过程的endianess(这时候有两个stage的地址翻译过程)。
(3)执行到这里说明CPU处于EL1,这种状态下没有权限访问sctlr_el2,只能是访问sctlr_el1。sctlr_el1可以通过EE和E0E来控制EL1和EL0状态下是little endian还是big endian。EE bit控制了EL1下的数据访问以及EL1和EL0 stage 1地址翻译的过程的endianess。E0E bit用来控制EL0状态下的数据访问的endianess。此外,需要注意的是:由于修改了system control register(设定endianess状态),因此需要一个isb来同步(具体包括两部分的内容,一是确认硬件已经执行完毕了isb之前的所有指令,包括修改system control寄存器的那一条指令,另外一点是确保isb之后的指令从新来过,例如取指,校验权限等)。
(4)执行到这里说明CPU处于EL2,首先设定的是hcr_el2寄存器,Hypervisor Configuration Register。该寄存器的大部分bit 值在reset状态的时候就是0值,只不过bit 31(Register Width Control)是implementation defined,因此这里set 31为1,确保Low level的EL1也是Aarch64的
(5)这一段代码是对Generic timers进行配置。要想理解这段代码,我们需要简单的了解一些ARMv8上Generic timer的运作逻辑。一个全局范围的system counter、各个PE上自己专属的local timer以及连接这些组件之间的bus或者信息传递机制组成了Generic Timer。对于PE而言,通过寄存器访问,它能看到的是physical counter(实际的system counter计数)、virtual counter(physical counter基础上的offset)、physical timer、virtual timer等。NTHCTL_EL2,Counter-timer Hypervisor Control register,用来控制系统中的physical counter和virutal counter如何产生event stream以及在EL1和EL0状态访问physical counter和timer的硬件行为的。在EL1(EL0)状态的时候访问physical counter和timer有两种配置,一种是允许其访问,另外一种就是trap to EL2。这里的设定是:不陷入EL2(对应的bit设置为1)。更详细的信息可以参考ARMv8 ARM文档。cntvoff_el2是virtual counter offset,所谓virtual counter,其值就是physical counter的值减去一个offset的值(也就是cntvoff_el2的值了),这里把offset值清零,因此virtual counter的计数和physical counter的计数是一样的。
(6)这一段代码是对GIC V3进行配置。ID_AA64PFR0_EL1,AArch64 Processor Feature Register 0,该寄存器描述了PE实现的feature。GIC bits [27:24]描述了该PE是否实现了system register来访问GIC,如果没有(GIC bits 等于0)那么就略过GIC V3的设定。ICC_SRE_EL2,Interrupt Controller System Register Enable register (EL2),该寄存器用来(在EL2状态时候)控制如何访问GIC CPU interface模块的,可以通过memory mapped方式,也可以通过system register的方式。将SRE bit设定为1确保通过system register方式进行GIC interface cpu寄存器的访问。将enable bit设定为1确保在EL1状态的时候可以通过ICC_SRE_EL1寄存器对GIC进行配置而不是陷入EL2。
下面我们进入第二段代码:
mrs x0, midr_el1 -----------------------------(1)
mrs x1, mpidr_el1
msr vpidr_el2, x0
msr vmpidr_el2, x1mov x0, #0x0800 // Set/clear RES{1,0} bits ---------------(2)
CPU_BE( movk x0, #0x33d0, lsl #16 ) // Set EE and E0E on BE systems
CPU_LE( movk x0, #0x30d0, lsl #16 ) // Clear EE and E0E on LE systems
msr sctlr_el1, x0mov x0, #0x33ff-------Disable Coprocessor traps to EL2
msr cptr_el2, x0 // Disable copro. traps to EL2#ifdef CONFIG_COMPAT-----是否支持64 bit kernel上运行32bit 的application
msr hstr_el2, xzr // Disable CP15 traps to EL2
#endifmrs x0, pmcr_el0------------------------------(3)
ubfx x0, x0, #11, #5 // to EL2 and allow access to
msr mdcr_el2, x0 // all PMU counters from EL1
msr vttbr_el2, xzr ----清除Stage-2 translation table base address registeradrp x0, __hyp_stub_vectors
add x0, x0, #:lo12:__hyp_stub_vectors
msr vbar_el2, x0 ---------------设定EL2的异常向量表的基地址mov x0, #(PSR_F_BIT | PSR_I_BIT | PSR_A_BIT | PSR_D_BIT |\
PSR_MODE_EL1h)
msr spsr_el2, x0 ------------------------------(4)
msr elr_el2, lr
mov w20, #BOOT_CPU_MODE_EL2 // This CPU booted in EL2
eret-------------------------------------(5)
ENDPROC(el2_setup)
(1)midr_el1和mpidr_el1都属于标识该PE信息的read only寄存器。MIDR_EL1,Main ID Register主要给出了该PE的architecture信息,Implementer是谁等等信息。MPIDR_EL1,Multiprocessor Affinity Register,该寄存器保存了processor ID。vpidr_el2和vmpidr_el2是上面的两个寄存器是对应的,只不过是for virtual processor的。
(2)这段代码实际上是将0x33d00800(BE)或者0x30d00800(LE)写入sctlr_el1寄存器。BE和LE的设定和上面第一段代码中的描述是类似的,其他bit的设定请参考ARMv8 ARM文档
(3)PMCR_EL0,Performance Monitors Control Register,该寄存器的[15:11]标识了支持的Performance Monitors counter的数目,并将其设定到MDCR_EL2(Monitor Debug Configuration Register (EL2))中。MDCR_EL2中其他的bit都设定为0,其结果就是允许EL0和EL1进行debug的操作(而不是trap to EL2),允许EL1访问Performance Monitors counter(而不是trap to EL2)。
(4)当系统发生了异常并进入EL2,SPSR_EL2,Saved Program Status Register (EL2)会保存处理器状态,ELR_EL2,Exception Link Register (EL2)会保存返回发生exception的现场的返回地址。这里是设定SPSR_EL2和ELR_EL2的初始值。w20寄存器保存了cpu启动时候的Eexception level ,因此w20被设定为BOOT_CPU_MODE_EL2。
(5)eret指令是用来返回发生exception的现场。实际上,这个指令仅仅是模拟了一次异常返回而已,SPSR_EL2和ELR_EL2都已经设定OK,执行该指令会使得CPU返回EL1状态,并且将SPSR_EL2的值赋给PSTATE,ELR_ELR就是返回地址(实际上也恰好是函数的返回地址)。
完成了el2_setup这个函数分析之后,我们再回头思考这样的问题:为何是el2_setup?为了没有el3_setup?当一个SOC的实现在包括了EL3的支持,那么CPU CORE缺省应该进入EL3状态,为何这里只是判断EL2还是EL1,从而执行不同的流程,如果是EL3状态,代码不就有问题了吗?实际上,即便是由于SOC支持TrustZone而导致cpu core上电后进入EL3,这时候,接管cpu控制的一定不是linux kernel(至少目前来看linux kernel不会做Secure monitor),而是Secure Platform Firmware(也就是传说中的secure monitor),它会进行硬件平台的初始化,loading trusted OS等等,等到完成了secure world的构建之后,把控制权转交给non-secure world,这时候,CPU core多半处于EL2(如果支持虚拟化)或者EL1(不支持虚拟化)。因此,对于linux kernel而言,它感知不到secure world(linux kernel一般也不会做Trusted OS),仅仅是在non-secure world中呼风唤雨,可以是Hypervisor或者rich OS。
3、set_cpu_boot_mode_flag
在进入这个函数的时候,有一个前提条件:w20寄存器保存了cpu启动时候的Eexception level ,具体代码如下:
ENTRY(set_cpu_boot_mode_flag)
adr_l x1, __boot_cpu_mode
cmp w20, #BOOT_CPU_MODE_EL2
b.ne 1f
add x1, x1, #4
1: str w20, [x1] // This CPU has booted in EL1
dmb sy
dc ivac, x1 // Invalidate potentially stale cache line
ret
ENDPROC(set_cpu_boot_mode_flag)
由于系统启动之后仍然需要了解cpu启动时候的Eexception level,因此,有一个全局变量__boot_cpu_mode用来保存启动时候的CPU mode。代码很简单,大家自行体会就OK了。
4、__vet_fdt
在进入具体函数之前,x21和x24都被设定成了指定的值。x21被设定为fdt在RAM中的物理地址(参考preserve_boot_args函数),x24被设定为__PHYS_OFFSET,定义为:
#define __PHYS_OFFSET (KERNEL_START – TEXT_OFFSET)
#define KERNEL_START _text
KERNEL_START是kernel开始运行的虚拟地址,更确切的说是内核正文段开始的虚拟地址。 在链接脚本文件中(参考arch/arm64/kernel下的vmlinux.lds.S),KERNEL_START被设定为:
. = PAGE_OFFSET + TEXT_OFFSET;
.head.text : {
_text = .;
HEAD_TEXT
}
因此,KERNEL_START的值和PAGE_OFFSET以及TEXT_OFFSET这两个offset的设定有关。TEXT_OFFSET标识了内核正文段的offset,其实如果该宏被定义为KERNEL_TEXT_OFFSET会更好理解。我们知道,操作系统运行在内核空间,应用程序运行在用户空间,假设内核空间的首地址是x(一般也是RAM的首地址),那么是否让kernel运行在x地址呢?对于arm,在内核空间的开始有32kB(0x00008000)的空间用于保存内核的页表(也就是进程0的PGD)以及bootload和kernel之间参数的传递,对于ARM64,在其Makefile中定义了这个offset是512KB(0x00080000)。
ifeq ($(CONFIG_ARM64_RANDOMIZE_TEXT_OFFSET), y)
TEXT_OFFSET := $(shell awk 'BEGIN {srand(); printf "0x%03×000\n", int(512 * rand())}')
else
TEXT_OFFSET := 0x00080000
endif
CONFIG_ARM64_RANDOMIZE_TEXT_OFFSET是randomize内核的启动地址,估计是防止黑客入侵之类的,我们这里简化处理,就是固定为0x00080000好了。
搞定了TEXT_OFFSET,我们再来看看PAGE_OFFSET,在arch/arm64/include/asm/memory.h中,PAGE_OFFSET被定义为:
#define VA_BITS (CONFIG_ARM64_VA_BITS)
#define PAGE_OFFSET (UL(0xffffffffffffffff) << (VA_BITS – 1))
VA_BITS定义了用户空间虚拟地址的bit数(该值也就是定义了用户态程序能够访问的地址空间的size),假设VA_BITS被设定为39个bit,那么PAGE_OFFSET就是0xffffffc0-00000000。PAGE_OFFSET的名字也不好(个人观点,可能有误),OFFSET表明的是一个偏移,内核空间被划分成一个个的page,PAGE_OFFSET看起来应该是定义以page为单位的偏移。但是,以什么为基准的偏移呢?PAGE_OFFSET的名字中没有给出,当然实际上,这个符号是定义以整个address space的起始地址(也就是0)为基准。另外,虽然这个地址是要求page对齐,但是实际上,这个符号仍然定义的是虚拟地址的offset(而不是page的offset)。根据上面的理由,我觉得定义成KERNEL_IMG_OFFSET会更好理解一些。一句话总结:PAGE_OFFSET定义了将kernel image安放在虚拟地址空间的哪个位置上。
OK,经过漫长的说明之后,__PHYS_OFFSET实际上就是kernel image的首地址(并不是__PHYS_OFFSET的位置开始就是真实的kernel image,实际上从__PHYS_OFFSET开始,首先是TEXT_OFFSET的保留区域,然后才是真正的kernel image)。实际上,__PHYS_OFFSET定义的是一个虚拟地址而不是物理地址,这里的PHYS严重影响了该符号的含义,实际上adrp这条指令可以将一个虚拟地址转换成物理地址(在没有打开MMU的时候)。而函数__vet_fdt主要是对这个bootloader传递给kernel的fdt参数进行验证,看是否OK,主要验证的内容包括:
(1)是否是8字节对齐的
(2)是否在kernel space的前512M内
__vet_fdt:
tst x21, #0x7----是否是8字节对齐的
b.ne 1f
cmp x21, x24-----是否在小于kernel space的首地址
b.lt 1f
mov x0, #(1 << 29)
add x0, x0, x24
cmp x21, x0
b.ge 1f-------是否大于kernel space的首地址+512M
ret
1:
mov x21, #0----------传递的fdt地址有误,清零
ret
ENDPROC(__vet_fdt)
四、参考文献
1、Documentation/arm64/booting.txt
2、ARM Architecture Reference Manual
change log:
1、2015-11-30,增加对el2_setup的思考。
2、2015-12-1,(1)修正对PAGE_OFFSET的描述。(2)增加adrp和adr_l的描述(3)增加对__PHYS_OFFSET符号名字的置疑
原文地址: http://www.wowotech.net/linux_kenrel/arm64_initialize_1.html