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
判定标准:
- 程序启动时打印
Start: EL1,证明初始处于EL1。 - 随后打印
SVC handler: EL1,证明从EL0触发的SVC异常成功被EL1的向量表捕获,且处理器已回到EL1。 - 若在
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,需按版本调整)。