Skip to content
ATRAPSLLCPublic
forked from swenson/disc-v

About

RISC-V disassembler in Rust.

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Latest commit

 

History

43 Commits

Folders and files

Repository files navigation

disc-v

CI crates.io docs.rs

A RISC-V disassembler in Rust. It is no_std, does not allocate, and its output follows GNU objdump's (see Output format).

It is meant to be embedded in various Rust tools: debuggers, emulators, etc.

use disc_v::{disassemble, Isa};

let code = [0x13, 0x05, 0x10, 0x00, 0x82, 0x80];
for ins in disassemble(Isa::Rv64, 0x1000, &code) {
    println!("{:x}: {ins}", ins.pc());
}
// 1000: li a0,1
// 1004: ret

Single instructions can be decoded from bytes with decode_bytes, or from an integer with decode. An Instruction has the mnemonic, operands, address, length and raw encoding, and without_aliases() shows it as encoded (c.li a0,1 rather than li a0,1), like objdump -M no-aliases.

The minimum supported Rust version is 1.85.1.

Choosing extensions

decode, decode_bytes and disassemble decode every supported extension that does not conflict with another. To decode for a particular target, so that other extensions' instructions are shown as .insn (and their aliases are not used), use a Decoder:

use disc_v::{Decoder, Extension, Isa};

// Presets.
let dec = Decoder::RVA23U64;
let dec = Decoder::RV64GC;

// From an ISA string, as passed to compilers with -march.
let dec = Decoder::from_march("rv32imac_zicsr_zba_zbb").unwrap();

// By adding and removing extensions. Implied extensions follow: D implies F,
// and removing F removes D.
let dec = Decoder::RV64GC.with(Extension::Zicond).unwrap().without(Extension::D);
let dec = Decoder::with_only(Isa::Rv32, [Extension::M, Extension::C]).unwrap();

for ins in dec.disassemble(0x1000, &[0x05, 0x45]) {
    println!("{ins}");
}

Some extensions give the same encodings different meanings, so they cannot be enabled together; the defaults leave out the one that conflicts. For example, Zcmp and Zcmt (compressed push/pop and table jumps, common in size-optimized embedded code) reuse the encodings of Zcd, the compressed double-precision loads and stores that C includes when D is enabled. with returns an error for a conflicting extension, and try_with leaves the decoder unchanged instead:

use disc_v::{Decoder, Extension};

assert!(Decoder::RV32GC.with(Extension::Zcmp).is_err());
assert_eq!(Decoder::RV32GC.try_with(Extension::Zcmp), Decoder::RV32GC);

// Removing Zcd keeps the other compressed instructions (Zca, Zcf).
let dec = Decoder::RV32GC.without(Extension::Zcd).with(Extension::Zcmp).unwrap();
assert_eq!(dec.decode(0, 0xb862).to_string(), "cm.push {ra,s0-s1},-16");

let dec = Decoder::from_march("rv32imac_zcmp_zcmt").unwrap();

RV32E and RV64E, which have only registers x0-x15, are the E extension: Decoder::from_march("rv32emc"), or with(Extension::E). Instructions that use x16-x31 are then shown as .insn.

Zfinx, Zdinx, Zhinx and Zhinxmin keep floating-point values in the integer registers. They use F's encodings, so they conflict with F: Decoder::from_march("rv32imc_zicsr_zdinx") decodes fadd.d a0,a0,a2 rather than fadd.d fa0,fa0,fa2.

Vendor extensions are never decoded by default, since each vendor's cores use the custom opcode spaces differently. Enable them by name, or with the Extensions::XTHEAD and Extensions::XCV bundles. T-Head's and CORE-V's extensions mostly conflict with each other.

use disc_v::{Decoder, Extensions, Isa};

let dec = Decoder::from_march("rv64gc_xtheadba_xtheadbb").unwrap();
assert_eq!(dec.decode(0, 0x02c5950b).to_string(), "th.addsl a0,a1,a2,1");

