赞
踩
对于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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。