当前位置:   article > 正文

【精读Uboot】异常向量的设置_vbar_el3

vbar_el3

1、异常基础知识

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

异常分类

虽然异常有很多种,但是基本可以分成两类,异步异常(asynchronous exception)和同步异常(synchronous exception)。同步异常又可以细分成两个类别,一种我们称之为synchronous abort,例如未定义的指令、data abort、prefetch instruction abort、SP未对齐异常,debug exception等等。还有一种是正常指令执行造成的,包括SVC/HVC/SMC指令,这些指令的使命就是产生异常。

在_start.S中,我们将vectors的地址设置进了了vbar_el3寄存器,也就让芯片知道了异常发生时,应该跳转到这个地址执行异常处理。异常向量表许多不同类型的异常和中断,如同步异常(synchronous)、IRQ中断、FIQ中断和SError错误。

2、vectors

异常向量表中的每个条目是128字节对齐的,整个异常向量表必须按2K字节对齐。使用stp指令将x29x30寄存器的值保存到栈上,然后通过bl指令调用_exception_entry函数。接下来,通过bl指令调用do_bad_sync函数来处理同步异常。最后,通过b指令跳转到exception_exit标签。

.align	11
	.globl	vectors
vectors:
	.align	7		/* Current EL Synchronous Thread */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_bad_sync
	b	exception_exit
	
	.align	7		/* Current EL IRQ Thread */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_bad_irq
	b	exception_exit

	.align	7		/* Current EL FIQ Thread */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_bad_fiq
	
	.align	7		/* Current EL Error Thread */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_bad_error
	b	exception_exit
	
	.align	7		 /* Current EL (SP_ELx) Synchronous Handler */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_sync
	b	exception_exit

	.align	7		 /* Current EL (SP_ELx) IRQ Handler */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_irq
	b	exception_exit

	.align	7		 /* Current EL (SP_ELx) FIQ Handler */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_fiq
	b	exception_exit

	.align	7		 /* Current EL (SP_ELx) Error Handler */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_error
	b	exception_exit
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

为什么需要按照2K对齐?

2K对齐后,地址的最低11位为零。这也就和vbar_el3的寄存器定义吻合上了。只有2K对齐才能将正确的异常向量地址设置进vbar_el3寄存器。

image-20230707165232084

为什么每个条目是128字节对齐?

下表是ARM手册中的一个异常向量表定义。每一项与这个基址有一个定义好了的偏移量,例如第二个偏移量是0x80,所有偏移量之间都相差了128字节。每个表有16项,每项128字节(32条指令)大小。程序中必须严格按照这个顺序设置异常向量。

image-20230707184721730

参考《aarch64_exception_and_interrupt_handling_100933_0100_en.pdf》

3、_exception_entry

_exception_entry的作用是保存异常发生时的寄存器信息。