let dec = Decoder::with_only(Isa::Rv32, Extensions::GC | Extensions::XCV).unwrap();
assert_eq!(dec.decode(0, 0x00c5857b).to_string(), "cv.add.h a0,a1,a2");

Cargo features

The vector and vendor extensions' tables are behind Cargo features, all on by default:

Feature Extensions
vector V, Zvbb, Zvbc, Zvkg, Zvkned, Zvknha, Zvknhb, Zvksed, Zvksh, Zvfbfmin, Zvfbfwma
vendor-thead XThead*
vendor-corev XCV*

The decoder chooses extensions at run time, so the linker keeps every table it is built with. A program that does not need these extensions can leave them out:

disc-v = { version = "0.1", default-features = false }

An RV32IMC program that disassembles with Decoder::with_only is about 73 KB (code and read-only data) without these features, and 130 KB with them, built for riscv32imc-unknown-none-elf with opt-level = "s" and LTO. Without its feature, an extension's instructions are shown as .insn.

Output format

The text is what objdump -D -b binary prints for the same bytes: the same mnemonics, aliases (li, ret, csrr, ...) and operand syntax. Immediates are decimal (li a0,-1107), while shift amounts and upper immediates are hex (slli a0,a0,0x3, lui a0,0xbad).

Branch and jump targets are absolute addresses, always written with 0x: beq a0,a1,0x1008. When objdump disassembles an ELF file it instead writes the target as bare hex followed by a symbol, as in beq a0,a1,1008 <foo+0x8>. disc-v has no symbol information, so it uses objdump's raw-binary form, which is unambiguous.

A few encodings are decoded differently from objdump where binutils and the ISA specification disagree; see Differences from the specification.

Supported instructions

  • RV32I, RV64I and RV128I; RV32E and RV64E (as the E extension)
  • M, A, F, D, Q and C
  • Zicsr and Zifencei
  • Zba, Zbb, Zbc and Zbs
  • Zicond
  • Zawrs
  • Zicbom, Zicboz and Zicbop; Zihintntl and Zihintpause
  • Zimop and Zcmop, with the Zicfiss and Zicfilp instructions
  • Zcb
  • Zfh and Zfhmin
  • Zfinx, Zdinx, Zhinx and Zhinxmin, which conflict with F and so are not decoded by default
  • Zfa
  • V (vectors)
  • Zvbb and Zvbc; Zvkg, Zvkned, Zvknha, Zvknhb, Zvksed and Zvksh
  • H (hypervisor)
  • Zalasr, Zacas and Zabha
  • Zfbfmin, Zvfbfmin and Zvfbfwma (BFloat16)
  • Zbkb, Zbkc and Zbkx; Zknd, Zkne, Zknh, Zksed and Zksh (scalar cryptography)
  • Zca, Zcf and Zcd (the parts of C); Zcmp and Zcmt, which conflict with Zcd and so are not decoded by default
  • Zilsd (RV32 load and store pairs); Zclsd, which conflicts with Zcf and so is not decoded by default
  • ecall, ebreak, sret, mret, dret, wfi and sfence.vma; Svinval, Smrnmi (mnret) and Ssctr (sctrclr)
  • Vendor extensions, which are not decoded by default (see also Cargo features):
    • T-Head: XTheadBa, XTheadBb, XTheadBs, XTheadCmo, XTheadCondMov, XTheadFMemIdx, XTheadFmv, XTheadInt, XTheadMac, XTheadMemIdx, XTheadMemPair and XTheadSync
    • CORE-V (RV32): XCValu, XCVbi, XCVbitmanip, XCVelw, XCVmac, XCVmem and XCVsimd

48- and 64-bit encodings are recognized (so disassembly stays in sync) but shown as .insn, as objdump does.

Testing

