Skip to content

Commit 3477f9a

Browse files
committed
Add initial VM skeleton
1 parent 0903e9d commit 3477f9a

14 files changed

+356
-0
lines changed

Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ version = "0.1.0"
44
authors = ["Nervos Core Dev <dev@nervos.org>"]
55

66
[dependencies]
7+
byteorder = "1"
8+
goblin = "0.0.17"
9+

Makefile

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
test:
2+
RUSTFLAGS="--cfg ckb_test" cargo test --all -- --nocapture
3+
4+
fmt:
5+
cargo fmt --all -- --check
6+
7+
clippy:
8+
cargo clippy --all -- -D warnings -D clone_on_ref_ptr -D unused_extern_crates -D enum_glob_use
9+
10+
.PHONY: test clippy fmt

clippy.toml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
single-char-binding-names-threshold = 4

rustfmt.toml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
max_width = 100
2+
tab_spaces = 4
3+
reorder_imports = true
4+
reorder_modules = true
5+
use_try_shorthand = true

src/decoder.rs

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use super::instructions::{rv32i, Instruction, InstructionFactory};
2+
use super::machine::Machine;
3+
use super::Error;
4+
5+
pub struct Decoder {
6+
factories: Vec<InstructionFactory>,
7+
}
8+
9+
impl Decoder {
10+
pub fn new() -> Decoder {
11+
Decoder {
12+
factories: Vec::new(),
13+
}
14+
}
15+
16+
pub fn add_instruction_factory(&mut self, factory: InstructionFactory) {
17+
self.factories.push(factory);
18+
}
19+
20+
pub fn decode(&self, machine: &Machine) -> Result<Instruction, Error> {
21+
let mut instruction: u32 = u32::from(machine.memory.load16(machine.pc as usize)?);
22+
if instruction & 0x3 == 0x3 {
23+
instruction |= u32::from(machine.memory.load16(machine.pc as usize + 2)?) << 16;
24+
}
25+
for factory in &self.factories {
26+
if let Some(instruction) = factory(instruction) {
27+
return Ok(instruction);
28+
}
29+
}
30+
Err(Error::InvalidInstruction(instruction))
31+
}
32+
}
33+
34+
pub fn build_rv32imac_decoder() -> Decoder {
35+
let mut decoder = Decoder::new();
36+
decoder.add_instruction_factory(rv32i::factory);
37+
decoder
38+
}

src/instructions/mod.rs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
mod utils;
2+
3+
pub mod rv32i;
4+
5+
use super::machine::Machine;
6+
use super::Error;
7+
use std::fmt::{self, Display};
8+
9+
#[derive(Debug)]
10+
pub enum Instruction {
11+
RV32I(rv32i::Instruction),
12+
}
13+
14+
impl Instruction {
15+
pub fn execute(&self, machine: &mut Machine) -> Result<(), Error> {
16+
match self {
17+
Instruction::RV32I(instruction) => instruction.execute(machine),
18+
}
19+
}
20+
}
21+
22+
impl Display for Instruction {
23+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
24+
// TODO: change to real disasm feature instead of simply delegating
25+
// to std::fmt::Debug
26+
write!(f, "{:?}", self)
27+
}
28+
}
29+
30+
pub type InstructionFactory = fn(instruction: u32) -> Option<Instruction>;

src/instructions/rv32i.rs

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use super::super::machine::Machine;
2+
use super::super::Error;
3+
use super::utils::{extract_opcode, extract_rd, extract_utype_immediate, update_register};
4+
use super::{Instruction as GenericInstruction, Instruction::RV32I};
5+
6+
#[derive(Debug)]
7+
pub enum Instruction {
8+
AUIPC(usize, u32),
9+
}
10+
11+
impl Instruction {
12+
pub fn execute(&self, machine: &mut Machine) -> Result<(), Error> {
13+
match self {
14+
Instruction::AUIPC(rd, imm) => {
15+
let value = machine.pc + imm;
16+
machine.pc += 4;
17+
update_register(machine, *rd, value);
18+
}
19+
}
20+
Ok(())
21+
}
22+
}
23+
24+
pub fn factory(instruction: u32) -> Option<GenericInstruction> {
25+
match extract_opcode(instruction) {
26+
0x17 => Some(RV32I(Instruction::AUIPC(
27+
extract_rd(instruction),
28+
extract_utype_immediate(instruction),
29+
))),
30+
_ => None,
31+
}
32+
}

