diff --git a/Cargo.lock b/Cargo.lock index efd633d..56d869a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,7 @@ dependencies = [ "criterion", "elf", "rstest", + "thiserror", "tracing", "tracing-subscriber", ] diff --git a/crates/emu/Cargo.toml b/crates/emu/Cargo.toml index 7532892..c79ed86 100644 --- a/crates/emu/Cargo.toml +++ b/crates/emu/Cargo.toml @@ -17,6 +17,7 @@ workspace = true brisc-hw.workspace = true # External +thiserror.workspace = true elf = { version = "0.8.0", default-features = false } # `test-utils` feature diff --git a/crates/emu/README.md b/crates/emu/README.md index d8402a5..ca1b57a 100644 --- a/crates/emu/README.md +++ b/crates/emu/README.md @@ -59,12 +59,15 @@ const HELLO_WORLD_ELF: &str = "7f454c460201010000000000000000000200f30001000000e struct ExampleKernel; impl Kernel for ExampleKernel { + type Error = (); + fn syscall( &mut self, sysno: XWord, mem: &mut M, p_reg: &mut PipelineRegister, - ) -> PipelineResult { + _context: &mut () + ) -> Result { match sysno { 0x5D => { let exit_code = p_reg.registers[REG_A0 as usize]; @@ -95,14 +98,16 @@ impl Kernel for ExampleKernel { #[derive(Default)] struct ExampleEmuConfig; -impl EmuConfig for ExampleEmuConfig { +impl EmuConfig<'_> for ExampleEmuConfig { type Memory = SimpleMemory; type Kernel = ExampleKernel; + type Context = (); } let elf = const_hex::decode(HELLO_WORLD_ELF).unwrap(); let mut emu = StEmu::::builder() .with_kernel(ExampleKernel) + .with_ctx(()) .with_elf(&elf) .unwrap() .build(); diff --git a/crates/emu/src/cfg.rs b/crates/emu/src/cfg.rs index d3b72fe..4abdbbe 100644 --- a/crates/emu/src/cfg.rs +++ b/crates/emu/src/cfg.rs @@ -1,12 +1,15 @@ //! Emulator type configuration trait -use brisc_hw::{kernel::Kernel, memory::Memory}; +use brisc_hw::memory::Memory; /// The [`EmuConfig`] trait defines the type configuration for the emulator. -pub trait EmuConfig { +pub trait EmuConfig<'ctx> { /// The [Memory] type used by the emulator. type Memory: Memory; /// The kernel used by the emulator. - type Kernel: Kernel; + type Kernel; + + /// The external state passed to the kernel. + type Context: 'ctx; } diff --git a/crates/emu/src/st/builder.rs b/crates/emu/src/st/builder.rs index da1423f..5d20134 100644 --- a/crates/emu/src/st/builder.rs +++ b/crates/emu/src/st/builder.rs @@ -7,9 +7,9 @@ use brisc_hw::{pipeline::PipelineRegister, XWord}; /// A builder for the [`StEmu`] emulator. #[derive(Debug)] -pub struct StEmuBuilder +pub struct StEmuBuilder<'ctx, Config> where - Config: EmuConfig, + Config: EmuConfig<'ctx>, { /// The starting program counter. pub pc: XWord, @@ -17,20 +17,22 @@ where pub memory: Option, /// The system call interface for the emulator. pub kernel: Option, + /// The emulator's external context. + pub ctx: Option, } -impl Default for StEmuBuilder +impl<'ctx, Config> Default for StEmuBuilder<'ctx, Config> where - Config: EmuConfig, + Config: EmuConfig<'ctx>, { fn default() -> Self { - Self { pc: 0, memory: None, kernel: None } + Self { pc: 0, memory: None, kernel: None, ctx: None } } } -impl StEmuBuilder +impl<'ctx, Config> StEmuBuilder<'ctx, Config> where - Config: EmuConfig, + Config: EmuConfig<'ctx>, Config::Memory: Default, { /// Loads an elf file into the emulator builder, initializing the program counter and memory. @@ -42,9 +44,9 @@ where } } -impl StEmuBuilder +impl<'ctx, Config> StEmuBuilder<'ctx, Config> where - Config: EmuConfig, + Config: EmuConfig<'ctx>, { /// Assigns the entry point of the program. pub const fn with_pc(mut self, pc: XWord) -> Self { @@ -64,16 +66,23 @@ where self } + /// Assigns the context to the emulator. + pub fn with_ctx(mut self, ctx: Config::Context) -> Self { + self.ctx = Some(ctx); + self + } + /// Builds the emulator with the current configuration. /// /// ## Panics /// /// Panics if the memory or kernel is not set. - pub fn build(self) -> StEmu { + pub fn build(self) -> StEmu<'ctx, Config> { StEmu { register: PipelineRegister::new(self.pc), memory: self.memory.expect("Memory not instantiated"), kernel: self.kernel.expect("Kernel not instantiated"), + ctx: self.ctx.expect("Context not instantiated"), } } } diff --git a/crates/emu/src/st/mod.rs b/crates/emu/src/st/mod.rs index aaed48b..4a64a93 100644 --- a/crates/emu/src/st/mod.rs +++ b/crates/emu/src/st/mod.rs @@ -2,21 +2,37 @@ use crate::cfg::EmuConfig; use brisc_hw::{ - errors::{PipelineError, PipelineResult}, - kernel::Kernel, + errors::PipelineError, + kernel::{AsyncKernel, Kernel}, pipeline::{ decode_instruction, execute, instruction_fetch, mem_access, writeback, PipelineRegister, }, }; +use thiserror::Error; mod builder; pub use builder::StEmuBuilder; +/// An error that can occur during emulation. +#[derive(Error, Debug)] +pub enum EmulationError { + /// An error that occurred in the pipeline. + #[error(transparent)] + Pipeline(#[from] PipelineError), + + /// An error that occurred in the kernel. + #[error(transparent)] + Kernel(E), +} + +/// A [`Result`] type aslias for emulation results. +pub type EmulationResult = Result>; + /// Single-cycle RISC-V processor emulator. #[derive(Debug, Default)] -pub struct StEmu +pub struct StEmu<'ctx, Config> where - Config: EmuConfig, + Config: EmuConfig<'ctx>, { /// The pipeline register. pub register: PipelineRegister, @@ -24,19 +40,34 @@ where pub memory: Config::Memory, /// The system call interface. pub kernel: Config::Kernel, + /// The emulator's context. + pub ctx: Config::Context, } -impl StEmu +impl<'ctx, Config> StEmu<'ctx, Config> where - Config: EmuConfig, + Config: EmuConfig<'ctx>, { /// Creates a new [`StEmuBuilder`]. - pub fn builder() -> StEmuBuilder { + pub fn builder() -> StEmuBuilder<'ctx, Config> { StEmuBuilder::default() } + /// Destroys the emulator and returns the context. + pub fn take_ctx(self) -> Config::Context { + self.ctx + } +} + +impl<'ctx, Config> StEmu<'ctx, Config> +where + Config: EmuConfig<'ctx>, + Config::Kernel: Kernel + 'ctx, +{ /// Executes the program until it exits, returning the final [PipelineRegister]. - pub fn run(&mut self) -> PipelineResult { + pub fn run( + &mut self, + ) -> EmulationResult>::Error> { while !self.register.exit { self.cycle()?; } @@ -46,7 +77,61 @@ where /// Execute a single cycle of the processor in full. #[inline(always)] - pub fn cycle(&mut self) -> PipelineResult<()> { + pub fn cycle( + &mut self, + ) -> EmulationResult<(), >::Error> { + let r = &mut self.register; + + // Execute all pipeline stages sequentially. + let cycle_res = instruction_fetch(r, &self.memory) + .and_then(|_| decode_instruction(r)) + .and_then(|_| execute(r)) + .and_then(|_| mem_access(r, &mut self.memory)) + .and_then(|_| writeback(r)); + + // Handle system calls. + match cycle_res { + Ok(()) => {} + Err(PipelineError::SyscallException(syscall_no)) => { + self.kernel + .syscall(syscall_no, &mut self.memory, r, &mut self.ctx) + .map_err(EmulationError::Kernel)?; + + // Exit emulation if the syscall terminated the program. + if r.exit { + return Ok(()); + } + } + Err(e) => return Err(e.into()), + } + + r.advance(); + Ok(()) + } +} + +impl<'ctx, Config> StEmu<'ctx, Config> +where + Config: EmuConfig<'ctx>, + Config::Kernel: AsyncKernel + 'ctx, +{ + /// Executes the program until it exits, returning the final [PipelineRegister]. + pub async fn run_async( + &mut self, + ) -> EmulationResult>::Error> + { + while !self.register.exit { + self.cycle_async().await?; + } + + Ok(self.register) + } + + /// Execute a single cycle of the processor in full. + #[inline(always)] + pub async fn cycle_async( + &mut self, + ) -> EmulationResult<(), >::Error> { let r = &mut self.register; // Execute all pipeline stages sequentially. @@ -60,14 +145,17 @@ where match cycle_res { Ok(()) => {} Err(PipelineError::SyscallException(syscall_no)) => { - self.kernel.syscall(syscall_no, &mut self.memory, r)?; + self.kernel + .syscall(syscall_no, &mut self.memory, r, &mut self.ctx) + .await + .map_err(EmulationError::Kernel)?; // Exit emulation if the syscall terminated the program. if r.exit { return Ok(()); } } - Err(e) => return Err(e), + Err(e) => return Err(e.into()), } r.advance(); diff --git a/crates/emu/src/test_utils.rs b/crates/emu/src/test_utils.rs index 4b79ffe..c710953 100644 --- a/crates/emu/src/test_utils.rs +++ b/crates/emu/src/test_utils.rs @@ -2,7 +2,6 @@ use crate::{cfg::EmuConfig, st::StEmu}; use brisc_hw::{ - errors::PipelineResult, kernel::Kernel, memory::{Memory, SimpleMemory}, pipeline::PipelineRegister, @@ -48,6 +47,7 @@ pub fn run_riscv_test(test_path: &PathBuf) -> f64 { let elf_bytes = fs::read(test_path).unwrap(); let mut hart = StEmu::::builder() .with_kernel(RiscvTestKernel) + .with_ctx(()) .with_elf(&elf_bytes) .unwrap() .build(); @@ -78,32 +78,37 @@ pub fn run_riscv_test(test_path: &PathBuf) -> f64 { #[derive(Default)] struct TestStEmuConfig; -impl EmuConfig for TestStEmuConfig { +impl EmuConfig<'_> for TestStEmuConfig { type Memory = SimpleMemory; type Kernel = RiscvTestKernel; + + type Context = (); } -#[derive(Default)] +#[derive(Default, Debug)] struct RiscvTestKernel; -impl Kernel for RiscvTestKernel { +impl Kernel<()> for RiscvTestKernel { + type Error = (); + fn syscall( &mut self, sysno: XWord, mem: &mut M, p_reg: &mut PipelineRegister, - ) -> PipelineResult { + _: &mut (), + ) -> Result { match sysno { 0x5D => { - let exit_code = p_reg.registers[REG_A0 as usize]; + let exit_code = p_reg.registers[REG_A0]; p_reg.exit_code = exit_code; p_reg.exit = true; } 0x40 => { - let fd = p_reg.registers[REG_A0 as usize]; - let ptr = p_reg.registers[REG_A1 as usize]; - let len = p_reg.registers[REG_A2 as usize]; + let fd = p_reg.registers[REG_A0]; + let ptr = p_reg.registers[REG_A1]; + let len = p_reg.registers[REG_A2]; let raw_msg = mem.read_memory_range(ptr, len).unwrap(); let msg = String::from_utf8_lossy(&raw_msg); diff --git a/crates/hw/src/kernel.rs b/crates/hw/src/kernel.rs index e74620b..dd0ab51 100644 --- a/crates/hw/src/kernel.rs +++ b/crates/hw/src/kernel.rs @@ -1,26 +1,62 @@ //! Linux kernel interface. -use crate::{errors::PipelineResult, memory::Memory, pipeline::PipelineRegister}; +use crate::{memory::Memory, pipeline::PipelineRegister}; use brisc_isa::XWord; /// The [`Kernel`] trait defines the interface for performing system calls. -pub trait Kernel { +pub trait Kernel { + /// The error type returned by the kernel. + type Error; + /// Perform a system call with the given arguments. fn syscall( &mut self, syscall_no: XWord, memory: &mut M, p_reg: &mut PipelineRegister, - ) -> PipelineResult; + ctx: &mut S, + ) -> Result; } -impl Kernel for () { +impl Kernel for () { + type Error = (); + fn syscall( &mut self, _: XWord, _: &mut M, _: &mut PipelineRegister, - ) -> PipelineResult { + _: &mut S, + ) -> Result { unimplemented!() } } + +/// The [`Kernel`] trait defines the interface for performing asynchronous system calls. +pub trait AsyncKernel { + /// The error type returned by the kernel. + type Error; + + /// Perform a system call with the given arguments. + fn syscall( + &mut self, + syscall_no: XWord, + memory: &mut M, + p_reg: &mut PipelineRegister, + ctx: &mut S, + ) -> impl core::future::Future>; +} + +impl AsyncKernel for () { + type Error = (); + + async fn syscall( + &mut self, + _: XWord, + _: &mut M, + _: &mut PipelineRegister, + _: &mut S, + ) -> Result { + unimplemented!(); + } +} diff --git a/crates/hw/src/memory/errors.rs b/crates/hw/src/memory/errors.rs index 9918793..5b23acc 100644 --- a/crates/hw/src/memory/errors.rs +++ b/crates/hw/src/memory/errors.rs @@ -1,26 +1,19 @@ //! Memory related errors use crate::memory::Address; -use alloc::string::String; -use core::fmt::{Debug, Display}; +use core::fmt::Debug; use thiserror::Error; /// An error type for memory operations. #[derive(Error, Debug, Clone, Eq, PartialEq)] -pub enum MemoryError -where - T: Display + Debug + Clone + Eq + PartialEq, -{ +pub enum MemoryError { /// The page at the given index could not be found. #[error("Page not found at page index {0:08x}")] PageNotFound(Address), /// Unaligned memory access. #[error("Unaligned memory access at address {0:08x}")] UnalignedAccess(Address), - /// Custom memory error. - #[error("Memory error: {0}")] - Custom(#[from] T), } /// Type alias for a [Result] with [Result::Err] = [MemoryError]. -pub type MemoryResult = Result>; +pub type MemoryResult = Result; diff --git a/crates/hw/src/pipeline/decode.rs b/crates/hw/src/pipeline/decode.rs index 00d78a0..ae0cbb2 100644 --- a/crates/hw/src/pipeline/decode.rs +++ b/crates/hw/src/pipeline/decode.rs @@ -25,7 +25,7 @@ pub fn decode_instruction(register: &mut PipelineRegister) -> PipelineResult<()> // Throw an interrupt if the instruction is a system call. if instruction.is_system_call() { - return Err(PipelineError::SyscallException(register.registers[REG_A7 as usize])); + return Err(PipelineError::SyscallException(register.registers[REG_A7])); } Ok(()) diff --git a/crates/hw/src/pipeline/execute.rs b/crates/hw/src/pipeline/execute.rs index 46d3765..4153256 100644 --- a/crates/hw/src/pipeline/execute.rs +++ b/crates/hw/src/pipeline/execute.rs @@ -31,16 +31,16 @@ pub fn execute(p_reg: &mut PipelineRegister) -> PipelineResult<()> { } Instruction::RegisterArithmetic(_, funct) => execute_reg_arithmetic(p_reg, funct)?, Instruction::Lui(u_type) => u_type.imm, - Instruction::Auipc(u_type) => p_reg.pc + u_type.imm, + Instruction::Auipc(u_type) => p_reg.pc.wrapping_add(u_type.imm), Instruction::Jal(j_type) => { let result = p_reg.next_pc; - p_reg.next_pc = p_reg.pc + j_type.imm; + p_reg.next_pc = p_reg.pc.wrapping_add(j_type.imm); result } Instruction::Jalr(i_type) => { let result = p_reg.next_pc; let rs1 = p_reg.rs1_value.ok_or(PipelineError::MissingState("rs1_value"))?; - p_reg.next_pc = (rs1 + i_type.imm) & !1; + p_reg.next_pc = (rs1.wrapping_add(i_type.imm)) & !1; result } Instruction::Fence => { @@ -87,7 +87,7 @@ fn execute_branch( ) -> PipelineResult { let rs1 = p_reg.rs1_value.ok_or(PipelineError::MissingState("rs1_value"))?; let rs2 = p_reg.rs2_value.ok_or(PipelineError::MissingState("rs2_value"))?; - let target = p_reg.pc + b_type.imm; + let target = p_reg.pc.wrapping_add(b_type.imm); match funct { BranchFunction::Beq => { @@ -258,7 +258,7 @@ fn execute_imm_arithmetic_word( let rs1 = p_reg.rs1_value.ok_or(PipelineError::MissingState("rs1_value"))? as Word; let result = match funct { - ImmediateArithmeticWordFunction::Addiw => (i_type.imm as Word) + rs1, + ImmediateArithmeticWordFunction::Addiw => (i_type.imm as Word).wrapping_add(rs1), ImmediateArithmeticWordFunction::Slliw => rs1 << (i_type.imm & 0x1F), ImmediateArithmeticWordFunction::Srliw => rs1 >> (i_type.imm & 0x1F), ImmediateArithmeticWordFunction::Sraiw => ((rs1 as i32) >> (i_type.imm & 0x1F)) as Word, diff --git a/crates/isa/src/arch.rs b/crates/isa/src/arch.rs index 5f87ead..811f29d 100644 --- a/crates/isa/src/arch.rs +++ b/crates/isa/src/arch.rs @@ -73,97 +73,97 @@ cfg_if! { } /// hardwired zero -pub const REG_ZERO: XWord = 0; +pub const REG_ZERO: usize = 0; /// return address -pub const REG_RA: XWord = 1; +pub const REG_RA: usize = 1; /// stack pointer -pub const REG_SP: XWord = 2; +pub const REG_SP: usize = 2; /// global pointer -pub const REG_GP: XWord = 3; +pub const REG_GP: usize = 3; /// thread pointer -pub const REG_TP: XWord = 4; +pub const REG_TP: usize = 4; /// temporary register 0 -pub const REG_T0: XWord = 5; +pub const REG_T0: usize = 5; /// temporary register 1 -pub const REG_T1: XWord = 6; +pub const REG_T1: usize = 6; /// temporary register 2 -pub const REG_T2: XWord = 7; +pub const REG_T2: usize = 7; /// saved register / frame pointer -pub const REG_S0_FP: XWord = 8; +pub const REG_S0_FP: usize = 8; /// saved register 1 -pub const REG_S1: XWord = 9; +pub const REG_S1: usize = 9; /// function argument 0 / return value 0 -pub const REG_A0: XWord = 10; +pub const REG_A0: usize = 10; /// function argument 1 / return value 1 -pub const REG_A1: XWord = 11; +pub const REG_A1: usize = 11; /// function argument 2 -pub const REG_A2: XWord = 12; +pub const REG_A2: usize = 12; /// function argument 3 -pub const REG_A3: XWord = 13; +pub const REG_A3: usize = 13; /// function argument 4 -pub const REG_A4: XWord = 14; +pub const REG_A4: usize = 14; /// function argument 5 -pub const REG_A5: XWord = 15; +pub const REG_A5: usize = 15; /// function argument 6 -pub const REG_A6: XWord = 16; +pub const REG_A6: usize = 16; /// function argument 7 -pub const REG_A7: XWord = 17; +pub const REG_A7: usize = 17; /// saved register 2 -pub const REG_S2: XWord = 18; +pub const REG_S2: usize = 18; /// saved register 3 -pub const REG_S3: XWord = 19; +pub const REG_S3: usize = 19; /// saved register 4 -pub const REG_S4: XWord = 20; +pub const REG_S4: usize = 20; /// saved register 5 -pub const REG_S5: XWord = 21; +pub const REG_S5: usize = 21; /// saved register 6 -pub const REG_S6: XWord = 22; +pub const REG_S6: usize = 22; /// saved register 7 -pub const REG_S7: XWord = 23; +pub const REG_S7: usize = 23; /// saved register 8 -pub const REG_S8: XWord = 24; +pub const REG_S8: usize = 24; /// saved register 9 -pub const REG_S9: XWord = 25; +pub const REG_S9: usize = 25; /// saved register 10 -pub const REG_S10: XWord = 26; +pub const REG_S10: usize = 26; /// saved register 11 -pub const REG_S11: XWord = 27; +pub const REG_S11: usize = 27; /// temporary register 3 -pub const REG_T3: XWord = 28; +pub const REG_T3: usize = 28; /// temporary register 4 -pub const REG_T4: XWord = 29; +pub const REG_T4: usize = 29; /// temporary register 5 -pub const REG_T5: XWord = 30; +pub const REG_T5: usize = 30; /// temporary register 6 -pub const REG_T6: XWord = 31; +pub const REG_T6: usize = 31;