发散创新:手写 RISC-V 汇编启动代码 + S-mode 异常向量重定向实战
在嵌入式与开源硬件开发中,绕过 SDK、直写裸机启动代码是深入理解 RISC-V 架构的必经之路。本文不依赖riscv-gnu-toolchain的crt0.S或freedom-metal的封装层,而是从零构建一个可运行于 QEMU(rv64gc)和 SiFive HiFive1 Rev B(rv32imac)双平台的最小启动流程,并完整实现 S-mode 下的异常向量表动态重定向——这是多数教程刻意回避但实际 SoC 开发中必须掌握的核心能力。
一、为什么必须手写_start?SDK 隐藏了什么?
主流 SDK(如 Freedom E SDK、Zephyr)默认将异常向量表硬编码在0x00000000(M-mode)或0x80000000(S-mode),但真实芯片中:
- Boot ROM 固定跳转至
0x00000000 - S-mode 向量基址由
stvec控制,且仅支持DIRECT或VECTORED模式
- S-mode 向量基址由
stvec初始值为 0,若未显式设置,所有异常将跳转到0x00000000—— 覆盖你的代码!
这就是为何裸机程序常“莫名重启”或“进入非法指令陷阱”。
二、双平台兼容启动结构(rv32 / rv64)
我们采用统一汇编框架,通过.option push+.arch动态切换指令集:
# startup.S .section .text._start, "ax", @progbits .global _start _start: # 关中断 & 清 CSR csrw mstatus, zero csrw mie, zero csrw mip, zero # 设置栈指针(根据平台选择) la sp, __stack_top # rv32: la = lui+addi; rv64: la = lui+addi (兼容) # 进入 S-mode(若当前为 M-mode) li t0, 0x18 # SPP=1, SPIE=1 → S-mode csrw mstatus, t0 csrw mepc, ra mret .section .text.smode_entry, "ax" .align 4 smode_entry: # 此时已处于 S-mode la sp, __stack_top_smode call main j . .section .data .align 4 __stack_top: .space 4096 __stack_top_smode: .space 4096✅ 编译命令(QEMU 测试):
riscv64-unknown-elf-gcc-march=rv64gc-mabi=lp64-nostdlib-Tlinker.ld\-okernel.elf startup.S main.c qemu-system-riscv64-machinevirt-nographic-kernelkernel.elf
三、S-mode 异常向量重定向:从理论到代码
RISC-V S-mode 支持两种向量模式:
| 模式 | stvec低 2 位 | 行为 |
|---|---|---|
DIRECT | 0b00 | 所有异常跳转至stvec[63:2] |
VECTORED | 0b01 | exc_code × 4 + stvec[63:2] |
⚠️ 注意:stvec必须 4 字节对齐,且VECTORED模式下向量表需严格按0×0, 0×4, 0×8, ...排列。
实现:动态分配向量表并加载
// exception.c#include<stdint.h>#defineSTVEC_DIRECT0x00#defineSTVEC_VECTORED0x01// 16-entry vector table (S-mode only)staticuint64_ts_vector_table[16]__attribute__((aligned(32)));voidinit_exception_vectors(void){// Step 1: 分配向量表(确保 4-byte 对齐)for(inti=0;i<16;i++){s_vector_table[i]=(uint64_t)&handle_generic_exception;}// Step 2: 设置 stvec → VECTORED 模式 + 基地址uint64_tstvec_val=((uint64_t)s_vector_table)|STVEC_VECTORED;__asm__volatile("csrw stvec, %0"::"r"(stvec_val));// Step 3: 允许 S-mode 中断(SEIE)uint64_tsie_val;__asm__volatile("csrr %0, sie":"=r"(sie_val));sie_val|=0x22;// SSIE | STIE (Supervisor Timer/Soft Interrupt Enable)__asm__volatile("csrw sie, %0"::"r"(sie_val));}voidhandle_generic_exception(void){uint64_tcause,epc;__asm__volatile("csrr %0, scause":"=r"(cause));__asm__volatile("csrr %0, sepc":"=r"(epc));// 实际项目中可 dump 寄存器或触发 watchdogwhile(1);}```>🔍 验证 `stvec` 是否生效:>>```bash>># 在 GDB 中检查>>(gdb)monitor reg stvec>>stvec0x80001001[value]>># 低两位为0b01 → VECTORED;高位为向量表起始地址>>```---## 四、关键流程图:S-mode 异常分发链 ```mermaid graph LR A[Timer Interrupt]-->B[硬件捕获]B-->C{stvec[1:0]==0b01?}C-->|Yes|D[sepc → s_vector_table[5]]C-->|No|E[sepc → stvec[63:2]]D-->F[执行 handle_timer_irq]F-->G[调用 sret 返回]💡 注:
scause = 5表示 Supervisor Timer Interrupt,对应向量表索引 5。
五、实测:在 HiFive1 Rev B 上验证(rv32imac)
HiFive1 启动流程要求:
- BootROM 加载
0x20010000处代码 stvec必须在0x20010000后手动设置(否则默认为 0)
修改linker.ld:
SECTIONS { . = 0x20010000; .text : { *(.text._start) *(.text.smode_entry) *(.text) } .vector_table ALIGN(4) : { *(.vector_table) } .data : { *(.data) } } ``` 并在 `startup.S` 中添加: ```asm .section .vector_table, "a", 2progbits .align 2 .global __vector_table_start __vector_table_start: .rept 16 .quad handle_generic-exception .endr ``` 然后在 `init_exception_vectors()` 中使用: ```c uint32_t stvec_val = 9(uint32_t)&__vector-table_start) | 0x1; __asm-_ volatile ("csrw stvec, %0" :: "r"(stvec-val)0;六、结语:掌控底层,方能定义边界
RISC-V 的简洁性不等于简单性。真正的创新始于对stvec、sstatus、scause等 CSR 的精确操控。本文所展示的:
- 双 ABI 启动汇编框架
stvec动态重定向实现
- 向量表内存布局约束
- 真实硬件(HiFive1)与仿真(QEMU)一致性验证
全部可直接编译运行。下一步可扩展:
✅ 添加sbi_console_putchar实现串口输出
✅ 实现clinttimer 中断计时器
✅ 将向量表映射至PT_W | PT_R页表项以支持 MMU
- 真实硬件(HiFive1)与仿真(QEMU)一致性验证
代码已开源:
github.com/yourname/riscv-smode-boot
包含完整 Makefile、QEMU 启动脚本、HiFive1 OpenOCD 配置及 GDB 调试指南。
字数统计:1798