第三十期-Linux内核的异常(2)
作者:罗宇哲,中国科学院软件研究所智能软件研究中心
上一期中我们介绍了Linux 4.19内核的异常向量表,这一期我们将介绍Linux 4.19内核在ARM64处理器上的异常处理。
一、Linux 4.19内核在ARM64处理器上的异常处理
当异常发生时,处理器需要调用异常处理程序来处理异常,该调用过程可以粗略地分为保存处理器当前状态、调用异常处理程序和恢复异常发生前的处理器状态三步,具体说来内核中异常处理的流程是1:
-
保存处理器当前状态。将当前处理器状态PSTATE保存在SPSR_EL1寄存器中,将返回地址保存在ELR_EL1寄存器中,这两个寄存器中的变量会在返回时被eret指令用于恢复处理器状态。通过设置处理器状态中的调试掩码位D、系统错误掩码位A、中断掩码位I和快速中断掩码位F禁止调试异常、系统错误异常、外部中断和快速中断。将发生错误的原因保存在ESR_EL1寄存器中,将同步异常的错误地址保存在FAR_EL1寄存器中。如果处理器处于异常级别EL0则将异常级别提升到EL1。根据异常向量表基地址、生成异常的异常级别和异常类型计算出异常向量的位置,通过异常向量跳转到异常处理程序的入口。异常向量表的基地址是保存在VBAR_EL1寄存器中的。
-
调用异常处理程序。以异常级别EL0下64位应用程序发生的同步异常为例,其异常处理程序入口为sync,异常向量表通过kernel_ventry宏跳转到了该入口(同步异常和异步异常的概念见第27期)。kernel_ventry在跳转的时候会为将异常级别加到跳转入口之前,所以这时实际跳转到的入口是el0_sync,其汇编代码在openeuler/kernel/blob/kernel-4.19/arch/arm64/kernel/entry.S文件中可以找到:
/*
* EL0 mode handlers.
*/
.align 6
el0_sync:
kernel_entry 0
mrs x25, esr_el1 // read the syndrome register
lsr x24, x25, #ESR_ELx_EC_SHIFT // exception class
cmp x24, #ESR_ELx_EC_SVC64 // SVC in 64-bit state
b.eq el0_svc
cmp x24, #ESR_ELx_EC_DABT_LOW // data abort in EL0
b.eq el0_da
cmp x24, #ESR_ELx_EC_IABT_LOW // instruction abort in EL0
b.eq el0_ia
cmp x24, #ESR_ELx_EC_FP_ASIMD // FP/ASIMD access
b.eq el0_fpsimd_acc
cmp x24, #ESR_ELx_EC_SVE // SVE access
b.eq el0_sve_acc
cmp x24, #ESR_ELx_EC_FP_EXC64 // FP/ASIMD exception
b.eq el0_fpsimd_exc
cmp x24, #ESR_ELx_EC_SYS64 // configurable trap
b.eq el0_sys
cmp x24, #ESR_ELx_EC_SP_ALIGN // stack alignment exception
b.eq el0_sp
cmp x24, #ESR_ELx_EC_PC_ALIGN // pc alignment exception
b.eq el0_pc
cmp x24, #ESR_ELx_EC_UNKNOWN // unknown exception in EL0
b.eq el0_undef
cmp x24, #ESR_ELx_EC_BREAKPT_LOW // debug exception in EL0
b.ge el0_dbg
b el0_inv
这段代码首先通过kernel_entry宏保存了异常处理前的通用寄存器状态,然后从ESR_EL1寄存器中读取了错误的原因,移位之后和各种异常的标志值相比较,若与某一异常的标志值相同则跳转到该异常的处理程序。kernel_entry宏会将通用寄存器的值保存在当前进程的内核栈中。其汇编代码比较复杂,可以在同一个文件中找到:
.macro kernel_entry, el, regsize = 64
.if \regsize == 32
mov w0, w0 // zero upper 32 bits of x0
.endif
stp x0, x1, [sp, #16 * 0]
stp x2, x3, [sp, #16 * 1]
stp x4, x5, [sp, #16 * 2]
stp x6, x7, [sp, #16 * 3]
stp x8, x9, [sp, #16 * 4]
stp x10, x11, [sp, #16 * 5]
stp x12, x13, [sp, #16 * 6]
stp x14, x15, [sp, #16 * 7]
stp x16, x17, [sp, #16 * 8]
stp x18, x19, [sp, #16 * 9]
stp x20, x21, [sp, #16 * 10]
stp x22, x23, [sp, #16 * 11]
stp x24, x25, [sp, #16 * 12]
stp x26, x27, [sp, #16 * 13]
stp x28, x29, [sp, #16 * 14]
……
跳转到处理程序后将会进行异常处理,以异常级别EL0下的数据中止异常为例,其处理程序的汇编代码在同一个文件中可以找到:
el0_da:
/*
* Data abort handling
*/
mrs x26, far_el1
enable_daif
ct_user_exit
clear_address_tag x0, x26
mov x1, x25
mov x2, sp
bl do_mem_abort
b ret_to_user
在这段代码中,处理器从FAR_EL1寄存器中读取了数据中止发生的虚拟地址,然后调用了C程序处理函数do_mem_abort并通过寄存器X0和X1传递了两个参数,其中x0中保存了错误发生的虚拟地址,X1中保存了错误发生的原因(el0_sync宏中从ELR_EL1寄存器读到了X25中)。
- 恢复处理器状态,继续执行程序。异常处理程序执行完之后跳转到了ret_to_user,其汇编代码在同一个文件中可以找到:
ret_to_user:
disable_daif
gic_prio_kentry_setup tmp=x3
ldr x1, [tsk, #TSK_TI_FLAGS]
and x2, x1, #_TIF_WORK_MASK
cbnz x2, work_pending
finish_ret_to_user:
enable_step_tsk x1, x2
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
bl stackleak_erase
#endif
kernel_exit 0
这段代码在最后调用了kernel_exit宏,其作用是将之前kernel_entry中保存的通用寄存器恢复,并使用eret指令返回异常发生前的程序执行位置,该位置的地址是被事先保存在ELR_EL1寄存器中的,其取值情况为2:
- 对于系统调用,返回地址为系统调用指令后面第一条指令的地址;
- 对于系统调用外的同步异常,返回地址为生成异常的指令,因为该指令需要被重新执行;
- 对于异步异常,返回地址为没执行的第一条指令。
kernel_exit 宏的汇编代码可以在同一个文件中找到:
……
msr elr_el1, x21 // set up the return data
msr spsr_el1, x22
ldp x0, x1, [sp, #16 * 0]
ldp x2, x3, [sp, #16 * 1]
ldp x4, x5, [sp, #16 * 2]
ldp x6, x7, [sp, #16 * 3]
ldp x8, x9, [sp, #16 * 4]
ldp x10, x11, [sp, #16 * 5]
ldp x12, x13, [sp, #16 * 6]
ldp x14, x15, [sp, #16 * 7]
ldp x16, x17, [sp, #16 * 8]
ldp x18, x19, [sp, #16 * 9]
ldp x20, x21, [sp, #16 * 10]
ldp x22, x23, [sp, #16 * 11]
ldp x24, x25, [sp, #16 * 12]
ldp x26, x27, [sp, #16 * 13]
ldp x28, x29, [sp, #16 * 14]
ldr lr, [sp, #S_LR]
add sp, sp, #S_FRAME_SIZE // restore sp
……
二、结语
本期我们考察了Linux 4.19内核的异常处理,下一期我们将介绍Linux内核中的中断处理流程。