3. 异常级别切换实验

任务目标

在ARMv8-A架构中,EL0(用户态)与EL1(内核态)之间的切换是操作系统运行的基础。本实验将编写一个运行在EL1的简单程序,通过执行ERET指令主动降级到EL0,并在EL0执行一段代码后再通过SVC指令触发异常返回EL1,从而完整观察异常级别的双向切换过程。

环境与依赖

  • 硬件:支持ARMv8-A架构的处理器(如Raspberry Pi 3/4、QEMU的virt平台)
  • 工具链:aarch64-linux-gnu-gcc、aarch64-linux-gnu-objdump(或等效工具)
  • 运行方式:QEMU模拟器(推荐,便于调试)或裸机环境

核心实现

以下代码实现完整的EL1→EL0→EL1切换流程。程序启动时处于EL1,配置EL0所需的异常向量表和栈空间,然后通过ERET进入EL0。在EL0中执行一段标记代码后,通过SVC #0触发同步异常,异常向量表捕获后返回EL1,最终在EL1中打印当前异常级别以验证切换成功。

// el_switch.c - ARMv8 EL1/EL0 切换实验
// 编译: aarch64-linux-gnu-gcc -c el_switch.c -o el_switch.o
//       aarch64-linux-gnu-ld -Ttext=0x400000 el_switch.o -o el_switch.elf
// 运行: qemu-system-aarch64 -M virt -cpu cortex-a53 -nographic -kernel el_switch.elf

#define CURRENT_EL_MASK  0x3C  // CurrentEL 寄存器中 EL 字段掩码

// 获取当前异常级别
static unsigned long get_current_el(void) {
    unsigned long el;
    asm volatile("mrs %0, CurrentEL" : "=r"(el));
    return (el & CURRENT_EL_MASK) >> 2;  // 返回 0=EL0, 1=EL1, 2=EL2, 3=EL3
}

// 简易串口输出(QEMU virt 平台 PL011)
#define UART_BASE 0x09000000
#define UART_DR   (*(volatile unsigned int *)(UART_BASE + 0x000))
#define UART_FR   (*(volatile unsigned int *)(UART_BASE + 0x018))

static void uart_putc(char c) {
    while (UART_FR & (1 << 5));  // 等待 TX FIFO 空
    UART_DR = c;
}

static void uart_puts(const char *s) {
    while (*s) uart_putc(*s++);
}

static void print_el(const char *msg) {
    char buf[32];
    int i = 0;
    unsigned long el = get_current_el();
    uart_puts(msg);
    uart_puts(": EL");
    buf[i++] = '0' + el;
    buf[i] = '\0';
    uart_puts(buf);
    uart_putc('\n');
}

// EL0 代码入口
static void el0_entry(void) __attribute__((naked));
static void el0_entry(void) {
    asm volatile(
        "mov x0, #0xE0\n"        // 标记值 0xE0
        "msr tpidr_el0, x0\n"    // 写入 EL0 专用寄存器
        "svc #0\n"               // 触发同步异常返回 EL1
        "1: b 1b\n"              // 不应执行到这里
    );
}

// 异常向量表(仅处理 SVC)
__attribute__((aligned(0x800)))
void exception_vector_table(void) {
    asm volatile(
        // 当前 EL 的 SP_EL0 同步异常(SVC 从 EL0 触发时走这里)
        ".org 0x400\n"
        "b svc_handler\n"
        ".org 0x800\n"  // 向量表大小 2KB,其余填充
    );
}

// SVC 异常处理函数
static void svc_handler(void) {
    // 此时已回到 EL1
    print_el("SVC handler");
    // 返回 EL0 继续执行(但 el0_entry 中 SVC 后是死循环,实际不会返回)
    asm volatile("eret");
}

// EL1 入口点
void _start(void) {
    unsigned long el = get_current_el();
    
    // 确认当前在 EL1
    if (el != 1) {
        uart_puts("Error: 必须在 EL1 启动\n");
        while (1);
    }
    
    // 配置 VBAR_EL1 指向异常向量表
    asm volatile(
        "ldr x0, =exception_vector_table\n"
        "msr vbar_el1, x0\n"
        "isb\n"
    );
    
    // 设置 EL0 栈指针(使用独立栈区域)
    asm volatile(
        "ldr x0, =el0_stack_top\n"
        "msr sp_el0, x0\n"
    );
    
    // 构造 EL0 所需的 SPSR_EL1:清除 M[3:0] 设为 0(EL0),保留其他位
    asm volatile(
        "mov x0, #0\n"
        "msr spsr_el1, x0\n"      // EL0h 模式
        "ldr x0, =el0_entry\n"
        "msr elr_el1, x0\n"       // 异常返回地址指向 el0_entry
        "eret\n"                  // 切换到 EL0
    );
    
    // 不会执行到这里
    while (1);
}

// EL0 栈空间
__attribute__((aligned(16)))
unsigned char el0_stack[1024];
__attribute__((aligned(16)))
unsigned char *el0_stack_top = el0_stack + sizeof(el0_stack);

编译与运行

# 编译链接
aarch64-linux-gnu-gcc -c el_switch.c -o el_switch.o
aarch64-linux-gnu-ld -Ttext=0x400000 el_switch.o -o el_switch.elf

# QEMU 运行
qemu-system-aarch64 -M virt -cpu cortex-a53 -nographic -kernel el_switch.elf

验证方法

运行程序后,串口输出应依次出现以下内容:

Start: EL1
SVC handler: EL1

判定标准:

  1. 程序启动时打印Start: EL1,证明初始处于EL1。
  2. 随后打印SVC handler: EL1,证明从EL0触发的SVC异常成功被EL1的向量表捕获,且处理器已回到EL1。
  3. 若在el0_entry中通过mrs x1, CurrentEL读取并输出,可观察到EL0阶段值为0(本实验通过写入tpidr_el0寄存器间接验证EL0执行环境已生效)。

关键说明

  • 异常级别读取:CurrentEL寄存器低两位编码当前EL,需右移2位得到0/1/2/3。
  • ERET行为:ERET从ELR_EL1加载返回地址,从SPSR_EL1恢复处理器状态,实现降级到EL0。
  • SVC触发:EL0执行SVC #0时,处理器自动跳转到EL1的异常向量表偏移0x400处(当前EL使用SP_EL0的同步异常入口)。
  • 向量表对齐:VBAR_EL1要求向量表2KB对齐,代码中通过__attribute__((aligned(0x800)))保证。
  • 栈配置:进入EL0前必须设置SP_EL0,否则EL0代码无法使用栈(本实验EL0代码未使用栈,但为完整性已配置)。

常见问题

  • 程序启动即崩溃:检查是否在EL1启动(QEMU默认启动在EL2,需使用-cpu cortex-a53强制EL1,或添加EL2降级代码)。
  • SVC后未进入handler:确认向量表地址已写入VBAR_EL1且对齐正确;检查SVC imm值不影响入口选择。
  • 串口无输出:确认QEMU的virt平台UART基地址为0x09000000(部分QEMU版本使用0x10000000,需按版本调整)。