深入MTK平台bootloader启动之【 Pre-loader -> Lk】分析笔记

2023-11-06

1、bootloader到kernel启动总逻辑流程图

ARM架构中,EL0/EL1是必须实现,EL2/EL3是选配,ELx跟层级对应关系:

EL0 -- app

EL1 -- Linux kernel 、lk

EL2 -- hypervisor(虚拟化)

EL3 -- ARM trust firmware 、pre-loader

若平台未实现EL3(atf),pre-loader直接加载lk:


image

若平台实现EL3,则需要先加载完ATF再由ATF去加载lk:


bootloader 启动分两个阶段,一个是pre-loader加载lk(u-boot)阶段,另一个是lk加载kernel阶段。下面跟着流程图简述第一个阶段的加载流程。

1-3:设备上电起来后,跳转到Boot ROM(不是flash)中的boot code中执行把pre-loader加载起到ISRAM, 因为当前DRAM(RAM分SRAM跟DRAM,简单来说SRAM就是cache,DRAM就是普通内存)还没有准备好,所以要先把pre-loader load到芯片内部的ISRAM(Internal SRAM)中。

4-6:pre-loader初始化好DRAM后就将lk从flash(nand/emmc)中加载到DRAM中运行;

7-8:解压bootimage成ramdisk跟kernel并载入DRAM中,初始化dtb;

9-11:lk跳转到kernl初始化, kernel初始化完成后fork出init进程, 然后拉起ramdisk中的init程序,进入用户空间初始化,init进程fork出zygote进程..直到整个Android启动完成.


2、从pre-loader到lk(mt6580为例)

Pre-loader主要干的事情就是初始化某些硬件,比如: UART,GPIO,DRAM,TIMER,RTC,PMIC 等等,建立起最基本的运行环境,最重要的就是初始化DRAM.

时序图:


点击查看大图

源码流程如下:

./bootloader/preloader/platform/mt6580/src/init/init.s

.section .text.start
...

.globl _start
...

    /* set the cpu to SVC32 mode */
    MRS	r0,cpsr
    BIC	r0,r0,#0x1f
    ORR	r0,r0,#0xd3
    MSR	cpsr,r0

    /* disable interrupt */
    MRS r0, cpsr
    MOV r1, #INT_BIT
    ORR r0, r0, r1
    MSR cpsr_cxsf, r0
    
...
setup_stk :
    /* setup stack */
    LDR r0, stack
    LDR r1, stacksz
...

entry :
    LDR r0, =bldr_args_addr
    
    /* 跳转到C代码 main 入口 */
    B   main

init.s 主要干的事情是切换系统到管理模式(svc)(如果平台有实现el3,那么pre-loader运行在el3,否则运行在el1),禁止irq/fiq,设置stack等, 然后jump到c代码main函数入口。 

进入源码分析。

./bootloader/preloader/platform/mt6580/src/core/main.c