src/instructions/utils.rs

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use super::super::machine::Machine;
2+
use RISCV_GENERAL_REGISTER_NUMBER;
3+
4+
#[inline(always)]
5+
pub fn extract_opcode(instruction: u32) -> u32 {
6+
instruction & 0x7F
7+
}
8+
9+
#[inline(always)]
10+
pub fn extract_rd(instruction: u32) -> usize {
11+
((instruction >> 7) & 0x1F) as usize
12+
}
13+
14+
#[inline(always)]
15+
pub fn extract_utype_immediate(instruction: u32) -> u32 {
16+
instruction & 0xFFFF_F000
17+
}
18+
19+
pub fn update_register(machine: &mut Machine, register_index: usize, value: u32) {
20+
let register_index = register_index % RISCV_GENERAL_REGISTER_NUMBER;
21+
if register_index > 0 {
22+
machine.registers[register_index] = value;
23+
}
24+
}

src/lib.rs

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
extern crate byteorder;
2+
extern crate goblin;
3+
4+
mod decoder;
5+
mod instructions;
6+
mod machine;
7+
mod memory;
8+
9+
use machine::Machine;
10+
use std::io::Error as IOError;
11+
12+
pub const RISCV_PAGESIZE: usize = 1 << 12;
13+
pub const RISCV_GENERAL_REGISTER_NUMBER: usize = 32;
14+
// 128 MB
15+
pub const RISCV_MAX_MEMORY: usize = 128 << 20;
16+
17+
#[derive(Debug)]
18+
pub enum Error {
19+
ParseError,
20+
Alignment,
21+
OutOfBound,
22+
InvalidInstruction(u32),
23+
IO(IOError),
24+
Unimplemented,
25+
}
26+
27+
pub fn run(program: &[u8], args: &[String]) -> Result<u8, Error> {
28+
let mut machine = Machine::default();
29+
machine.load(program)?;
30+
machine.run(args)
31+
}

src/machine.rs

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use super::decoder::{build_rv32imac_decoder, Decoder};
2+
use super::memory::Memory;
3+
use super::{Error, RISCV_GENERAL_REGISTER_NUMBER, RISCV_MAX_MEMORY};
4+
use goblin::elf::program_header::PT_LOAD;
5+
use goblin::elf::Elf;
6+
7+
pub struct Machine {
8+
// TODO: while CKB doesn't need it, other environment could benefit from
9+
// parameterized register size.
10+
pub registers: [u32; RISCV_GENERAL_REGISTER_NUMBER],
11+
pub pc: u32,
12+
pub memory: Box<Memory>,
13+
14+
decoder: Decoder,
15+
running: bool,
16+
exit_code: u8,
17+
}
18+
19+
impl Machine {
20+
pub fn default() -> Machine {
21+
// While a real machine might use whatever random data left in the memory(or
22+
// random scrubbed data for security), we are initializing everything to 0 here
23+
// for deterministic behavior.
24+
Machine {
25+
registers: [0; RISCV_GENERAL_REGISTER_NUMBER],
26+
pc: 0,
27+
// TODO: add real MMU object with proper permission checks, right now
28+
// a flat buffer is enough for experimental use.
29+
memory: Box::new(vec![0; RISCV_MAX_MEMORY]),
30+
decoder: build_rv32imac_decoder(),
31+
running: false,
32+
exit_code: 0,
33+
}
34+
}
35+
36+
pub fn load(&mut self, program: &[u8]) -> Result<(), Error> {
37+
let elf = Elf::parse(program).map_err(|_e| Error::ParseError)?;
38+
for program_header in &elf.program_headers {
39+
if program_header.p_type == PT_LOAD {
40+
// TODO: page alignment
41+
self.memory.mmap(
42+
program_header.p_vaddr as usize,
43+
program_header.p_filesz as usize,
44+
program,
45+
program_header.p_offset as usize,
46+
)?;
47+
}
48+
}
49+
self.pc = elf.header.e_entry as u32;
50+
Ok(())
51+
}
52+
53+
pub fn run(&mut self, _args: &[String]) -> Result<u8, Error> {
54+
self.running = true;
55+
while self.running {
56+
let instruction = self.decoder.decode(self)?;
57+
instruction.execute(self)?;
58+
}
59+
Ok(self.exit_code)
60+
}
61+
}

