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日志,应看到以下关键事件序列:

  1. 启动后从M-Mode执行mret进入U-Mode(日志显示特权级切换)
  2. U-Mode执行ecall触发异常(日志显示trap,mcause=8)
  3. M-Mode陷阱处理程序被调用,读取系统调用号并返回结果
  4. 执行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会读到错误值