Besides unit tests, the decoder is checked against two references:

  • tests/riscv_opcodes.rs checks it against the encodings in riscv-opcodes, in both directions: every listed instruction decodes to its name, and every encoding disc-v accepts is listed. The data is in tests/data/riscv-opcodes.txt, generated by scripts/gen-riscv-opcodes.py.
  • tests/objdump.rs compares the output with GNU objdump (after rewriting objdump's 1008 <foo+0x8> targets as 0x1008) for every compressed encoding, random 32-bit encodings, random encodings of every instruction in riscv-opcodes, and random encodings in the custom opcode spaces, where the vendor extensions are. It needs a RISC-V binutils (riscv64-elf-objdump, riscv64-unknown-elf-objdump or riscv64-linux-gnu-objdump) and is skipped if none is found; set DISC_V_REQUIRE_BINUTILS=1 to make that an error, as CI does.

panic-check/ checks that disc-v cannot panic: a no_std program that calls every public function on inputs the compiler cannot see, whose panic handler calls a function that does not exist, so that it only links if no panic is reachable. scripts/panic-check.sh builds it for RISC-V and Arm at optimization levels 2, 3, s and z, as CI does with the stable compiler and the minimum supported version, and lists the source of any panics it finds. (Debug builds can still panic, such as on a debug assertion, as can MarchError's Debug output, which uses core's Debug for str; see panic-check/README.md.)

CSR names come from riscv-opcodes, via scripts/gen-csr-names.py, and so do the vector instruction tables in src/opcodes/v.rs, via scripts/gen-vector-opcodes.py, which encodes objdump's operand order. The generators format their output with rustfmt, so generated code is formatted and linted like the rest of the crate.

Differences from the specification

Where binutils and the ISA specification disagree on whether an encoding is valid, disc-v follows the specification, with the exceptions below. The differences from binutils, such as RV32 shift amounts of 32 or more (reserved, but decoded by objdump), are listed in known_difference in tests/objdump.rs. Where riscv-opcodes disagrees with the specification, disc-v follows the specification, and the differences are listed in NOT_IN_SPEC in tests/riscv_opcodes.rs.

riscv-opcodes does not describe vendor extensions. disc-v's T-Head tables follow the T-Head extension specification, and its CORE-V tables follow LLVM and the CV32E40P user manual, with objdump as the test reference. Where they disagree with binutils, disc-v follows the specification: the CORE-V extensions are only for RV32, and cv.elw is encoded with funct3 3 (binutils uses 6, the encoding of cv.beqimm).

disc-v decodes a few encodings that the specification reserves, where they have plausible uses:

  • aes64ks1i with a round number above 10. The AES key schedule has uses besides AES itself, and objdump decodes these too.

Origins

disc-v is built on the disassembler from the Caliptra MCU emulator (emulator/app/src/dis.rs, imported at commit 20f7fcff1b51d69b03d0b352685fc7360536ad9a). The Caliptra authors ported it to Rust and added the bit-manipulation extensions (Zba, Zbb, Zbc, Zbs).

That code is itself a port of Michael Clark and SiFive's C riscv-disassembler.

The name is a nod to Oxide Computer's disc-v, an earlier Rust port of the same C disassembler by Wladimir J. van der Laan and Adam H. Leventhal. That repository has no license, so this crate does not use any of its code.

License

Copyright (c) 2026 Christopher Swenson.

disc-v is licensed under the Apache License, Version 2.0 (LICENSE).

It is derived from Michael Clark and SiFive's riscv-disassembler, which is MIT licensed (Copyright (c) 2016-2017 Michael Clark, (c) 2017-2018 SiFive, Inc.), by way of the Caliptra port, which is Apache-2.0 licensed. The original MIT copyright and permission notice is kept in NOTICE, and source files derived from that code say so in their headers.

The test data and CSR names derived from riscv-opcodes are Copyright (c) 2022 RISC-V International, under the BSD-3-Clause license (tests/data/LICENSE-riscv-opcodes).

About

RISC-V disassembler in Rust.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages