第三十期-Linux内核的异常(2)

作者:罗宇哲,中国科学院软件研究所智能软件研究中心

上一期中我们介绍了Linux 4.19内核的异常向量表,这一期我们将介绍Linux 4.19内核在ARM64处理器上的异常处理。

一、Linux 4.19内核在ARM64处理器上的异常处理

当异常发生时,处理器需要调用异常处理程序来处理异常,该调用过程可以粗略地分为保存处理器当前状态、调用异常处理程序和恢复异常发生前的处理器状态三步,具体说来内核中异常处理的流程是1

  1. 保存处理器当前状态。将当前处理器状态PSTATE保存在SPSR_EL1寄存器中,将返回地址保存在ELR_EL1寄存器中,这两个寄存器中的变量会在返回时被eret指令用于恢复处理器状态。通过设置处理器状态中的调试掩码位D、系统错误掩码位A、中断掩码位I和快速中断掩码位F禁止调试异常、系统错误异常、外部中断和快速中断。将发生错误的原因保存在ESR_EL1寄存器中,将同步异常的错误地址保存在FAR_EL1寄存器中。如果处理器处于异常级别EL0则将异常级别提升到EL1。根据异常向量表基地址、生成异常的异常级别和异常类型计算出异常向量的位置,通过异常向量跳转到异常处理程序的入口。异常向量表的基地址是保存在VBAR_EL1寄存器中的。

  2. 调用异常处理程序。以异常级别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中)。

  1. 恢复处理器状态,继续执行程序。异常处理程序执行完之后跳转到了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内核中的中断处理流程。
第三十期-Linux内核的异常(2)


  1. 《Linux内核深度解析》,余华兵著,2019 ↩︎

  2. 《Linux内核深度解析》,余华兵著,2019 ↩︎