实验四 异常处理

中断、异常和陷阱指令是操作系统的基石,现代操作系统就是由中断驱动的。本实验和实验五的目的在于深刻理解中断的原理和机制,掌握CPU访问中断控制器的方法,掌握Arm体系结构的中断机制和规范,实现时钟中断服务和部分异常处理等。

陷入操作系统

如下图所示,操作系统是一个多入口的程序,执行陷阱(Trap)指令,出现异常、发生中断时都会陷入到操作系统。

../_images/enter_into_os.png

ARMv8的中断与异常处理

注意

访问Arm官网下载并阅读 ARM Cortex-A Series Programmer’s Guide for ARMv8-AAArch64 Exception and Interrupt Handling 等技术参考手册。

ARMv8 架构定义了两种执行状态(Execution States),AArch64 和 AArch32。分别对应使用64位宽通用寄存器或32位宽通用寄存器的执行 1

../_images/aarch64_exception_levels_2.svg

上图所示为AArch64中的异常级别(Exception levels)的组织。可见AArch64中共有4个异常级别,分别为EL0,EL1,EL2和EL3。在AArch64中,Interrupt是Exception的子类型,称为异常。 AArch64 中有四种类型的异常 2

  • Sync(Synchronous exceptions,同步异常),在执行时触发的异常,例如在尝试访问不存在的内存地址时。

  • IRQ (Interrupt requests,中断请求),由外部设备产生的中断

  • FIQ (Fast Interrupt Requests,快速中断请求),类似于IRQ,但具有更高的优先级,因此 FIQ 中断服务程序不能被其他 IRQ 或 FIQ 中断。

  • SError (System Error,系统错误),用于外部数据中止的异步中断。

当异常发生时,处理器将执行与该异常对应的异常处理代码。在ARM架构中,这些异常处理代码将会被保存在内存的异常向量表中。每一个异常级别(EL0,EL1,EL2和EL3)都有其对应的异常向量表。需要注意的是,与x86等架构不同,该表包含的是要执行的指令,而不是函数地址 3

异常向量表的基地址由VBAR_ELn给出,然后每个表项都有一个从该基地址定义的偏移量。 每个表有16个表项,每个表项的大小为128(0x80)字节(32 条指令)。 该表实际上由4组,每组4个表项组成。 分别是:

  • 发生于当前异常级别的异常且SPSel寄存器选择SP0 4 , Sync、IRQ、FIQ、SError对应的4个异常处理。

  • 发生于当前异常级别的异常且SPSel寄存器选择SPx 4 , Sync、IRQ、FIQ、SError对应的4个异常处理。

  • 发生于较低异常级别的异常且执行状态为AArch64, Sync、IRQ、FIQ、SError对应的4个异常处理。

  • 发生于较低异常级别的异常且执行状态为AArch32, Sync、IRQ、FIQ、SError对应的4个异常处理。

异常向量表

新建 src/bsp/prt_vector.S 文件,参照这里 3 定义异常向量表如下:

 1    .section .os.vector.text, "ax"
 2
 3    .global  OsVectorTable
 4    .type  OsVectorTable,function
 5
 6    .align 13
 7
 8OsVectorTable:
 9.set    VBAR, OsVectorTable
10.org VBAR                                // Synchronous, Current EL with SP_EL0
11    EXC_HANDLE  0 OsExcDispatch
12
13.org (VBAR + 0x80)                       // IRQ/vIRQ, Current EL with SP_EL0
14    EXC_HANDLE  1 OsExcDispatch
15
16.org (VBAR + 0x100)                      // FIQ/vFIQ, Current EL with SP_EL0
17    EXC_HANDLE  2 OsExcDispatch
18
19.org (VBAR + 0x180)                      // SERROR, Current EL with SP_EL0
20    EXC_HANDLE  3 OsExcDispatch
21
22.org (VBAR + 0x200)                      // Synchronous, Current EL with SP_ELx
23    EXC_HANDLE  4 OsExcDispatch
24
25.org (VBAR + 0x280)                      // IRQ/vIRQ, Current EL with SP_ELx
26    EXC_HANDLE  5 OsExcDispatch
27
28.org (VBAR + 0x300)                      // FIQ/vFIQ, Current EL with SP_ELx
29    EXC_HANDLE  6 OsExcDispatch
30
31.org (VBAR + 0x380)                      // SERROR, Current EL with SP_ELx
32    EXC_HANDLE  7 OsExcDispatch
33
34.org (VBAR + 0x400)                      // Synchronous, EL changes and the target EL is using AArch64
35    EXC_HANDLE  8 OsExcDispatchFromLowEl
36
37.org (VBAR + 0x480)                      // IRQ/vIRQ, EL changes and the target EL is using AArch64
38    EXC_HANDLE  9 OsExcDispatch
39
40.org (VBAR + 0x500)                      // FIQ/vFIQ, EL changes and the target EL is using AArch64
41    EXC_HANDLE  10 OsExcDispatch
42
43.org (VBAR + 0x580)                      // SERROR, EL changes and the target EL is using AArch64
44    EXC_HANDLE  11 OsExcDispatch
45
46.org (VBAR + 0x600)                      // Synchronous, L changes and the target EL is using AArch32
47    EXC_HANDLE  12 OsExcDispatch
48
49.org (VBAR + 0x680)                      // IRQ/vIRQ, EL changes and the target EL is using AArch32
50    EXC_HANDLE  13 OsExcDispatch
51
52.org (VBAR + 0x700)                      // FIQ/vFIQ, EL changes and the target EL is using AArch32
53    EXC_HANDLE  14 OsExcDispatch
54
55.org (VBAR + 0x780)                      // SERROR, EL changes and the target EL is using AArch32
56    EXC_HANDLE  15 OsExcDispatch
57
58    .text

