来自未来的 RISC-V 解释器

Lobsters Hottest 工具

摘要

Rust 编写的 RISC-V 解释器的更新,支持模块化、no_std、编译时执行,并严格符合规范,同时利用了众多 Rust nightly 版本的特性。

<p><a href="https://lobste.rs/s/potdi9/risc_v_interpreter_from_future">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/09/01 23:49

# 来自未来的RISC-V解释器 来源:https://abundance.build/blog/2026-08-31-risc-v-interpreter-from-the-future/ 前一段时间我发布的RISC-V解释器(https://abundance.build/blog/2026-03-14-first-crates-on-crates.io)经历了大量改进,我认为现在它已达到可供更多人使用的状态。以下是一些能引起您兴趣的特性:完全模块化与泛型设计、无 panic、`no_std`(且零内存分配)、可在编译时运行(`const fn`)、严格遵循 RISC-V 规范(适用于区块链场景)、通过 RISC-V 架构认证测试(https://github.com/riscv/riscv-arch-test),同时在实现以上所有特性时保持了高性能。这一切的代价是什么?正如文章标题所示:使用了约30个 nightly Rust 特性——从高级 const 泛型到保证尾调用,以实现一些在当前稳定版 Rust 中无法达成的功能。希望其中大部分特性能在不远的将来进入稳定版。 以下细节对应 ab-riscv-interpreter(https://crates.io/crates/ab-riscv-interpreter)和 ab-riscv-primitives(https://crates.io/crates/ab-riscv-primitives)的 0.2 版本。 --- ## 完全模块化与泛型设计 RISC-V 规范本身是模块化的,由若干基础指令集变体和大量扩展组成。解释器的实现方式与之相同:基础指令集和每个扩展均独立实现,并能按照规范允许的任意方式组合。不仅如此,内存、寄存器文件甚至通用寄存器类型等元素也均为泛型设计。扩展相关的环境细节同样通过泛型处理,因此浮点数、向量等额外寄存器也能以模块化方式实现。 ### 指令定义与解码 指令定义与解码实际上是在独立的 crate 中实现的。如果您只需要符合规范的解码器而无需完整解释器,完全可以单独使用它。以下是一个简单扩展的指令解码示例: ```rust /// RISC-V Zicond指令(整数条件操作) #[instruction] #[derive(Debug, Clone, Copy)] #[derive_const(PartialEq, Eq)] pub enum ZicondInstruction { /// `czero.eqz rd, rs1, rs2` - 若 `rs2 == 0` 则将零移至 `rd`,否则移入 `rs1` CzeroEqz { rd: Reg, rs1: Reg, rs2: Reg }, /// `czero.nez rd, rs1, rs2` - 若 `rs2 != 0` 则将零移至 `rd`,否则移入 `rs1` CzeroNez { rd: Reg, rs1: Reg, rs2: Reg }, } #[instruction] const impl Instruction for ZicondInstruction where Reg: [const] Register, { type Reg = Reg; #[inline(always)] #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))] fn try_decode(instruction: u32) -> Option<Self> { let opcode = (instruction & 0b111_1111) as u8; let rd_bits = ((instruction >> 7) & 0x1f) as u8; let funct3 = ((instruction >> 12) & 0b111) as u8; let rs1_bits = ((instruction >> 15) & 0x1f) as u8; let rs2_bits = ((instruction >> 20) & 0x1f) as u8; let funct7 = ((instruction >> 25) & 0x7f) as u8; // 两条 Zicond 指令共享 opcode=0x33 (OP) 和 funct7=0x07 match (opcode, funct7) { (0b011_0011, 0b000_0111) => { let rd = Reg::from_bits(rd_bits)?; let rs1 = Reg::from_bits(rs1_bits)?; let rs2 = Reg::from_bits(rs2_bits)?; match funct3 { 0b101 => Some(Self::CzeroEqz { rd, rs1, rs2 }), 0b111 => Some(Self::CzeroNez { rd, rs1, rs2 }), _ => None, } } _ => None, } } #[inline(always)] fn alignment() -> u8 { align_of::<Self>() as u8 } #[inline(always)] fn size(&self) -> u8 { size_of::<Self>() as u8 } } ``` 如您所见,实现相对基础,基本符合阅读规范后的预期。为确保可组合性,存在一些轻微要求(例如仅通过 `Self::` 枚举实例化、避免使用 `return`),以便后续处理。 枚举定义上的 `#[instruction]` 还支持多种选项来指定依赖关系: ```rust #[instruction(inherit = [Rv32ZaamoInstruction])] #[derive(Debug, Clone, Copy)] #[derive_const(PartialEq, Eq)] #[rustfmt::skip] pub enum Rv32ZabhaInstruction { AmoswapB { rd: Reg, rs1: Reg, rs2: Reg, aq: bool, rl: bool }, // ... 其他指令 AmomaxuH { rd: Reg, rs1: Reg, rs2: Reg, aq: bool, rl: bool }, /// 字节比较交换。仅在实现 `Zacas` 时存在。 #[instruction(if = [Rv32ZacasInstruction])] AmocasB { rd: Reg, rs1: Reg, rs2: Reg, aq: bool, rl: bool }, /// 半字比较交换。仅在实现 `Zacas` 时存在。 #[instruction(if = [Rv32ZacasInstruction])] AmocasH { rd: Reg, rs1: Reg, rs2: Reg, aq: bool, rl: bool }, } ``` 如您所见,两者都通过简单的继承/依赖关系表达,并能根据其他指令的存在性来约束指令。尚未实现的是指令冲突检测,但待首个此类扩展(很可能是 Zcd)实现后即可添加。 组合解码本质上是各 `try_decode()` 函数的串联。此处引入了多种新类型以确保无效指令不会被解码,并且可以在寄存器类型上指定额外约束以维护正确的不变量: ```rust #[instruction] const impl Instruction for Rv32ZcmpOnlyInstruction where Reg: [const] ZcmpRegister, { type Reg = Reg; // ... } ``` 您也可以排除不希望支持的指令,使其不被解码。例如,以下代码排除了 `ecall` 指令,同时保留其他所有指令: ```rust #[instruction( ignore = [Ecall], inherit = [ Rv64ZcaInstruction, Rv64ZcbInstruction, Rv64ZcmpInstruction, Rv64Instruction, Rv64MInstruction, Rv64BInstruction, Rv64ZbcInstruction, Rv64ZknInstruction, ZicondInstruction, ], )] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ContractInstruction {} ``` 还支持重排指令,详情请参阅宏定义。 ### 有状态宏 您可能会惊讶于依赖关系以这种方式表达,然后解码体被串联。这需要在不同调用间共享数据的有状态宏,而 Rust 并不直接支持此功能。解决方案是通过构建脚本扫描所有文件并生成实现,而过程宏仅通过 `include!()`(https://doc.rust-lang.org/core/macro.include.html)替换原始代码: ```rust use ab_riscv_macros::process_instruction_macros; use std::error::Error; fn main() -> Result<(), Box<dyn Error>> { process_instruction_macros()?; Ok(()) } ``` `process_instruction_macros()`(https://docs.rs/ab-riscv-macros/0.1.1/ab_riscv_macros/fn.process_instruction_macros.html)维护 crate 中所有指令的信息,并从依赖项中拉取 crate 元数据(https://doc.rust-lang.org/cargo/reference/build-scripts.html#the-links-manifest-key),因此能够解析指令间的依赖并生成必要实现。 一个复杂之处在于,如您所见,使用了如 const trait 语法等 nightly 特性,而 `syn` 尚不支持它们。解决方案是一个小技巧(https://docs.rs/ab-riscv-macros-common/0.1.0/src/ab_riscv_macros_common/code_utils.rs.html),将 nightly 语法转换为有效的稳定版 Rust 语法,在处理完成后又转换回原格式。 ### 指令执行 指令执行也需要遵循特定要求,并标注 `#[instruction_execution]`(https://docs.rs/ab-riscv-macros/0.1.1/ab_riscv_macros/attr.instruction_execution.html)宏,但除此之外也符合预期: ```rust #[instruction_execution] const impl ExecutableInstruction for Rv32ZbsInstruction where Reg: [const] Register, Regs: [const] RegisterFile, { #[inline(always)] #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))] fn execute( self, Rs1Rs2OperandValues { rs1_value, rs2_value }: Rs1Rs2OperandValues<<Self::Reg as Register>::Type>, _regs: &mut Regs, _env: &mut Env, _memory: &mut Memory, _program_counter: &mut PC, ) -> ExecutionResult<Self::Reg> { match self { Self::Bset { rd, rs1: _, rs2: _ } => { let index = rs2_value & 0x1f; let result = rs1_value | (1u32 << index); ExecutionResult::Continue { rd, value: result } } Self::Bseti { rd, rs1: _, shamt } => { let index = shamt; let result = rs1_value | (1u32 << index); ExecutionResult::Continue { rd, value: result } } Self::Bclr { rd, rs1: _, rs2: _ } => { let index = rs2_value & 0x1f; let result = rs1_value & !(1u32 << index); ExecutionResult::Continue { rd, value: result } } Self::Bclri { rd, rs1: _, shamt } => { let index = shamt; let result = rs1_value & !(1u32 << index); ExecutionResult::Continue { rd, value: result } } Self::Binv { rd, rs1: _, rs2: _ } => { let index = rs2_value & 0x1f; let result = rs1_value ^ (1u32 << index); ExecutionResult::Continue { rd, value: result } } Self::Binvi { rd, rs1: _, shamt } => { let index = shamt; let result = rs1_value ^ (1u32 << index); ExecutionResult::Continue { rd, value: result } } Self::Bext { rd, rs1: _, rs2: _ } => { let index = rs2_value & 0x1f; let result = (rs1_value >> index) & 1; ExecutionResult::Continue { rd, value: result } } Self::Bexti { rd, rs1: _, shamt } => { let index = shamt; let result = (rs1_value >> index) & 1; ExecutionResult::Continue { rd, value: result } } } } } ``` API 以这种方式设计有其充分理由,而宏在底层生成的内容更为有趣! `fn execute()` 以单个 `match` 编写,便于解析。随后会生成大量代码(上述实现约生成 1900 行)。首先,为每条指令提取独立函数,形式如下: ```rust #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))] #[inline(always)] const fn execute_rv32_zbs_instruction_bset<Reg, Regs, Env, Memory, PC>( rd: Reg, rs1_value: <Self as Instruction>::Reg as Register>::Type, rs2_value: <Self as Instruction>::Reg as Register>::Type, regs: &mut Regs, env: &mut Env, memory: &mut Memory, program_counter: &mut PC, ) -> ExecutionResult<<Self as Instruction>::Reg> where Reg: [const] Register, Regs: [const] RegisterFile, { { let _ = rd; let _ = rs1_value; let _ = rs2_value; let _ = regs; let _ = env; let _ = memory; let _ = program_counter; } { let index = rs2_value & 0x1f; let result = rs1_value | (1u32 << index); ExecutionResult::Continue { rd, value: result, } } } ``` 随后,原始 `match` 分支被替换为对该函数的调用。最终,所有依赖项的所有 `match` 分支被合并成一个大型 `match` 语句用于执行。 这产生了一个合理快速且紧凑的实现,但远未达到性能峰值。为实现峰值性能,还会生成带有平台特定 ABI 的间接线程化实现,总体结构如下(x86-64 平台): ```rust // ... impl ThreadedExecutableInstruction for Rv32ZbsInstruction where Reg: Register, Regs: RegisterFile, PC: InstructionFetcher<Memory>, { #[inline(always)] fn execute_threaded( instruction_fetcher: PC, regs: &mut Regs, env: Env, memory: &mut Memory, ) -> ThreadedExecutionResult<Self> { if !OpaqueThreadedExecutionResult::< Rv32ZbsInstruction, >::platform_supported() { ::core::hint::cold_path(); return ThreadedExecutionResult::failed( instruction_fetcher.get_pc(), ExecutionError::UnsupportedPlatform, ); } unsafe { execute_rv32_zbs_instruction_threaded::<Reg, Regs, Env, Memory, PC>( instruction_fetcher, regs, env, memory, ) } } } ``` 底层细节示例: ```rust // ... #[rustc_align(64)] #[cfg_attr(any(not(miri), target_feature = "avx"), target_feature(enable = "avx"))] unsafe extern "sysv64" fn execute_rv32_zbs_instruction_bset_threaded<Reg, Regs, Env, Memory, PC>( instruction: Rv32ZbsInstruction, mut instruction_fetcher: PC, regs: &mut Regs, mut env: Env, memory: &mut Memory, ) -> OpaqueThreadedExecutionResult<Rv32ZbsInstruction> where Reg: Register, Regs: RegisterFile, PC: InstructionFetcher<Memory>, { let Rs1Rs2Operands { rs1, rs2 } = instruction.get_rs1_rs2_operands(); let rs1_value = regs.read(rs1); let rs2_value = regs.read(rs2); let Rv32ZbsInstruction::Bset { rd, rs1: _, rs2: _ } = instruction else { unsafe { ::core::hint::unreachable_unchecked(); } }; unsafe { instruction_fetcher.advance(Instruction::size(&instruction)); } let execution_result = execute_rv32_zbs_instruction_bset::<Reg, Regs, Env, Memory, PC>( rd, rs1_value, rs2_value, regs, &mut env, memory, &mut instruction_fetcher, ); let control_flow = match execution_result { ExecutionResult::Continue { rd, value } => { regs.write(rd, value); Ok(::core::ops::ControlFlow::Continue(())) } ExecutionResult::ContinueNoWrite => Ok(::core::ops::ControlFlow::Continue(())), ExecutionResult::Branch { offset } => { if unsafe { instruction_fetcher .try_set_pc_relative(Instruction::size(&instruction), offset) } { Ok(::core::ops::ControlFlow::Continue(())) } else { unsafe { become rv32_zbs_instruction_threaded_branch_failed::<Reg, Regs, Env, Memory, PC>( instruction, instruction_fetcher, regs, env, memory, ) } } } ExecutionResult::Jump { target } => instruction_fetcher.set_pc(memory, target), ExecutionResult::Break => { ::core::hint::cold_path(); return unsafe { OpaqueThreadedExecutionResult::new( ThreadedExecutionResult::stopped(instruction_fetcher.get_pc()), ) }; } ExecutionResult::Err(error) => { ::core::hint::cold_path(); return unsafe { OpaqueThreadedExecutionResult::new( ThreadedExecutionResult::failed(instruction_fetcher.get_pc(), error), ) }; } }; match control_flow { Ok(::core::ops::ControlFlow::Continue(())) => {} Ok(::core::ops::ControlFlow::Break(())) => { ::core::hint::cold_path(); return unsafe { OpaqueThreadedExecutionResult::new( ThreadedExecutionResult::stopped(instruction_fetcher.get_pc()), ) }; } Err(error) => { ::core::hint::cold_path(); return unsafe { OpaqueThreadedExecutionResult::new( ThreadedExecutionResult::failed(instruction_fetcher.get_pc(), error), ) }; } } let (instruction, handler) = match dispatch_rv32_zbs_instruction::<Reg, Regs, Env, Memory, PC>( &mut instruction_fetcher, memory, ) { Rv32ZbsInstructionThreadedDispatchResult::Next { instruction, handler } => { (instruction, handler) } Rv32ZbsInstructionThreadedDispatchResult::Break => { ::core::hint::cold_path(); return unsafe { OpaqueThreadedExecutionResult::new( ThreadedExecutionResult::stopped(instruction_fetcher.get_pc()), ) }; } Rv32ZbsInstructionThreadedDispatchResult::Err(error) => { ::core::hint::cold_path(); return unsafe { OpaqueThreadedExecutionResult::new( ThreadedExecutionResult::failed(instruction_fetcher.get_pc(), error), ) }; } }; unsafe { become handler(instruction, instruction_fetcher, regs, env, memory) } } // ... #[inline(always)] fn dispatch_rv32_zbs_instruction<Reg, Regs, Env, Memory, PC>( instruction_fetcher: &mut PC, memory: &Memory, ) -> Rv32ZbsInstructionThreadedDispatchResult< Rv32ZbsInstruction, unsafe extern "sysv64" fn( Rv32ZbsInstruction, PC, &mut Regs, Env, &mut Memory, ) -> OpaqueThreadedExecutionResult<Rv32ZbsInstruction>, > where Reg: Register, Regs: RegisterFile, PC: InstructionFetcher<Memory>, { let instruction = loop { match instruction_fetcher.peek_instruction(memory) { FetchInstructionResult::Instruction(instruction) => { break instruction; } FetchInstructionResult::Continue => { ::core::hint::cold_path(); } FetchInstructionResult::Break => { ::core::hint::cold_path(); return Rv32ZbsInstructionThreadedDispatchResult::Break; } FetchInstructionResult::Err(error) => { ::core::hint::cold_path(); return Rv32ZbsInstructionThreadedDispatchResult::Err(error); } } }; let handler = match instruction { Rv32ZbsInstruction::Bset { .. } => { execute_rv32_zbs_instruction_bset_threaded::<Reg, Regs, Env, Memory, PC> } // ... 其他指令 }; Rv32ZbsInstructionThreadedDispatchResult::Next { instruction, handler } } ```

相似文章

Rust类型系统中的Lisp

Hacker News Top

一个嵌入在Rust trait系统中的Lisp解释器,支持在编译时进行递归函数、闭包和延续传递风格。

Rust 中的尾调用解释器 – Jimmy Ostler

Hacker News Top

Jimmy Ostler 深入探讨了 Rust 中的尾调用解释器,实现并基准测试了多种虚拟机分发技术,包括 switch 分发、子例程线程化和尾调用优化的机器。

突破 RISC-V 模拟的极限

Hacker News Top

这篇博客文章探讨了如何通过在非 RISC-V 机器上使用提前重编译器来加速 RISC-V 执行,该重编译器通过尾调用连接基本块,并利用 Clang 的 preserve_none 调用约定,作为 Axiom 的 OpenVM 项目的一部分。

用 Rust 重写

Hacker News Top

本文评估了2026年的‘Rewrite It In Rust’运动,讨论了现实世界中的性能提升、诸如新错误和平台支持等挑战,并提倡增量重写而非完全重写。