嵌入式工程师学ARMv8系列(四)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, @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

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

  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语言环境的准备过程。
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