hnuOS lab4

lab 4 异常处理

实验目的

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


实验过程记录

前序知识提要

陷入操作系统


执行trap执行,出现异常,发生中断时都会陷入OS。

ARMv8 的中断和异常处理

[!NOTE] Title
查看技术参考手册了解架构-AArch 64 异常模型 — Learn the architecture - AArch64 Exception Model

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

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

异常类型

AArch64 中有四种类型的异常
了解架构-AArch 64 异常模型 — Learn the architecture - AArch64 Exception Model

  • Sync(Synchronous exceptions,同步异常),在执行时触发的异常,例如在尝试访问不存在的内存地址时,如系统调用。
  • IRQ (Interrupt requests,中断请求),由外部设备产生的中断
  • FIQ (Fast Interrupt Requests,快速中断请求),类似于IRQ,但具有更高的优先级,因此 FIQ 中断服务程序不能被其他 IRQ 或 FIQ 中断。
  • SError (System Error,系统错误),用于外部数据中止的异步中断

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

异常向量表


了解架构-AArch 64 异常模型 — Learn the architecture - AArch64 Exception Model
异常向量表的基地址由==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 文件,参照以上vector-table, 定义异常向量表如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
.section .os.vector.text, "ax" //定义段

.global OsVectorTable //定义全局函数
.type OsVectorTable,function

.align 13

OsVectorTable:
.set VBAR, OsVectorTable //将VBAR设置为向量表起始地址
.org VBAR // Synchronous, Current EL with SP_EL0
EXC_HANDLE 0 OsExcDispatch

.org (VBAR + 0x80) // IRQ/vIRQ, Current EL with SP_EL0
EXC_HANDLE 1 OsExcDispatch

.org (VBAR + 0x100) // FIQ/vFIQ, Current EL with SP_EL0
EXC_HANDLE 2 OsExcDispatch

.org (VBAR + 0x180) // SERROR, Current EL with SP_EL0
EXC_HANDLE 3 OsExcDispatch

.org (VBAR + 0x200) // Synchronous, Current EL with SP_ELx
EXC_HANDLE 4 OsExcDispatch

.org (VBAR + 0x280) // IRQ/vIRQ, Current EL with SP_ELx
EXC_HANDLE 5 OsExcDispatch

.org (VBAR + 0x300) // FIQ/vFIQ, Current EL with SP_ELx
EXC_HANDLE 6 OsExcDispatch

.org (VBAR + 0x380) // SERROR, Current EL with SP_ELx
EXC_HANDLE 7 OsExcDispatch

.org (VBAR + 0x400) // Synchronous, EL changes and the target EL is using AArch64
EXC_HANDLE 8 OsExcDispatchFromLowEl

.org (VBAR + 0x480) // IRQ/vIRQ, EL changes and the target EL is using AArch64
EXC_HANDLE 9 OsExcDispatch

.org (VBAR + 0x500) // FIQ/vFIQ, EL changes and the target EL is using AArch64
EXC_HANDLE 10 OsExcDispatch

.org (VBAR + 0x580) // SERROR, EL changes and the target EL is using AArch64
EXC_HANDLE 11 OsExcDispatch

.org (VBAR + 0x600) // Synchronous, L changes and the target EL is using AArch32
EXC_HANDLE 12 OsExcDispatch

.org (VBAR + 0x680) // IRQ/vIRQ, EL changes and the target EL is using AArch32
EXC_HANDLE 13 OsExcDispatch

.org (VBAR + 0x700) // FIQ/vFIQ, EL changes and the target EL is using AArch32
EXC_HANDLE 14 OsExcDispatch

.org (VBAR + 0x780) // SERROR, EL changes and the target EL is using AArch32
EXC_HANDLE 15 OsExcDispatch

.text
  • 向量表布局
    ARMv8 的异常向量表包含 ​​16 个条目​​,每个条目占用 ​​128 字节(0x80)​​,对应不同的异常类型和上下文状态。代码中通过 .org 指令定位到每个条目的起始地址。
  • 异常处理宏
    每个条目通过 ==EXC_HANDLE 宏==跳转到统一的异常分发函数:
    • EXC_HANDLE <num>, OsExcDispatch
      将异常编号 <num> 和跳转目标 OsExcDispatch 传递给宏。
    • OsExcDispatchFromLowEl
      特殊处理从低 EL(如用户态)触发的同步异常(如系统调用)。

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

