对于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错误。
异常向量表中的每个条目是128字节对齐的,整个异常向量表必须按2K字节对齐。使用stp
指令将x29
和x30
寄存器的值保存到栈上,然后通过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
2K对齐后,地址的最低11位为零。这也就和vbar_el3的寄存器定义吻合上了。只有2K对齐才能将正确的异常向量地址设置进vbar_el3寄存器。
下表是ARM手册中的一个异常向量表定义。每一项与这个基址有一个定义好了的偏移量,例如第二个偏移量是0x80,所有偏移量之间都相差了128字节。每个表有16项,每项128字节(32条指令)大小。程序中必须严格按照这个顺序设置异常向量。
参考《aarch64_exception_and_interrupt_handling_100933_0100_en.pdf》
_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
首先将x1
到x28
的寄存器值保存到栈指针sp
中,每个寄存器对的值都通过[sp, #-16]!
的方式存储,这会将栈指针(sp)向下递减16字节,然后将寄存器的值存储到新的栈位置。然后通过b指令跳转到_save_el_regs
标签,进入异常处理程序的第二部分。根据当前的异常等级(EL)选择执行不同的分支。通过switch_el
指令,根据x11
寄存器的值,选择跳转到不同的分支,保存对应的ESR
和ELR
寄存器的值。
如果当前处于在EL3
异常等级(Hypervisor或Secure Monitor模式)下,程序跳转到3
标签。首先使用mrs
指令将ESR_EL3
的值加载到x1
寄存器中,然后将ELR_EL3
的值加载到x2
寄存器中。之后,通过b
指令跳转到0
标签。然后指令stp x2, x0, [sp, #-16]!
将x2
和x0
的值保存到栈帧中。这里的[sp, #-16]!
将栈指针(SP)向下递减16个字节,然后将x2
和x0
的值存储到新的栈位置。
接下来的mov x0, sp
指令将栈指针(SP)的值加载到x0
寄存器中。这是为了将栈指针的值传递给上层的异常处理程序或返回给调用者。至此,_save_el_regs
完成了保存el寄存器的工作,并返回调用点继续执行。
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();
}
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
*/
}
}
代码从 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