1.ARM64的异常等级
ARM64包含4个异常等级:
EL0:非特权模式,常用来跑应用程序;
EL1:特权模式,常用来跑内核;
EL2:虚拟化监控程序,例如hypervisor;
EL3:安全模式,例如secure monitor;
2.同步异常和异步异常
同步异常是由正在运行的指令,或指令运行的结果,出错造成的异常;而异步异常则不必由运行的指令造成,可以在程序运行中的任意时刻(异步)发生。
同步异常包括:
1.系统调用,svc, hvc, SMC等;
2.MMU引发的异常;
3.SP和PC对齐检查;
4.未分配的指令;
异步异常:
IRQ中断;
FIQ中断;
SError
注:在官方手册D1.12有详细列出那些是同步异常;
3.异常入口
当异常发生时刻:
a.CPU硬件做了哪些事情:
(1) PSTATE保存到SPSR_ELx;
(2) 返回地址保存到ELR_ELx;
(3) PSTATE寄存器里的DAIF域都设置为1,相当于把调试异常、系统错误关闭;
(4)更新ESR_ELx寄存器,该寄存器包含同步异常发生的原因;
(5)切换到对应的EL, 然后跳转到异常向量表里执行;
b.操作系统做哪些事情?
(1)根据异常发生的类型,跳转到合适的异常向量表;
(2)异常向量表的每个表项,保存有一个异常处理的跳转函数,跳转到对应的异常处理函数处理异常;
4 异常的返回
操作系统执行一条eret语句;
从ELR_ELx寄存器恢复PC指针;
从SPSR_ELx寄存器恢复处理器的状态;
5.异常返回地址
返回地址的两个寄存器:
x30:子函数的返回地址,使用ret指令来返回;
ELR_Elx:异常返回地址,使用eret返回;
ELR_ELx保存了异常返回地址:
(1)对于异步异常,它是中断发生时的下一条指令,或没有执行的第一条指令;
(2)对于不是system call的同步异常,它是触发同步异常的那一条指令;
(3)对于system call, 它是svc指令的下一条指令;
6.异常处理的路由
(1)异常发生时,异常处理可以在当前或更高EL,EL0不能处理异常;
(2)同步异常是可以在当前EL处理的,比如在EL1里发生的同步异常;
(3)对于异步异常,可以路由到EL1/EL2/EL3处理,需要配置HCR以及SCR相关寄存器;
路由方法:
1.选择异常级别:
当异常发生时,PC可以有三个基地址VBAR_EL1、VBAR_EL3、VBAR_EL1(secure)供选择;路由规则如下:
举两例来说明下:
第一个红色框的内容表示:在此种配置(安全模式)下,EL0和EL1状态下产生的异步异常,会导致CPU进入EL1。
第一个红色框的内容表示:在此种配置(非安全模式)下,EL0和EL1状态下产生的异步异常,会导致CPU进入EL1,而EL2状态下产生的异常,不会导致exception level切换。
Linux内核只支持EL0和EL1,EL0对应用户态,EL1对应内核态,当CPU运行在用户态时,产生的异步异常会导致CPU切换到EL1,当CPU运行在内核态时,产生的异步异常不会导致exception level的切换。
这里配置为非安全模式vbar_el1:
//set vector table addr to vbar
ldr x10, =vectors
msr vbar_el1,
2.选择异常级别相关的偏移:
配置好基地址后,再根据下表配置异常向量表的偏移地址
说明:
(1)实际上有四张表,每张表有四个异常入口,分别对应同步异常,IRQ,FIQ和出错异常。
(2)每一个异常入口占用0x80 bytes(不同于ARMv7之前的4bytes)空间,也就是说,每一个异常入口可以放置多条指令,而不仅仅是一条跳转指令。
四张表类型:
(1)如果发生异常并不会导致exception level切换,并且使用的栈指针是SP_EL0,那么使用第一张异常向量表。
(2)如果发生异常并不会导致exception level切换,并且使用的栈指针是SP_EL1/2/3,那么使用第二张异常向量表。
(3)如果发生异常会导致exception level切换,并且比目的exception level低一级的exception level运行在AARCH64模式,那么使用第三张异常向量表。
(4)如果发生异常会导致exception level切换,并且比目的exception level低一级的exception level运行在AARCH32模式,那么使用第四张异常向量表。在Linux中,用户态EL0, 内核态EL1, 结合上面路由规则,可得到如下结论:
(1)第一章异常向量表,用不到; 因为EL0使用SP_EL0,但发生EL0异常会routing到EL1;
(2)第二张表,用于CPU运行在EL1即内核态,发生异常时,exception level不发生切换;
(3)第三张表用于CPU运行在EL0即用户态的AARCH32模式时,发生异常;
(4)第四张表,用于CPU运行在EL0即用户态的AARCH64时,发生异常;
3.填充异常向量表
根据以上规则,填充异常向量表如下,根据实际发生的不同异常类型,跳转到对应的处理函数;
/*
* Vector Table
*
* ARM64的异常向量表一共占用2048个字节
* 分成4组,每组4个表项,每个表项占128字节
* 参见ARMv8 spec v8.6第D1.10节
* align 11表示2048字节对齐
*/
.align 11
.global vectors
vectors:
/* Current EL with SP0
当前系统运行在EL1时使用EL0的栈指针SP
这是一种异常错误的类型
*/
vtentry el1_sync_invalid
vtentry el1_irq_invalid
vtentry el1_fiq_invalid
vtentry el1_error_invalid
/* Current EL with SPx
当前系统运行在EL1时使用EL1的栈指针SP
这说明系统在内核态发生了异常
Note: 我们暂时只实现IRQ中断
*/
vtentry el1_sync_invalid
vtentry el1_irq
vtentry el1_fiq_invalid
vtentry el1_error_invalid
/* Lower EL using AArch64
在用户态的aarch64的程序发生了异常
*/
vtentry el0_sync_invalid
vtentry el0_irq_invalid
vtentry el0_fiq_invalid
vtentry el0_error_invalid
/* Lower EL using AArch32
在用户态的aarch32的程序发生了异常
*/
vtentry el0_sync_invalid
vtentry el0_irq_invalid
vtentry el0_fiq_invalid
vtentry el0_error_invalid
el1_sync_invalid:
//inv_entry 1, BAD_SYNC
kernel_entry
mov x0, sp
mov x1, 0
mrs x2, esr_el1
bl bad_mode
kernel_exit
el1_irq_invalid:
inv_entry 1, BAD_IRQ
el1_fiq_invalid:
inv_entry 1, BAD_FIQ
el1_error_invalid:
inv_entry 1, BAD_ERROR
el0_sync_invalid:
inv_entry 0, BAD_SYNC
el0_irq_invalid:
inv_entry 0, BAD_IRQ
el0_fiq_invalid:
inv_entry 0, BAD_FIQ
el0_error_invalid:
inv_entry 0,
7.栈的选择
(1)每个异常登记EL都有对应的栈指针寄存器SP:SP_EL0, SP_EL1, SP_EL2, SP_EL3;
(2)栈必须16字节对齐,硬件可以检测栈指针对齐;
(3)当异常发生时,跳转到目标异常等级时,硬件会自动选择SP_ELx;
(3)操作系统负责分配和保证,每个异常等级EL对应的栈,是可用的;
8.异常处理的执行模式
(1)异常发生时,切换到更高级别EL,这个EL运行在哪个模式?
HCR_EL2.RW记录EL1要运行的哪个模式,1:aarch64, 0:aarch32;
(2)异常发生后,执行模式可以改变;
一个aarch32模式运行的应用程序,异常出现后,可以在aarch64模式处理异常;
9.异常返回的执行模式
从一个异常返回时,SPSR寄存器记录了:
(1)返回到哪个EL?SPSR.M[3:0]
(2)返回目标EL的执行模式?SPSR.M[4],1表示aarch32,0表示aarch64;
案例1:树莓派开机默认运行在EL2模式, 请切换到EL1模式, 切换后,打印出EL的值为1;
提示: 从EL2切换到EL1,需要做一下事情:
(1)设置HCR_EL2寄存器,Bit31位设置为1,表示EL1要运行在aarch64模式;
(2)设置SCTRL_EL1寄存器,设置小端,关闭MMU;
(3)设置SPSR_EL2寄存器,设置切换模式M域为EL1h, 另外关闭所有DAIF;
(4)设置异常返回寄存器elr_el2, 将返回到EL1时执行的函数;
(5) 调用eret;
其中(3)(4)是一个典型的一场返回操作;
主要实现代码如下:
master:
/* init uart and print string */
bl __init_uart
mrs x5, CurrentEL
cmp x5, #CurrentEL_EL3
b.eq el3_entry
b el2_entry
el2_entry:
bl print_el
/*
* set arm64 from EL2 to EL1, need to do list
* 1.set HCR_EL2, bit31=1?aarch64:aarch32;
* 2.set SCTLR_EL1, big/little endian and disable MMU;
* 3.set SPSR_EL2, set M filed to EL1h, and disable all DAIF;
* 4.set ELR_EL2, return to el1_entry;
*/
/* The execution state for EL1 is AArch64 */
ldr x0, =HCR_HOST_NVHE_FLAGS
msr hcr_el2, x0
/* set big/little endian, disable MMU*/
ldr x0, = SCTLR_VALUE_MMU_DISABLED
msr sctlr_el1, x0
/*
* 1.set M_field to EL1h;
* 2.disable all DAIF;
*/
ldr x0, = SPSR_EL1
msr spsr_el2, x0
/*
* set return function
*/
adr x0, _el1_entry
msr elr_el2, x0
eret
el3_entry:
eret
.globl _el1_entry
_el1_entry:
bl print_el
adr x0, _bss
adr x1, _ebss
sub x1, x1, x0
bl memzero
mov sp, #LOW_MEMORY
单步调式,观察PC寄存器值变化如下:
执行eret后,CPU自动从ELR_el2获取返回地址,继续执行
10 异常向量表
(1)每个异常等级EL都有自己的异常向量表,EL0除外;
(2)异常向量表的基地址需要设置到VBAR_ELx寄存器中;
(3)VBAR_EL1寄存器必须以2KB对齐;
(4)每个表项可存放32条指令,一共128字节;
(5)大的异常类型分类有4类,分别是同步/IRQ/FIQ/Serror;
ARM64的异常向量表格式如下
地址偏移(基地址为VBAR_ELn) |
异常类型 |
描述 |
+0x000 |
同步 |
Current EL with SP0 |
+0x080 |
IRQ/vIRQ |
Current EL with SP0 |
+0x100 |
FIQ/vFIQ |
Current EL with SP0 |
+0x180 |
SError/vSError |
Current EL with SP0 |
+0x200 |
同步 |
Current EL with SP1 |
+0x280 |
IRQ/vIRQ |
Current EL with SP1 |
+0x300 |
FIQ/vFIQ |
Current EL with SP1 |
+0x380 |
SError/vSError |
Current EL with SP1 |
+0x400 |
同步 |
Current EL with SP2 |
+0x480 |
IRQ/vIRQ |
Current EL with SP2 |
+0x500 |
FIQ/vFIQ |
Current EL with SP2 |
+0x580 |
SError/vSError |
Current EL with SP2 |
+0x600 |
同步 |
Current EL with SP3 |
+0x680 |
IRQ/vIRQ |
Current EL with SP3 |
+0x700 |
FIQ/vFIQ |
Current EL with SP3 |
+0x780 |
SError/vSError |
Current EL with SP3 |
内核里的异常向量表实现arch/arm64/kernel/entry.S :
/*
* Exception vectors.
*/
.pushsection ".entry.text", "ax"
.align 11
SYM_CODE_START(vectors)
kernel_ventry 1, sync_invalid // Synchronous EL1t
kernel_ventry 1, irq_invalid // IRQ EL1t
kernel_ventry 1, fiq_invalid // FIQ EL1t
kernel_ventry 1, error_invalid // Error EL1t
kernel_ventry 1, sync // Synchronous EL1h
kernel_ventry 1, irq // IRQ EL1h
kernel_ventry 1, fiq // FIQ EL1h
kernel_ventry 1, error // Error EL1h
kernel_ventry 0, sync // Synchronous 64-bit EL0
kernel_ventry 0, irq // IRQ 64-bit EL0
kernel_ventry 0, fiq // FIQ 64-bit EL0
kernel_ventry 0, error // Error 64-bit EL0
#ifdef CONFIG_COMPAT
kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_compat, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_compat, 32 // Error 32-bit EL0
#else
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
#endif
SYM_CODE_END(vectors)
kernel_ventry宏定义实现:
/*
* Bad Abort numbers
*-----------------
*/
#define BAD_SYNC 0
#define BAD_IRQ 1
#define BAD_FIQ 2
#define BAD_ERROR 3
.macro kernel_ventry, el, label, regsize = 64
.align 7
#...
sub sp, sp, #PT_REGS_SIZE
#...
b el\()\el\()_\label
.endm
案例,实现一个简单的异常向量表,用一个数据对齐异常,PC指针的跳转;
#define BAD_SYNC 0
#define BAD_IRQ 1
#define BAD_FIQ 2
#define BAD_ERROR 3
/*
处理无效的异常向量
*/
.macro inv_entry el, reason
//kernel_entry el
mov x0, sp
mov x1, #\reason
mrs x2, esr_el1
b bad_mode
.endm
/*
vector table entry
每个表项是128字节, align 7表示128字节对齐
*/
.macro vtentry label
.align 7
b \label
.endm
/*
* Vector Table
*
* ARM64的异常向量表一共占用2048个字节
* 分成4组,每组4个表项,每个表项占128字节
* 参见ARMv8 spec v8.6第D1.10节
* align 11表示2048字节对齐
*/
.align 11
.global vectors
vectors:
/* Current EL with SP0
当前系统运行在EL1时使用EL0的栈指针SP
这是一种异常错误的类型
*/
vtentry el1_sync_invalid
vtentry el1_irq_invalid
vtentry el1_fiq_invalid
vtentry el1_error_invalid
/* Current EL with SPx
当前系统运行在EL1时使用EL1的栈指针SP
这说明系统在内核态发生了异常
Note: 我们暂时只实现IRQ中断
*/
vtentry el1_sync_invalid
vtentry el1_irq_invalid
vtentry el1_fiq_invalid
vtentry el1_error_invalid
/* Lower EL using AArch64
在用户态的aarch64的程序发生了异常
*/
vtentry el0_sync_invalid
vtentry el0_irq_invalid
vtentry el0_fiq_invalid
vtentry el0_error_invalid
/* Lower EL using AArch32
在用户态的aarch32的程序发生了异常
*/
vtentry el0_sync_invalid
vtentry el0_irq_invalid
vtentry el0_fiq_invalid
vtentry el0_error_invalid
el1_sync_invalid:
inv_entry 1, BAD_SYNC
el1_irq_invalid:
inv_entry 1, BAD_IRQ
el1_fiq_invalid:
inv_entry 1, BAD_FIQ
el1_error_invalid:
inv_entry 1, BAD_ERROR
el0_sync_invalid:
inv_entry 0, BAD_SYNC
el0_irq_invalid:
inv_entry 0, BAD_IRQ
el0_fiq_invalid:
inv_entry 0, BAD_FIQ
el0_error_invalid:
inv_entry 0, BAD_ERROR
string_test:
.string "t"
.global trigger_alignment
trigger_alignment:
ldr x0, =0x80002
ldr x1, [x0]
单步调试如下
异常执行完后,返回
串口打印:
qemu-system-aarch64 -machine raspi4 -nographic -kernel benos.bin
booting at EL2
booting at EL1
printk init done
<0x800880> func_c
image layout:
.text.boot: 0x00080000 - 0x000800d8 ( 216 B)
.text: 0x000800d8 - 0x0008329c ( 12740 B)
.rodata: 0x0008329c - 0x000834ee ( 594 B)
.data: 0x000834ee - 0x00083828 ( 826 B)
.bss: 0x000838d8 - 0x000a3ce8 (132112 B)
Bad mode for Sync Abort handler detected, far:0x0 esr:0x2000000
test and:p=0x2
test or:p=0x3
test andnot:p=0x1
el=1
11 同步异常的解析
查看异常综合信息寄存器:ESR_ELx
当异常发生时,在异常向量表处,对应处理函数里,读取ESR的EC域,根据EC域分类,进一步解析ISS域具体异常原因:
失效地址寄存器FAR
FAR寄存器保存发生异常时刻的虚拟地址;
异常解析部分代码:
static const char * const bad_mode_handler[] = {
"Sync Abort",
"IRQ",
"FIQ",
"SError"
};
static const char *data_fault_code[] = {
[0] = "Address size fault, level0",
[1] = "Address size fault, level1",
[2] = "Address size fault, level2",
[3] = "Address size fault, level3",
[4] = "Translation fault, level0",
[5] = "Translation fault, level1",
[6] = "Translation fault, level2",
[7] = "Translation fault, level3",
[9] = "Access flag fault, level1",
[10] = "Access flag fault, level2",
[11] = "Access flag fault, level3",
[13] = "Permission fault, level1",
[14] = "Permission fault, level2",
[15] = "Permission fault, level3",
[0x21] = "Alignment fault",
[0x35] = "Unsupported Exclusive or Atomic access",
};
static const char *esr_get_dfsc_string(unsigned int esr)
{
return data_fault_code[esr & 0x3f];
}
static const char *esr_class_str[] = {
[0 ... ESR_ELx_EC_MAX] = "UNRECOGNIZED EC",
[ESR_ELx_EC_UNKNOWN] = "Unknown/Uncategorized",
[ESR_ELx_EC_WFx] = "WFI/WFE",
[ESR_ELx_EC_CP15_32] = "CP15 MCR/MRC",
[ESR_ELx_EC_CP15_64] = "CP15 MCRR/MRRC",
[ESR_ELx_EC_CP14_MR] = "CP14 MCR/MRC",
[ESR_ELx_EC_CP14_LS] = "CP14 LDC/STC",
[ESR_ELx_EC_FP_ASIMD] = "ASIMD",
[ESR_ELx_EC_CP10_ID] = "CP10 MRC/VMRS",
[ESR_ELx_EC_CP14_64] = "CP14 MCRR/MRRC",
[ESR_ELx_EC_ILL] = "PSTATE.IL",
[ESR_ELx_EC_SVC32] = "SVC (AArch32)",
[ESR_ELx_EC_HVC32] = "HVC (AArch32)",
[ESR_ELx_EC_SMC32] = "SMC (AArch32)",
[ESR_ELx_EC_SVC64] = "SVC (AArch64)",
[ESR_ELx_EC_HVC64] = "HVC (AArch64)",
[ESR_ELx_EC_SMC64] = "SMC (AArch64)",
[ESR_ELx_EC_SYS64] = "MSR/MRS (AArch64)",
[ESR_ELx_EC_IMP_DEF] = "EL3 IMP DEF",
[ESR_ELx_EC_IABT_LOW] = "IABT (lower EL)",
[ESR_ELx_EC_IABT_CUR] = "IABT (current EL)",
[ESR_ELx_EC_PC_ALIGN] = "PC Alignment",
[ESR_ELx_EC_DABT_LOW] = "DABT (lower EL)",
[ESR_ELx_EC_DABT_CUR] = "DABT (current EL)",
[ESR_ELx_EC_SP_ALIGN] = "SP Alignment",
[ESR_ELx_EC_FP_EXC32] = "FP (AArch32)",
[ESR_ELx_EC_FP_EXC64] = "FP (AArch64)",
[ESR_ELx_EC_SERROR] = "SError",
[ESR_ELx_EC_BREAKPT_LOW] = "Breakpoint (lower EL)",
[ESR_ELx_EC_BREAKPT_CUR] = "Breakpoint (current EL)",
[ESR_ELx_EC_SOFTSTP_LOW] = "Software Step (lower EL)",
[ESR_ELx_EC_SOFTSTP_CUR] = "Software Step (current EL)",
[ESR_ELx_EC_WATCHPT_LOW] = "Watchpoint (lower EL)",
[ESR_ELx_EC_WATCHPT_CUR] = "Watchpoint (current EL)",
[ESR_ELx_EC_BKPT32] = "BKPT (AArch32)",
[ESR_ELx_EC_VECTOR32] = "Vector catch (AArch32)",
[ESR_ELx_EC_BRK64] = "BRK (AArch64)",
};
static const char *esr_get_class_string(unsigned int esr)
{
return esr_class_str[esr >> ESR_ELx_EC_SHIFT];
}
void parse_esr(unsigned int esr)
{
unsigned int ec = ESR_ELx_EC(esr);
printk("ESR info:\n");
printk(" ESR = 0x%08x\n", esr);
printk(" Exception class = %s, IL = %u bits\n", esr_get_class_string(esr), (esr & ESR_ELx_IL) ? 32 : 16);
if (ec == ESR_ELx_EC_DABT_LOW || ec == ESR_ELx_EC_DABT_CUR) {
printk(" Data abort:\n");
if ((esr & ESR_ELx_ISV)) {
printk(" Access size = %u byte(s)\n", 1U << ((esr & ESR_ELx_SAS) >> ESR_ELx_SAS_SHIFT));
printk(" SSE = %lu, SRT = %lu\n", (esr & ESR_ELx_SSE) >> ESR_ELx_SSE_SHIFT, (esr & ESR_ELx_SRT_MASK) >> ESR_ELx_SRT_SHIFT);
printk(" SF = %lu, AR = %lu\n", (esr & ESR_ELx_SF) >> ESR_ELx_SF_SHIFT, (esr & ESR_ELx_AR) >> ESR_ELx_AR_SHIFT);
}
printk(" SET = %lu, FnV = %lu\n", (esr >> ESR_ELx_SET_SHIFT) & 3, (esr >> ESR_ELx_FnV_SHIFT) & 1);
printk(" EA = %lu, S1PTW = %lu\n", (esr >> ESR_ELx_EA_SHIFT) & 1, (esr >> ESR_ELx_S1PTW_SHIFT) & 1);
printk(" CM = %lu, WnR = %lu\n", (esr & ESR_ELx_CM) >> ESR_ELx_CM_SHIFT, (esr & ESR_ELx_WNR) >> ESR_ELx_WNR_SHIFT);
printk(" DFSC = %s\n", esr_get_dfsc_string(esr));
}
}
void bad_mode(struct pt_regs *regs, int reason, unsigned int esr)
{
printk("Bad mode for %s handler detected, far:0x%x esr:0x%x- %s\n",
bad_mode_handler[reason], read_sysreg(far_el1),
esr, esr_get_class_string(esr));
parse_esr(esr);
}