_exception_entry:
	stp	x27, x28, [sp, #-16]!
	stp	x25, x26, [sp, #-16]!
	stp	x23, x24, [sp, #-16]!
	stp	x21, x22, [sp, #-16]!
	stp	x19, x20, [sp, #-16]!
	stp	x17, x18, [sp, #-16]!
	stp	x15, x16, [sp, #-16]!
	stp	x13, x14, [sp, #-16]!
	stp	x11, x12, [sp, #-16]!
	stp	x9, x10, [sp, #-16]!
	stp	x7, x8, [sp, #-16]!
	stp	x5, x6, [sp, #-16]!
	stp	x3, x4, [sp, #-16]!
	stp	x1, x2, [sp, #-16]!
	b	_save_el_regs			/* jump to the second part */
	
_save_el_regs:
	/* Could be running at EL3/EL2/EL1 */
	switch_el x11, 3f, 2f, 1f
3:	mrs	x1, esr_el3
	mrs	x2, elr_el3
	b	0f
2:	mrs	x1, esr_el2
	mrs	x2, elr_el2
	b	0f
1:	mrs	x1, esr_el1
	mrs	x2, elr_el1
0:
	stp	x2, x0, [sp, #-16]!
	mov	x0, sp
	ret
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

首先将x1x28的寄存器值保存到栈指针sp中,每个寄存器对的值都通过[sp, #-16]!的方式存储,这会将栈指针(sp)向下递减16字节,然后将寄存器的值存储到新的栈位置。然后通过b指令跳转到_save_el_regs标签,进入异常处理程序的第二部分。根据当前的异常等级(EL)选择执行不同的分支。通过switch_el指令,根据x11寄存器的值,选择跳转到不同的分支,保存对应的ESRELR寄存器的值。

如果当前处于在EL3异常等级(Hypervisor或Secure Monitor模式)下,程序跳转到3标签。首先使用mrs指令将ESR_EL3的值加载到x1寄存器中,然后将ELR_EL3的值加载到x2寄存器中。之后,通过b指令跳转到0标签。然后指令stp x2, x0, [sp, #-16]!x2x0的值保存到栈帧中。这里的[sp, #-16]!将栈指针(SP)向下递减16个字节,然后将x2x0的值存储到新的栈位置。

接下来的mov x0, sp指令将栈指针(SP)的值加载到x0寄存器中。这是为了将栈指针的值传递给上层的异常处理程序或返回给调用者。至此,_save_el_regs完成了保存el寄存器的工作,并返回调用点继续执行。

4、do_bad_irq

efi_restore_gd针对支持efi的场景,目的是恢复gd,然后打印arm64的寄存器内容,陷入panic进行reset。如果使用了hang()函数,程序则会陷入无限循环。

arch/arm/lib/interrupts_64.c
void do_bad_irq(struct pt_regs *pt_regs, unsigned int esr)
{
	efi_restore_gd();
	printf("Bad mode in \"Irq\" handler, esr 0x%08x\n", esr);
	show_regs(pt_regs);
	show_efi_loaded_images(pt_regs);
	panic("Resetting CPU ...\n");
}

static void panic_finish(void)
{
	putc('\n');
#if defined(CONFIG_PANIC_HANG)
	hang();
#else
	udelay(100000);	/* allow messages to go out */
	do_reset(NULL, 0, 0, NULL);
#endif
	while (1)
		;
}

void panic_str(const char *str)
{
	puts(str);
	panic_finish();
}

void panic(const char *fmt, ...)
{
#if CONFIG_IS_ENABLED(PRINTF)
	va_list args;
	va_start(args, fmt);
	vprintf(fmt, args);
	va_end(args);
#endif
	panic_finish();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

reset过程

do_reset 函数负责输出信息、延时一段时间、禁用中断,然后调用 reset_cpu 函数执行系统重启操作。reset_cpu 函数通过触发 watchdog 定时器过期来实现系统重启。在 imx_watchdog_expire_now 函数中,会根据传入的参数配置 watchdog 控制寄存器,并连续写入三次以确保配置生效。最后,在写入配置值之后进入一个无限循环,程序会停留在此处,直到系统重启。

int do_reset(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[])
{
	puts ("resetting ...\n");

	mdelay(50);				/* wait 50 ms */
	disable_interrupts(); //空函数
	reset_cpu();
	return 0;
}
void __attribute__((weak)) reset_cpu(void)
{
	struct watchdog_regs *wdog = (struct watchdog_regs *)WDOG1_BASE_ADDR;

	imx_watchdog_expire_now(wdog, false);
}
static void imx_watchdog_expire_now(struct watchdog_regs *wdog, bool ext_reset)
{
	u16 wcr = WCR_WDE;

	if (ext_reset)
		wcr |= WCR_SRS; /* do not assert internal reset */
	else
		wcr |= WCR_WDA; /* do not assert external reset */

	/* Write 3 times to ensure it works, due to IMX6Q errata ERR004346 */
	writew(wcr, &wdog->wcr);
	writew(wcr, &wdog->wcr);
	writew(wcr, &wdog->wcr);

	while (1) {
		/*
		 * spin before reset
		 */
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

5、exception_exit

代码从 exception_exit 标签处开始执行。ldp x2, x0, [sp],#16 指令将栈顶的两个双字(16字节)数据(x2 和 x0)加载到寄存器中,并将栈指针自增16个字节。switch_el x11, 3f, 2f, 1f 指令根据当前异常的处理级别选择跳转目标。x11 寄存器的值决定了跳转的目标标签。如果异常处理级别为 EL3,则跳转到标签 3 执行。在 3 处,通过 msr elr_el3, x2 指令将 x2 寄存器中的值写入 ELR_EL3 寄存器,然后跳转到 _restore_regs 过程。在 _restore_regs 过程中,通过一系列的 ldp 指令,将之前保存在栈上的通用寄存器依次加载回来。每个 ldp 指令将栈顶的两个双字(16字节)数据加载到指定的寄存器中,并将栈指针自增16个字节。最后,通过 eret 指令执行异常返回,将程序控制权恢复到异常发生时的指令位置,从而结束异常处理过程。

exception_exit:
	ldp	x2, x0, [sp],#16
	switch_el x11, 3f, 2f, 1f
3:	msr	elr_el3, x2
	b	_restore_regs
2:	msr	elr_el2, x2
	b	_restore_regs
1:	msr	elr_el1, x2
	b	_restore_regs

_restore_regs:
	ldp	x1, x2, [sp],#16
	ldp	x3, x4, [sp],#16
	ldp	x5, x6, [sp],#16
	ldp	x7, x8, [sp],#16
	ldp	x9, x10, [sp],#16
	ldp	x11, x12, [sp],#16
	ldp	x13, x14, [sp],#16
	ldp	x15, x16, [sp],#16
	ldp	x17, x18, [sp],#16
	ldp	x19, x20, [sp],#16
	ldp	x21, x22, [sp],#16
	ldp	x23, x24, [sp],#16
	ldp	x25, x26, [sp],#16
	ldp	x27, x28, [sp],#16
	ldp	x29, x30, [sp],#16
	eret
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/129956
推荐阅读
相关标签
  

闽ICP备14008679号