Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Debugger support for guests (WIP) #812

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
32 changes: 32 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions ceno_emul/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
231 changes: 231 additions & 0 deletions ceno_emul/src/gdb.rs
Original file line number Diff line number Diff line change
@@ -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<u32>,
}

impl Target for MyTarget {
type Error = anyhow::Error;
type Arch = gdbstub_arch::riscv::Riscv32;

#[inline(always)]
fn base_ops(&mut self) -> BaseOps<Self::Arch, Self::Error> {
BaseOps::SingleThread(self)
}

// opt-in to support for setting/removing breakpoints
#[inline(always)]
fn support_breakpoints(&mut self) -> Option<BreakpointsOps<Self>> {
Some(self)
}
}

impl SingleRegisterAccess<()> for MyTarget {
fn read_register(
&mut self,
_thread_id: (),
reg_id: <Riscv32 as Arch>::RegId,
buf: &mut [u8],
) -> TargetResult<usize, Self> {
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: <Riscv32 as Arch>::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<SingleRegisterAccessOps<'_, (), Self>> {
Some(self)
}

fn read_registers(
&mut self,
regs: &mut gdbstub_arch::riscv::reg::RiscvCoreRegs<u32>,
) -> 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<u32>,
) -> TargetResult<(), Self> {
for (i, reg) in enumerate(&regs.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<usize, 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_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<SingleThreadResumeOps<Self>> {
Some(self)
}
}

// TODO(Matthias): also support reverse stepping.
impl SingleThreadResume for MyTarget {
fn resume(&mut self, _signal: Option<Signal>) -> 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<SingleThreadSingleStepOps<'_, Self>> {
Some(self)
}
}

impl SingleThreadSingleStep for MyTarget {
fn step(&mut self, _signal: Option<Signal>) -> 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<SwBreakpointOps<Self>> {
Some(self)
}
}

impl SwBreakpoint for MyTarget {
fn add_sw_breakpoint(
&mut self,
addr: u32,
_kind: <Riscv32 as Arch>::BreakpointKind,
) -> TargetResult<bool, Self> {
// 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: <Riscv32 as Arch>::BreakpointKind,
) -> TargetResult<bool, Self> {
// 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))
}
}
3 changes: 3 additions & 0 deletions ceno_emul/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![deny(clippy::cargo)]
#![feature(step_trait)]
#![feature(unsigned_is_multiple_of)]
mod addr;
pub use addr::*;

Expand Down Expand Up @@ -28,3 +29,5 @@ pub use syscalls::{KECCAK_PERMUTE, keccak_permute::KECCAK_WORDS};
pub mod test_utils;

pub mod host_utils;

pub mod gdb;
2 changes: 1 addition & 1 deletion ceno_emul/src/vm_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub struct VMState {
memory: HashMap<WordAddr, Word>,
registers: [Word; VMState::REG_COUNT],
// Termination.
halted: bool,
pub halted: bool,
tracer: Tracer,
}

Expand Down