use core::fmt;
use core::fmt::Write as _;
use crate::core::clock::GlobalTime;
use crate::core::device::DeviceClass;
use crate::core::space::{AddressSpace, MemAttrs, RequesterId};
use crate::core::state::{ChunkReader, MachineShape, StateReader, StateWriter};
use crate::machine::Machine;
use super::arch::Arch;
#[must_use]
pub fn debug_attrs(requester: RequesterId) -> MemAttrs {
MemAttrs::DEBUG.with_requester(requester)
}
#[derive(Debug)]
pub enum TargetError {
NoSuchCpu,
NoSuchRegister,
Fault,
Unmapped,
Unsupported,
LayoutMismatch {
class: &'static str,
expected: u32,
found: u32,
},
Machine(crate::Error),
}
impl TargetError {
#[must_use]
pub const fn code(&self) -> u8 {
match self {
TargetError::NoSuchCpu => 3, TargetError::Fault | TargetError::Machine(_) => 5, TargetError::Unmapped => 14, TargetError::NoSuchRegister | TargetError::Unsupported => 22, TargetError::LayoutMismatch { .. } => 8, }
}
}
impl fmt::Display for TargetError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TargetError::NoSuchCpu => f.write_str("no such cpu"),
TargetError::NoSuchRegister => f.write_str("no such register"),
TargetError::Fault => f.write_str("the guest bus refused the access"),
TargetError::Unmapped => f.write_str("nothing is mapped at that virtual address"),
TargetError::Unsupported => f.write_str("unsupported"),
TargetError::LayoutMismatch {
class,
expected,
found,
} => write!(
f,
"`{class}` state version {found} but its gdb register map was written \
against version {expected}"
),
TargetError::Machine(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for TargetError {}
impl From<crate::Error> for TargetError {
fn from(e: crate::Error) -> TargetError {
TargetError::Machine(e)
}
}
pub type TargetResult<T> = Result<T, TargetError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StopKind {
Trap,
Breakpoint {
hardware: bool,
},
Watchpoint {
addr: u64,
},
Interrupt,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Stop {
pub cpu: usize,
pub kind: StopKind,
}
impl Stop {
#[must_use]
pub const fn signal(&self) -> u8 {
match self.kind {
StopKind::Interrupt => 2,
_ => 5, }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct WatchSupport {
pub write: bool,
pub read: bool,
pub access: bool,
}
pub trait DebugTarget {
fn cpu_count(&self) -> usize;
fn cpu_path(&self, cpu: usize) -> TargetResult<&str>;
fn arch(&self, cpu: usize) -> TargetResult<&'static Arch>;
fn read_registers(&self, cpu: usize) -> TargetResult<Vec<u8>>;
fn write_registers(&mut self, cpu: usize, data: &[u8]) -> TargetResult<()>;
fn read_register(&self, cpu: usize, index: usize) -> TargetResult<Vec<u8>>;
fn write_register(&mut self, cpu: usize, index: usize, data: &[u8]) -> TargetResult<()>;
fn read_memory(&self, cpu: usize, addr: u64, dst: &mut [u8]) -> TargetResult<()>;
fn write_memory(&mut self, cpu: usize, addr: u64, src: &[u8]) -> TargetResult<()>;
fn add_breakpoint(&mut self, addr: u64, hardware: bool) -> TargetResult<()>;
fn remove_breakpoint(&mut self, addr: u64, hardware: bool) -> TargetResult<()>;
fn watch_support(&self) -> WatchSupport {
WatchSupport::default()
}
fn add_watchpoint(&mut self, _cpu: usize, _addr: u64, _len: u64) -> TargetResult<()> {
Err(TargetError::Unsupported)
}
fn remove_watchpoint(&mut self, _cpu: usize, _addr: u64, _len: u64) -> TargetResult<()> {
Err(TargetError::Unsupported)
}
fn step(&mut self, cpu: usize) -> TargetResult<Stop>;
fn begin_resume(&mut self) {}
fn resume(&mut self) -> TargetResult<Option<Stop>>;
fn monitor(&mut self, _cpu: usize, _command: &str) -> Option<String> {
None
}
}
const FREE_SLICE: GlobalTime = GlobalTime::from_nanos(10_000_000);
const FINE_TICKS: u32 = 4096;
const TRANSLATION_GRANULE: u64 = 1024;
const MAX_TICKS_PER_INSN: u32 = 4096;
#[derive(Debug)]
struct Cpu {
device: usize,
path: String,
class: &'static DeviceClass,
arch: &'static Arch,
domain: crate::core::clock::DomainId,
requester: RequesterId,
space: Option<usize>,
}
#[derive(Debug)]
struct Watch {
cpu: usize,
addr: u64,
len: u64,
shadow: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Breakpoint {
addr: u64,
hardware: bool,
}
#[derive(Debug)]
pub struct MachineTarget<'a> {
machine: &'a mut Machine,
cpus: Vec<Cpu>,
breakpoints: Vec<Breakpoint>,
watchpoints: Vec<Watch>,
suppress: Vec<Option<u64>>,
}
impl<'a> MachineTarget<'a> {
#[must_use]
pub fn new(machine: &'a mut Machine) -> MachineTarget<'a> {
let mut cpus = Vec::new();
for (index, entry) in machine.devices().iter().enumerate() {
let Some(arch) = super::arch::for_class(entry.class().name) else {
continue;
};
let Some(domain) = entry.domain() else {
continue;
};
cpus.push(Cpu {
device: index,
path: entry.path().to_string(),
class: entry.class(),
arch,
domain,
requester: entry.requester(),
space: entry.space_index(),
});
}
let suppress = vec![None; cpus.len()];
MachineTarget {
machine,
cpus,
breakpoints: Vec::new(),
watchpoints: Vec::new(),
suppress,
}
}
#[must_use]
pub fn machine(&self) -> &Machine {
self.machine
}
pub fn read_physical(&self, cpu: usize, addr: u64, dst: &mut [u8]) -> TargetResult<()> {
let entry = self.cpu(cpu)?;
let space = self.space(entry)?;
space
.read_bytes(addr, dst, debug_attrs(entry.requester))
.map_err(|_| TargetError::Fault)
}
pub fn write_physical(&mut self, cpu: usize, addr: u64, src: &[u8]) -> TargetResult<()> {
{
let entry = self.cpu(cpu)?;
let space = self.space(entry)?;
space
.write_bytes(addr, src, debug_attrs(entry.requester))
.map_err(|_| TargetError::Fault)?;
}
self.resync_watchpoints();
Ok(())
}
pub fn machine_mut(&mut self) -> &mut Machine {
self.machine
}
fn cpu(&self, index: usize) -> TargetResult<&Cpu> {
self.cpus.get(index).ok_or(TargetError::NoSuchCpu)
}
fn space(&self, cpu: &Cpu) -> TargetResult<&AddressSpace> {
let index = cpu.space.ok_or(TargetError::Fault)?;
self.machine
.spaces()
.get(index)
.map(|entry| entry.space().as_ref())
.ok_or(TargetError::Fault)
}
pub fn translate(&self, cpu: usize, va: u64) -> TargetResult<u64> {
let device = self.cpu(cpu)?.device;
let entry = self
.machine
.devices()
.get(device)
.ok_or(TargetError::NoSuchCpu)?;
entry
.device()
.debug_translate(va)
.phys(va)
.ok_or(TargetError::Unmapped)
}
fn chunks(&self, cpu: usize, addr: u64, len: usize) -> TargetResult<Vec<(u64, usize, usize)>> {
let mut out = Vec::new();
let mut at = 0usize;
while at < len {
let va = addr.wrapping_add(at as u64);
let to_boundary = TRANSLATION_GRANULE - (va & (TRANSLATION_GRANULE - 1));
let run = to_boundary.min((len - at) as u64) as usize;
out.push((self.translate(cpu, va)?, at, run));
at += run;
}
Ok(out)
}
fn chunk(&self, cpu: &Cpu) -> TargetResult<Vec<u8>> {
if !cpu.arch.check() {
return Err(TargetError::LayoutMismatch {
class: cpu.class.name,
expected: cpu.arch.verified_version,
found: cpu.class.version,
});
}
let entry = self
.machine
.devices()
.get(cpu.device)
.ok_or(TargetError::NoSuchCpu)?;
let mut writer = StateWriter::new(MachineShape::new());
{
let mut chunk = writer.chunk(&cpu.path, cpu.class.name, cpu.class.version)?;
entry.device().save(&mut chunk)?;
}
let bytes = writer.to_vec()?;
let reader = StateReader::new(&bytes)?;
let (_, _, data) = reader.load_raw(&cpu.path)?;
if data.len() < cpu.arch.chunk_reach() {
return Err(TargetError::LayoutMismatch {
class: cpu.class.name,
expected: cpu.arch.verified_version,
found: cpu.class.version,
});
}
Ok(data.to_vec())
}
fn set_chunk(&mut self, cpu: usize, data: &[u8]) -> TargetResult<()> {
let device = self.cpu(cpu)?.device;
let entry = self
.machine
.devices()
.get(device)
.ok_or(TargetError::NoSuchCpu)?;
let mut reader = ChunkReader::new(data);
entry.device().load(&mut reader)?;
Ok(())
}
fn field(chunk: &[u8], offset: usize, bytes: usize) -> TargetResult<u64> {
let slice = chunk
.get(offset..offset.checked_add(bytes).ok_or(TargetError::Fault)?)
.ok_or(TargetError::Fault)?;
let mut value: u64 = 0;
for (i, byte) in slice.iter().enumerate() {
value |= u64::from(*byte) << (i * 8);
}
Ok(value)
}
fn pc_of(&self, index: usize) -> TargetResult<u64> {
let cpu = self.cpu(index)?;
let chunk = self.chunk(cpu)?;
let reg = cpu
.arch
.regs
.get(cpu.arch.pc)
.ok_or(TargetError::NoSuchRegister)?;
Self::field(&chunk, reg.offset, reg.bytes)
}
fn retired(&self, index: usize) -> TargetResult<Option<u64>> {
let cpu = self.cpu(index)?;
let Some(counter) = cpu.arch.retire else {
return Ok(None);
};
let chunk = self.chunk(cpu)?;
Self::field(&chunk, counter.offset, counter.bytes).map(Some)
}
fn tick(&mut self) -> TargetResult<()> {
let now = self.machine.now();
let mut deadline: Option<GlobalTime> = None;
{
let forest = self.machine.clocks();
for cpu in &self.cpus {
let Ok(tick) = forest.ticks(cpu.domain) else {
continue;
};
let mut ahead = 1u64;
while let Ok(at) =
forest.global_time_of_tick(cpu.domain, tick.saturating_add(ahead))
{
if at > now {
deadline = Some(match deadline {
Some(best) if best <= at => best,
_ => at,
});
break;
}
ahead += 1;
if ahead > 1024 {
break;
}
}
}
}
let deadline = deadline.unwrap_or_else(|| now.saturating_add(FREE_SLICE));
self.machine.step_until(deadline)?;
Ok(())
}
fn poll_watchpoints(&mut self) -> TargetResult<Option<(usize, u64)>> {
if self.watchpoints.is_empty() {
return Ok(None);
}
let mut hit = None;
for i in 0..self.watchpoints.len() {
let (cpu, addr, len) = {
let watch = &self.watchpoints[i];
(watch.cpu, watch.addr, watch.len)
};
let mut now = vec![0u8; usize::try_from(len).unwrap_or(0)];
if self.read_memory(cpu, addr, &mut now).is_err() {
continue;
}
let watch = &mut self.watchpoints[i];
if watch.shadow != now {
watch.shadow = now;
if hit.is_none() {
hit = Some((cpu, addr));
}
}
}
Ok(hit)
}
fn resync_watchpoints(&mut self) {
for i in 0..self.watchpoints.len() {
let (cpu, addr, len) = {
let watch = &self.watchpoints[i];
(watch.cpu, watch.addr, watch.len)
};
let mut now = vec![0u8; usize::try_from(len).unwrap_or(0)];
if self.read_memory(cpu, addr, &mut now).is_ok() {
self.watchpoints[i].shadow = now;
}
}
}
fn breakpoint_hit(&mut self) -> TargetResult<Option<Stop>> {
if self.breakpoints.is_empty() {
return Ok(None);
}
for index in 0..self.cpus.len() {
let pc = self.pc_of(index)?;
if self.suppress.get(index).copied().flatten() == Some(pc) {
continue;
}
if let Some(slot) = self.suppress.get_mut(index) {
*slot = None;
}
if let Some(point) = self.breakpoints.iter().find(|b| b.addr == pc) {
return Ok(Some(Stop {
cpu: index,
kind: StopKind::Breakpoint {
hardware: point.hardware,
},
}));
}
}
Ok(None)
}
}
fn perms_text(perms: crate::core::space::Perms) -> String {
let bit = |set: bool, c: char| if set { c } else { '-' };
[
bit(perms.contains(crate::core::space::Perms::READ), 'r'),
bit(perms.contains(crate::core::space::Perms::WRITE), 'w'),
bit(perms.contains(crate::core::space::Perms::EXEC), 'x'),
]
.into_iter()
.collect()
}
const MONITOR_HELP: &str = "\
rsemu monitor commands (addresses are hex, lengths decimal):
devices the device tree, with class and instance path
spaces the machine's address spaces
map [space] what is mapped where, for this CPU's space or a named one
x <addr> [len] read guest memory at a VIRTUAL address, through this CPU
xp <addr> [len] read guest memory at a PHYSICAL address, no translation
translate <addr> where this CPU's MMU maps a virtual address
time the machine's current virtual instant
hash the machine state hash (ROADMAP.md \u{a7}0)
Every read here sets MemAttrs::debug, so nothing it looks at changes.
";
const MONITOR_DUMP_DEFAULT: u64 = 64;
const MONITOR_DUMP_MAX: u64 = 1024;
impl MachineTarget<'_> {
fn monitor_addr(text: Option<&str>) -> Result<u64, String> {
let text = text.ok_or_else(|| String::from("an address is needed\n"))?;
let body = text.strip_prefix("0x").or_else(|| text.strip_prefix("0X"));
u64::from_str_radix(body.unwrap_or(text), 16)
.map_err(|_| format!("`{text}` is not a hex address\n"))
}
fn monitor_len(text: Option<&str>) -> Result<u64, String> {
let Some(text) = text else {
return Ok(MONITOR_DUMP_DEFAULT);
};
let len: u64 = text
.parse()
.map_err(|_| format!("`{text}` is not a length\n"))?;
if len == 0 || len > MONITOR_DUMP_MAX {
return Err(format!("a length must be 1..={MONITOR_DUMP_MAX}\n"));
}
Ok(len)
}
fn monitor_dump(
&self,
cpu: usize,
addr: Option<&str>,
len: Option<&str>,
physical: bool,
) -> String {
let (addr, len) = match (Self::monitor_addr(addr), Self::monitor_len(len)) {
(Ok(a), Ok(l)) => (a, l),
(Err(e), _) | (_, Err(e)) => return e,
};
let mut buf = vec![0u8; len as usize];
let read = if physical {
self.read_physical(cpu, addr, &mut buf)
} else {
self.read_memory(cpu, addr, &mut buf)
};
if let Err(e) = read {
return format!("{e}\n");
}
let mut out = String::new();
for (row, chunk) in buf.chunks(16).enumerate() {
let at = addr.wrapping_add(row as u64 * 16);
let _ = write!(out, "{at:08x} ");
for byte in chunk {
let _ = write!(out, " {byte:02x}");
}
for _ in chunk.len()..16 {
out.push_str(" ");
}
out.push_str(" |");
for byte in chunk {
out.push(if byte.is_ascii_graphic() || *byte == b' ' {
char::from(*byte)
} else {
'.'
});
}
out.push_str("|\n");
}
out
}
fn monitor_translate(&self, cpu: usize, addr: Option<&str>) -> String {
let addr = match Self::monitor_addr(addr) {
Ok(a) => a,
Err(e) => return e,
};
match self.translate(cpu, addr) {
Ok(pa) if pa == addr => format!("{addr:#x} -> {pa:#x} (identity)\n"),
Ok(pa) => format!("{addr:#x} -> {pa:#x}\n"),
Err(e) => format!("{addr:#x}: {e}\n"),
}
}
fn monitor_map(&self, cpu: usize, name: Option<&str>) -> String {
let index = match name {
Some(name) => {
match self
.machine
.spaces()
.iter()
.position(|entry| entry.name() == name)
{
Some(i) => i,
None => return format!("no address space named `{name}`\n"),
}
}
None => match self.cpu(cpu).ok().and_then(|entry| entry.space) {
Some(i) => i,
None => return String::from("this CPU has no address space\n"),
},
};
let Some(entry) = self.machine.spaces().get(index) else {
return String::from("no such address space\n");
};
let space = entry.space();
let Some(view) = space.try_view() else {
return String::from("the address space is being rebuilt; try again\n");
};
let mut rows: Vec<(u64, u64, String, String)> = view
.mappings()
.map(|(_, m)| {
(
m.base,
m.region.len(),
m.region.name().to_string(),
perms_text(m.perms),
)
})
.collect();
rows.sort_by_key(|(base, len, _, _)| (*base, *len));
let mut out = format!("{} ({} bits)\n", entry.name(), space.bits());
for (base, len, name, perms) in rows {
let _ = writeln!(
out,
" {base:#014x}-{:#014x} {name} {perms}",
base.saturating_add(len).saturating_sub(1)
);
}
out
}
}
impl DebugTarget for MachineTarget<'_> {
fn cpu_count(&self) -> usize {
self.cpus.len()
}
fn cpu_path(&self, cpu: usize) -> TargetResult<&str> {
Ok(self.cpu(cpu)?.path.as_str())
}
fn arch(&self, cpu: usize) -> TargetResult<&'static Arch> {
Ok(self.cpu(cpu)?.arch)
}
fn read_registers(&self, cpu: usize) -> TargetResult<Vec<u8>> {
let entry = self.cpu(cpu)?;
let chunk = self.chunk(entry)?;
let mut out = Vec::with_capacity(entry.arch.packet_len());
for reg in entry.arch.regs {
let slice = chunk
.get(reg.offset..reg.offset + reg.bytes)
.ok_or(TargetError::NoSuchRegister)?;
out.extend_from_slice(slice);
}
Ok(out)
}
fn write_registers(&mut self, cpu: usize, data: &[u8]) -> TargetResult<()> {
let entry = self.cpu(cpu)?;
if data.len() != entry.arch.packet_len() {
return Err(TargetError::NoSuchRegister);
}
let mut chunk = self.chunk(entry)?;
let mut at = 0usize;
for reg in entry.arch.regs {
let src = data.get(at..at + reg.bytes).ok_or(TargetError::Fault)?;
let dst = chunk
.get_mut(reg.offset..reg.offset + reg.bytes)
.ok_or(TargetError::Fault)?;
dst.copy_from_slice(src);
at += reg.bytes;
}
self.set_chunk(cpu, &chunk)
}
fn read_register(&self, cpu: usize, index: usize) -> TargetResult<Vec<u8>> {
let entry = self.cpu(cpu)?;
let reg = entry
.arch
.regs
.get(index)
.ok_or(TargetError::NoSuchRegister)?;
let chunk = self.chunk(entry)?;
chunk
.get(reg.offset..reg.offset + reg.bytes)
.map(<[u8]>::to_vec)
.ok_or(TargetError::NoSuchRegister)
}
fn write_register(&mut self, cpu: usize, index: usize, data: &[u8]) -> TargetResult<()> {
let entry = self.cpu(cpu)?;
let reg = *entry
.arch
.regs
.get(index)
.ok_or(TargetError::NoSuchRegister)?;
if data.len() != reg.bytes {
return Err(TargetError::NoSuchRegister);
}
let mut chunk = self.chunk(entry)?;
let dst = chunk
.get_mut(reg.offset..reg.offset + reg.bytes)
.ok_or(TargetError::Fault)?;
dst.copy_from_slice(data);
self.set_chunk(cpu, &chunk)
}
fn read_memory(&self, cpu: usize, addr: u64, dst: &mut [u8]) -> TargetResult<()> {
let entry = self.cpu(cpu)?;
let space = self.space(entry)?;
let attrs = debug_attrs(entry.requester);
for (pa, at, len) in self.chunks(cpu, addr, dst.len())? {
space
.read_bytes(pa, &mut dst[at..at + len], attrs)
.map_err(|_| TargetError::Fault)?;
}
Ok(())
}
fn write_memory(&mut self, cpu: usize, addr: u64, src: &[u8]) -> TargetResult<()> {
{
let entry = self.cpu(cpu)?;
let space = self.space(entry)?;
let attrs = debug_attrs(entry.requester);
for (pa, at, len) in self.chunks(cpu, addr, src.len())? {
space
.write_bytes(pa, &src[at..at + len], attrs)
.map_err(|_| TargetError::Fault)?;
}
}
self.resync_watchpoints();
Ok(())
}
fn add_breakpoint(&mut self, addr: u64, hardware: bool) -> TargetResult<()> {
let point = Breakpoint { addr, hardware };
if !self.breakpoints.contains(&point) {
self.breakpoints.push(point);
}
Ok(())
}
fn remove_breakpoint(&mut self, addr: u64, hardware: bool) -> TargetResult<()> {
self.breakpoints
.retain(|b| !(b.addr == addr && b.hardware == hardware));
Ok(())
}
fn watch_support(&self) -> WatchSupport {
WatchSupport {
write: true,
read: false,
access: false,
}
}
fn add_watchpoint(&mut self, cpu: usize, addr: u64, len: u64) -> TargetResult<()> {
if len == 0 || len > 4096 {
return Err(TargetError::Unsupported);
}
self.cpu(cpu)?;
if self
.watchpoints
.iter()
.any(|w| w.cpu == cpu && w.addr == addr && w.len == len)
{
return Ok(());
}
let mut shadow = vec![0u8; usize::try_from(len).map_err(|_| TargetError::Unsupported)?];
self.read_memory(cpu, addr, &mut shadow)?;
self.watchpoints.push(Watch {
cpu,
addr,
len,
shadow,
});
Ok(())
}
fn remove_watchpoint(&mut self, cpu: usize, addr: u64, len: u64) -> TargetResult<()> {
self.watchpoints
.retain(|w| !(w.cpu == cpu && w.addr == addr && w.len == len));
Ok(())
}
fn step(&mut self, cpu: usize) -> TargetResult<Stop> {
let before_pc = self.pc_of(cpu)?;
let before_retired = self.retired(cpu)?;
for _ in 0..MAX_TICKS_PER_INSN {
self.tick()?;
let moved = match before_retired {
Some(before) => self.retired(cpu)? != Some(before),
None => self.pc_of(cpu)? != before_pc,
};
if moved {
break;
}
}
if let Some(slot) = self.suppress.get_mut(cpu) {
*slot = None;
}
if let Some((watched, addr)) = self.poll_watchpoints()? {
return Ok(Stop {
cpu: watched,
kind: StopKind::Watchpoint { addr },
});
}
Ok(Stop {
cpu,
kind: StopKind::Trap,
})
}
fn begin_resume(&mut self) {
for index in 0..self.cpus.len() {
let here = self.pc_of(index).ok();
if let Some(slot) = self.suppress.get_mut(index) {
*slot = here;
}
}
}
fn resume(&mut self) -> TargetResult<Option<Stop>> {
if self.breakpoints.is_empty() && self.watchpoints.is_empty() {
let deadline = self.machine.now().saturating_add(FREE_SLICE);
self.machine.run_until(deadline)?;
return Ok(None);
}
for _ in 0..FINE_TICKS {
self.tick()?;
if let Some(stop) = self.breakpoint_hit()? {
return Ok(Some(stop));
}
if let Some((cpu, addr)) = self.poll_watchpoints()? {
return Ok(Some(Stop {
cpu,
kind: StopKind::Watchpoint { addr },
}));
}
}
Ok(None)
}
fn monitor(&mut self, cpu: usize, command: &str) -> Option<String> {
let mut words = command.split_whitespace();
match words.next()? {
"help" => Some(String::from(MONITOR_HELP)),
"devices" => {
let mut out = String::new();
for entry in self.machine.devices() {
out.push_str(entry.path());
out.push_str(" ");
out.push_str(entry.class().name);
out.push('\n');
}
Some(out)
}
"spaces" => {
let mut out = String::new();
for entry in self.machine.spaces() {
let space = entry.space();
let _ = writeln!(
out,
"{} {} bits, {} bytes",
entry.name(),
space.bits(),
space.size()
);
}
Some(out)
}
"map" => Some(self.monitor_map(cpu, words.next())),
"x" => Some(self.monitor_dump(cpu, words.next(), words.next(), false)),
"xp" => Some(self.monitor_dump(cpu, words.next(), words.next(), true)),
"translate" => Some(self.monitor_translate(cpu, words.next())),
"time" => Some(format!("{} ns\n", self.machine.now().as_nanos())),
"hash" => Some(match self.machine.state_hash() {
Ok(hash) => format!("{hash:#018x}\n"),
Err(e) => format!("cannot hash state: {e}\n"),
}),
_ => None,
}
}
}