关于异常的基本知识
什么是异常

对于AArch64而言,exception是指cpu的某些异常状态或者一些系统的事件(可能来自外部,也可能来自内部),这些状态或者事件可以导致cpu去执行一些预先设定的,具有更高执行权利的软件(也叫exception handler)。执行exception handler可以进行异常的处理,从而让系统平滑的运行。exception handler执行完毕之后,需要返回发生异常的现场。

异常等级(exception level)

AArch64 异常模型由多个异常级别(EL0 - EL3)组成,EL0 和 EL1 具有安全和非安全两种模式对应。 EL2是hypervisor级别,只存在于non-secure 模式。 EL3 是最高优先级,仅在安全模式下存在。简单可以理解为ELx, x越大,等级越高,执行特权就越高

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

关于el3,el2,el1,el0可以看下面的图进行理解

在这里插入图片描述

模式
EL0normal用户应用程序,非特权执行
EL1操作系统内核
EL2Hypervisor(vm 虚拟化) 对虚拟化的支持,没有Secure state,只有Non-secure state
EL3底层固件,包括secure monitor 安全状态与非安装状态之间的切换,支持EL0/EL1的Secure 和Non-secure之间的切换

之前说到异常发生后系统将切换到具有更高执行权限的状态,在AArch64是通过exception level来实现的。AArch64最多支持EL0~EL3四个exception level, EL0的execution privilege最低,EL3的execution privilege最高,当发生异常的时候,系统的exception会迁移到(route)到更高的exception level或者 维持不变,但是绝不会降低。 此外, 不会有任何的异常会去到EL0。 而AArch32, cpu没有异常等级,而是采用processor mode模式,例如User, FIQ, IRQ, Abort, Undefined, System, 这些不同的mode对应privilege(其他mode)和no-privilege(User mode)。

关于AArch64的exception level比较复杂,一般会有如下几种情况:

  • 不支持security state, 不支持虚拟化; AArch64有两个exception level, 分别是: EL0(对应user mode的application), EL1(guest OS);
  • 不支持security state, 支持虚拟化; AArch64有三个exception level, 分别是: EL0(对应的user mode的application), EL1(guest OS)和EL2(Hypervisor)
  • 支持security state, 支持虚拟化;AArch64有3个exception level, 分别是:EL0(对应trusted service),EL1(guest OS)和EL3(Secure monitor)
  • 支持security, 支持虚拟化:AArch64有4个exception level,分别是:(对应trusted service),EL1(trusted OS kernel),EL2(Hypervisor)和EL3(Secure monitor)
典型的Exception Level使用模型
异常级别运行的软件
EL0Application
EL1Linux kernel
EL2Hypervisor(可以理解为上面跑多个虚拟OS)
EL3Secure Monitor(ARM Tursted Firmware)
异常相关术语
术语说明
Taking an exceptionPE第一次回应一个异常,此时PE state称为taken from, 之后PE状态为taken to
Returning from exception当异常返回指令被提交运行,PE state就是return from exception
异常级别不同异常级别,异常的优先级不同如EL3的异常高于EL1的异常
精准异常找到某条指令,这条指令前的所有指令都执行完毕,这条指令之后的所有指令都未执行(执行的需要回退),这样PE状态就被记录下载,异常处理完成后就可以恢复。除了SError irq之外,其它的都是精准异常
同步异常(1)异常的产生是和cpu core执行的指令或者试图改变执行权限引起的异常(2)硬件提供给handler的返回地址就是产生异常的那一条指令所在的地址(3)synchronous exception又可以细分成两个类别:a). 一种我们称之为synchronous abort,例如未定义的指令、data abort、prefetch instruction abort、SP未对齐异常,debug exception等等;b). 还有一种是正常指令执行造成的,包括SVC/HVC/SMC指令,这些指令的使命就是产生异常。
异步异常asynchronous exception基本上可以类似大家平常说的中断,它是毫无预警的,丝毫不考虑cpu core感受的外部事件(需要注意的是:外部并不是表示外设,这里的外部是针对cpu core而言,有些中断是来自SOC的其他HW block,例如GIC,这时候,对于processor或者cpu(指soc)而言,这些事件是内部的),这些事件打断了cpu core对当前软件的执行,因此称之interrupt。interrupt或者说asynchronous exception有下面的特点:(1)异常和CPU执行的指令无关。(2)返回地址是硬件保存下来并提供给handler,以便进行异常返回现场的处理。这个返回地址并非产生异常时的指令根据这个定义IRQ、FIQ和SError interrupt属于asynchronous exception。
SError interruptSError interrupt是发生了external abort导致的异步异常(或称中断)。external abort来自memory system, 是访问外部memory system产生的异常(当然不是所有的来自memory system的abort都是external abort,例如来自MMU的abort就不是external abort,这里的external是针对processor而非cpu core而言,因此MMU实际上是internal的)。external abort发生在processor通过bus访问memory的时候(可能是直接对某个地址的读或者写,也可能是取指令导致的memory access),processor在bus上发起一次transaction,在这个过程中发生了abort,abort来自processor之外的memory block、device block或者interconnection block,用来告知processor,搞不定了,你自己看着办吧。external abort可以被实现成synchronous exception(precise exception),也可以实现成asynchronous exception(imprecise exception)。如果external abort是asynchronous的,那么它可以通过SError interrupt来通知cpu core
异常级别切换

在AARCH64状态下,异常级别的切换只能发生在触发了异常,或者异常处理返回过程中,其中:

  • 当发生异常时,异常级别要不增加,要不保持不变;
  • 当从异常处理返回时,异常级别只能减小或者保持不变。

在发生异常即将进入或者从异常处理返回到另一个异常级别(包括异常级别不变的情况)时,待进入/返回的异常级别称之为目标异常级别。每个异常级别都有一个明确的目标异常级别,这个目标异常级别要么是默认定义的,要么是通过系统寄存器的相应bit定义的(注意没有异常级别的目标异常级别是EL0)

既然涉及到异常级别,那就不得不说一下使用异常级别需要注意的问题了。安全和非安全这是物理隔离的,但是异常级别却是需要进行切换的。比如我们从非安全到安全,是不能直接切换过去的,需要借助el3这个电梯,可以借助这个过去。

在el1上访问某些寄存器的时候,突然系统hard fault,这时就要看aarch64的芯片手册了,看这个寄存器是在那个异常级别下可以访问的。有些寄存器在不对应的异常级别,读为零,写无效。比如GIC的某些寄存器。

  • Secure state:在这种状态下,PE可以访问安全状态和非安全状态下的物理地址空间;
  • Non-secure state:只能访问非安全物理地址空间。

参考: ARMV8 datasheet学习笔记4:AArch64系统级体系结构之编程模型(1)-EL/ET/ST - jasonactions - 博客园 (cnblogs.com)

AArch64之异常处理 – Wenhui’s Rotten Pen

pre-loader流程

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

img

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

img

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加载到芯片内部的ISRAM(internal SRAM)中。

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

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

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

  1. 从pre-loader到lk

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

img

main函数小结:

  • 各种硬件初始化(uart, pmic, wdt, timer, mem…)
  • 获取系统启动模式等,保存在全局变量中
  • Security check, 跟secro.img相关
  • 如果系统已经实现el3, 否则进入tz初始化
  • 获取lk加载到DRAM得地址(固定值), 然后从ROM中找到lk分区得地址,如果没有找到jump_addr, 则goto error
  • battery check: 没有电池就会陷入while(1)
  • jump 到lk (如果有实现el3,则会先jump到el3,然后再回到lk)

重点函数分析:

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);
    }
 
...
}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