> 技术文档 > 嵌入式工程师学ARMv8系列(四)Uniproton操作系统启动汇编分析

嵌入式工程师学ARMv8系列(四)Uniproton操作系统启动汇编分析


嵌入式工程师学ARMv8系列(四)Uniproton操作系统启动汇编分析

文章目录

  • 嵌入式工程师学ARMv8系列(四)Uniproton操作系统启动汇编分析
  • 前言
  • 一、Uniproton启动分析
    • 0. 如何找到某个操作系统的入口函数
    • 1. Uniproton操作系统启动分析
  • 结尾

前言

在前面的文章中简单介绍了如下的ARMv8的基础和汇编指令基础,总结如下:

  1. ARMv8有EL0~EL3四个异常等级
  2. ARMv8有安全和非安全世界之分。
  3. 有X0到X30共31个通用寄存器
  4. 有SP_ELn(n=0-3),ELR_ELn(n=1-3),SPSR_ELn(n=1-3)等特殊寄存器。
  5. MSR、MRS和ERET指令作用
  6. 加载指令LDR和STR及其相关变种。
  7. 带有BF字眼大多是bit操作。
  8. 要学会用百度和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.ldxxx.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的启动要注意几点:

  1. 当前处于哪个EL等级。ARMv8中定义了cpu在启动后位于最高的异常等级,但是由于Uniproton前有ATF、uboot或他只作为AMP的从核,所以启动等级需要确认下之前启动的配置,此处为EL2。
  2. 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, @functionOsElxState://第一个函数,是一个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.SOsEnterReset: B OsEnterReset .section .text, \"ax\" .balign 4

上述代码中主要做了如下几件事:

  1. 判断当前异常等级是否为Uniproton需要的EL1等级;
  2. 如果不是,那么配置EL2,确保能够跳转到EL1;
  3. 配置跳转后EL1所需环境,配置ELR_EL2表示执行eret后跳转函数位置;配置SPSR_EL2表示执行eret后跳转到哪个EL(SPSR_M_EL1H)并关闭相关异常;
  4. 执行eret完成到EL1的跳转,执行函数为Start。
  5. 在start函数中完成SP,即SP_EL1的初始化。
  6. 随后就可以进行C语言程序的执行了。

结尾

简而言之本文描述了如下几点:

  1. 如何确定一个操作系统运行的第一个程序。
  2. 分析了Uniproton的启动到C语言环境的准备过程。