void main(u32 *arg)
{
    struct bldr_command_handler handler;
    u32 jump_addr, jump_arg;

    /* get the bldr argument */
    bldr_param = (bl_param_t *)*arg;

// 初始化uart 
    mtk_uart_init(UART_SRC_CLK_FRQ, CFG_LOG_BAUDRATE);
    
// 这里干了很多事情,包括各种的平台硬件(timer,pmic,gpio,wdt...)初始化工作.
    bldr_pre_process();

    handler.priv = NULL;
    handler.attr = 0;
    handler.cb   = bldr_cmd_handler;

// 这里是获取启动模式等信息保存到全局变量g_boot_mode和g_meta_com_type 中.
	BOOTING_TIME_PROFILING_LOG("before bldr_handshake");
    bldr_handshake(&handler);
	BOOTING_TIME_PROFILING_LOG("bldr_handshake");

// 下面跟 secro img 相关,跟平台设计强相关.
    /* security check */
    sec_lib_read_secro();
    sec_boot_check();
    device_APC_dom_setup();

	BOOTING_TIME_PROFILING_LOG("sec_boot_check");

/* 如果已经实现EL3,那么进行tz预初始化 */
#if CFG_ATF_SUPPORT
    trustzone_pre_init();
#endif

/* bldr_load_images
此函数要做的事情就是把lk从ROM中指定位置load到DRAM中,开机log中可以看到具体信息:
[PART] load "lk" from 0x0000000001CC0200 (dev) to 0x81E00000 (mem) [SUCCESS]
这里准备好了jump到DRAM的具体地址,下面详细分析.
*/
    if (0 != bldr_load_images(&jump_addr)) {
        print("%s Second Bootloader Load Failed\n", MOD);
        goto error;
    }

/* 
该函数的实现体是platform_post_init,这里要干的事情其实比较简单,就是通过
hw_check_battery去判断当前系统是否存在电池(判断是否有电池ntc脚来区分),
如果不存在就陷入while(1)卡住了,所以在es阶段调试有时候
需要接电源调试的,就需要改这里面的逻辑才可正常开机 
*/
    bldr_post_process();

// atf 正式初始化,使用特有的系统调用方式实现.
#if CFG_ATF_SUPPORT
    trustzone_post_init();
#endif

/* 跳转传入lk的参数,包括boot time/mode/reason 等,这些参数在
   platform_set_boot_args 函数获取。
*/
    jump_arg = (u32)&(g_dram_buf->boottag);


/* 执行jump系统调用,从 pre-loader 跳转到 lk执行,
 
 
如果实现了EL3情况就要 复杂一些,需要先跳转到EL3初始化,然后再跳回lk,pre-loader执行在EL3, LK执行在EL1)
从log可以类似看到这些信息: [BLDR] jump to 0x81E00000 [BLDR] <0x81E00000>=0xEA000007 [BLDR] <0x81E00004>=0xEA0056E2 */ #if CFG_ATF_SUPPORT /* 64S3,32S1,32S1 (MTK_ATF_BOOT_OPTION = 0) * re-loader jump to LK directly and then LK jump to kernel directly */ if ( BOOT_OPT_64S3 == g_smc_boot_opt && BOOT_OPT_32S1 == g_lk_boot_opt && BOOT_OPT_32S1 == g_kernel_boot_opt) { print("%s 64S3,32S1,32S1, jump to LK\n", MOD); bldr_jump(jump_addr, jump_arg, sizeof(boot_arg_t)); } else { // 如果 el3 使用aarch64实现,则jump到atf. print("%s Others, jump to ATF\n", MOD); bldr_jump64(jump_addr, jump_arg, sizeof(boot_arg_t)); } #else bldr_jump(jump_addr, jump_arg, sizeof(boot_arg_t)); #endif // 如果没有取到jump_addr,则打印错误提示,进入while(1)等待. error: platform_error_handler(); }

main 函数小结:

1、各种硬件初始化(uart、pmic、wdt、timer、mem..);

2、获取系统启动模式等,保存在全局变量中;

3、Security check,跟secro.img相关;

4、如果系统已经实现el3,则进入tz初始化;

5、获取lk加载到DRAM的地址(固定值),然后从ROM中找到lk分区的地址, 如果没找到jump_addr,则 goto error;

6、battery check,如果没有电池就会陷入while(1);

7、jump到lk(如果有实现el3,则会先jump到el3,然后再回到lk)


3、重点函数分析

bldr_load_images

函数主要干的事情就是找到lk分区地址和lk加载到DRAM中的地址, 准备好jump到lk执行,如下源码分析:

static int bldr_load_images(u32 *jump_addr)
{
    int ret = 0;
    blkdev_t *bootdev;
    u32 addr = 0;
    char *name;
    u32 size = 0;
    u32 spare0 = 0;
    u32 spare1 = 0;

...
/* 这个地址是一个固定值,可以查到定义在:
   ./bootloader/preloader/platform/mt6580/default.mak:95:
   CFG_UBOOT_MEMADDR := 0x81E00000
   从log中可以看到:
   [BLDR] jump to 0x81E00000
*/
    addr = CFG_UBOOT_MEMADDR;
    
/* 然后去ROM找到lk所在分区地址 */
    ret = bldr_load_part("lk", bootdev, &addr, &size);
    if (ret)
       return ret;
    *jump_addr = addr;
    
}

// 这个函数逻辑很简单,就不需要多说了.
int bldr_load_part(char *name, blkdev_t *bdev, u32 *addr, u32 *size)
{
    part_t *part = part_get(name);

    if (NULL == part) {
        print("%s %s partition not found\n", MOD, name);
        return -1;
    }

    return part_load(bdev, part, addr, 0, size);
}

// 真正的load实现是在part_load函数.
int part_load(blkdev_t *bdev, part_t *part, u32 *addr, u32 offset, u32 *size)
{
    int ret;
    img_hdr_t *hdr = (img_hdr_t *)img_hdr_buf;
    part_hdr_t *part_hdr = &hdr->part_hdr;
    gfh_file_info_t *file_info_hdr = &hdr->file_info_hdr;

    /* specify the read offset */
    u64 src = part->startblk * bdev->blksz + offset;
    u32 dsize = 0, maddr = 0;
    u32 ms;

// 检索分区头是否正确。
    /* retrieve partition header. */
    if (blkdev_read(bdev, src, sizeof(img_hdr_t), (u8*)hdr,0) != 0) {
        print("[%s]bdev(%d) read error (%s)\n", MOD, bdev->type, part->name);
        return -1;
    }

    if (part_hdr->info.magic == PART_MAGIC) {

        /* load image with partition header */
        part_hdr->info.name[31] = '\0';

    /*
        输出分区的各种信息,从log中可以看到:
        [PART] Image with part header
        [PART] name : lk
        [PART] addr : FFFFFFFFh mode : -1
        [PART] size : 337116
        [PART] magic: 58881688h
    */
        print("[%s]Img with part header\n", MOD);
        print("[%s]name:%s\n", MOD, part_hdr->info.name);
        print("[%s]addr:%xh\n", MOD, part_hdr->info.maddr);
        print("[%s]size:%d\n", MOD, part_hdr->info.dsize);
        print("[%s]magic:%xh\n", MOD, part_hdr->info.magic);

        maddr = part_hdr->info.maddr;
        dsize = part_hdr->info.dsize;
        src += sizeof(part_hdr_t);

        memcpy(part_info + part_num, part_hdr, sizeof(part_hdr_t));
        part_num++;
    } else {
        print("[%s]%s img not exist\n", MOD, part->name);
        return -1;
    }

// 如果maddr没有定义,那么就使用前面传入的地址addr.
    if (maddr == PART_HEADER_MEMADDR/*0xffffffff*/)
        maddr = *addr;

    if_overlap_with_dram_buffer((u32)maddr, ((u32)maddr + dsize));

    ms = get_timer(0);
    if (0 == (ret = blkdev_read(bdev, src, dsize, (u8*)maddr,0)))
        *addr = maddr;
    ms = get_timer(ms);

/* 如果一切顺利就会打印出关键信息:
   [PART] load "lk" from 0x0000000001CC0200 (dev) to 0x81E00000 (mem) [SUCCESS]
   [PART] load speed: 25324KB/s, 337116 bytes, 13ms
*/
    print("\n[%s]load \"%s\" from 0x%llx(dev) to 0x%x (mem) [%s]\n", MOD,
        part->name, src, maddr, (ret == 0) ? "SUCCESS" : "FAILED");

    if( ms == 0 )
        ms+=1;

    print("[%s]load speed:%dKB/s,%d bytes,%dms\n", MOD, ((dsize / ms) * 1000) / 1024, dsize, ms);


    return ret;
}

bldr_post_process

函数主要干的事情就是从pmic去检查是否有电池存在,如果没有就等待, 如下源码分析,比较简单:

// 就是包了一层而已.
static void bldr_post_process(void)
{
    platform_post_init();
}

// 重点是这个函数:
void platform_post_init(void)
{
    /* normal boot to check battery exists or not */
    if (g_boot_mode == NORMAL_BOOT && !hw_check_battery() && usb_accessory_in()) {
...
        pl_charging(1);
        do {
            mdelay(300);
            
            /* 检查电池是否存在, 如果使用电源调试则需要修改此函数逻辑 */
            if (hw_check_battery())
                break;
            /* 喂狗,以免超时被狗咬 */
            platform_wdt_all_kick();
        } while(1);
        /* disable force charging mode */
        pl_charging(0);
    }

...
}
Pre-loader 到 Lk的源码分析到这就完成了.
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深入MTK平台bootloader启动之【 Pre-loader -> Lk】分析笔记 的相关文章

  • PX4 自定义bootloader生成

    本文主要是记录一下自己在这方面的学习 xff0c 方便以后回顾 xff0c 也希望对其他朋友有用 本着不重复造轮子的精神 xff0c 这里引文不在复制粘贴 xff0c 直接给出链接 生成bootloader的两种方式 以STM32H7作为主
  • 设计一款STM32的BootLoader

    参考文章 xff1a https blog csdn net qingtian506 article details 9128899 之前很想做一个属于STM32的BootLoader xff0c 但是想想没什么实际用处就没有下手 xff0
  • Ardupilot 编译Bootloader

    1 清理之前的编译中间文件 xff0c 一定要清理一下 xff0c 能避免很多奇怪的问题 span class token punctuation span span class token operator span waf distcl
  • Bootrom概述

    1 Bootrom 是指on chip bootrom 在CPU芯片内部 内嵌有小的boot程序 bootloader 类似于PC机主板上的BIOS的存储区域 2 Bootloader怎么得到 如果对开发板有些改动 还能使用开发板的boot
  • 基于ARM Cortex-M0+内核的bootloader程序升级原理及代码解析

    本文主要讲述BootLoader程序升级原理及一些代码的解析 力图用通俗易懂的语言描述清楚BootLoader升级的主要关键点 BootLoader 升级原理概述 首次接触这一块时 有一个概念叫IAP 在应用编程 通俗一点讲便是通过一段已有
  • MIUI解BL锁失败

    最后解决办法是 换USB 2 0接口 分析问题 或许可能是软件兼容性不好 USB3 0影响读取设备信息 导致无法解锁
  • FPGA UltraScale+ 利用ICAP原语实现Multiboot功能

    例程参考 https blog csdn net xiaomingzi55 article details 124365631 1 这个贴子说的很清楚 唯一一点就是它是ICAP2 这样写是没问题的 1 对于BPI模式来说 可以通过RS 1
  • uboot启动流程分析

    FS4412 SOC的启动过程 在图中有 Cortax A9 其是芯片核心 也就是中央处理器 CPU Internal Rom 是一个只读存储器 里面存储了代码 总大小为64K 它的功能是用于读写pin脚 其作用是用来告诉系统从何处去读取u
  • 【STM32】制作一个bootloader

    工作环境 STM32CubeMX Keil 相关环境准备这里就不介绍了 bootloader是什么 bootloader就是单片机启动时候运行的一段小程序 这段程序负责单片机固件的更新 也就是单片机选择性的自己给自己下载程序 可以更新 可以
  • ARM qemu 系统模拟器可以在没有内核参数的情况下从卡映像启动吗?

    我看过很多如何运行 QEMU ARM 板模拟器的示例 在每种情况下 除了 SD 卡图像参数之外 QEMU 还始终提供内核参数 即 qemu system arm M versatilepb kernel vmlinuz 2 6 18 6 v
  • 加载引导加载程序的第二阶段

    我正在尝试为 x86 机器创建一个小型操作系统 并开始为相当小的引导加载程序编写代码 我创建的引导加载程序非常简单 它从位于主引导记录后面的扇区加载一个小的第二引导加载程序 并跳转到该代码 主引导记录中的引导加载程序代码似乎运行良好 当它尝
  • 组装 - 在 bochs 中运行引导加载程序时出现问题

    我目前正在尝试在 bochs 中编译并运行一个简单的引导加载程序 目前 这是我的 bootloader asm 文件 BITS 16 ORG 0x7C00 Where the code gets mapped top jmp top Loo
  • 所有磁盘扇区在汇编中是如何迭代的?

    在学习汇编的过程中 我正在编写一个操作系统 我已经成功编写了将第二个 512 字节扇区附加到初始 512 字节引导加载程序所需的代码 define KBDINT 0x16 define VIDINT 0x10 define DISKINT
  • 简单的 NASM“启动程序”无法正确访问内存?

    请注意 当我说引导程序时 我并不是指引导操作系统的程序 我的意思是 一个简单的程序 当您启动计算机并执行某些操作时就会运行 好吧 所以我不是极其精通汇编 NASM 但我认为我对它有足够的掌握来编写简单的引导程序 Well I thought
  • 在 x86 程序集中制作鼠标处理程序

    我正在 NASM 程序集中编写操作系统 但在制作鼠标处理程序 指向设备 BIOS 接口处理程序 时遇到问题 我尝试在互联网上搜索如何做到这一点 但没有成功 这是设置的代码 call checkPS2 PS2 routines jc NOMO
  • ARM 系统上的 Bootrom 与引导加载程序有什么区别

    我主要来自 x86 系统背景 其中 BIOS 固件 负责从 PowerON 加载引导加载程序 如 GRUB 进而加载操作系统 我现在一直在阅读 ARM 系统上的等效启动顺序 网上似乎有文章提到了两个术语 bootrom 和 bootload
  • 如何在启动操作系统之前进行一些安全验证?

    我有一个可启动闪存盘 其中包含定制的 Ubunto 我想将闪存盘传递给未知的人 但它存在一些安全问题 我想确保未知的人无法更改闪存盘内容 因此 我想计算闪存内容的哈希值并在每次启动时验证它 并在验证失败或哈希不匹配时防止启动操作系统 为此
  • 没有操作系统直接运行的程序叫什么名字?

    当我试图提出有关该主题的其他问题时 我很难正确表达我的问题 那么直接在相关计算机上运行的程序的正确名称是什么 一个可以描述内核和引导加载程序的术语 因为它们是在没有操作系统的情况下直接执行的 C 标准称之为 独立环境 我觉得这个术语和我见过
  • ARM 的启动过程是怎样的?

    我们知道 对于X86架构 按下电源按钮后 机器开始执行0xFFFFFFF0处的代码 然后开始执行BIOS中的代码以进行硬件初始化 BIOS 执行后 它使用引导加载程序将操作系统映像加载到内存中 最后 操作系统代码开始运行 对于ARM架构 使
  • 将引导加载程序存储在软盘映像上的哪里?

    我将编写并测试引导加载程序 为了做到这一点 我计划将引导加载程序复制到软盘映像文件上并将其安装在虚拟机中 但是 我不确定将引导加载程序的机器代码放在哪里 它是否只是转储到文件的前几个字节中 软盘的引导扇区是第一个扇区 如果您谈论的是原始软盘

随机推荐

  • 【Vue2.0源码学习】模板编译篇-模板解析(代码生成阶段)

    文章目录 1 前言 2 如何根据AST生成render函数 3 回归源码 3 1 元素节点 3 2 文本节点 3 3 注释节点 4 总结 1 前言 经过前几篇文章 我们把用户所写的模板字符串先经过解析阶段解析生成对应的抽象语法树AST 接着
  • VMware Workstation 17 Pro的下载&&安装&&使用

    目录 一 下载 二 安装 三 检查网络连接 方式一 简便版 方式二 麻烦版 四 使用 创建虚拟机 使用命令 快照的使用 拍摄快照 恢复快照 克隆虚拟机 移除虚拟机 一 下载 下载地址 Windows 虚拟机 Workstation Pro
  • 14-堆排序

    堆 Heap 是一种常见的数据结构 常用于存储数据 其本质上是一棵完全二叉树 下面我们来看看如何用数组实现堆结构及其相关功能 堆的定义 首先来看一下堆的存储结构 堆可以看成是一颗完全二叉树 首先什么是二叉树 借助百度中的解释 二叉树 bin
  • arXiv是个什么东西?

    arXiv只是个提交论文预印本 preprint 的平台 而且里面的论文都没有经过同行评审 peer review 所以文章质量参差不齐 比较有名的计算机检索数据库DBLP数据库可以检索arXiv里的文章 DBLP把arXiv归类为非正式发
  • opencv 学习:reshape函数

    在opencv中 reshape函数比较有意思 它既可以改变矩阵的通道数 又可以对矩阵元素进行序列化 非常有用的一个函数 函数原型 C Mat Mat reshape int cn int rows 0 const 参数比较少 但设置的时候
  • BUAA_C程序括号匹配检查

    问题描述 编写一程序检查C源程序文件中 等括号是否匹配 并输出第一个检测到的不匹配的括号及所对应括号所在的行号 程序中只有一个括号不匹配 注意 1 除了括号可能不匹配外 输入的C源程序无其它语法错误 2 字符常量 字符串常量及注释中括号不应
  • 基于近半年Twitter与Github趋势分析_12大分类500+ChatGPT最新开源GitHub存储库(涵盖ChatGPT开发全框架、全编程语言及教程)——每周更新

    目录 前言 令人惊叹的开源ChatGPT资源 Awesome lists 提示工程 聊天机器人 浏览器扩展及插件 CLIs命令行界面标准应用程序 Reimplementations重实现模型 教程 NLP自然语言处理 Langchain U
  • Vue-自定义指令

    Vue 自定义指令 1 什么是自定义指令 vue 官方提供了 v text v for v model v if 等常用的指令 除此之外vue 还允许开发者自定义指令 2 自定义指令的分类 私有自定义指令 在每个vue 组件中 可以在dir
  • Safari开发者工具

    Safari开发者工具 1 开发者功能 2 开发者功能可以干什么 2 1 捕获模拟器的请求 1 开发者功能 Safari gt 首选项 gt 高级 gt 开启 在菜单栏中显示 开发 菜单 2 开发者功能可以干什么 2 1 捕获模拟器的请求
  • Android 自定义View :虚线矩形

    预览效果 涉及参数 斜线起点坐标 斜线可以忽略 斜线终点坐标 斜线可以忽略 矩形左上角坐标 矩形右下角坐标 其中 前两个参数用于绘制预览效果中矩形上方的斜线 如果不需要可以移除 本案例涉及视频外一个点指向视频内某块区域 因此参数略微复杂 除
  • CMD 命令换行

    CMD 命令换行 在执行较长的 cmd 命令或制作 cmd 命令脚本时 为了方便编写和阅读 有时需要在命令中加入适当的换行 基于不同的命令 有两种换行方式 普通命令 在要换行的地方输入 然后回车 再继续命令的输入 控制命令 如 if for
  • 如何使实时数据采集处理系统保持数据的高速传输

    如何使实时数据采集处理系统保持数据的高速传输 1引言 当前 越来越多的设计应用领域要求具有高精度的A D转换和实时处理功能 在实时数据采集处理系统设计中 一般需要考虑数据采集以及对采集数据的处理 而对于大数据量的实时数据采集处理系统来说 保
  • Linux服务器Java输出文件中文乱码

    使用下面语句查看编码 String encoding System getProperty file encoding 结果输出 ANSI X3 4 1968 从而导致中文乱码通过 locale 查看服务器系统编码 需要修改 1在tomca
  • 【论文阅读笔记】BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding

    BERT的出现使我们终于可以在一个大数据集上训练号一个深的神经网络 应用在很多NLP应用上面 BERT Pre training of Deep Bidirectional Transformers for Language Underst
  • css实现容器高度 适应 屏幕高度

    元素的高度默认是auto 被内容自动撑开 100 使得html的height与屏幕的高度相等 50 使得html的height等于屏幕的一半 若想让一个 div 的高度与屏幕高度自适应 始终充满屏幕 需要从html层开始层层添加height
  • 文本生成评估指标:ROUGE、BLEU详谈

    目录 1 自动摘要与机器翻译 1 自动摘要和机器翻译的定义和目标 2 自动摘要和机器翻译领域的挑战 2 ROUGE Recall Oriented Understudy for Gisting Evaluation 1 ROUGE 的目的和
  • AI虚拟点读机--详细注释解析恩培作品7

    感谢恩培大佬对项目进行了完整的实现 并将代码进行开源 供大家交流学习 一 项目简介 本项目最终达到的效果为手势控制虚拟点读机 如下所示 项目用python实现 调用opencv等库 使用SVM对字体进行分类 由以下步骤组成 1 使用Open
  • cd命令、pwd命令和环境变量PWD、OLDPWD的关联

    1 cd命令 cd命令这里不多介绍 cd 命令是返回上次所在的目录 2 PWD和OLDPWD环境变量 dai ubuntu env PWD home dai OLDPWD dai ubuntu 3 关联 1 当你输入 cd 命令返回上次的目
  • R语言之匹配篇

    2019独角兽企业重金招聘Python工程师标准 gt gt gt match match函数的声明如下 match x table nomatch NA integer incomparables NULL x 向量 要匹配的值 tabl
  • 深入MTK平台bootloader启动之【 Pre-loader -> Lk】分析笔记

    1 bootloader到kernel启动总逻辑流程图 ARM架构中 EL0 EL1是必须实现 EL2 EL3是选配 ELx跟层级对应关系 EL0 app EL1 Linux kernel lk EL2 hypervisor 虚拟化 EL3