2. 实现系统调用入口
任务目标
在RISC-V双特权级(U-Mode与M-Mode)环境下,通过ecall指令实现一次完整的系统调用闭环:U-Mode应用程序发起调用,M-Mode陷阱处理程序捕获并解析调用号,执行对应服务(获取当前特权级),将结果返回U-Mode。
环境与依赖
- QEMU模拟器(
qemu-system-riscv32或qemu-system-riscv64) - RISC-V交叉编译器(
riscv64-unknown-elf-gcc或riscv32-unknown-elf-gcc) - 无需操作系统,直接运行在机器模式(M-Mode)的裸机程序
核心实现原理
系统调用涉及三个关键寄存器:mcause(记录陷阱原因,ecall from U-Mode值为8)、mepc(保存触发异常的指令地址,需加4以正确返回)、a7(约定存放系统调用号)。M-Mode陷阱处理程序读取mcause判断来源,根据a7分发到对应服务函数,结果写入a0后执行mret返回U-Mode。
完整代码实现
#include <stdint.h>
// 系统调用号定义
#define SYS_GET_PRIV_LEVEL 0
// 简单输出寄存器值的辅助宏(用于调试,实际验证通过QEMU退出码)
#define DEBUG_OUT(x) asm volatile("li a0, %0" :: "i"(x))
// 获取当前特权级(M-Mode下执行时为3,U-Mode下为0)
static uint32_t get_priv_level(void) {
uint32_t level;
asm volatile("csrr %0, mstatus" : "=r"(level));
// mstatus.MPP[12:11] 记录进入M-Mode前的特权级
return (level >> 11) & 0x3;
}
// 系统调用分发函数
static uint32_t syscall_handler(uint32_t syscall_num) {
switch (syscall_num) {
case SYS_GET_PRIV_LEVEL:
return get_priv_level();
default:
return 0xDEAD; // 未知系统调用号
}
}
// M-Mode陷阱处理入口
void trap_entry(void) __attribute__((interrupt));
void trap_entry(void) {
uint32_t mcause_val, syscall_num, ret_val;
asm volatile("csrr %0, mcause" : "=r"(mcause_val));
// 检查是否来自U-Mode的ecall(mcause值为8)
if (mcause_val == 8) {
// 读取系统调用号(a7寄存器)
asm volatile("mv %0, a7" : "=r"(syscall_num));
// 调用处理函数
ret_val = syscall_handler(syscall_num);
// 将结果写入a0寄存器
asm volatile("mv a0, %0" :: "r"(ret_val));
// mepc加4,跳过ecall指令继续执行
uint32_t mepc_val;
asm volatile("csrr %0, mepc" : "=r"(mepc_val));
mepc_val += 4;
asm volatile("csrw mepc, %0" :: "r"(mepc_val));
}
// 非ecall异常:死循环等待调试
// 函数末尾自动执行mret(由interrupt属性保证)
}
// U-Mode应用程序
__attribute__((naked)) void user_app(void) {
asm volatile(
"li a7, 0\n" // 系统调用号:获取特权级
"ecall\n" // 触发系统调用
"li a0, 1\n" // 返回值非0表示成功
"wfi\n" // 停止
);
}
// 启动代码:配置并切换到U-Mode执行用户程序
void start(void) {
// 设置U-Mode的ecall跳转到trap_entry
uint32_t mtvec_val = (uint32_t)trap_entry;
asm volatile("csrw mtvec, %0" :: "r"(mtvec_val));
// 设置mstatus.MPP=0(U-Mode),执行mret后进入U-Mode
uint32_t mstatus_val;
asm volatile("csrr %0, mstatus" : "=r"(mstatus_val));
mstatus_val &= ~(0x3 << 11); // 清除MPP
asm volatile("csrw mstatus, %0" :: "r"(mstatus_val));
// 设置mepc指向用户程序入口
asm volatile("csrw mepc, %0" :: "r"(user_app));
// 切换到U-Mode执行
asm volatile("mret");
}
// 链接脚本入口
void _start(void) {
start();
while(1); // 不应到达此处
}
链接脚本(link.ld)
OUTPUT_ARCH(riscv)
ENTRY(_start)
MEMORY
{
RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 64K
}
SECTIONS
{
.text : {
*(.text._start)
*(.text*)
} > RAM
.data : { *(.data*) } > RAM
.bss : { *(.bss*) } > RAM
}
编译与运行
# 编译(以32位为例)
riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 -nostdlib -T link.ld \
-o syscall_demo.elf syscall_demo.c
# 运行于QEMU
qemu-system-riscv32 -machine virt -bios none -kernel syscall_demo.elf \
-nographic -d int -D qemu_log.txt
# 查看异常日志确认系统调用流程
cat qemu_log.txt | grep -E "trap|ecall|mret"
验证方法与判定标准
运行后观察QEMU日志,应看到以下关键事件序列:
- 启动后从M-Mode执行
mret进入U-Mode(日志显示特权级切换) - U-Mode执行
ecall触发异常(日志显示trap,mcause=8) - M-Mode陷阱处理程序被调用,读取系统调用号并返回结果
- 执行
mret返回U-Mode,用户程序继续执行到wfi停止
通过条件:日志中完整出现上述4个阶段,且无“非法指令”或“异常嵌套”错误。若需验证返回值正确性,可在user_app的wfi前加入对a0的检查(例如与预期值0比较,不等则进入死循环),通过观察程序是否停在wfi判断调用是否成功。
常见问题
- 编译报错“unknown CSR”:确认使用
-march=rv32im而非rv32i,部分CSR需要特权架构支持 - 程序卡死无输出:检查
mtvec是否正确设置,以及mepc+4是否跳过完整指令(ecall为32位指令,加4正确) - 返回值错误:确认
mstatus.MPP在进入U-Mode前被清零,否则get_priv_level会读到错误值