src/memory.rs

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use super::Error;
2+
3+
use byteorder::{LittleEndian, ReadBytesExt};
4+
use std::io::{Cursor, Seek, SeekFrom};
5+
6+
pub trait Memory {
7+
fn mmap(
8+
&mut self,
9+
addr: usize,
10+
size: usize,
11+
source: &[u8],
12+
offset: usize,
13+
) -> Result<usize, Error>;
14+
15+
// TODO: maybe parameterize those?
16+
fn load16(&self, addr: usize) -> Result<u16, Error>;
17+
fn load32(&self, addr: usize) -> Result<u32, Error>;
18+
}
19+
20+
// Here we build a flat memory based Memory object as a starting point for fast
21+
// iteration. Later we might want to re-evaluate this to see if we need a real
22+
// MMU system.
23+
// Current system is lacking the following features needed in a real production
24+
// system:
25+
//
26+
// * mmap should work on pages, not arbitrary memory segments
27+
// * disallow unaligned address on page boundary
28+
// * read/write/execute permission checking
29+
impl Memory for Vec<u8> {
30+
fn mmap(
31+
&mut self,
32+
addr: usize,
33+
size: usize,
34+
source: &[u8],
35+
offset: usize,
36+
) -> Result<usize, Error> {
37+
if addr + size > self.len() || offset + size > source.len() {
38+
return Err(Error::OutOfBound);
39+
}
40+
let (_, right) = self.split_at_mut(addr);
41+
let (slice, _) = right.split_at_mut(size);
42+
slice.copy_from_slice(&source[offset..offset + size]);
43+
Ok(addr)
44+
}
45+
46+
fn load16(&self, addr: usize) -> Result<u16, Error> {
47+
if addr + 4 > self.len() {
48+
return Err(Error::OutOfBound);
49+
}
50+
let mut reader = Cursor::new(&self);
51+
reader
52+
.seek(SeekFrom::Start(addr as u64))
53+
.map_err(Error::IO)?;
54+
// NOTE: Base RISC-V ISA is defined as a little-endian memory system.
55+
reader.read_u16::<LittleEndian>().map_err(Error::IO)
56+
}
57+
58+
fn load32(&self, addr: usize) -> Result<u32, Error> {
59+
if addr + 4 > self.len() {
60+
return Err(Error::OutOfBound);
61+
}
62+
let mut reader = Cursor::new(&self);
63+
reader
64+
.seek(SeekFrom::Start(addr as u64))
65+
.map_err(Error::IO)?;
66+
// NOTE: Base RISC-V ISA is defined as a little-endian memory system.
67+
reader.read_u32::<LittleEndian>().map_err(Error::IO)
68+
}
69+
}

tests/programs/minimal

4.23 KB
Binary file not shown.

tests/programs/minimal.c

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* First, compile riscv-gnu-toolchain with `--with-arch=rv32imac --with-abi=ilp32`,
3+
* then compile current file with riscv32-unknown-elf-gcc -o minimal minimal.c
4+
*/
5+
int main(int argc, char* argv[])
6+
{
7+
if (argc == 1) {
8+
return 1;
9+
}
10+
if (argv[1][0] == 'a') {
11+
return 2;
12+
}
13+
return 0;
14+
}

tests/test_minimal.rs

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
extern crate ckb_riscv;
2+
3+
use ckb_riscv::run;
4+
use std::fs::File;
5+
use std::io::Read;
6+
7+
#[test]
8+
pub fn test_minimal_with_no_args() {
9+
let mut file = File::open("tests/programs/minimal").unwrap();
10+
let mut buffer = Vec::new();
11+
file.read_to_end(&mut buffer).unwrap();
12+
13+
let result = run(&buffer, &Vec::new());
14+
assert!(result.is_ok());
15+
assert_eq!(result.unwrap(), 1);
16+
}
17+
18+
#[test]
19+
pub fn test_minimal_with_a() {
20+
let mut file = File::open("tests/programs/minimal").unwrap();
21+
let mut buffer = Vec::new();
22+
file.read_to_end(&mut buffer).unwrap();
23+
24+
let result = run(&buffer, &vec!["a".to_string()]);
25+
assert!(result.is_ok());
26+
assert_eq!(result.unwrap(), 2);
27+
}
28+
29+
#[test]
30+
pub fn test_minimal_with_b() {
31+
let mut file = File::open("tests/programs/minimal").unwrap();
32+
let mut buffer = Vec::new();
33+
file.read_to_end(&mut buffer).unwrap();
34+
35+
let result = run(&buffer, &vec!["b".to_string()]);
36+
assert!(result.is_ok());
37+
assert_eq!(result.unwrap(), 0);
38+
}

0 commit comments

Comments
 (0)