异常管理
前置知识: arm ABI
在ARM的C ABI中,函数参数确实是通过寄存器x0, x1, x2等传递的。具体来说,前六个整数或指针类型的参数会依次存放在x0到x5寄存器中。如果参数个数超过六个,那么超出的参数将通过栈来传递。
对于返回值,如果是单个整数或较小的数据类型,通常直接使用x0寄存器来返回。如果返回值是一个复合类型(即大小超过4个字节的数据),则返回值会被存储在内存中,并通过x0寄存器传递指向该内存的指针。对于更大的结构体,特别是超过16字节的结构体,返回值会被写入通过x8(XR)寄存器指向的内存位置。
理解这个对我们继续探索汇编与C的接口会有帮助
先放一个全量的代码
/*
* Copyright (c) 2023 Institute of Parallel And Distributed Systems (IPADS), Shanghai Jiao Tong University (SJTU)
* Licensed under the Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
* PURPOSE.
* See the Mulan PSL v2 for more details.
*/
#include <common/asm.h>
#include <common/debug.h>
#include <common/vars.h>
#include <arch/machine/registers.h>
#include <arch/machine/esr.h>
#include <arch/machine/smp.h>
#include "irq_entry.h"
.extern syscall_table
.extern hook_syscall
.extern finish_switch
.extern do_pending_resched
.macro exception_entry label
/* Each entry of the exeception table should be 0x80 aligned */
.align 7
b \label
.endm
/* See more details about the bias in registers.h */
.macro exception_enter
sub sp, sp, #ARCH_EXEC_CONT_SIZE
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]
mrs x21, sp_el0
mrs x22, elr_el1
mrs x23, spsr_el1
stp x30, x21, [sp, #16 * 15]
stp x22, x23, [sp, #16 * 16]
.endm
.macro exception_exit
ldp x22, x23, [sp, #16 * 16]
ldp x30, x21, [sp, #16 * 15]
msr sp_el0, x21
msr elr_el1, x22
msr spsr_el1, x23
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]
add sp, sp, #ARCH_EXEC_CONT_SIZE
eret
.endm
.macro switch_to_cpu_stack
mrs x24, TPIDR_EL1
add x24, x24, #OFFSET_LOCAL_CPU_STACK
ldr x24, [x24]
mov sp, x24
.endm
.macro switch_to_thread_ctx
mrs x24, TPIDR_EL1
add x24, x24, #OFFSET_CURRENT_EXEC_CTX
ldr x24, [x24]
mov sp, x24
.endm
/*
* Vector table offsets from vector table base address from ARMv8 Manual
* Address | Exception Type | Description
* ============================================================================
* VBAR_Eln+0x000 | Synchronous | SPSel=0
* +0x080 | IRQ/vIRQ | Current EL
* +0x100 | FIQ/vFIQ | with Stack Pointer
* +0x180 | SError/vSError | shared with EL0
* ============================================================================
* VBAR_Eln+0x200 | Synchronous | SPSel=1
* +0x280 | IRQ/vIRQ | Current EL
* +0x300 | FIQ/vFIQ | with dedicated
* +0x380 | SError/vSError | Stack Pointer
* ============================================================================
* VBAR_Eln+0x400 | Synchronous |
* +0x480 | IRQ/vIRQ | Lower EL
* +0x500 | FIQ/vFIQ | using AArch64
* +0x580 | SError/vSError |
* ============================================================================
* VBAR_Eln+0x600 | Synchronous |
* +0x680 | IRQ/vIRQ | Lower EL
* +0x700 | FIQ/vFIQ | using AArch32
* +0x780 | SError/vSError |
* ============================================================================
*/
/* el1_vector should be set in VBAR_EL1. The last 11 bits of VBAR_EL1 are reserved. */
.align 11
EXPORT(el1_vector)
exception_entry sync_el1t // Synchronous EL1t
exception_entry irq_el1t // IRQ EL1t
exception_entry fiq_el1t // FIQ EL1t
exception_entry error_el1t // Error EL1t
exception_entry sync_el1h // Synchronous EL1h
exception_entry irq_el1h // IRQ EL1h
exception_entry fiq_el1h // FIQ EL1h
exception_entry error_el1h // Error EL1h
exception_entry sync_el0_64 // Synchronous 64-bit EL0
exception_entry irq_el0_64 // IRQ 64-bit EL0
exception_entry fiq_el0_64 // FIQ 64-bit EL0
exception_entry error_el0_64 // Error 64-bit EL0
exception_entry sync_el0_32 // Synchronous 32-bit EL0
exception_entry irq_el0_32 // IRQ 32-bit EL0
exception_entry fiq_el0_32 // FIQ 32-bit EL0
exception_entry error_el0_32 // Error 32-bit EL0
/*
* The selected stack pointer can be indicated by a suffix to the Exception Level:
* - t: SP_EL0 is used
* - h: SP_ELx is used
*
* ChCore does not enable or handle irq_el1t, fiq_xxx, and error_xxx.
* The SPSR_EL1 of idle threads is set to 0b0101, which means interrupt
* are enabled during the their execution and SP_EL1 is selected (h).
* Thus, irq_el1h is enabled and handled.
*
* Similarly, sync_el1t is also not enabled while we simply reuse the handler for
* sync_el0 to handle sync_el1h (e.g., page fault during copy_to_user and fpu).
*/
irq_el1h:
/* Simply reusing exception_enter/exit is OK. */
exception_enter
#ifndef CHCORE_KERNEL_RT
switch_to_cpu_stack
#endif
bl handle_irq_el1
/* should never reach here */
b .
irq_el1t:
fiq_el1t:
fiq_el1h:
error_el1t:
error_el1h:
sync_el1t:
bl unexpected_handler
sync_el1h:
exception_enter
mov x0, #SYNC_EL1h
mrs x1, esr_el1
mrs x2, elr_el1
bl handle_entry_c
str x0, [sp, #16 * 16] /* store the return value as the ELR_EL1 */
exception_exit
sync_el0_64:
exception_enter
#ifndef CHCORE_KERNEL_RT
switch_to_cpu_stack
#endif
mrs x25, esr_el1
lsr x24, x25, #ESR_EL1_EC_SHIFT
cmp x24, #ESR_EL1_EC_SVC_64
b.eq el0_syscall
mov x0, SYNC_EL0_64
mrs x1, esr_el1
mrs x2, elr_el1
bl handle_entry_c
#ifdef CHCORE_KERNEL_RT
bl do_pending_resched
#else
switch_to_thread_ctx
#endif
exception_exit
el0_syscall:
/* hooking syscall: ease tracing or debugging */
#if ENABLE_HOOKING_SYSCALL == ON
sub sp, sp, #16 * 8
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]
mov x0, x8
bl hook_syscall
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]
add sp, sp, #16 * 8
#endif
adr x27, syscall_table // syscall table in x27
uxtw x16, w8 // syscall number in x16
ldr x16, [x27, x16, lsl #3] // find the syscall entry
blr x16
/* Ret from syscall */
// bl disable_irq
#ifdef CHCORE_KERNEL_RT
str x0, [sp]
bl do_pending_resched
#else
switch_to_thread_ctx
str x0, [sp]
#endif
exception_exit
irq_el0_64:
exception_enter
#ifndef CHCORE_KERNEL_RT
switch_to_cpu_stack
#endif
bl handle_irq
/* should never reach here */
b .
error_el0_64:
sync_el0_32:
irq_el0_32:
fiq_el0_32:
error_el0_32:
bl unexpected_handler
fiq_el0_64:
exception_enter
#ifndef CHCORE_KERNEL_RT
switch_to_cpu_stack
#endif
bl handle_fiq
/* should never reach here */
b .
/* void eret_to_thread(u64 sp) */
BEGIN_FUNC(__eret_to_thread)
mov sp, x0
dmb ish /* smp_mb() */
#ifdef CHCORE_KERNEL_RT
bl finish_switch
#endif
exception_exit
END_FUNC(__eret_to_thread)
几个可能会有疑惑的点
-
.align 7
是按照2^7对齐的意思,也就是表格里面的0x80
-
exception_enter
和exception_exit
类似trampoline, 只是保存和重新取出寄存器, 两两保存 -
看这两个函数也可以看出,不同于xv6的trampoline在内核页表上规定了一个特殊的位置(trampoline page)用于保存寄存器,chcore直接将寄存器保存在了内核栈上
-
两种设计的权衡
trampoline page实现起来较易,但如果要支持多线程和抢占式调度,则相对麻烦
早期 Unix 选择 trampoline page 主要是因为当时的硬件限制,而现代 ARM OS 选择内核栈是因为现代硬件的进步和对系统安全性和性能的更高要求。两种方法各有优劣,选择哪种方法需要根据具体的硬件架构和操作系统设计目标进行权衡。
一些早期的操作系统,例如 xv6-riscv,仍然使用 trampoline page 来处理用户态到内核态的转换。2 Linux 内核中也使用了 trampoline 的概念,但其作用和实现方式与早期 Unix 中的 trampoline page 不同。Linux 中的 trampoline 主要用于处理内核地址空间布局随机化 (KASLR) 等安全特性。1
总而言之,选择 trampoline page 还是内核栈是一个 trade-off 的过程。Trampoline page 实现简单,节省内存,但安全性较低,难以支持多处理器和抢占式调度。内核栈实现复杂,占用内存较多,但安全性更高,更易于支持多处理器和抢占式调度。现代操作系统大多选择内核栈,是因为现代硬件的性能提升使得内核栈的开销可以接受,并且内核栈带来的安全性提升和功能扩展更加重要。
-
C ABI兼容:以handle_entry_c为例,
sync_el1h:
exception_enter
mov x0, #SYNC_EL1h
mrs x1, esr_el1
mrs x2, elr_el1
bl handle_entry_c
str x0, [sp, #16 * 16] /* store the return value as the ELR_EL1 */
exception_exit
u64 handle_entry_c(int type, u64 esr, u64 address)
x0, x1, x2就是c函数的args, 返回值置于x0, 正如前面的arm ABI
至于异常向量表,文档已经说得很清楚了
AArch64 中的每个异常级别都有其自己独立的异常向量表,其虚拟地址由该异常级别下的异常向量基地址寄存器(
VBAR_EL3
,VBAR_EL2
和VBAR_EL1
)决定。每个异常向量表中包含 16 个条目,每个条目里存储着发生对应异常时所需执行的异常处理程序代码。以上表格给出了每个异常向量条目的偏移量。在 ChCore 中,仅使用了 EL0 和 EL1 两个异常级别,因此仅需要对 EL1 异常向量表进行初始化即可。在本实验中,ChCore 内除系统调用外所有的同步异常均交由
handle_entry_c
函数进行处理。**遇到异常时,硬件将根据 ChCore 的配置执行对应的汇编代码,将异常类型和当前异常处理程序条目类型作为参数传递,**对于 sync_el1h 类型的异常,跳转handle_entry_c
使用 C 代码处理异常。对于 irq_el1t、fiq_el1t、fiq_el1h、error_el1t、error_el1h、sync_el1t 则跳转unexpected_handler
处理异常。