上下文的保存和恢复

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
.global OsExcHandleEntry
.type OsExcHandleEntry, function

.macro SAVE_EXC_REGS // 保存通用寄存器的值到栈中
stp x1, x0, [sp,#-16]!
stp x3, x2, [sp,#-16]!
stp x5, x4, [sp,#-16]!
stp x7, x6, [sp,#-16]!
stp x9, x8, [sp,#-16]!
stp x11, x10, [sp,#-16]!
stp x13, x12, [sp,#-16]!
stp x15, x14, [sp,#-16]!
stp x17, x16, [sp,#-16]!
stp x19, x18, [sp,#-16]!
stp x21, x20, [sp,#-16]!
stp x23, x22, [sp,#-16]!
stp x25, x24, [sp,#-16]!
stp x27, x26, [sp,#-16]!
stp x29, x28, [sp,#-16]!
stp xzr, x30, [sp,#-16]!
.endm

.macro RESTORE_EXC_REGS // 从栈中恢复通用寄存器的值
ldp xzr, x30, [sp],#16
ldp x29, x28, [sp],#16
ldp x27, x26, [sp],#16
ldp x25, x24, [sp],#16
ldp x23, x22, [sp],#16
ldp x21, x20, [sp],#16
ldp x19, x18, [sp],#16
ldp x17, x16, [sp],#16
ldp x15, x14, [sp],#16
ldp x13, x12, [sp],#16
ldp x11, x10, [sp],#16
ldp x9, x8, [sp],#16
ldp x7, x6, [sp],#16
ldp x5, x4, [sp],#16
ldp x3, x2, [sp],#16
ldp x1, x0, [sp],#16
.endm

.macro EXC_HANDLE vecId handler
SAVE_EXC_REGS // 保存寄存器宏

mov x1, #\vecId // x1 记录异常类型
b \handler // 跳转到异常处理
.endm
  • stp(Store Pair)​​ 指令用于同时存储两个寄存器到栈,[sp,#-16]! 表示:
    • sp = sp - 16(栈向下增长)
    • 存储 x1, x0 到 [sp] 和 [sp+8]
  • ldp(Load Pair)​​ 指令从栈中加载两个寄存器,[sp],#16 表示:
    • 从 [sp] 和 [sp+8] 加载数据到寄存器。
    • sp = sp + 16(栈指针恢复)。
  • xzr(零寄存器)​​ 用于占位,因为 x31 是 xzr(写入无效)。

[!NOTE] Title
在 ​​ARMv8-A (AArch64) 架构​​中,​xzr(Zero Register,零寄存器)​​ 是一个特殊的寄存器,它的值始终为 ​​0​​,并且任何写入操作对它都无效(即无法修改它的值)。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
     .global OsExcHandleEntry //EL1触发的异常
.type OsExcHandleEntry, function

.global OsExcHandleEntryFromLowEl//从低特权级(如 EL0 用户态)触发的异常(如系统调用)。
.type OsExcHandleEntryFromLowEl, function


.section .os.init.text, "ax"
.globl OsExcDispatch
.type OsExcDispatch, @function
.align 4
OsExcDispatch:
// 保存关键系统寄存器到栈
mrs x5, esr_el1
mrs x4, far_el1
mrs x3, spsr_el1
mrs x2, elr_el1
stp x4, x5, [sp,#-16]!
stp x2, x3, [sp,#-16]!
//调用c处理函数
mov x0, x1 // x0: 异常类型
mov x1, sp // x1: 栈指针
bl OsExcHandleEntry // 跳转到实际的 C 处理函数, x0, x1分别为该函数的第1,2个参数。
//恢复系统寄存器
ldp x2, x3, [sp],#16
add sp, sp, #16 // 跳过far, esr, HCR_EL2.TRVM==1的时候,EL1不能写far, esr
msr spsr_el1, x3
msr elr_el1, x2
dsb sy
isb
//恢复上下文并返回
RESTORE_EXC_REGS // 恢复上下文,EXC_HANDLE保存的寄存器值

eret //从异常返回


.globl OsExcDispatchFromLowEl
.type OsExcDispatchFromLowEl, @function
.align 4

OsExcDispatchFromLowEl:
mrs x5, esr_el1
mrs x4, far_el1
mrs x3, spsr_el1
mrs x2, elr_el1
stp x4, x5, [sp,#-16]!
stp x2, x3, [sp,#-16]!

mov x0, x1
mov x1, sp
bl OsExcHandleFromLowElEntry

ldp x2, x3, [sp],#16
add sp, sp, #16 // 跳过far, esr, HCR_EL2.TRVM==1的时候,EL1不能写far, esr
msr spsr_el1, x3
msr elr_el1, x2
dsb sy
isb

RESTORE_EXC_REGS // 恢复上下文

eret //从异常返回

[!NOTE]

  1. ​保存系统寄存器​

    • esr_el1​:记录异常原因(如中断类型、系统调用号)。
    • far_el1​:触发异常的访问地址(如页错误地址)。
    • spsr_el1​:异常发生时的 CPU 状态(如 PSTATE)。
    • elr_el1​:异常返回地址(即被中断的指令地址)。
    • 通过 stp 指令将这些寄存器压栈,形成 ​​异常上下文结构体​​。
  2. ​调用 C 处理函数​

    • x0​:传递异常类型(如 0=同步异常,1=IRQ)。
    • x1​:传递栈指针(指向保存的上下文),供 C 函数解析。
    • bl OsExcHandleEntry​:跳转到 C 函数,实现具体逻辑(如中断处理、系统调用分发)。
  3. ​恢复系统寄存器​

    • 从栈中恢复 elr_el1 和 spsr_el1,确保异常返回后能继续执行原程序。
    • dsb sy + isb​:保证内存和指令同步,避免乱序执行问题。
  4. ​恢复通用寄存器并返回​

    • RESTORE_EXC_REGS​:恢复所有通用寄存器(x0-x30)。
    • eret​:返回到 elr_el1 指向的地址,并恢复 spsr_el1 中的 CPU 状态。

异常处理函数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "prt_typedef.h"
#include "os_exc_armv8.h"

extern U32 PRT_Printf(const char *format, ...);

// ExcRegInfo 格式与 OsExcDispatch 中寄存器存储顺序对应
void OsExcHandleEntry(U32 excType, struct ExcRegInfo *excRegs)
{
PRT_Printf("Catch a exception.\n");
}

// ExcRegInfo 格式与 OsExcDispatchFromLowEl 中寄存器存储顺序对应
void OsExcHandleFromLowElEntry(U32 excType, struct ExcRegInfo *excRegs)
{
PRT_Printf("Catch a exception from low exception level.\n");
}

新建 src/bsp/os_exc_armv8.h 文件,定义 ExcRegInfo 结构。存储的就是刚刚压到栈里的那些值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef ARMV8_EXC_H
#define ARMV8_EXC_H

#include "prt_typedef.h"
#define XREGS_NUM       31

struct ExcRegInfo { //保存异常发生时的上下文
    // 以下字段的内存布局与TskContext保持一致
    uintptr_t elr;                  // 返回地址
    uintptr_t spsr;
    uintptr_t far;
    uintptr_t esr;
    uintptr_t xzr;
    uintptr_t xregs[XREGS_NUM];     // 0~30 : x30~x0

};

#endif /* ARMV8_EXC_H */

启用fpu实现系统调用

cpu启动后进入的是el1或以上级别
在 main 函数中我们首先返回到 EL0 级别,然后通过 SVC 调用一条系统调用.
从异常返回:eret指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
S32 main(void)
{

    const char Test_SVC_str[] = "Hello, my first system call!";

    PRT_UartInit();

    PRT_Printf("            _       _ _____      _             _             _   _ _   _ _   _           \n");
    PRT_Printf("  _ __ ___ (_)_ __ (_) ____|   _| | ___ _ __  | |__  _   _  | | | | \\ | | | | | ___ _ __ \n");
    PRT_Printf(" | '_ ` _ \\| | '_ \\| |  _|| | | | |/ _ \\ '__| | '_ \\| | | | | |_| |  \\| | | | |/ _ \\ '__|\n");
    PRT_Printf(" | | | | | | | | | | | |__| |_| | |  __/ |    | |_) | |_| | |  _  | |\\  | |_| |  __/ |   \n");
    PRT_Printf(" |_| |_| |_|_|_| |_|_|_____\\__,_|_|\\___|_|    |_.__/ \\__, | |_| |_|_| \\_|\\___/ \\___|_|   \n");
    PRT_Printf("                                                     |___/                               \n");


    PRT_Printf("ctr-a h: print help of qemu emulator. ctr-a x: quit emulator.\n\n");

    // 回到异常 EL 0级别,模拟系统调用,查看异常的处理,了解系统调用实现机制。
    // 《Bare-metal Boot Code for ARMv8-A Processors》
    OS_EMBED_ASM(
        "MOV    X1, #0b00000\n" // Determine the EL0 Execution state.
        "MSR    SPSR_EL1, X1\n"//
        "ADR    x1, EL0Entry\n" // Points to the first instruction of EL0 code
        " MSR    ELR_EL1, X1\n"//pc更新为ELR_EL1,将指向EL0Entry
        "eret\n"  // 返回到 EL 0 级别
        "EL0Entry: \n"
        "MOV x0, %0 \n" //参数1,为输入操作数&Test_SVC_str[0]
        "MOV x8, #1\n" //在linux中,用x8传递 syscall number,保持一致。
        "SVC 0\n"    // 系统调用
        "B .\n" // 死循环,以上代码只用于演示,EL0级别的栈未正确设置
        ::"r"(&Test_SVC_str[0])
    );

    // 在 EL1 级别上模拟系统调用
    // OS_EMBED_ASM("SVC 0");
    return 0;



}

系统调用实现

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ExcRegInfo 格式与 OsExcDispatchFromLowEl 中寄存器存储顺序对应
extern void TryPutc(unsigned char ch);
void MyFirstSyscall(char *str)
{
    while (*str != '\0') {
        TryPutc(*str);
        str++;
    }
}

// ExcRegInfo 格式与 OsExcDispatch 中寄存器存储顺序对应
void OsExcHandleFromLowElEntry(U32 excType, struct ExcRegInfo *excRegs)
{ // 提取异常级别
    int ExcClass = (excRegs->esr&0xfc000000)>>26;
    if (ExcClass == 0x15){ //SVC instruction execution in AArch64 state.
        PRT_Printf("Catch a SVC call.\n");
        // syscall number存在x8寄存器中, x0为参数1
        //从寄存器上下文获取系统调用号和参数
        int syscall_num = excRegs->xregs[(XREGS_NUM - 1)- 8]; //uniproton存储的顺序x0在高,x30在低
        uintptr_t param0 = excRegs->xregs[(XREGS_NUM - 1)- 0];
        PRT_Printf("syscall number: %d, param 0: 0x%x\n", syscall_num, param0);
        //根据系统调用号分发逻辑
        switch(syscall_num){
            case 1:
                MyFirstSyscall((void *)param0);
                break;
            default:
                PRT_Printf("Unimplemented syscall.\n");
        }
    }else{
        PRT_Printf("Catch a exception.\n");
    }

}

成功输出

作业

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

  • 禁用fpu后进行调试
  • 在main.c处打上断点逐步调试
  • 可以看到,程序在 PRT_Printf函数内部的TryPrintf遇到异常,查找osVectorTable
  • 查看异常向量表可知,这是一个同步异常,在使用SP_ELx时当前EL出现 异常,异常级别为非用户级
  • 查看esr-el1,elr-el1有

[!NOTE]
ARMv8 异常处理简介 - 内核工匠 - 博客园
异常链接寄存器ELR(Exception Link Registers)包含异常返回地址。当处理器发生异常时,返回地址将保存在异常级别对应的ELR中。
异常综合表征寄存器ESR_ELn包含的异常信息用以异常处理程序确定异常原因。

ESR_ELn的BIT[31:26]指示处理程序执行对应的异常,查看Arm A-profile 架构寄存器 — Arm A-profile Architecture Registers
我们可以知道这是由fpu引起的异常
ELR_EL1代表EL1出现异常时要返回的地址,查看一下
这告诉我们,发生异常时下一条待执行的指令:
指令 str q0, [sp, #80],将128位向量寄存器q0的值保存到栈指针sp偏移80字节的内存位置
也就是说,我们在这条指令出错,原因是我们禁用了fpu,CPU无法执行浮点或SIMD指令(如str q0)。此时若程序尝试执行这类指令,会触发​​未定义指令异常​​(同步异常),表现为ESR_EL1的EC字段指示非法操作。

ds

异常发生时,CPU会自动保存PCELR_EL1、状态到SPSR_EL1,并跳转到异常向量表。ERET指令会恢复这些寄存器值,返回用户态