ARM64基础12:ARM64的异常模式及异常向量表

  • Post author:
  • Post category:其他




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, x10
	isb



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, BAD_ERROR



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 
	bl	kernel_main
	b 	proc_hang		// should never come here

单步调式,观察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]
	ret

单步调试如下

在这里插入图片描述

异常执行完后,返回

在这里插入图片描述

串口打印:

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);  

}