- 1 AArch64启动过程
- 2 初始化异常向量表
- 3 初始化寄存器
- 4 配置MMU和Cache
- 5 使能NEON和浮点
- 6 改变异常级别
1 AArch64启动过程
AArch64启动过程,其实就是初始化必要的硬件环境。大概有以下内容:
- 初始化异常向量表
- 初始化寄存器
- 配置MMU和Cache
- 使能NEON和浮点
- 改变异常级别
2 初始化异常向量表
参见ARM架构体系透视5.2-初始化异常-AArch64和ARM架构体系透视5.3-初始化异常-AArch32
3 初始化寄存器
涉及的寄存器:
- 通用目的寄存器
- 堆栈指针寄存器
- 系统控制寄存器
3.1 初始化通用目的寄存器
为什么需要初始化寄存器?
ARM processors use some non-reset flip-flops. This can cause X-propagation issues in simulations. Register initialization helps reduce the possibility of the issue.
翻译成大白话就是:
上电复位时,寄存器的值不确定。需要初始化到确定状态。
下面是一个初始化通用寄存器为0的示例:
MOV X0, XZR MOV X1, XZR MOV X2, XZR MOV X3, XZR MOV X4, XZR MOV X5, XZR MOV X6, XZR MOV X7, XZR MOV X8, XZR MOV X9, XZR MOV X10, XZR MOV X11, XZR MOV X12, XZR MOV X13, XZR MOV X14, XZR MOV X15, XZR MOV X16, XZR MOV X17, XZR MOV X18, XZR MOV X19, XZR MOV X20, XZR MOV X21, XZR MOV X22, XZR MOV X23, XZR MOV X24, XZR MOV X25, XZR MOV X26, XZR MOV X27, XZR MOV X28, XZR MOV X29, XZR MOV X30, XZR
如果处理器实现了NEON和FP扩展,还需要初始化相应的浮点寄存器。
MSR CPTR_EL3, XZR MSR CPTR_EL2, XZR FMOV D0, XZR FMOV D1, XZR FMOV D2, XZR FMOV D3, XZR FMOV D4, XZR FMOV D5, XZR FMOV D6, XZR FMOV D7, XZR FMOV D8, XZR FMOV D9, XZR FMOV D10, XZR FMOV D11, XZR FMOV D12, XZR FMOV D13, XZR FMOV D14, XZR FMOV D15, XZR FMOV D16, XZR FMOV D17, XZR FMOV D18, XZR FMOV D19, XZR FMOV D20, XZR
3.2 初始化堆栈指针寄存器
某些指令可以隐含地使用堆栈指针寄存器
(SP),比如push
和pop
。在使用之前,必须进行初始化。
多核系统中,堆栈指针必须指向不同的内存地址,避免覆盖彼此的堆栈区域。如果使用了不同异常级别的SP
寄存器,必须全部初始化。
下面的示例,展示了如何初始化当前异常级上的SP
寄存器。假设SP
指向的堆栈位于stack_top
,堆栈大小是CPU_STACK_SIZE
字节。
ADR X1, stack_top ADD X1, X1, #4 MRS X2, MPIDR_EL1 AND X2, X2, #0xFF // X2 == CPU编号 MOV X3, #CPU_STACK_SIZE MUL X3, X2, X3 // 为每一个CPU核创建独立的堆栈空间 SUB X1, X1, X3 MOV SP, X1
3.3 初始化系统控制寄存器
某些系统寄存器上电时不会复位。因此,必须在使用它们之前,根据软件需求初始化这些寄存器。下面的示例展示了如何初始化HCR_EL2
、SCTLR_EL2
和SCTLR_EL1
这些系统控制寄存器。
MSR HCR_EL2, XZR LDR X1, =0x30C50838 MSR SCTLR_EL2, X1 MSR SCTLR_EL1, X1
这儿,只是个简单的示例,需要初始化的系统寄存器有许多。理论上,应该初始化所有没有reset
值的系统寄存器。但是,某些寄存器是实现时定义的reset
值,这依赖于某个处理器的具体实现。关于通用系统控制寄存器,请参考ARMv8
架构官方手册以及CPU相关厂商的技术参考手册(TRM
)。
4 配置MMU和Cache
MMU
和cache
配置包含以下内容:
- 清除、失效
Cache
- 设置
MMU
- 使能
MMU
和Cache
4.1 清除、失效Cache
reset
之后,高速缓存Cache
中的内容都是非法的。ARMv8-A
处理器复位后,硬件自动失效所有Cache
,所以,软件无需处理。但是,在某些情况下,仍然需要清除、失效D-Cache
,比如某个Core
核心掉电的过程中。
下面的示例,展示了如何在EL3
,通过循环调用DC CISW
指令,清除、失效L1
级D-Cache
。可以作为参考,修改其它等级上的Cache
或者其它Cache
操作。
// 禁止L1-Cache MRS X0, SCTLR_EL3 // 读取SCTLR_EL3 BIC X0, X0, #(0x1 << 2) // 禁止D-Cache MSR SCTLR_EL3, X0 // 写SCTLR_EL3 // 使D-Cache无效,以使代码通用 // 首先,计算Cache大小,然后循环遍历每个set + way MOV X0, #0x0 // X0 = Cache level MSR CSSELR_EL1, X0 // 0x0是L1-Dcache;0x2是L2-Dcache. MRS X4, CCSIDR_EL1 // 读取Cache大小的标识 AND X1, X4, #0x7 ADD X1, X1, #0x4 // X1 = Cache Line大小 LDR X3, =0x7FFF AND X2, X3, X4, LSR #13 // X2 = Cache Set Number – 1. LDR X3, =0x3FF AND X3, X3, X4, LSR #3 // X3 = Cache Associativity Number – 1. CLZ W4, W3 // X4 = way position in the CISW instruction. MOV X5, #0 // X5 = way counter way_loop. way_loop: MOV X6, #0 // X6 = set counter set_loop. set_loop: LSL X7, X5, X4 ORR X7, X0, X7 // Set way. LSL X8, X6, X1 ORR X7, X7, X8 // Set set. DC cisw, X7 // Clean and Invalidate cache line. ADD X6, X6, #1 // Increment set counter. CMP X6, X2 // Last set reached yet? BLE set_loop // If not, iterate set_loop, ADD X5, X5, #1 // else, next way. CMP X5, X3 // Last way reached yet? BLE way_loop // If not, iterate way_loop.
4.2 设置MMU
ARMv8-A
处理器使用VMSAv8-64
虚拟内存架构执行下面的操作:
- 物理地址到虚拟地址的转换
- 确定内存属性,检查访问权限
地址转换由地址页表定义,并由MMU
管理。每个异常级都有一个专用的地址页表。地址页表必须在使能MMU
之前建立好。
VMSAv8-64
使用64位地址描述符,描述地址页表中的每一项。它支持:
- 最高
48
位输入、输出地址 - 三种颗粒度:
4K
、16K
、64K
- 最高4级地址页表遍历
更多细节,请参考ARMv8
架构官方手册中的AArch64 Virtual Memory System Architecture
部分内容。
下面两个例子,构建一个EL3
转换页表,颗粒度大小为4K
,内存大小为4G
。
0-1G
配置为正常的可缓存内存1-4G
配置为Device-nGnRnE
内存
页表包含512
个2M
大小的Level2
内存块和3
个1G
大小的Level1
内存块。
示例1:首先,初始化页表控制寄存器。然后,循环使用store
指令构建页表,很方便移植。
/* 初始化转换表控制寄存器 */ LDR X1, =0x3520 // 4G空间、4K颗粒度 // Inner-shareable MSR TCR_EL3, X1 // Normal Inner and Outer Cacheable. LDR X1, =0xFF440400 // ATTR0 Device-nGnRnE ATTR1 Device. MSR MAIR_EL3, X1 // ATTR2 Normal Non-Cacheable. // ATTR3 Normal Cacheable. ADR X0, ttb0_base // ttb0_base必须是4K对齐的地址 MSR TTBR0_EL3, X0 /** * 1. 使用store指令,循环往页表所在内存写入表项 * 2. 建立Level-1转换表 * 3. 第1项指向Level-2页表(level2_pagetable) */ LDR X1, = level2_pagetable // 必须是4K对齐的地址 LDR X2, =0xFFFFF000 AND X2, X1, X2 // NSTable=0 APTable=0 XNTable=0 PXNTable=0. ORR X2, X2, 0x3 STR X2, [X0], #8 /* 第2项是1G大小的内存块(0x40000000 ~ 0x7FFFFFFF)*/ LDR X2, =0x40000741 // Executable Inner and Outer Shareable. STR X2, [X0], #8 // R/W at all ELs secure memory // AttrIdx=000 Device-nGnRnE. /* 第3项是1G大小的内存块(0x80000000 ~ 0xBFFFFFFF)*/ LDR X2, =0x80000741 STR X2, [X0], #8 /* 第4项是1G大小的内存块(0xC0000000 ~ 0xFFFFFFFF)*/ LDR X2, =0xC0000741 STR X2, [X0], #8 // 建立Level-2转换表 LDR X0, =level2_pagetable // level2_pagetable基地址 LDR X2, =0x0000074D // Executable Inner and Outer Shareable. // R/W at all ELs secure memory. // AttrIdx=011 Normal Cacheable. MOV X4, #512 // Level-2表项个数512 LDR X5, =0x00200000 // 每次增加2M地址空间 loop: STR X2, [X0], #8 // 每项占据2个WORD大小 ADD X2, X2, X5 SUBS X4, X4, #1 BNE loop
示例2:在编译时,创建一个section
作为转换页表。该方法对于模拟来说,速度很快。使用GNU
汇编语法编写。上面示例1中,初始化转换表控制寄存器的代码仍然需要。
/* 定义一个宏,存放小端字节序的64位值 */ .macro PUT_64B high, low .word \low .word \high .endm /* 创建一个表项,指向下一级的页表 */ .macro TABLE_ENTRY PA, ATTR PUT_64B \ATTR, (\PA) + 0x3 .endm /* 创建一个表项,指向一个1G大小的内存块 */ .macro BLOCK_1GB PA, ATTR_HI, ATTR_LO PUT_64B \ATTR_HI, ((\PA) & 0xC0000000) | \ATTR_LO | 0x1 .endm /* 创建一个表项,指向一个2M大小的内存块 */ .macro BLOCK_2MB PA, ATTR_HI, ATTR_LO PUT_64B \ATTR_HI, ((\PA) & 0xFFE00000) | \ATTR_LO | 0x1 .endm .align 12 // 按照2^12大小对齐,也就是满足4K大小的颗粒度 ttb0_base: TABLE_ENTRY level2_pagetable, 0 BLOCK_1GB 0x40000000, 0, 0x740 BLOCK_1GB 0x80000000, 0, 0x740 BLOCK_1GB 0xC0000000, 0, 0x740 .align 12 // 按照2^12大小对齐,也就是满足4K大小的颗粒度 level2_pagetable: .set ADDR, 0x000 // 当前page的地址 .rept 0x200 BLOCK_2MB (ADDR << 20), 0, 0x74C .set ADDR, ADDR+2 .endr
4.3 使能MMU
和cache
在使能MMU
和cache
之前,必须先初始化它们。所有的ARMv8-A
处理器要求,为了让MMU
和Cache
支持硬件一致性(hardware coherency)
,必须先设置SMPEN
标志位。
下面的示例,展示了如何设置SMPEN
标志位,并使能MMU
和Cache
的方法。
/* 位于CPUECTLR寄存器中 */ MRS X0, S3_1_C15_C2_1 ORR X0, X0, #(0x1 << 6) // SMP标志位 MSR S3_1_C15_C2_1, X0 /* 使能MMU和Cache */ MRS X0, SCTLR_EL3 ORR X0, X0, #(0x1 << 2) // C标志位(D-Cache) ORR X0, X0, #(0x1 << 12) // I标志位(I-Cache) ORR X0, X0, #0x1 // M标志位(MMU) MSR SCTLR_EL3, X0 DSB SY ISB
5 使能NEON和浮点
在AArch64
状态下,不需要使能对NEON
和FP
寄存器的访问。但是,仍然可以捕获对NEON
和FP
寄存器的访问。
下面的示例,展示了如何在所有异常级下,禁止捕获对NEON
和FP
寄存器的访问。
/* 在EL3和EL2,禁止捕获对这些寄存器的访问 */ MSR CPTR_EL3, XZR MSR CPTR_EL3, XZR /* 在EL1和EL0,禁止捕获对这些寄存器的访问 */ MOV X1, #(0x3 << 20) // 设置FPEN,禁止EL1对这些寄存器的访问 MSR CPACR_EL1, X1 ISB
6 改变异常级别
ARMv8-A
架构新引入了4
个异常级别:EL0
、EL1
、EL2
、EL3
。
有时候,在测试用例中,必须在这些异常级之间进行切换。陷入异常或从异常返回时,处理器都会改变异常级。对于异常级的更多细节,请参考ARMv8
架构官方手册中的Exception levels
部分内容。
6.1 EL3
→ EL0
(AArch64
)
reset
之后,处理器会进入EL3
。此时,较低的异常级EL
的控制寄存器和异常状态没有明确。为了进入较低异常级,必须初始化相关执行状态和控制寄存器。然后,调用ERET
指令,执行一个伪造
的异常返回。
下面的示例,展示了如何从EL3
切换到非安全EL0
的过程:
/* 在进入EL2之前,初始化SCTLR_EL2和HCR_EL2寄存器,保存相关值 */ MSR SCTLR_EL2, XZR MSR HCR_EL2, XZR /* 确定EL2执行状态 */ MRS X0, SCR_EL3 ORR X0, X0, #(1<<10) // RW EL2执行状态是AArch64AArch64. ORR X0, X0, #(1<<0) // NS EL1 is Non-secure world. MSR SCR_EL3, x0 MOV X0, #0b01001 // DAIF=0000 MSR SPSR_EL3, X0 // M[4:0]=01001 EL2h must match SCR_EL3.RW // Determine EL2 entry. ADR X0, el2_entry // el2_entry points to the first instruction of MSR ELR_EL3, X0 // EL2 code. ERET el2_entry: /* 在进入EL1时,初始化SCTLR_EL1寄存器 */ MSR SCTLR_EL1, XZR /* 确定EL1执行状态 */ MRS X0, HCR_EL2 ORR X0, X0, #(1<<31) // RW=1 EL1执行状态是AArch64 MSR HCR_EL2, X0 MOV X0, #0b00101 // DAIF=0000 MSR SPSR_EL2, X0 // M[4:0]=00101 EL1h must match HCR_EL2.RW. ADR X0, el1_entry // el1_entry points to the first instruction of MSR ELR_EL2, X0 // EL1 code. ERET el1_entry: /* 确定EL0执行状态 */ MOV X0, #0b00000 // DAIF=0000 M[4:0]=00000 EL0t. MSR SPSR_EL1, X0 ADR x0, el0_entry // el1_entry指向`EL0`代码的第一条指令 MSR ELR_EL1, X0 ERET el0_entry: /* EL0代码 */
6.2 AArch64 EL2
→ AArch32 EL1
有时候,可能在不同的异常级上混合执行状态。比如,较高异常级使用AArch64
状态,而较低异常级被允许运行在AArch64
或AArch32
状态下。因此,有可能从较高异常的AArch64
状态,切换到较低异常级的AArch32
状态。
下面的示例,展示了如何从AArch64 EL2
切换到AArch32 EL1
。
/* 在进入EL1时,初始化SCTLR_EL1寄存器 */ MSR SCTLR_EL1, XZR MRS X0, HCR_EL2 BIC X0, X0, #(1<<31) // RW=0 EL1执行状态是AArch32 MSR HCR_EL2, X0 MOV X0, #0b10011 // DAIF=0000 MSR SPSR_EL2, X0 // M[4:0]=10011 EL1 is SVC mode must match HCR_EL2.RW. // Determine EL1 Execution state. ADR X0, el1_entry // el1_entry points to the first instruction of SVC MSR ELR_EL2, X0 // mode code. ERET el1_entry: /* EL1代码 */