来自未来的 RISC-V 解释器
摘要
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
一个嵌入在Rust trait系统中的Lisp解释器,支持在编译时进行递归函数、闭包和延续传递风格。
Rust 中的尾调用解释器 – Jimmy Ostler
Jimmy Ostler 深入探讨了 Rust 中的尾调用解释器,实现并基准测试了多种虚拟机分发技术,包括 switch 分发、子例程线程化和尾调用优化的机器。
突破 RISC-V 模拟的极限
这篇博客文章探讨了如何通过在非 RISC-V 机器上使用提前重编译器来加速 RISC-V 执行,该重编译器通过尾调用连接基本块,并利用 Clang 的 preserve_none 调用约定,作为 Axiom 的 OpenVM 项目的一部分。
用 Rust 重写
本文评估了2026年的‘Rewrite It In Rust’运动,讨论了现实世界中的性能提升、诸如新错误和平台支持等挑战,并提倡增量重写而非完全重写。
将OCaml运行时从C语言逐行翻译为Rust
该项目详细介绍了将OCaml运行时从C语言逐行翻译为Rust的过程,旨在提高安全性和性能。