原创翻译,转载请注明出处。
arm64的异常模型由一组异常级别(EL0-EL3)组成。EL0,EL1有安全模式和非安全模式的区别。EL2是虚拟机管理级别并且只有非安全模式。EL3是最高优先级并且只存在安全模式中。
为了描述方便,下面将使用术语“boot loader”来简化所有执行在cpu将控制权转交给内核之前的软件的称呼。这里包含了安全监视器(secure monitor)和虚拟机管理器(hypervisor)的代码,或者可能是少量用来准备一个最小的启动环境的指令。
基本上,boot loader至少提供以下几个功能:
- 安装与初始化物理内存
- 安装设备树
- 解压内核镜像
- 启动内核镜像
1、安装与初始化物理内存
boot loader需要初始化物理内存,内核将使用这些内存来存储volatile类型的数据。这个是与机器有关的,可能使用了内部算法自动的定位并取得物理内存的大小,
或者可能是机器有关内存方面的特性,也可能是boot loader设计者知道的获取内存某种方法。(囧)
2、安装设备树
dtb(device tree blob)必须位于8-BYTE对齐的位置并且不能超过2MB的大小。因为dtb会被映射到最大2MB的缓存块上,它不能放在任何映射了特定属性的2M区域内。
注意,在内核4.2以前,要求将DTB放在内核镜像里以text_offset为起始位置的512M区域内。
3、解压内核镜像(这个是可选的)
arm64(aarch64)的内核当前并不提供自解压功能,因此需要解压在boot loader里完成(比如gzip格式)。如果boot loader不支持解压,可以使用不压缩的镜像来启动。
4、启动内核镜像
解压后的内核镜像包含64byte的头,头结构定义如下:
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, "ARMx64" */ u32 res5; /* reserved (used for PE COFF offset) */
头结构说明:
(1)、在内核3.17版本以前,所有字段都是小端字节序,除非有特别说明。
(2)、code0/code1 是为了响应 stext 分支。
(3)、如果以EFI(可扩展固件接口 Extensible Firmware Interface)启动,code0/code1一开始就会被跳过。res5是指PE头和有EFI入口点(efi_stub_entry)的PE头的偏移。当efi完成了它的工作,就会跳转到 code0 的位置继续正常的启动流程。
(4)、在内核3.17版本以前,text_offset字段的字节序是不确定的。举个栗子,在内核字节序里,image_size 为0,text_offset为 0x80000。如果 image_size 是一个非0值,必须注意了,这时image_size 是小端字节序。如果 image_size 为0,那么text_offset可以认为是 0x80000。
(5)、flags 字段(内核3.17版本引入的)是一个小端字节序的64bit字段,它的组成如下:
Bit 0: 内核字节序标识, 1是大端, 0是小端; Bit 1-2:内核页的大小, 0 - 表示未说明,1 - 表示4K大小,2 - 16K,3 - 64K; Bit 3:内核物理布局, 0 - 基地址2MB对齐并且基地址应该离DRAM的基地址越可能的近,因为是线性映射,所以内存地址低于它的不能访问。 1 - 基地址2MB对齐,可以位于物理内存的任何地方。 Bit 4-63:保留字段。
(6)如果 image_size 为0,在内核镜像启动结束之后,bootloader应提供尽量多的空闲内存给内核使用。这个空间的数量会随着不同的特性变化,实际上是没有明确的限制的。
内核镜像位于任何可用的 text_offset 大小的字节数的内存基地址上,这个地址必须是2MB对齐的。2MB对齐的基地址与内核镜像起始位置这之间的区域对内核是没有特别的意义的,可以用作它用。
在内核镜像起始的位置起,至少 image_size 大小的字节数必须是空闲的,以供内核使用。
注意:在内核4.6版本之前不能使用低于镜像大小的物理偏移的内存,所以推荐镜像放在离物理内存地址起始位置尽可能近的地方。
如果 initd/initramfs 在启动的时候传递给了内核,它必须整个属于1GB对齐的物理内存窗口到32GB大小之间,以全部覆盖内核镜像为好。
任何描述给内核的内存(包括低于镜像起始地址的),如果没有标记为保留的(dtb里的 /memreserve指定)将被内核认为是可以使用的。
在跳转到内核之前,下面的条件必须满足:
(1)禁用所有的具有DMA能力的设备,这样内存就不会被伪造的网络数据包或者硬盘数据污染。这将节省你大量的调试时间。
(2)主CPU的通用寄存器设置:
x0 = dtb在系统内存的物理地址 x1 = 0 (保留给以后使用) x2 = 0 (保留给以后使用) x3 = 0 (保留给以后使用)
(3)CPU模式
所有的中断都必须在 PSTATE.DAIF (Debug,SError,IRQ,FIQ) 中设置掩码位。CPU必须处于EL2(推荐模式,方便虚拟化扩展访问)或者非安全模式的EL1模式中。
(4)Caches,MMUs
MMU必须关闭。
指令缓存可以开启或关闭。
对应于内核镜像的地址范围应该清理成PoC(PoC不知道是啥)。要使能系统缓存或者其他一致性主缓存,要求缓存维护通过VA,而不是 set/way 操作。
系统缓存 (依赖体系结构的缓存维护通过VA操作的)必须被配置并且使能,而不依赖体系结构通过VA维护的系统缓存必须禁止。
(5)定时器
CNTFRQ 必须对定时器频率是可编程的,并且CNTVOFF必须对在所有CPU上具有一致性的值是可编程的。如果进入内核时是在EL1模式,CNTHCTL_EL2 必须有EL1PCTEN (bit 0)设置可用。
(6)一致性
所有CPU通过内核启动必须是相同一致的内核入口的一部分。这将要求“IMPLEMENTATION DEFINED”的初始化来使能每个CPU来接收维护操作。
(7)系统寄存器
所有可写的系统寄存器在这内核镜像将要进入的异常级别(EL)必须在一个更高的异常级别(EL)通过软件初始化,来防止在一个未知的状态执行。
在一个有GICv3的中断控制器的系统可以使用v3模式:
1、如果是 EL3 : ICC_SRE_EL3.Enable (bit 3) 必须初始化为 0b1. ICC_SRE_EL3.SRE (bit 0) 必须初始化为 0b1. 2、内核是在 EL1 : ICC.SRE_EL2.Enable (bit 3) 必须初始化为 0b1 ICC_SRE_EL2.SRE (bit 0) 必须初始化为 0b1. 3、DT或者ACPI表必须在GICv3中断控制器中。
上述的CPU模式,缓存,MMU,定时器,一致性,系统寄存器对应所有的CPU,所有CPU必须在相同异常级别进入内核。
bootloader在进入内核(每个cpu)都有如下规则:
(1)主CPU直接跳转到内核镜像的第一条指令。dtb传给每个CPU必须包含“enable-method”属性,这个属性在下面会描述。
bootloader会生成这些设备树的属性并在内核入口之前插入到二进制执行文件中。
(2)带有“spin-table”使能方法的CPU必须有一个“cpu-release-addr”的属性节点。这个属性标识符以64bit自然对齐并在内存中初始化为0。
这些CPU在内核之外的保留的内存区域(dtb里的 /memreserve/ 的指定区域)空转,并轮询“cpu-release-addr”地址,该地址也在保留区域内。
“wfe”指令可以用来插入减少这种busy-loop的开销,并且主CPU会发出“sev”(嘛东西。)。 当读取“cpu-release-addr”返回一个非0值,这个CPU必须跳转到这个值的地址。
这个值就是一个简单的64bit的小端的数值,所有这些cpu必须转换成它自己的原生字节序之后才能跳转过去。
(3)具有“psci”的使能方法的CPU应该停留在内核之外的保留内存区域。内核会发出“CPU_ON”的调用来将CPU带入内核。
设备树应该包含一个“psci”节点。可以参考Documentation/devicetree/bindings/arm/psci.txt。
(4)从CPU上的通用寄存器设置:
x0 = 0 (reserved for future use) x1 = 0 (reserved for future use) x2 = 0 (reserved for future use) x3 = 0 (reserved for future use)