
简介这是一份与《RISC-V体系结构编程与实践》第二章配套的实验代码包面向嵌入式开发者、RTOS工程师及操作系统学习者目标是借助MySBI启动接口与BenOS微型内核打通RISC-V底层启动、中断处理与内核调度等关键实践环节。压缩包共17个文件体积仅7KB包含sbi_main.c、boot.S、kernel.c等C/汇编源码以及用于链接的ld脚本、头文件和Makefile还有riscv64-benos_defconfig内核配置文件结构精炼适合直接对照书本章节进行编译与调试。目前已有304人学习下载。通过研读并运行这些代码可以直观理解MySBI如何完成硬件初始化和系统调用分发看清BenOS中任务调度、内存管理、中断服务等核心机制的落地实现从而将RISC-V架构与操作系统原理真正串成一条可实践的技能链路。 去年年底我把一套实验代码翻出来重构了一遍就是标题里这套“MySBI与BenOS实验代码”。说白了它是在 RISC-V 64 位环境下从零写的一个最小化 SBI 实现MySBI外加一个跑在它上面的微型操作系统内核BenOS。SBI 全称 Supervisor Binary Interface是 RISC-V 架构里 M 态机器态和 S 态监管者态之间的约定层类似 x86 世界里 BIOS/UEFI 和操作系统之间的接口但比 BIOS 简单、干净得多。这篇文章就把这套代码的核心思路、实现细节和踩坑过程完整拆开讲一遍适合对操作系统启动流程、RISC-V 特权级切换、裸机编程感兴趣的开发者参考哪怕你之前只写过用户态程序跟着走一遍也能把整个启动链路串明白。1. 内容整体设计与思路拆解1.1 为什么需要 SBI从裸机到 OS 的“第一公里”很多初次接触 RISC-V 底层开发的人会困惑我写了一个内核为什么不能直接扔到 QEMU 里跑这里的关键在于特权级设计。RISC-V 规定 S 态的操作系统不能直接访问 M 态的 CSR控制和状态寄存器比如mstatus、mepc、mtvec这些。但开机时 CPU 默认跑在 M 态内存初始化、时钟配置、控制台输出这些基础能力都留在 M 态。没有 SBI内核连往屏幕上打一个字符都做不到因为串口寄存器往往被映射在 M 态才能安全访问的地址空间里。MySBI 的作用就是充当这个“翻译官”。它跑在 M 态初始化硬件环境然后降级到 S 态把控制权交给 BenOS。之后 BenOS 每次想干特权操作比如关中断、读时间、输出字符就通过ecall指令陷入 M 态由 MySBI 代为完成。这种架构把硬件相关代码和内核逻辑彻底隔离也让同一份内核可以在不同平台间迁移——只要每个平台提供一个符合规范的 SBI 就行。RustSBI、OpenSBI 都是这么做的MySBI 是这个思路的最小化复刻整个实现不到 500 行汇编加 C。1.2 方案选型为什么全套走“最小可用”路线我在这套实验代码里刻意做了一堆减法。首先是语言选择MySBI 全部用汇编和少量 C 实现关键路径trap 入口、上下文切换用汇编初始化逻辑用 C。原因是 trap 处理需要精确控制栈指针和寄存器保存顺序汇编最直白而 C 代码可读性好适合写初始化设备、解析启动参数这类逻辑。内核侧 BenOS 同样保持极简只实现了中断开启、定时器、串口输出和进程调度雏形运行级别拿到 32MB 内存直接做线性映射。另一个关键决策是面向 QEMUvirt机器做适配。QEMU 的 virt 平台是 RISC-V 开发的事实标准调试方便支持-nographic模式把串口输出重定向到终端还能用 GDB 直接连上去单步调试。选择它意味着我不需要考虑真实开发板上的 DDR 初始化、Flash 烧录这些事能聚焦在特权级切换和 SBI 规范本身。这对实验性质的项目来说是最优解五分钟就能跑起来十分钟就能用 GDB 看清每次 trap 的进出。提示如果你手头有真实 RISC-V 开发板比如全志 D1 或者 SiFive 的 HiFive 系列这套代码的 SBI 部分稍作修改主要是内存基址和串口地址也能跑。但第一步务必先在 QEMU 上把逻辑跑通真实硬件的问题排查成本至少翻三倍。2. 核心细节解析MySBI 的 trap 入口与上下文切换2.1 第一次进入 M 态启动汇编到底干了什么MySBI 的入口在start.SCPU 上电后从 0x80000000 开始执行QEMU virt 平台把复位向量指向这里。这段汇编只做三件事设置栈指针、清零 BSS 段、跳转到 C 代码的sbi_main()。但细看每一行都有讲究。.section .text.init .globl _start _start: csrr a0, mhartid li t0, 1 bgeu a0, t0, .Lmhartid_ok csrr a1, mscratch bnez a1, .Linit_done la sp, _stack_top call sbi_main第 1 条csrr a0, mhartid读取当前硬件线程 IDQEMU 默认只开了一个核所以这里拿到的值必然是 0。但代码里依然做了多核对齐的检查目的有二一是防止在配置了多核的 QEMU 参数下执行出错二是展示一个规范 SBI 应有的姿态——它要能识别主核和从核只让主核走完整初始化。mscratch这个 CSR 后续会被用作 trap 时保存上下文指针的临时存储在启动早期先检查它是否为 0是为了区分是冷启动还是 warm reset这个细节在调真实板子的时候很重要。2.2 trap 分发程序一进一出寄存器必须完整trap 处理是整个 SBI 的核心也是最容易写崩的地方。无论 CPU 收到中断还是异常硬件会自动跳转到mtvec指向的地址同时保存mepc发生 trap 时的指令地址和mcausetrap 原因。但其余 31 个通用寄存器硬件一概不管必须由软件自己保存所以“进入 trap 时把全部寄存器压栈、退出时原样恢复”就成了写这个程序的铁律。我实现的trap_entry做了三件事通过csrrw sp, mscratch, sp交换栈指针把真实栈顶存进mscratch同时从mscratch拿出预先存好的上下文结构指针作为临时栈。把所有通用寄存器压到临时栈上保存mstatus、mepc、mcause、mtval到固定偏移量。把sp指向的上下文结构体的地址放入a0调用trap_handler_c。trap_entry: csrrw sp, mscratch, sp sd ra, 0*8(sp) sd gp, 1*8(sp) sd tp, 2*8(sp) sd t0, 3*8(sp) # ... 逐个保存 sd sp, 31*8(sp) csrr t0, mepc sd t0, 32*8(sp) csrr t0, mstatus sd t0, 33*8(sp) csrr t0, mcause sd t0, 34*8(sp) mv a0, sp call trap_handler_c这里有个反直觉的点sd sp, 31*8(sp)这行保存的并不是当前栈指针的值而是csrrw交换之前的原始用户栈指针。原因在于交换后sp已经指向了上下文结构体而mscratch里才是用户态的栈顶。硬件设计者用这种机制让 M 态代码能快速获取被中断者的栈同时保证自己不会污染对方的栈空间。我第一次写的时候没仔细想这层导致恢复上下文时 sp 被覆盖成上下文结构体地址整个栈彻底错乱进程一跑就“神秘崩溃”。2.3 ecall 分发识别“你是谁你要干什么”trap_handler_c里先通过mcause判断 trap 类型最高位置 1 表示这是中断否则是异常。MySBI 只处理两类异常ecall from U-mode和ecall from S-mode。前者留给 BenOS 的用户程序做系统调用后者是 BenOS 请求 SBI 服务的通道。uintptr_t trap_handler_c(trap_frame_t *tf) { uintptr_t mcause tf-mcause; uintptr_t mepc tf-mepc; if ((mcause 0x8000000000000000ULL) 0) { uintptr_t code mcause 0xFFF; if (code 9) { // ecall from S-mode sbi_ecall_handler(tf-a0, tf-a1, tf-a2, tf-a3); tf-mepc 4; // 关键跳过 ecall 指令 } else if (code 8) { // ecall from U-mode syscall_handler(tf); } } return mepc; }看懂这段代码需要先理解一个硬性事实ecall触发 trap 后mepc指向的是ecall这条指令本身如果不把它加 4返回后会无限重复执行同一条ecall形成死循环。所以每个异常处理程序入口处都习惯性做mepc 4。如果你在自己的代码里看到奇怪的重复踩 trap 现象十有八九是把这行漏了。另外注意a0到a7这 8 个寄存器正好能传 8 个参数这是 RISC-V 调用约定的天然优势SBI 规范里sbi_ecall就是靠a7传功能号、a0-a3传具体参数区分不同服务的。3. 在 MySBI 上启动 BenOS约束与适配3.1 RustSBI vs 自研 MySBI启动流程对比如果你之前用过 RustSBI 或 OpenSBI对下面这条启动链应该很熟ROM - SBI - boot loader - kernel。RustSBI 会把下一阶段代码的入口地址通过a0hartid、a1DTB 地址传下去内核再通过这些信息自举。MySBI 走差不多的路线但因为整个系统都是自己写的把 DTB 省了用固定地址直接告诉 BenOS“你的入口在 0x80200000内存范围我写在头文件里了”。这让 BenOS 的启动变得异常简单它的_start只做三件事——关中断、设栈顶、清 BSS然后跳进kernel_main()。实际上 BenOS 的第一行代码甚至不用主动关中断因为 MySBI 在mret之前已经保证mstatus.MIE 0中断在下述时刻之前都是关着的。但代码里我还是显式加了一句清sie这是防御性编程习惯防止未来改动了 SBI 侧逻辑导致中断状态失控。void kernel_main(uintptr_t hartid, uintptr_t dtb) { // 1. 初始化串口通过 SBI 的调试控制台扩展 // 2. 设置中断委托把 S 态时钟中断直接下放给内核 // 3. 设置首次时钟中断 // 4. 打印启动 banner // 5. 进入 idle 循环等待下一次中断 }3.2 SBI 调用规则a7 传功能号a0-a6 传参数BenOS 里的sbi_call()是标准的 RISC-V SBI 调用封装直接内联汇编避免函数调用开销。原版代码如下static inline long sbi_call(long a0, long a1, long a2, long a3, long a4, long a5, long a6, long a7) { register long r_a0 __asm__(a0) a0; register long r_a1 __asm__(a1) a1; register long r_a2 __asm__(a2) a2; register long r_a3 __asm__(a3) a3; register long r_a4 __asm__(a4) a4; register long r_a5 __asm__(a5) a5; register long r_a6 __asm__(a6) a6; register long r_a7 __asm__(a7) a7; __asm__ __volatile__(ecall : r(r_a0) : r(r_a1), r(r_a2), r(r_a3), r(r_a4), r(r_a5), r(r_a6), r(r_a7) : memory); return r_a0; }这套规范是 SBI 规范文档里定义的扩展 ID 放在a7函数 ID 放在a6参数从a0开始。比如sbi_console_putchar的功能号是 1sbi_set_timer的功能号是 0。每次写完一个 SBI 扩展我都建议对照规范文档核对一遍功能号不要凭记忆写。我踩过一次坑把sbi_set_timer功能号写成了 0x54494D45“TIME”的 ASCII这就是自定义扩展才会用的命名方案标准扩展撞上这个值直接静默失败搞了我一晚上。3.3 BenOS 的链接脚本内存布局一错全盘崩内核的链接脚本是这个项目里最容易出“离奇 bug”的部分。MySBI 把 BenOS 的加载地址定在 0x80200000所以链接脚本必须以这个地址为起点OUTPUT_ARCH(riscv) ENTRY(_start) BASE_ADDRESS 0x80200000; SECTIONS { . BASE_ADDRESS, .text : { *(.text.init) *(.text) } . ALIGN(4K); .data : { *(.data) } . ALIGN(4K); .bss : { *(.bss) } . ALIGN(4K); _stack_top .; . . 16K; }注意几个细节ENTRY(_start)告诉链接器入口符号.text.init段要放在最前面因为 CPU 一跳进来就要执行这段代码栈顶_stack_top定义在 BSS 后面栈向下生长所以这里 16K 的栈空间实际上覆盖了 BSS 的末尾部分。如果你把栈定义在 BSS 前面初始化过程中一旦调用函数就会踩到后续未初始化的数据段产生“局部变量莫名其妙被清零”的灵异事件。4. 实操过程与核心环节实现4.1 环境准备与编译工具链整个项目只需要两个核心工具链RISC-V GCC 工具链和 QEMU。Ubuntu/Debian 系可以直接装sudo apt install gcc-riscv64-unknown-elf qemu-system-misc gdb-multiarchGCC 用riscv64-unknown-elf-前缀QEMU 用qemu-system-riscv64调试器用gdb-multiarch因为它支持多架构。我最初图省事装了gdb-riscv64后来发现它对 RISC-V 的支持不如gdb-multiarch全面特别是向量寄存器显示上差别很大所以建议直接上 multiarch 版本。编译命令很简单但有两个必须注意的 flagriscv64-unknown-elf-gcc -marchrv64gc -mabilp64 -mcmodelmedany -nostdlib -fno-builtin -Iinclude -c src/start.S -o build/start.o riscv64-unknown-elf-gcc -marchrv64gc -mabilp64 -mcmodelmedany -nostdlib -fno-builtin -Iinclude -c src/sbi_main.c -o build/sbi_main.o riscv64-unknown-elf-ld -T linker.ld -o build/my_sbi.elf build/start.o build/sbi_main.o riscv64-unknown-elf-objcopy -O binary build/my_sbi.elf build/my_sbi.bin-marchrv64gc是告诉编译器生成 RV64 通用指令集代码-mcmodelmedany则让你能在不依赖高 20 位地址固定的情况下访问整个 64 位地址空间——裸机上这个选项基本是必需品。-nostdlib去掉标准库因为我们的链接脚本里根本没有 libc。如果某个文件报“undefined reference to memxxx”大概率是编译器插入了内建函数调用需要显式加-fno-builtin再加-ffreestanding让它完全进入独立环境模式。4.2 运行与验证QEMU 里的完整启动链路用下面的命令把整个系统拉起来qemu-system-riscv64 -machine virt -bios my_sbi.bin -kernel benos.bin -nographic-bios指定我们的 MySBI 固件-kernel指定 BenOS 内核镜像-nographic把串口重定向到当前终端。启动后你会看到类似这样的输出MySBI v0.1.0 Runtime: 0x80000000 - 0x80020000 BenOS: 0x80200000 - 0x80400000 BenOS: kernel booting... [0] Hello from BenOS on hart 0 BenOS: timer interrupt fired, ticks 1 BenOS: timer interrupt fired, ticks 2这里有一个隐含的过程值得展开QEMU 在-bios和-kernel同时指定时会把两个镜像分别加载到 0x80000000 和 0x80200000随后把 PC 设置为 0x80000000 开始执行。MySBI 初始化完成后通过mret跳转至 0x80200000 进入 BenOS。这条链路里没有引导程序因为 QEMU 的virt平台 ROM 默认已经把跳板做好了。如果你在-kernel里指定的是 ELF 文件而不是 binQEMU 会自动解析 ELF 的入口地址并加载所有段这比手工 objcopy 转 bin 更省心——但有一个缺点无法控制加载地址如果你把 BenOS 做成了位置无关可执行文件还是建议统一走 bin 流程避免地址错乱。4.3 关键场景一sbi_console_putchar 如何把字符送到串口sbi_console_putchar看起来最简单但它恰好是贯穿 M 态和 S 态的典型案例。BenOS 里的 printf 最终会逐字符调用它void putchar(char c) { sbi_call(c, 0, 0, 0, 0, 0, 0, 1); // a7 1 console_putchar }MySBI 侧对应的处理函数如下static void sbi_console_putchar(uintptr_t c) { while ((read_reg(UART16550_LSR) UART_LSR_THRE) 0) ; write_reg(UART16550_THR, c); }等等这里有个我特别想强调的细节为什么 QEMU virt 平台用 16550 的 UART但我们却能在sbi_console_putchar里直接往内存映射寄存器写字符而不是像真正裸机那样先初始化串口时钟答案藏在 QEMU 的virt机器定义里这台虚拟机器在出厂状态就配置好了 UART 的时钟和波特率我们只需要等待 THR发送保持寄存器空闲即可。真机上不是这回事你得先配置 UART 的时钟分频、数据位、停止位、校验位才能开始输出第一个字符。这也是为什么我说“先跑 QEMU 再碰真板”是绝对的黄金法则把串口初始化这部分暂时当成一个黑盒能让你把所有注意力集中在特权级切换和 SBI 语义上。等你在 QEMU 上把整个 trap 流程调通后再回头补齐真机的uart_init()排查范围会小很多。4.4 关键场景二时钟中断与 mret 的配合时钟中断是第二个关键场景。BenOS 里设置了一个周期性的定时器static void timer_init() { uint64_t interval 1000000; // 约 1ms取决于平台时钟频率 sbi_call(interval, 0, 0, 0, 0, 0, 0, 0); // a70 set_timer }MySBI 侧的sbi_set_timer会操作 RISC-V 的time比较寄存器来触发下一次中断static void sbi_set_timer(uint64_t stime_value) { write_csr(mtimecmp, stime_value); }但这里有个容易踩的大坑mtimecmp是 M 态才可访问的寄存器S 态内核不能直接用所以必须通过 SBIecall来设置。而time寄存器本身在 S 态是可以用rdtime指令读的所以 BenOS 可以在用户态用rdtime做时间戳。这一进一出的区别正好体现了 SBI 设计的核心哲学——可读的能力尽量下放可写的高危操作全部收口到 M 态。真要在 S 态直接读mtimecmp会触发 illegal instruction 异常而不是返回一个随机值这一点我在调试时反复确认过。当中断发生时如果mstatus.MIE 1且mstatus.MPIE是 0CPU 会在 trap 入口自动把 MIE 清零防止嵌套中断。等mret执行时硬件又会把 MPIE 的值恢复回 MIE。所以mret这条指令本质上是“原子地恢复先前状态并跳转”你必须保证 trap 入口保存的mstatus完整无缺恢复时机不能有偏移否则中断开关状态会错乱。我因为这个偏移问题写错过一版每触发一次定时器中断CPU 的中断使能状态就反相一次表现就是内核跑着跑着就“假死”间隔长短完全随机排查了好久才用 GDB 对比寄存器恢复前后的值找出问题。5. 工具链调试GDB QEMU 组合拳5.1 搭建调试会话从启动到断点没有调试器的裸机开发约等于闭眼开车。QEMU 的 GDB stub 是我用过的最顺手的调试方式在启动 QEMU 时加一个-s参数就能开启端口 1234 的监听qemu-system-riscv64 -machine virt -bios my_sbi.bin -kernel benos.bin -nographic -s另开一个终端连接 GDBgdb-multiarch build/my_sbi.elf (gdb) target remote :1234 (gdb) hbreak *0x80200000 (gdb) continuehbreak是硬件断点在还没有初始化任何断点相关硬件之前它是稳定可用的。如果不用硬件断点break软件断点需要先把目标地址的指令替换成ebreak这在没有 MMU 初始化的早期阶段很容易出问题。另一个好用的调试姿势是查看当前特权级RISC-V 没有直接读取当前特权级的指令但可以通过mstatus的MPRV、MPP字段推算。5.2 一条命令定位 trap 死循环在我调试过程中最常用的排障路径是在trap_entry入口设断点然后检查mcause的值。因为一旦出现意外 trap说明代码里某个行为触发了异常。例如(gdb) x/20i $pc 0x80200100 trap_entry: csrrw sp,mscratch,sp 0x80200104 trap_entry4: sd ra,0(sp) ... (gdb) p/x $mcause $1 0x2mcause值为 0x2 表示 illegal instruction。这时候你需要检查mepc指向的指令十有八九是一条在 S 态不可执行的指令比如直接写mstatus而不是通过 SBI或者把sret当mret用了。这种问题在用户态程序里根本不会出现但在裸机层几乎是日常。5.3 实测一次中断丢失的排查记录讲一个我亲身经历过的 bugBenOS 启动后第一个时钟中断能正常触发但之后再也没有第二个。我在trap_entry入口和sbi_set_timer两个地方都设了断点发现 trap 入口能进但sbi_set_timer断点永远不触发。这说明 BenOS 没有再次调用 SBI 设置下一次定时器。进一步查看 BenOS 的中断处理代码发现问题出在“调度器线程模型”的设计第一次中断到来时调度器把当前任务切换出去但新任务没有重新设置定时器。硬件的中断是电平触发还是边沿触发决定了你“设置一次”和“持续设置”的区别。在 RISC-V 上定时器中断必须每次都重新编程mtimecmp否则下一次永远不会来。这跟 ARM 的 private timer 不太一样后者可以配置成自动重载。所以正确做法是在每次时钟中断处理后立即安排下一次中断void timer_tick() { // 处理本轮 tick sbi_set_timer(get_cycle() INTERVAL); // 切换到下一个任务 }这类 bug 在书面上几乎不可能被描述清楚只有断开点、单步、反复观察寄存器才能找到根因。所以当你写这类底层代码时一定不要吝啬时间在 GDB 上特别是在第一次把整个系统跑通的那个阶段。6. 常见问题与排查技巧实录6.1 问题速查表下面这张表是我把这段时间遇到的高频问题整理出来的速查表遇到“灵异现象”时先对着查一遍通常能省下半天时间现象可能原因排查手段程序一跳进 S 态就崩溃mret的mstatus.MPP字段没配置成 S 态GDB 查看跳转前的mstatus确认MPP为 0b01串口有输出但乱码UART 时钟配置不对或波特率不匹配QEMU 里可以直接读 UART 寄存器确认配置中断只触发一次之后不再触发没有在中断处理中重新设置定时器或未清 pending在trap_entry和sbi_set_timer同时设断点ecall后返回地址错乱mepc没有加 4回到了原ecall检查trap_handler中mepc的修正逻辑栈变量莫名其妙被覆盖栈和 BSS 重叠或者栈溢出查看链接脚本的布局确认_stack_top定义在 BSS 后编译报 “undefined reference to memcpy”缺少-fno-builtin或-ffreestanding编译选项检查GDB 连不上 QEMU端口被占用或 QEMU 没开-sss -lntp查看端口6.2 一些可能省下你几个通宵的技巧调试裸机程序时最重要的一个经验其实就四个字二分排除。硬件环境引入的不确定性远大于软件所以先让它“假一点、笨一点”把中断关掉、把优化关掉、把随机性关掉确保你能在最小状态下看到确定性的行为然后再逐步打开。我第一次调这个系统时一上来就把所有功能全打开结果遇到异常根本分不清是 trap 入口写错还是调度器状态搞坏白白浪费了两天。后来我改成一次只开一个功能先单独验证串口输出再单独验证定时器最后再把它们组合起来整个流程顺畅了很多。另一个有用的小技巧是在启动早期加入一个“执行标记”。不需要用专门的调试串口直接在当前串口上输出一个递增的数字void early_debug_mark(int n) { putchar([); putchar(0 n); putchar(]); }这样无论哪一步崩了你都能立即知道“崩溃发生在第 n 步之前”比任何日志框架都好使。对裸机编程来说能精确定位崩溃点问题就解决了一半。还有一点关于 QEMU 的建议-d int,cpu_reset这个参数会打印每次中断和 CPU 复位的详细信息基本相当于给 CPU 装了一个“生命体征检测仪”。如果你怀疑某个中断异常触发开这个参数看输出能看到中断号和 trap 发生的地址比自己在代码里加打印要快得多。7. 迭代与扩展这套代码还能往哪走7.1 加入虚拟内存支持从“实模式”到“分页模式”目前 BenOS 整个运行在物理地址空间没有开启地址转换。这是迷你内核最简单的形态但也极大地限制了它的扩展性。下一步最有价值的改进就是把 MMU 打开建立页表映射让内核态运行在虚拟地址空间里。RISC-V 的 Sv39 分页机制是常见的教学起点39 位虚拟地址三级页表每级 9 位索引。开启方式是设置satp寄存器然后执行sfence.vma刷新 TLB。一旦开启了分页SBI 侧也会多一项工作处理缺页异常。mcause值为 12instruction page fault或 13load page fault时内核要把缺失的页从磁盘或缓存中加载到物理内存。这一步的工作量不可小觑但其价值是全方位的你将真正理解操作系统的虚拟地址空间布局、用户态和内核态的隔离、写时复制等一系列进阶概念。建议分两步走第一步只开恒等映射虚拟地址物理地址验证 MMU 开启过程没有 bug第二步再做非恒等映射把内核链接地址改到 0xffffffe000000000 附近彻底脱离物理地址的束缚。7.2 为 BenOS 增加更多 SBI 服务扩展你可以按 SBI 规范继续扩展 MySBI 的功能。比如实现sbi_hart_start/sbi_hart_stop来支持多核启动BenOS 可以唤醒其他核让它们各自进入调度循环。实现这个扩展的核心是 IPI处理器间中断在 QEMU 里通过 CLINT 的msip寄存器触发。更实用的是实现sbi_system_reset这样 BenOS 就能通过一个 SBI 调用来正常关机重启而不是让 QEMU 进程一直挂在那里。这个扩展的实现相当简单主要是往 QEMU 的test设备寄存器写值。7.3 把 MySBI 替换成 RustSBI当你的实验进入更复杂阶段时你可能会想试试 RustSBI 提供的更丰富的功能比如设备树传递、多核引导、更快的控制台输出。这时候 MySBI 的“教学价值”就体现出来了因为你已经理解了 SBI 的每一个环节切换到一个工业级实现时你能立刻看懂它的代码并找出它的设计决策的理由。反过来如果你一上来就用 RustSBI你大概只会把它当作一个“黑盒子固件”永远不知道它内部发生了什么。这正是我先写自研 MySBI 再对比 RustSBI 的原因。从实验代码到真正可用的小型操作系统中间还有很长的路。但从零到一这一段也就是从 CPU 上电到内核打印出第一行字符的这一段路是操作系统学习中信息密度最大、也最容易出成就感的一段。MySBI 与 BenOS 这套实验代码存在的意义就是让你不用背负 Linux 那样庞大的历史包袱也能把这段路完整走一遍。它的代码量、调试难度、学习曲线都处于一个恰到好处的位置对于教学和自学来说多一分则繁少一分则空。如果你也正在折腾 RISC-V 底层开发可以照着我这套流程走一遍不管最终是把代码改成自己的风格还是直接用 RustSBI 做底座那段“从零到打印出 Hello”的经历都会是你对操作系统理解最深刻的一课。我这套实验代码的完整目录我放在最后有兴趣的可以直接拿去跑sbi/放 MySBI 的启动和 trap 处理benos/放内核的启动和调度linker.ld是链接脚本Makefile一键编译。有问题随时在评论里交流。本文还有配套的精品资源点击获取