
嵌入式工程师学ARMv8系列(四)Uniproton操作系统启动汇编分析
ARMv8有EL0~EL3四个异常等级。ARMv8有安全和非安全世界之分。有X0到X30共31个通用寄存器。有SP_ELn(n=0-3),ELR_ELn(n=1-3),SPSR_ELn(n=1-3)等特殊寄存器。MSR、MRS和ERET指令作用加载指令LDR和STR及其相关变种。带有BF字眼大多是bit操作。要学会用百度和Deepseek。在了解了上述内容后,我们可以分析下一个嵌入式系统Unipr
嵌入式工程师学ARMv8系列(四)Uniproton操作系统启动汇编分析
文章目录
前言
在前面的文章中简单介绍了如下的ARMv8的基础和汇编指令基础,总结如下:
- ARMv8有EL0~EL3四个异常等级。
- ARMv8有安全和非安全世界之分。
- 有X0到X30共31个通用寄存器。
- 有SP_ELn(n=0-3),ELR_ELn(n=1-3),SPSR_ELn(n=1-3)等特殊寄存器。
- MSR、MRS和ERET指令作用
- 加载指令LDR和STR及其相关变种。
- 带有BF字眼大多是bit操作。
- 要学会用百度和Deepseek。
在了解了上述内容后,我们可以分析下一个嵌入式系统Uniproton的启动汇编函数,来对之前的内容进行回顾。
ARMv8手册:
https://download.csdn.net/download/qq_14825629/90510635?spm=1001.2014.3001.5503
uniproton源码:https://download.csdn.net/download/qq_14825629/90487467?spm=1001.2014.3001.5503
一、Uniproton启动分析
0. 如何找到某个操作系统的入口函数
我们知道一个程序要经过预处理、编译和连接的过程才能被编译成目标文件,当运行目标文件时往往以main函数开始执行。操作系统也是一个程序,也要经过预处理、编译和连接的过程,但是不会从main启动,而是从CPU指定的地址启动第一行代码,因此我们要通过连接器将我们想要执行的第一个程序放入启动地址。
连接器通过连接脚本完成程序连接,因此我们拿到一个新的操作系统源码时,需要找到连接文件就可以确定第一个被启动的函数,连接脚本往往以xxx.ld
或xxx.lds
命名。以Uniproton为例,查找ld文件,会找到很多板卡的连接脚本,我们以\UniProton-master\demos\rk3588\build\rk3588.ld
为例进行分析。
ENTRY(__text_start)//入口
_stack_size = 0x10000;
_heap_size = 0x10000;
MEMORY
{
IMU_SRAM (rwx) : ORIGIN = 0x7a000000, LENGTH = 0x800000
MMU_MEM (rwx) : ORIGIN = 0x7a800000, LENGTH = 0x800000
}
SECTIONS
{
__os_section_start = .;
text_start = .;
.start_bspinit :
{
__text_start = .;
KEEP(*(.text.bspinit))//第一个text段是bspinit,也就目标码的从bspinit段开始
} > IMU_SRAM
.start_text :
{
KEEP(*(.text.startup))//随后是startup段函数
} > IMU_SRAM
............//省略好多代码没有粘贴出来
.mmu.table.base :
{
PROVIDE (g_mmu_page_begin = .);
PROVIDE (g_mmu_page_end = g_mmu_page_begin + 0x8000);
} > MMU_MEM
}
通过上面对ld文件的分析,就确定了启动段是bspinit,在源码中全局搜索定位bspinit位于UniProton-master\demos\rk3588\bsp\start.S
中。
1. Uniproton操作系统启动分析
Uniproton操作系统在ARMv8架构的ARM core的启动要注意几点:
- 当前处于哪个EL等级。ARMv8中定义了cpu在启动后位于最高的异常等级,但是由于Uniproton前有ATF、uboot或他只作为AMP的从核,所以启动等级需要确认下之前启动的配置,此处为EL2。
- Uniproton运行于哪个异常等级。不同的SoC厂家、不同的操作系统厂家都有自己的设计,Uniproton运行于EL1。
下面代码是UniProton-master\demos\rk3588\bsp\start.S
内容,文中做了详细的注释可以参照这些注释了解启动过程。当然也可以只关注汇编的使用,不关心为何要这么启动。
#include "prt_buildef.h"
.global OsResetVector
.global mmu_init
.type mmu_init, function
.type start, function
.section .text.bspinit, "ax" //bspinit段,与ld文件对应,是系统启动的开始
.balign 4
#define HCR_EL2_FMO (1 << 3)//宏定义不占用空间
#define HCR_EL2_IMO (1 << 4)
#define HCR_EL2_AMO (1 << 5)
#define HCR_EL2_TWI (1 << 13)
#define HCR_EL2_TWE (1 << 14)
#define HCR_EL2_TVM (1 << 26)
#define HCR_EL2_TGE (1 << 27)
#define HCR_EL2_TDZ (1 << 28)
#define HCR_EL2_HCD (1 << 29)
#define HCR_EL2_TRVM (1 << 30)
#define HCR_EL2_RW (1 << 31)
#define SPSR_DBG_MASK (1 << 9)
#define SPSR_SERR_MASK (1 << 8)
#define SPSR_IRQ_MASK (1 << 7)
#define SPSR_FIQ_MASK (1 << 6)
#define SPSR_M_AARCH64 (0 << 4)
#define SPSR_M_AARCH32 (1 << 4)
#define SPSR_M_EL1H (5)
#define SPSR_M_EL2H (9)
#define CNTHCTL_EL2_EL1PCEN_EN (1 << 1)
#define CNTHCTL_EL2_EL1PCTEN_EN (1 << 0)
#define CPACR_EL1_FPEN_EN (3 << 20)
.global OsElxState
.type OsElxState, @function
OsElxState://第一个函数,是一个label不占用空间
MRS x6, CurrentEL//真正意义的第一行代码!读取CurrentEL到x6,即读取当前异常等级
MOV x2, #0x4//CurrentEL[3:2] is ELx 0x4>>2=0x1
CMP w6, w2//判断当前是否在EL1等级
BEQ Start//如果当前是EL1,跳转到Start函数;如果不是向下执行
OsEl2Entry://主要作用是配置好EL2,做好跳转准备
MRS x10, CNTHCTL_EL2//读取CNTHCTL_EL2,即hypervisor时钟控制寄存器
ORR x10, x10, #0x3//将bit[1:0]置1,即防止EL0的P/V定时器触发EL2
MSR CNTHCTL_EL2, x10
MRS x10, CNTKCTL_EL1//读取CNTKCTL_EL1到x10,即Kernel时钟控制寄存器
ORR x10, x10, #0x3//将bit[1:0]置1,即防止EL0的配置频率和控制寄存器
MSR CNTKCTL_EL1, x10
MRS x10, MIDR_EL1//MIDR_EL1,主核的ID号
MRS x1, MPIDR_EL1//MPIDR_EL1,多核ID号
MSR VPIDR_EL2, x10//写入虚拟主核ID,如果EL2不被允许,写无效
MSR VMPIDR_EL2, x1//写入虚拟多核ID,如果EL2不被允许,写无效
MOV x10, #0x33ff//64b'00110011_11111111
MSR CPTR_EL2, x10
MSR HSTR_EL2, xzr//禁止EL1或更低级别trap到EL2
MRS x10, CPACR_EL1//读取CPACR_EL1,即SVE、SIMD控制寄存器
MOV x10, #3 << 20//SVE、SIMD不会trap到EL2
MSR CPACR_EL1, x10
MOV x10, #(HCR_EL2_RW)//HCR_EL2_RW (1 << 31)
ORR x10, x10, #(HCR_EL2_HCD)//(1 << 31) | (1 << 29)
BIC x10, x10, #(HCR_EL2_TVM)//(1 << 31) | (1 << 29) & ~(1<<26)关闭EL1写虚拟内存trap到EL2
BIC x10, x10, #(HCR_EL2_TRVM)//(1 << 31) | (1 << 29) & ~(1<<26) & ~(1<<30)关闭EL1读虚拟内存trap到EL2
BIC x10, x10, #(HCR_EL2_TGE)//关闭EL0的异常到EL2
BIC x10, x10, #(HCR_EL2_AMO)//关闭比EL2低等级的sError tarp到EL2
BIC x10, x10, #(HCR_EL2_IMO)//关闭比EL2低等级的Irq tarp到EL2
BIC x10, x10, #(HCR_EL2_FMO)//关闭比EL2低等级的Fiq tarp到EL2
BIC x10, x10, #(HCR_EL2_TWI)//关闭比EL2低等级触发WFI
BIC x10, x10, #(HCR_EL2_TWE)//关闭比EL2低等级触发WFE
MSR HCR_EL2, x10//写入HCR_EL2
OsEl2SwitchToEl1:
ADR x0, Start//相对寻址,加载Start函数地址
MSR SP_EL1, XZR//SP_EL1栈地址为0
MSR ELR_EL2, x0//eret后跳到ELR_EL2地址,即为Start地址
MOV x0, XZR
LDR x20, =(SPSR_DBG_MASK | SPSR_SERR_MASK | \
SPSR_IRQ_MASK | SPSR_FIQ_MASK | SPSR_M_EL1H)//关闭各种中断,并配置跳转异常等级为EL1,使用EL_SP1作为栈
MSR SPSR_EL2, x20
TLBI ALLE1IS//清除所有TLB
IC IALLU//清除所有I-cache
DSB SY
ISB
ERET //PSTATE=SPSR_EL2,ELR_EL2=Start,SP=SP_EL1,即跳转到EL1,入口函数为Start,栈地址为0
Start:
LDR x1, =__os_sys_sp_end//设置SP,即SP_EL1,此后就可以运行C语言了
BIC sp, x1, #0xf//ABI规定16字节对齐
MRS x10, CNTKCTL_EL1//读取CNTKCTL_EL1,即Kernel时钟控制寄存器
ORR x10, x10, #0x3
MSR CNTKCTL_EL1, x10//再次防止EL0修改时钟和频率
/* enable FPU */
MRS x10, CPACR_EL1//读取CPACR_EL1,即SVE、SIMD控制寄存器
MOV x10, #3 << 20
MSR CPACR_EL1, x10//再次防止SVE、SIMD不会trap到EL2
ISB
BL mmu_init
B OsResetVector//\src\arch\cpu\armv8\common\hwi\prt_reset_vector.S
OsEnterReset:
B OsEnterReset
.section .text, "ax"
.balign 4
上述代码中主要做了如下几件事:
- 判断当前异常等级是否为Uniproton需要的EL1等级;
- 如果不是,那么配置EL2,确保能够跳转到EL1;
- 配置跳转后EL1所需环境,配置ELR_EL2表示执行eret后跳转函数位置;配置SPSR_EL2表示执行eret后跳转到哪个EL(SPSR_M_EL1H)并关闭相关异常;
- 执行eret完成到EL1的跳转,执行函数为Start。
- 在start函数中完成SP,即SP_EL1的初始化。
- 随后就可以进行C语言程序的执行了。
结尾
简而言之本文描述了如下几点:
- 如何确定一个操作系统运行的第一个程序。
- 分析了Uniproton的启动到C语言环境的准备过程。
更多推荐
所有评论(0)