diff --git a/Cargo.lock b/Cargo.lock index e5fc6dd4e..e0e10dd1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,6 +291,8 @@ dependencies = [ "anyhow", "ceno_rt", "elf", + "gdbstub", + "gdbstub_arch", "itertools 0.13.0", "num-derive", "num-traits", @@ -783,6 +785,30 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "gdbstub" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c683a9f13de31432e6097131d5f385898c7f0635c0f392b9d0fa165063c8ac" +dependencies = [ + "bitflags", + "cfg-if", + "log", + "managed", + "num-traits", + "paste", +] + +[[package]] +name = "gdbstub_arch" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328a9e9425db13770d0d11de6332a608854266e44c53d12776be7b4aa427e3de" +dependencies = [ + "gdbstub", + "num-traits", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1085,6 +1111,12 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + [[package]] name = "matchers" version = "0.1.0" diff --git a/ceno_emul/Cargo.toml b/ceno_emul/Cargo.toml index 2bc4c830f..611dbfd7c 100644 --- a/ceno_emul/Cargo.toml +++ b/ceno_emul/Cargo.toml @@ -13,6 +13,8 @@ version.workspace = true anyhow.workspace = true ceno_rt = { path = "../ceno_rt" } elf = "0.7" +gdbstub = "0.7.3" +gdbstub_arch = "0.3.1" itertools.workspace = true num-derive.workspace = true num-traits.workspace = true diff --git a/ceno_emul/src/gdb.rs b/ceno_emul/src/gdb.rs new file mode 100644 index 000000000..761784877 --- /dev/null +++ b/ceno_emul/src/gdb.rs @@ -0,0 +1,231 @@ +use std::collections::BTreeSet; + +use gdbstub::{ + arch::Arch, + common::Signal, + target::{ + Target, + TargetError::NonFatal, + TargetResult, + ext::{ + base::{ + BaseOps, + single_register_access::{SingleRegisterAccess, SingleRegisterAccessOps}, + singlethread::{ + SingleThreadBase, SingleThreadResume, SingleThreadResumeOps, + SingleThreadSingleStep, SingleThreadSingleStepOps, + }, + }, + breakpoints::{Breakpoints, BreakpointsOps, SwBreakpoint, SwBreakpointOps}, + }, + }, +}; +use gdbstub_arch::riscv::{Riscv32, reg::id::RiscvRegId}; +use itertools::enumerate; + +use crate::{ByteAddr, EmuContext, RegIdx, VMState, WordAddr}; + +// This should probably reference / or be VMState? +pub struct MyTarget { + state: VMState, + break_points: BTreeSet, +} + +impl Target for MyTarget { + type Error = anyhow::Error; + type Arch = gdbstub_arch::riscv::Riscv32; + + #[inline(always)] + fn base_ops(&mut self) -> BaseOps { + BaseOps::SingleThread(self) + } + + // opt-in to support for setting/removing breakpoints + #[inline(always)] + fn support_breakpoints(&mut self) -> Option> { + Some(self) + } +} + +impl SingleRegisterAccess<()> for MyTarget { + fn read_register( + &mut self, + _thread_id: (), + reg_id: ::RegId, + buf: &mut [u8], + ) -> TargetResult { + match reg_id { + RiscvRegId::Gpr(i) if (0..32).contains(&i) => { + buf.copy_from_slice(&self.state.peek_register(RegIdx::from(i)).to_le_bytes()); + Ok(4) + } + RiscvRegId::Pc => { + buf.copy_from_slice(&self.state.get_pc().0.to_le_bytes()); + Ok(4) + } + // TODO(Matthias): see whether we can make this more specific. + _ => Err(NonFatal), + } + } + + fn write_register( + &mut self, + _thread_id: (), + reg_id: ::RegId, + value: &[u8], + ) -> TargetResult<(), Self> { + let mut bytes = [0; 4]; + bytes.copy_from_slice(value); + let buf = u32::from_le_bytes(bytes); + match reg_id { + // Note: we refuse to write to register 0. + RiscvRegId::Gpr(i) if (1..32).contains(&i) => { + self.state.init_register_unsafe(RegIdx::from(i), buf) + } + RiscvRegId::Pc => self.state.set_pc(ByteAddr(buf)), + // TODO(Matthias): see whether we can make this more specific. + _ => return Err(NonFatal), + } + Ok(()) + } +} + +impl SingleThreadBase for MyTarget { + #[inline(always)] + fn support_single_register_access(&mut self) -> Option> { + Some(self) + } + + fn read_registers( + &mut self, + regs: &mut gdbstub_arch::riscv::reg::RiscvCoreRegs, + ) -> TargetResult<(), Self> { + for (i, reg) in enumerate(&mut regs.x) { + *reg = self.state.peek_register(i); + } + regs.pc = u32::from(self.state.get_pc()); + Ok(()) + } + + fn write_registers( + &mut self, + regs: &gdbstub_arch::riscv::reg::RiscvCoreRegs, + ) -> TargetResult<(), Self> { + for (i, reg) in enumerate(®s.x) { + self.state.init_register_unsafe(i, *reg); + } + self.state.set_pc(ByteAddr::from(regs.pc)); + Ok(()) + } + + fn read_addrs(&mut self, start_addr: u32, data: &mut [u8]) -> TargetResult { + // TODO: deal with misaligned accesses + if !start_addr.is_multiple_of(4) { + return Err(NonFatal); + } + if !data.len().is_multiple_of(4) { + return Err(NonFatal); + } + let start_addr = WordAddr::from(ByteAddr(start_addr)); + + for (i, chunk) in enumerate(data.chunks_exact_mut(4)) { + let addr = start_addr + i * 4; + let word = self.state.peek_memory(addr); + chunk.copy_from_slice(&word.to_le_bytes()); + } + Ok(data.len()) + } + + fn write_addrs(&mut self, start_addr: u32, data: &[u8]) -> TargetResult<(), Self> { + // TODO: deal with misaligned accesses + if !start_addr.is_multiple_of(4) { + return Err(NonFatal); + } + if !data.len().is_multiple_of(4) { + return Err(NonFatal); + } + let start_addr = WordAddr::from(ByteAddr(start_addr)); + for (i, chunk) in enumerate(data.chunks_exact(4)) { + self.state.init_memory( + start_addr + i * 4, + u32::from_le_bytes(chunk.try_into().unwrap()), + ); + } + Ok(()) + } + + // most targets will want to support at resumption as well... + + #[inline(always)] + fn support_resume(&mut self) -> Option> { + Some(self) + } +} + +// TODO(Matthias): also support reverse stepping. +impl SingleThreadResume for MyTarget { + fn resume(&mut self, _signal: Option) -> Result<(), Self::Error> { + // TODO: iterate until either halt or breakpoint. + loop { + if self.state.halted() { + return Ok(()); + } + // TOOD: encountering an illegal instruction should NOT be a fatal error. + crate::rv32im::step(&mut self.state)?; + if self.break_points.contains(&u32::from(self.state.get_pc())) { + return Ok(()); + } + } + } + + // ...and if the target supports resumption, it'll likely want to support + // single-step resume as well + + #[inline(always)] + fn support_single_step(&mut self) -> Option> { + Some(self) + } +} + +impl SingleThreadSingleStep for MyTarget { + fn step(&mut self, _signal: Option) -> Result<(), Self::Error> { + // We might want to step with something higher level than rv32im::step, so we can go backwards in time? + crate::rv32im::step(&mut self.state)?; + Ok(()) + } +} + +// TODO: consider adding WatchKind, and perhaps hardware breakpoints? +impl Breakpoints for MyTarget { + // there are several kinds of breakpoints - this target uses software breakpoints + #[inline(always)] + fn support_sw_breakpoint(&mut self) -> Option> { + Some(self) + } +} + +impl SwBreakpoint for MyTarget { + fn add_sw_breakpoint( + &mut self, + addr: u32, + _kind: ::BreakpointKind, + ) -> TargetResult { + // assert_eq!(kind, 0); + // TODO: consider always succeeding, and supporting multiple of the same breakpoint? + // What does gdb expect? + // At the moment we fail, if the breakpoint already exists. + Ok(self.break_points.insert(addr)) + } + + fn remove_sw_breakpoint( + &mut self, + addr: u32, + _kind: ::BreakpointKind, + ) -> TargetResult { + // assert_eq!(kind, 0); + // TODO: consider always succeeding, and supporting multiple of the same breakpoint? + // What does gdb expect? + // At the moment we fail, if the breakpoint doesn't exist. + Ok(self.break_points.remove(&addr)) + } +} diff --git a/ceno_emul/src/lib.rs b/ceno_emul/src/lib.rs index 5e9d871ef..b4f030481 100644 --- a/ceno_emul/src/lib.rs +++ b/ceno_emul/src/lib.rs @@ -1,5 +1,6 @@ #![deny(clippy::cargo)] #![feature(step_trait)] +#![feature(unsigned_is_multiple_of)] mod addr; pub use addr::*; @@ -28,3 +29,5 @@ pub use syscalls::{KECCAK_PERMUTE, keccak_permute::KECCAK_WORDS}; pub mod test_utils; pub mod host_utils; + +pub mod gdb; diff --git a/ceno_emul/src/vm_state.rs b/ceno_emul/src/vm_state.rs index 49f22a747..745f3067a 100644 --- a/ceno_emul/src/vm_state.rs +++ b/ceno_emul/src/vm_state.rs @@ -21,7 +21,7 @@ pub struct VMState { memory: HashMap, registers: [Word; VMState::REG_COUNT], // Termination. - halted: bool, + pub halted: bool, tracer: Tracer, }