可以看到:针对4组,每组4类异常共16类异常均定义有其对应的入口,且其入口均定义为 EXC_HANDLE vecId handler 的形式。

提示

CPSR 寄存器中有当前栈的选择 bits[0] 0:SP_EL0,1:SP_ELX

在 prt_reset_vector.S 中的 OsEnterMain: 标号后加入代码

1OsVectTblInit: // 设置 EL1 级别的异常向量表
2    LDR x0, =OsVectorTable
3    MSR VBAR_EL1, X0

上下文保存与恢复

EXC_HANDLE 实际上是一个宏,其定义如下。

 1.global OsExcHandleEntry
 2.type   OsExcHandleEntry, function
 3
 4.macro SAVE_EXC_REGS  // 保存通用寄存器的值到栈中
 5    stp    x1, x0, [sp,#-16]!
 6    stp    x3, x2, [sp,#-16]!
 7    stp    x5, x4, [sp,#-16]!
 8    stp    x7, x6, [sp,#-16]!
 9    stp    x9, x8, [sp,#-16]!
10    stp    x11, x10, [sp,#-16]!
11    stp    x13, x12, [sp,#-16]!
12    stp    x15, x14, [sp,#-16]!
13    stp    x17, x16, [sp,#-16]!
14    stp    x19, x18, [sp,#-16]!
15    stp    x21, x20, [sp,#-16]!
16    stp    x23, x22, [sp,#-16]!
17    stp    x25, x24, [sp,#-16]!
18    stp    x27, x26, [sp,#-16]!
19    stp    x29, x28, [sp,#-16]!
20    stp    xzr, x30, [sp,#-16]!
21.endm
22
23.macro RESTORE_EXC_REGS  // 从栈中恢复通用寄存器的值
24    ldp    xzr, x30, [sp],#16
25    ldp    x29, x28, [sp],#16
26    ldp    x27, x26, [sp],#16
27    ldp    x25, x24, [sp],#16
28    ldp    x23, x22, [sp],#16
29    ldp    x21, x20, [sp],#16
30    ldp    x19, x18, [sp],#16
31    ldp    x17, x16, [sp],#16
32    ldp    x15, x14, [sp],#16
33    ldp    x13, x12, [sp],#16
34    ldp    x11, x10, [sp],#16
35    ldp    x9, x8, [sp],#16
36    ldp    x7, x6, [sp],#16
37    ldp    x5, x4, [sp],#16
38    ldp    x3, x2, [sp],#16
39    ldp    x1, x0, [sp],#16
40.endm
41
42.macro EXC_HANDLE vecId handler
43    SAVE_EXC_REGS // 保存寄存器宏
44
45    mov x1, #\vecId // x1 记录异常类型
46    b   \handler // 跳转到异常处理
47.endm

提示

注意把这部分代码放到 src/bsp/prt_vector.S 文件的开头

EXC_HANDLE 宏的主要作用是一发生异常就立即保存CPU寄存器的值,然后跳转到异常处理函数进行异常处理。

随后,我们继续在 src/bsp/prt_vector.S 文件中实现异常处理函数,包括 OsExcDispatch 和 OsExcDispatchFromLowEl。

 1    .global OsExcHandleEntry
 2    .type   OsExcHandleEntry, function
 3
 4    .global OsExcHandleEntryFromLowEl
 5    .type   OsExcHandleEntryFromLowEl, function
 6
 7
 8    .section .os.init.text, "ax"
 9    .globl OsExcDispatch
10    .type OsExcDispatch, @function
11    .align 4
12OsExcDispatch:
13    mrs    x5, esr_el1
14    mrs    x4, far_el1
15    mrs    x3, spsr_el1
16    mrs    x2, elr_el1
17    stp    x4, x5, [sp,#-16]!
18    stp    x2, x3, [sp,#-16]!
19
20    mov    x0, x1  // x0: 异常类型
21    mov    x1, sp  // x1: 栈指针
22    bl     OsExcHandleEntry  // 跳转到实际的 C 处理函数, x0, x1分别为该函数的第1,2个参数。
23
24    ldp    x2, x3, [sp],#16
25    add    sp, sp, #16        // 跳过far, esr, HCR_EL2.TRVM==1的时候,EL1不能写far, esr
26    msr    spsr_el1, x3
27    msr    elr_el1, x2
28    dsb    sy
29    isb
30
31    RESTORE_EXC_REGS // 恢复上下文
32
33    eret //从异常返回
34
35
36    .globl OsExcDispatchFromLowEl
37    .type OsExcDispatchFromLowEl, @function
38    .align 4
39OsExcDispatchFromLowEl:
40    mrs    x5, esr_el1
41    mrs    x4, far_el1
42    mrs    x3, spsr_el1
43    mrs    x2, elr_el1
44    stp    x4, x5, [sp,#-16]!
45    stp    x2, x3, [sp,#-16]!
46
47    mov    x0, x1
48    mov    x1, sp
49    bl     OsExcHandleFromLowElEntry
50
51    ldp    x2, x3, [sp],#16
52    add    sp, sp, #16        // 跳过far, esr, HCR_EL2.TRVM==1的时候,EL1不能写far, esr
53    msr    spsr_el1, x3
54    msr    elr_el1, x2
55    dsb    sy
56    isb
57
58    RESTORE_EXC_REGS // 恢复上下文
59
60    eret //从异常返回

OsExcDispatch 首先保存了4个系统寄存器到栈中,然后调用实际的异常处理 OsExcHandleEntry 函数。当执行完 OsExcHandleEntry 函数后,我们需要依序恢复寄存器的值。这就是操作系统课程中重点讲述的上下文的保存和恢复过程。

OsExcDispatchFromLowEl 与 OsExcDispatch 的操作除调用的实际异常处理函数不同外其它完全一致。

异常处理函数

新建 src/bsp/prt_exc.c 文件,实现实际的 OsExcHandleEntry 和 OsExcHandleFromLowElEntry 异常处理函数。

 1#include "prt_typedef.h"
 2#include "os_exc_armv8.h"
 3
 4extern U32 PRT_Printf(const char *format, ...);
 5
 6// ExcRegInfo 格式与 OsExcDispatch 中寄存器存储顺序对应
 7void OsExcHandleEntry(U32 excType, struct ExcRegInfo *excRegs)
 8{
 9    PRT_Printf("Catch a exception.\n");
10}
11
12// ExcRegInfo 格式与 OsExcDispatchFromLowEl 中寄存器存储顺序对应
13void OsExcHandleFromLowElEntry(U32 excType, struct ExcRegInfo *excRegs)
14{
15    PRT_Printf("Catch a exception from low exception level.\n");
16}

注意到上面两个异常处理函数的第2个参数是 struct ExcRegInfo * 类型,而在 src/bsp/prt_vector.S 中我们为该参数传递是栈指针 sp。所以该结构需与异常处理寄存器保存的顺序保持一致。

新建 src/bsp/os_exc_armv8.h 文件,定义 ExcRegInfo 结构。

 1#ifndef ARMV8_EXC_H
 2#define ARMV8_EXC_H
 3
 4#include "prt_typedef.h"
 5
 6#define XREGS_NUM       31
 7
 8struct ExcRegInfo {
 9    // 以下字段的内存布局与TskContext保持一致
10    uintptr_t elr;                  // 返回地址
11    uintptr_t spsr;
12    uintptr_t far;
13    uintptr_t esr;
14    uintptr_t xzr;
15    uintptr_t xregs[XREGS_NUM];     // 0~30 : x30~x0
16};
17
18#endif /* ARMV8_EXC_H */

提示

注意把上面的新增文件加入构建系统。

触发异常

注释掉 FPU 启用代码,构建系统并执行发现没有任何信息输出,通过调试将会观察到异常。

系统调用

提示

下面请启用 FPU。

系统调用是通用操作系统为应用程序提供服务的方式,理解系统调用对理解通用操作系统的实现非常重要。下面我们来实现1条简单的系统调用。

EL 0 是用户程序所在的级别,而在lab1中我们已经知道CPU启动后进入的是EL1或以上级别。

在 main 函数中我们首先返回到 EL0 级别,然后通过 SVC 调用一条系统调用.

 1S32 main(void)
 2{
 3
 4    const char Test_SVC_str[] = "Hello, my first system call!";
 5
 6    PRT_UartInit();
 7
 8    PRT_Printf("            _       _ _____      _             _             _   _ _   _ _   _           \n");
 9    PRT_Printf("  _ __ ___ (_)_ __ (_) ____|   _| | ___ _ __  | |__  _   _  | | | | \\ | | | | | ___ _ __ \n");
10    PRT_Printf(" | '_ ` _ \\| | '_ \\| |  _|| | | | |/ _ \\ '__| | '_ \\| | | | | |_| |  \\| | | | |/ _ \\ '__|\n");
11    PRT_Printf(" | | | | | | | | | | | |__| |_| | |  __/ |    | |_) | |_| | |  _  | |\\  | |_| |  __/ |   \n");
12    PRT_Printf(" |_| |_| |_|_|_| |_|_|_____\\__,_|_|\\___|_|    |_.__/ \\__, | |_| |_|_| \\_|\\___/ \\___|_|   \n");
13    PRT_Printf("                                                     |___/                               \n");
14
15    PRT_Printf("ctr-a h: print help of qemu emulator. ctr-a x: quit emulator.\n\n");
16
17
18
19    // 回到异常 EL 0级别,模拟系统调用,查看异常的处理,了解系统调用实现机制。
20    // 《Bare-metal Boot Code for ARMv8-A Processors》
21    OS_EMBED_ASM(
22        "MOV    X1, #0b00000\n" // Determine the EL0 Execution state.
23        "MSR    SPSR_EL1, X1\n"
24        "ADR    x1, EL0Entry\n" // Points to the first instruction of EL0 code
25        " MSR    ELR_EL1, X1\n"
26        "eret\n"  // 返回到 EL 0 级别
27        "EL0Entry: \n"
28        "MOV x0, %0 \n" //参数1
29        "MOV x8, #1\n" //在linux中,用x8传递 syscall number,保持一致。
30        "SVC 0\n"    // 系统调用
31        "B .\n" // 死循环,以上代码只用于演示,EL0级别的栈未正确设置
32        ::"r"(&Test_SVC_str[0])
33    );
34
35
36    // 在 EL1 级别上模拟系统调用
37    // OS_EMBED_ASM("SVC 0");
38    return 0;
39
40}

备注

OS_EMBED_ASM 在 prt_typedef.h 中定义为 __asm__ __volatile__,用于 C 与 ASM 混合编程。

SVC 是 arm 中的系统调用指令,相当于 x86 中的 int 指令。

备注

汇编语法可以参考 GNU ARM Assembler Quick Reference 5 和 Arm Architecture Reference Manual Armv8 (Chapter C3 A64 Instruction Set Overview) 6

内联汇编中Clobbers的用途到底是什么? 7

系统调用实现

在 src/bsp/prt_exc.c 修改 OsExcHandleFromLowElEntry 函数实现 1 条系统调用。

 1extern void TryPutc(unsigned char ch);
 2void MyFirstSyscall(char *str)
 3{
 4    while (*str != '\0') {
 5        TryPutc(*str);
 6        str++;
 7    }
 8}
 9// ExcRegInfo 格式与 OsExcDispatch 中寄存器存储顺序对应
10void OsExcHandleFromLowElEntry(U32 excType, struct ExcRegInfo *excRegs)
11{
12    int ExcClass = (excRegs->esr&0xfc000000)>>26;
13    if (ExcClass == 0x15){ //SVC instruction execution in AArch64 state.
14        PRT_Printf("Catch a SVC call.\n");
15        // syscall number存在x8寄存器中, x0为参数1
16        int syscall_num = excRegs->xregs[(XREGS_NUM - 1)- 8]; //uniproton存储的顺序x0在高,x30在低
17        uintptr_t param0 = excRegs->xregs[(XREGS_NUM - 1)- 0];
18        PRT_Printf("syscall number: %d, param 0: 0x%x\n", syscall_num, param0);
19
20        switch(syscall_num){
21            case 1:
22                MyFirstSyscall((void *)param0);
23                break;
24            default:
25                PRT_Printf("Unimplemented syscall.\n");
26        }
27    }else{
28        PRT_Printf("Catch a exception.\n");
29
30    }
31}

lab4 作业

作业1

查找 启用FPU 前异常出现的位置和原因。禁用FPU后PRT_Printf工作不正常,需通过调试跟踪查看异常发生的位置和原因 elr_el1 esr_el1 寄存器

1

https://developer.arm.com/documentation/den0024/a/Fundamentals-of-ARMv8/Execution-states

2

https://developer.arm.com/documentation/den0024/a/AArch64-Exception-Handling/Synchronous-and-asynchronous-exceptions

3(1,2)

https://developer.arm.com/documentation/den0024/a/AArch64-Exception-Handling/AArch64-exception-table

4(1,2)

https://developer.arm.com/documentation/den0024/a/ARMv8-Registers/AArch64-special-registers/Stack-pointer

5

https://www.ic.unicamp.br/~celio/mc404-2014/docs/gnu-arm-directives.pdf

6

https://developer.arm.com/documentation/ddi0487/gb

7

https://cloud.tencent.com/developer/article/1520799