diff --git a/Cargo.lock b/Cargo.lock index 6f0c04d99..660f1d3cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,9 +266,18 @@ name = "ceno_emul" version = "0.1.0" dependencies = [ "anyhow", + "elf", "tracing", ] +[[package]] +name = "ceno_rt" +version = "0.1.0" +dependencies = [ + "riscv", + "riscv-rt", +] + [[package]] name = "ceno_zkvm" version = "0.1.0" @@ -475,6 +484,12 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "critical-section" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64009896348fc5af4222e9cf7d7d82a95a256c634ebcf61c53e4ea461422242" + [[package]] name = "crossbeam-channel" version = "0.5.13" @@ -570,6 +585,18 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "elf" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + [[package]] name = "env_logger" version = "0.7.1" @@ -1541,6 +1568,37 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "riscv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5c1b8bf41ea746266cdee443d1d1e9125c86ce1447e1a2615abd34330d33a9" +dependencies = [ + "critical-section", + "embedded-hal", +] + +[[package]] +name = "riscv-rt" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0d35e32cf1383183e8885d8a9aa4402a087fd094dc34c2cb6df6687d0229dfe" +dependencies = [ + "riscv", + "riscv-rt-macros", +] + +[[package]] +name = "riscv-rt-macros" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d100d466dbb76681ef6a9386f3da9abc570d57394e86da0ba5af8c4408486d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rustc-demangle" version = "0.1.23" diff --git a/Cargo.toml b/Cargo.toml index f8c1eb2bd..f55d1638b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "ceno_emul", + "ceno_rt", "gkr", "gkr-graph", "mpcs", diff --git a/ceno_emul/Cargo.toml b/ceno_emul/Cargo.toml index 5a80047a6..ed1096822 100644 --- a/ceno_emul/Cargo.toml +++ b/ceno_emul/Cargo.toml @@ -9,3 +9,4 @@ anyhow = { version = "1.0", default-features = false } tracing = { version = "0.1", default-features = false, features = [ "attributes", ] } +elf = { version = "0.7.4" } diff --git a/ceno_emul/src/elf.rs b/ceno_emul/src/elf.rs new file mode 100644 index 000000000..2842d6b33 --- /dev/null +++ b/ceno_emul/src/elf.rs @@ -0,0 +1,112 @@ +// Based on: https://github.com/risc0/risc0/blob/6b6daeafa1545984aa28581fca56d9ef13dcbae6/risc0/binfmt/src/elf.rs +// +// Copyright 2024 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +extern crate alloc; + +use alloc::collections::BTreeMap; + +use crate::addr::WORD_SIZE; +use anyhow::{anyhow, bail, Context, Result}; +use elf::{endian::LittleEndian, file::Class, ElfBytes}; + +/// A RISC Zero program +pub struct Program { + /// The entrypoint of the program + pub entry: u32, + + /// The initial memory image + pub image: BTreeMap, +} + +impl Program { + /// Initialize a RISC Zero Program from an appropriate ELF file + pub fn load_elf(input: &[u8], max_mem: u32) -> Result { + let mut image: BTreeMap = BTreeMap::new(); + let elf = ElfBytes::::minimal_parse(input) + .map_err(|err| anyhow!("Elf parse error: {err}"))?; + if elf.ehdr.class != Class::ELF32 { + bail!("Not a 32-bit ELF"); + } + if elf.ehdr.e_machine != elf::abi::EM_RISCV { + bail!("Invalid machine type, must be RISC-V"); + } + if elf.ehdr.e_type != elf::abi::ET_EXEC { + bail!("Invalid ELF type, must be executable"); + } + let entry: u32 = elf + .ehdr + .e_entry + .try_into() + .map_err(|err| anyhow!("e_entry was larger than 32 bits. {err}"))?; + if entry >= max_mem || entry % WORD_SIZE as u32 != 0 { + bail!("Invalid entrypoint"); + } + let segments = elf.segments().ok_or(anyhow!("Missing segment table"))?; + if segments.len() > 256 { + bail!("Too many program headers"); + } + for segment in segments.iter().filter(|x| x.p_type == elf::abi::PT_LOAD) { + let file_size: u32 = segment + .p_filesz + .try_into() + .map_err(|err| anyhow!("filesize was larger than 32 bits. {err}"))?; + if file_size >= max_mem { + bail!("Invalid segment file_size"); + } + let mem_size: u32 = segment + .p_memsz + .try_into() + .map_err(|err| anyhow!("mem_size was larger than 32 bits {err}"))?; + if mem_size >= max_mem { + bail!("Invalid segment mem_size"); + } + let vaddr: u32 = segment + .p_vaddr + .try_into() + .map_err(|err| anyhow!("vaddr is larger than 32 bits. {err}"))?; + if vaddr % WORD_SIZE as u32 != 0 { + bail!("vaddr {vaddr:08x} is unaligned"); + } + let offset: u32 = segment + .p_offset + .try_into() + .map_err(|err| anyhow!("offset is larger than 32 bits. {err}"))?; + for i in (0..mem_size).step_by(WORD_SIZE) { + let addr = vaddr.checked_add(i).context("Invalid segment vaddr")?; + if addr >= max_mem { + bail!( + "Address [0x{addr:08x}] exceeds maximum address for guest programs [0x{max_mem:08x}]" + ); + } + if i >= file_size { + // Past the file size, all zeros. + image.insert(addr, 0); + } else { + let mut word = 0; + // Don't read past the end of the file. + let len = core::cmp::min(file_size - i, WORD_SIZE as u32); + for j in 0..len { + let offset = (offset + i + j) as usize; + let byte = input.get(offset).context("Invalid segment offset")?; + word |= (*byte as u32) << (j * 8); + } + image.insert(addr, word); + } + } + } + Ok(Program { entry, image }) + } +} diff --git a/ceno_emul/src/lib.rs b/ceno_emul/src/lib.rs index 8c59c467b..c5359e442 100644 --- a/ceno_emul/src/lib.rs +++ b/ceno_emul/src/lib.rs @@ -12,3 +12,6 @@ pub use vm_state::VMState; mod rv32im; pub use rv32im::{DecodedInstruction, EmuContext, InsnCategory, InsnKind}; + +mod elf; +pub use elf::Program; diff --git a/ceno_emul/src/loader.rs b/ceno_emul/src/loader.rs new file mode 100644 index 000000000..e69de29bb diff --git a/ceno_emul/src/rv32im.rs b/ceno_emul/src/rv32im.rs index 99c2035d4..710d7e7d4 100644 --- a/ceno_emul/src/rv32im.rs +++ b/ceno_emul/src/rv32im.rs @@ -97,7 +97,7 @@ pub enum TrapCause { LoadAccessFault(ByteAddr), StoreAddressMisaligned(ByteAddr), StoreAccessFault, - EnvironmentCallFromUserMode, + EcallError, } #[derive(Clone, Debug, Default)] diff --git a/ceno_emul/src/tracer.rs b/ceno_emul/src/tracer.rs index f252ca348..78387ef49 100644 --- a/ceno_emul/src/tracer.rs +++ b/ceno_emul/src/tracer.rs @@ -77,6 +77,10 @@ impl StepRecord { pub fn memory_op(&self) -> Option { self.memory_op.clone() } + + pub fn is_busy_loop(&self) -> bool { + self.pc.before == self.pc.after + } } #[derive(Debug)] diff --git a/ceno_emul/src/vm_state.rs b/ceno_emul/src/vm_state.rs index 0da8f2b03..b478fc5ff 100644 --- a/ceno_emul/src/vm_state.rs +++ b/ceno_emul/src/vm_state.rs @@ -6,6 +6,7 @@ use crate::{ platform::Platform, rv32im::{DecodedInstruction, Emulator, Instruction, TrapCause}, tracer::{Change, StepRecord, Tracer}, + Program, }; use anyhow::{anyhow, Result}; use std::iter::from_fn; @@ -35,6 +36,19 @@ impl VMState { } } + pub fn new_from_elf(platform: Platform, elf: &[u8]) -> Result { + let mut state = Self::new(platform); + let program = Program::load_elf(elf, u32::MAX).unwrap(); + for (addr, word) in program.image.iter() { + let addr = ByteAddr(*addr).waddr(); + state.init_memory(addr, *word); + } + if program.entry != state.platform.pc_start() { + return Err(anyhow!("Invalid entrypoint {:x}", program.entry)); + } + Ok(state) + } + pub fn succeeded(&self) -> bool { self.succeeded } @@ -62,7 +76,11 @@ impl VMState { fn step(&mut self, emu: &Emulator) -> Result { emu.step(self)?; let step = self.tracer().advance(); - Ok(step) + if step.is_busy_loop() && !self.succeeded() { + Err(anyhow!("Stuck in loop {}", "{}")) + } else { + Ok(step) + } } } @@ -76,7 +94,7 @@ impl EmuContext for VMState { self.succeeded = true; Ok(true) } else { - self.trap(TrapCause::EnvironmentCallFromUserMode) + self.trap(TrapCause::EcallError) } } diff --git a/ceno_emul/tests/data/README.md b/ceno_emul/tests/data/README.md new file mode 100644 index 000000000..746886507 --- /dev/null +++ b/ceno_emul/tests/data/README.md @@ -0,0 +1,7 @@ +### Generate test programs: + +```bash +cd ceno_rt +cargo build --release --examples +cp ../target/riscv32im-unknown-none-elf/release/examples/ceno_rt_{mini,panic,mem} ../ceno_emul/tests/data/ +``` \ No newline at end of file diff --git a/ceno_emul/tests/data/ceno_rt_mem b/ceno_emul/tests/data/ceno_rt_mem new file mode 100755 index 000000000..6d266c444 Binary files /dev/null and b/ceno_emul/tests/data/ceno_rt_mem differ diff --git a/ceno_emul/tests/data/ceno_rt_mini b/ceno_emul/tests/data/ceno_rt_mini new file mode 100755 index 000000000..741cc17ed Binary files /dev/null and b/ceno_emul/tests/data/ceno_rt_mini differ diff --git a/ceno_emul/tests/data/ceno_rt_panic b/ceno_emul/tests/data/ceno_rt_panic new file mode 100755 index 000000000..927fa7df0 Binary files /dev/null and b/ceno_emul/tests/data/ceno_rt_panic differ diff --git a/ceno_emul/tests/test_elf.rs b/ceno_emul/tests/test_elf.rs new file mode 100644 index 000000000..7b7b8e13d --- /dev/null +++ b/ceno_emul/tests/test_elf.rs @@ -0,0 +1,35 @@ +use anyhow::Result; +use ceno_emul::{ByteAddr, EmuContext, StepRecord, VMState, CENO_PLATFORM}; + +#[test] +fn test_ceno_rt_mini() -> Result<()> { + let program_elf = include_bytes!("./data/ceno_rt_mini"); + let mut state = VMState::new_from_elf(CENO_PLATFORM, program_elf)?; + let _steps = run(&mut state)?; + Ok(()) +} + +#[test] +fn test_ceno_rt_panic() -> Result<()> { + let program_elf = include_bytes!("./data/ceno_rt_panic"); + let mut state = VMState::new_from_elf(CENO_PLATFORM, program_elf)?; + let res = run(&mut state); + + assert!(matches!(res, Err(e) if e.to_string().contains("EcallError"))); + Ok(()) +} + +#[test] +fn test_ceno_rt_mem() -> Result<()> { + let program_elf = include_bytes!("./data/ceno_rt_mem"); + let mut state = VMState::new_from_elf(CENO_PLATFORM, program_elf)?; + let _steps = run(&mut state)?; + + let value = state.peek_memory(ByteAddr(CENO_PLATFORM.ram_start()).waddr()); + assert_eq!(value, 6765, "Expected Fibonacci 20, got {}", value); + Ok(()) +} + +fn run(state: &mut VMState) -> Result> { + state.iter_until_success().collect() +} diff --git a/ceno_rt/.cargo/config.toml b/ceno_rt/.cargo/config.toml new file mode 100644 index 000000000..c08ff50ab --- /dev/null +++ b/ceno_rt/.cargo/config.toml @@ -0,0 +1,13 @@ +[target.riscv32im-unknown-none-elf] +rustflags = [ + "-C", "link-arg=-Tmemory.x", + #"-C", "link-arg=-Tlink.x", // Script from riscv_rt. + "-C", "link-arg=-Tceno_link.x", +] + +[build] +target = "riscv32im-unknown-none-elf" + +[profile.release] +panic = "abort" +lto = true diff --git a/ceno_rt/Cargo.toml b/ceno_rt/Cargo.toml new file mode 100644 index 000000000..c295821f2 --- /dev/null +++ b/ceno_rt/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "ceno_rt" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +riscv = "0.11.1" +riscv-rt = "0.12.2" diff --git a/ceno_rt/README.md b/ceno_rt/README.md new file mode 100644 index 000000000..72abeb763 --- /dev/null +++ b/ceno_rt/README.md @@ -0,0 +1,25 @@ +# Ceno VM Runtime + +This crate provides the runtime for program running on the Ceno VM. It provides: + +- Configuration of compilation and linking. +- Program startup and termination. +- Memory setup. + +### Build examples + +```bash +rustup target add riscv32im-unknown-none-elf + +cargo build --release --examples +``` + +### Development tools + +```bash +cargo install cargo-binutils +rustup component add llvm-tools + +# Look at the disassembly of a compiled program. +cargo objdump --release --example ceno_rt_mini -- --all-headers --disassemble +``` diff --git a/ceno_rt/build.rs b/ceno_rt/build.rs new file mode 100644 index 000000000..04f7d247e --- /dev/null +++ b/ceno_rt/build.rs @@ -0,0 +1,13 @@ +use std::{env, fs, path::PathBuf}; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + + // Put the linker script somewhere the linker can find it. + fs::write(out_dir.join("memory.x"), include_bytes!("memory.x")).unwrap(); + fs::write(out_dir.join("ceno_link.x"), include_bytes!("ceno_link.x")).unwrap(); + println!("cargo:rustc-link-search={}", out_dir.display()); + println!("cargo:rerun-if-changed=memory.x"); + println!("cargo:rerun-if-changed=ceno_link.x"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/ceno_rt/ceno_link.x b/ceno_rt/ceno_link.x new file mode 100644 index 000000000..00b2ea282 --- /dev/null +++ b/ceno_rt/ceno_link.x @@ -0,0 +1,37 @@ + +_stack_start = ORIGIN(REGION_STACK) + LENGTH(REGION_STACK); + +SECTIONS +{ + .text : + { + KEEP(*(.init)); + . = ALIGN(4); + *(.text .text.*); + } > ROM + + .rodata : ALIGN(4) + { + *(.srodata .srodata.*); + *(.rodata .rodata.*); + } > ROM + + .data : ALIGN(4) + { + /* Must be called __global_pointer$ for linker relaxations to work. */ + PROVIDE(__global_pointer$ = . + 0x800); + + *(.sdata .sdata.*); + *(.sdata2 .sdata2.*); + *(.data .data.*); + } > RAM + + .bss (NOLOAD) : ALIGN(4) + { + *(.sbss .sbss.*); + *(.bss .bss.*); + + . = ALIGN(4); + _sheap = .; + } > RAM +} diff --git a/ceno_rt/examples/ceno_rt_mem.rs b/ceno_rt/examples/ceno_rt_mem.rs new file mode 100644 index 000000000..382ce78c8 --- /dev/null +++ b/ceno_rt/examples/ceno_rt_mem.rs @@ -0,0 +1,54 @@ +#![no_main] +#![no_std] + +// Use volatile functions to prevent compiler optimizations. +use core::ptr::{read_volatile, write_volatile}; + +#[allow(unused_imports)] +use ceno_rt; +const OUTPUT_ADDRESS: u32 = 0x8000_0000; + +#[no_mangle] +#[inline(never)] +fn main() { + test_data_section(); + + let out = fibonacci_recurse(20, 0, 1); + test_output(out); +} + +/// Test the .data section is loaded and read/write works. +#[inline(never)] +fn test_data_section() { + // Use X[1] to be sure it is not the same as *OUTPUT_ADDRESS. + static mut X: [u32; 2] = [0, 42]; + + unsafe { + assert_eq!(read_volatile(&X[1]), 42); + write_volatile(&mut X[1], 99); + assert_eq!(read_volatile(&X[1]), 99); + } +} + +// A sufficiently complicated function to test the stack. +#[inline(never)] +fn fibonacci_recurse(count: u32, a: u32, b: u32) -> u32 { + let count = black_box(count); + if count == 0 { + a + } else { + fibonacci_recurse(count - 1, b, a + b) + } +} + +// Store the output to a specific memory location so the emulator tests can find it. +#[inline(never)] +fn test_output(out: u32) { + unsafe { + write_volatile(OUTPUT_ADDRESS as *mut u32, out); + } +} + +fn black_box(x: T) -> T { + unsafe { read_volatile(&x) } +} diff --git a/ceno_rt/examples/ceno_rt_mini.rs b/ceno_rt/examples/ceno_rt_mini.rs new file mode 100644 index 000000000..c3f8b88ab --- /dev/null +++ b/ceno_rt/examples/ceno_rt_mini.rs @@ -0,0 +1,8 @@ +#![no_main] +#![no_std] + +#[allow(unused_imports)] +use ceno_rt; + +#[no_mangle] +fn main() {} diff --git a/ceno_rt/examples/ceno_rt_panic.rs b/ceno_rt/examples/ceno_rt_panic.rs new file mode 100644 index 000000000..24a0b20b4 --- /dev/null +++ b/ceno_rt/examples/ceno_rt_panic.rs @@ -0,0 +1,10 @@ +#![no_main] +#![no_std] + +#[allow(unused_imports)] +use ceno_rt; + +#[no_mangle] +fn main() { + panic!("This is a panic message!"); +} diff --git a/ceno_rt/memory.x b/ceno_rt/memory.x new file mode 100644 index 000000000..712de56cd --- /dev/null +++ b/ceno_rt/memory.x @@ -0,0 +1,12 @@ +MEMORY +{ + RAM : ORIGIN = 0x80000000, LENGTH = 1024M + ROM : ORIGIN = 0x20000000, LENGTH = 16M +} + +REGION_ALIAS("REGION_TEXT", ROM); +REGION_ALIAS("REGION_RODATA", ROM); +REGION_ALIAS("REGION_DATA", RAM); +REGION_ALIAS("REGION_BSS", RAM); +REGION_ALIAS("REGION_HEAP", RAM); +REGION_ALIAS("REGION_STACK", RAM); diff --git a/ceno_rt/src/lib.rs b/ceno_rt/src/lib.rs new file mode 100644 index 000000000..0ca1c4032 --- /dev/null +++ b/ceno_rt/src/lib.rs @@ -0,0 +1,69 @@ +#![no_main] +#![no_std] +use core::arch::{asm, global_asm}; + +#[cfg(not(test))] +mod panic_handler { + use core::panic::PanicInfo; + + #[panic_handler] + #[inline(never)] + fn panic_handler(_panic: &PanicInfo<'_>) -> ! { + super::halt(1) + } +} + +pub fn halt(exit_code: u32) -> ! { + unsafe { + asm!( + // Set the first argument. + "mv a0, {}", + // Set the ecall code HALT. + "li t0, 0x0", + in(reg) exit_code, + ); + riscv::asm::ecall(); + } + #[allow(clippy::empty_loop)] + loop {} +} + +global_asm!( + " +// The entry point for the program. +.section .init +.global _start +_start: + + // Set the global pointer somewhere towards the start of RAM. + .option push + .option norelax + la gp, __global_pointer$ + .option pop + + // Set the stack pointer and frame pointer to the top of the stack. + la sp, _stack_start + mv fp, sp + + // Call the Rust start function. + jal zero, _start_rust + ", +); + +/// _start_rust is called by the assembly entry point and it calls the Rust main(). +#[no_mangle] +unsafe extern "C" fn _start_rust() -> ! { + main(); + halt(0) +} + +extern "C" { + fn main(); +} + +extern "C" { + // The address of this variable is the start of the stack (growing downwards). + static _stack_start: u8; + // The address of this variable is the start of the heap (growing upwards). + static _sheap: u8; +}