mod wire;
#[cfg(unix)]
use libc::{SIGHUP, SIGINT, SIGTERM, c_int, sighandler_t, signal as install_signal};
#[cfg(test)]
use std::cell::RefCell;
use std::collections::HashMap;
#[cfg(test)]
use std::io::Cursor;
use std::io::{self, Write};
use std::mem::replace;
use std::net::{Ipv4Addr, SocketAddr, TcpListener};
use std::path::PathBuf;
#[cfg(test)]
use std::rc::Rc;
use std::result;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, Receiver};
use std::sync::{LazyLock, OnceLock};
use std::time::Duration;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use serde_json::{Value, json};
use crate::backend::MemoryOps;
use crate::dbg_backend::{DebugCapability, WatchpointAccess, validate_hw_breakpoint};
use crate::disasm::{
decode_preceding, fallthrough_run_end, instruction_length, max_instruction_bytes,
};
use crate::error::{Error, Result};
use crate::expr::{Expr, ExprType, ExprValue, NumberRadix};
use crate::gdb::BreakpointConfig;
use crate::gdb::breakpoints::Breakpoint;
use crate::kd::KdMemorySource;
use crate::output;
use crate::repl::{DispatchContext, Flow, RemoteClient, ReplState, ReplStore, supports_capability};
use crate::session::{ContinueOutcome, Session};
use crate::symbols::{
FieldInfo, LocalVariableLocation, ParsedType, ProcedureLocal, SourceLocation,
parse_source_paths, parse_symbol_sources,
};
use crate::target::{SelectedFrame, Target};
use crate::triage_report::exception_code_name;
use crate::types::VirtAddr;
use crate::typeview::{Expand, FieldView, TypeView, find_field};
use crate::{Backend, TargetSpec};
use wire::{ClientMessage, Request};
const IDLE_TICK: Duration = Duration::from_millis(20);
const RUN_POLL: Duration = Duration::from_millis(50);
const STACK_FRAME_LIMIT: usize = 256;
const STEP_LINE_BUDGET: usize = 4096;
const MAX_LINE_SPAN: usize = 4096;
static TERMINATION: LazyLock<Arc<AtomicBool>> = LazyLock::new(|| Arc::new(AtomicBool::new(false)));
#[cfg(unix)]
extern "C" fn note_termination(_signal: c_int) {
TERMINATION.store(true, Ordering::SeqCst);
if let Some(cancel) = RUN_CANCEL.get() {
cancel.store(true, Ordering::SeqCst);
}
}
static RUN_CANCEL: OnceLock<Arc<AtomicBool>> = OnceLock::new();
fn install_termination_handler(cancel: &Arc<AtomicBool>) -> Arc<AtomicBool> {
let flag = Arc::clone(&TERMINATION);
let _ = RUN_CANCEL.set(Arc::clone(cancel));
#[cfg(unix)]
for signal in [SIGTERM, SIGHUP, SIGINT] {
unsafe {
install_signal(signal, note_termination as *const () as sighandler_t);
}
}
flag
}
const VARIABLES_BASE: i64 = 1 << 20;
const MAX_READ_MEMORY: usize = 1024 * 1024;
const MAX_VARIABLE_PAGE: usize = 1024;
const MAX_DISASSEMBLE_INSTRUCTIONS: usize = 4096;
#[derive(Clone, Copy, PartialEq, Eq)]
enum RunState {
Halted,
Running,
Detached,
}
struct FrameRef {
thread: i64,
index: usize,
ip: u64,
sp: u64,
frame_base: Option<u64>,
registers: HashMap<String, u64>,
seed_registers: HashMap<String, u64>,
}
#[derive(Clone, PartialEq, Eq, Hash)]
enum VarRef {
Locals(usize),
Registers(usize),
Fields {
type_name: String,
address: VirtAddr,
},
Elements {
element: ParsedType,
count: u32,
element_size: usize,
address: VirtAddr,
},
}
impl From<Expand> for VarRef {
fn from(expand: Expand) -> Self {
match expand {
Expand::Fields { type_name, address } => Self::Fields { type_name, address },
Expand::Elements {
element,
count,
element_size,
address,
} => Self::Elements {
element,
count,
element_size,
address,
},
}
}
}
struct StopInfo {
reason: &'static str,
description: String,
exception_id: Option<String>,
detail: Option<String>,
breakpoint_id: Option<u32>,
}
struct Server {
session: Option<Session>,
repl: Option<ReplStore>,
out: Box<dyn Write>,
rx: Receiver<ClientMessage>,
seq: i64,
state: RunState,
threads: Vec<String>,
frames: Vec<FrameRef>,
vars: Vec<VarRef>,
var_refs: HashMap<VarRef, i64>,
source_breakpoints: HashMap<String, Vec<Vec<u32>>>,
function_breakpoints: Vec<u32>,
instruction_breakpoints: Vec<u32>,
data_breakpoints: Vec<u32>,
last_stop: Option<StopInfo>,
verified: HashMap<u32, BreakpointState>,
debug_seq: u64,
cancel: Arc<AtomicBool>,
lines_start_at_1: bool,
columns_start_at_1: bool,
terminating: Arc<AtomicBool>,
supports_invalidated: bool,
configured: bool,
done: bool,
}
pub fn run(spec: Option<TargetSpec>, port: Option<u16>) -> Result<()> {
let (tx, rx) = mpsc::channel();
let cancel = Arc::new(AtomicBool::new(false));
let out: Box<dyn Write> = match port {
Some(port) => {
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, port));
let listener = TcpListener::bind(addr)
.map_err(|error| Error::DebugInfo(format!("failed to bind {addr}: {error}")))?;
let addr = listener
.local_addr()
.map_err(|error| Error::DebugInfo(error.to_string()))?;
eprintln!("ntoseye-dap: listening on {addr}");
let (stream, peer) = listener
.accept()
.map_err(|error| Error::DebugInfo(format!("accept failed: {error}")))?;
eprintln!("ntoseye-dap: client connected from {peer}");
let reader = stream
.try_clone()
.map_err(|error| Error::DebugInfo(error.to_string()))?;
wire::spawn_reader(reader, tx, Arc::clone(&cancel));
Box::new(stream)
}
None => {
wire::spawn_reader(io::stdin(), tx, Arc::clone(&cancel));
Box::new(io::stdout())
}
};
let session = match spec {
Some(spec) => Some(Session::open_with_progress(&spec, &mut |line| {
eprintln!("{line}")
})?),
None => None,
};
let mut server = Server::new(session, out, rx, Arc::clone(&cancel));
server.terminating = install_termination_handler(&cancel);
server.serve();
if let Some(session) = server.session.as_mut()
&& let Err(error) = session.cleanup_for_exit()
{
eprintln!("ntoseye-dap: cleanup failed: {error}");
}
Ok(())
}
impl Server {
fn new(
session: Option<Session>,
out: Box<dyn Write>,
rx: Receiver<ClientMessage>,
cancel: Arc<AtomicBool>,
) -> Self {
Self {
session,
repl: None,
out,
rx,
seq: 0,
state: RunState::Halted,
threads: Vec::new(),
frames: Vec::new(),
vars: Vec::new(),
var_refs: HashMap::new(),
source_breakpoints: HashMap::new(),
function_breakpoints: Vec::new(),
instruction_breakpoints: Vec::new(),
data_breakpoints: Vec::new(),
last_stop: None,
verified: HashMap::new(),
debug_seq: 0,
cancel,
lines_start_at_1: true,
columns_start_at_1: true,
terminating: Arc::new(AtomicBool::new(false)),
supports_invalidated: false,
configured: false,
done: false,
}
}
fn serve(&mut self) {
while !self.done {
if self.terminating.load(Ordering::SeqCst) {
self.detach_on_signal();
return;
}
match self.rx.recv_timeout(IDLE_TICK) {
Ok(message) => self.consume(message),
Err(mpsc::RecvTimeoutError::Timeout) => self.service(),
Err(mpsc::RecvTimeoutError::Disconnected) => return,
}
}
while let Ok(message) = self.rx.try_recv() {
if let ClientMessage::Message(message) = message
&& let Some(request) = Request::from_message(&message)
&& request.command == "disconnect"
{
self.respond(&request, Ok(None));
}
}
}
fn detach_on_signal(&mut self) {
eprintln!("ntoseye-dap: termination signal received; detaching");
self.shutdown_session();
self.send_event("terminated", json!({}));
self.done = true;
}
fn consume(&mut self, message: ClientMessage) {
match message {
ClientMessage::Message(message) => {
if let Some(request) = Request::from_message(&message) {
self.dispatch(request);
}
}
ClientMessage::Eof => self.done = true,
ClientMessage::Error(error) => {
eprintln!("ntoseye-dap: {error}");
self.done = true;
}
}
}
fn service(&mut self) {
if self.session.is_none() || !self.configured {
return;
}
match self.state {
RunState::Running => {
let cancel = Arc::clone(&self.cancel);
let outcome = self
.session
.as_mut()
.map(|session| session.wait_for_stop_bounded(Some(RUN_POLL), &cancel));
match outcome {
Some(Ok(ContinueOutcome::Running)) | None => {}
Some(Ok(outcome)) => self.report_stop(outcome),
Some(Err(error)) => {
self.emit_output("stderr", format!("target wait failed: {error}\n"));
let rip = self
.session
.as_mut()
.and_then(|session| session.run_status().rip)
.unwrap_or(0);
self.report_stop(ContinueOutcome::Halted { rip });
}
}
}
RunState::Halted => {
if let Some(session) = self.session.as_mut() {
session.service_idle();
}
}
RunState::Detached => {}
}
self.drain_debug_output();
}
fn next_seq(&mut self) -> i64 {
self.seq += 1;
self.seq
}
fn send(&mut self, message: Value) {
if wire::write_message(&mut self.out, &message).is_err() {
self.done = true;
}
}
fn send_event(&mut self, event: &str, body: Value) {
let seq = self.next_seq();
self.send(json!({"seq": seq, "type": "event", "event": event, "body": body}));
}
fn respond(&mut self, request: &Request, result: result::Result<Option<Value>, String>) {
let seq = self.next_seq();
let message = match result {
Ok(body) => {
let mut response = json!({
"seq": seq,
"type": "response",
"request_seq": request.seq,
"success": true,
"command": request.command,
});
if let Some(body) = body {
response["body"] = body;
}
response
}
Err(message) => json!({
"seq": seq,
"type": "response",
"request_seq": request.seq,
"success": false,
"command": request.command,
"message": message,
}),
};
self.send(message);
}
fn emit_output(&mut self, category: &str, text: impl Into<String>) {
self.send_event(
"output",
json!({"category": category, "output": text.into()}),
);
}
fn drain_debug_output(&mut self) {
let Some(session) = self.session.as_mut() else {
return;
};
for notice in session.take_notices() {
self.emit_output("important", format!("{notice}\n"));
}
let Some(session) = self.session.as_ref() else {
return;
};
let page = session.read_debug_output(self.debug_seq);
if page.lines.is_empty() && !page.dropped {
return;
}
self.debug_seq = page.next_seq;
if page.dropped {
self.emit_output(
"important",
"guest debug output overflowed; lines were dropped\n",
);
}
for line in page.lines {
self.emit_output("stdout", format!("{}\n", line.text));
}
}
fn shutdown_session(&mut self) {
let Some(mut session) = self.session.take() else {
return;
};
match session.cleanup_for_exit() {
Ok(()) => self.emit_output("console", "ntoseye: detached; guest resumed\n"),
Err(error) => {
let message = format!("ntoseye: detach failed, guest may still be halted: {error}");
eprintln!("{message}");
self.emit_output("important", format!("{message}\n"));
}
}
self.state = RunState::Detached;
}
fn session(&mut self) -> result::Result<&mut Session, String> {
self.session
.as_mut()
.ok_or_else(|| "no target is attached; send a launch or attach request".to_string())
}
fn dispatch(&mut self, request: Request) {
let result = match request.command.as_str() {
"initialize" => self.on_initialize(&request.arguments),
"launch" | "attach" => self.on_attach(&request.arguments),
"configurationDone" => self.on_configuration_done(&request),
"disconnect" => {
let released = !matches!(self.state, RunState::Detached);
self.shutdown_session();
self.done = true;
if released {
self.send_event("terminated", json!({}));
}
Ok(None)
}
"terminate" => {
self.shutdown_session();
self.send_event("terminated", json!({}));
Ok(None)
}
"threads" => self.on_threads(),
"stackTrace" => self.on_stack_trace(&request.arguments),
"scopes" => self.on_scopes(&request.arguments),
"variables" => self.on_variables(&request.arguments),
"setVariable" => self.on_set_variable(&request.arguments),
"evaluate" => self.on_evaluate(&request.arguments),
"continue" => self.on_continue(&request),
"pause" => self.on_pause(&request),
"next" => self.on_step(&request, StepMode::Over),
"stepIn" => self.on_step(&request, StepMode::Into),
"stepOut" => self.on_step(&request, StepMode::Out),
"setBreakpoints" => self.on_set_breakpoints(&request.arguments),
"setFunctionBreakpoints" => self.on_set_function_breakpoints(&request.arguments),
"setInstructionBreakpoints" => self.on_set_instruction_breakpoints(&request.arguments),
"dataBreakpointInfo" => self.on_data_breakpoint_info(&request.arguments),
"setDataBreakpoints" => self.on_set_data_breakpoints(&request.arguments),
"setExceptionBreakpoints" => Self::on_set_exception_breakpoints(&request.arguments),
"exceptionInfo" => self.on_exception_info(),
"readMemory" => self.on_read_memory(&request.arguments),
"writeMemory" => self.on_write_memory(&request.arguments),
"disassemble" => self.on_disassemble(&request.arguments),
"modules" => self.on_modules(&request.arguments),
other => Err(format!("unsupported request '{other}'")),
};
if matches!(
request.command.as_str(),
"configurationDone" | "continue" | "pause" | "next" | "stepIn" | "stepOut"
) {
if let Err(message) = result {
self.respond(&request, Err(message));
}
return;
}
self.respond(&request, result);
}
fn on_initialize(&mut self, args: &Value) -> Handled {
self.lines_start_at_1 = arg_bool(args, "linesStartAt1").unwrap_or(true);
self.columns_start_at_1 = arg_bool(args, "columnsStartAt1").unwrap_or(true);
self.supports_invalidated = arg_bool(args, "supportsInvalidatedEvent").unwrap_or(false);
Ok(Some(capabilities()))
}
fn on_attach(&mut self, args: &Value) -> Handled {
if self.session.is_some() {
self.apply_symbol_arguments(args)?;
self.send_event("initialized", json!({}));
self.emit_output(
"console",
"ntoseye: already attached from the command line; launch/attach arguments ignored\n",
);
return Ok(None);
}
let spec = target_spec(args)?;
let session = Session::open_with_progress(&spec, &mut |line| eprintln!("{line}"))
.map_err(|error| error.to_string())?;
self.session = Some(session);
self.repl = None;
self.debug_seq = 0;
self.apply_symbol_arguments(args)?;
self.send_event("initialized", json!({}));
Ok(None)
}
fn apply_symbol_arguments(&mut self, args: &Value) -> result::Result<(), String> {
let symbol_paths = arg_strings(args, "symbolPath");
let source_paths = arg_strings(args, "sourcePath");
if symbol_paths.is_empty() && source_paths.is_empty() {
return Ok(());
}
let session = self.session()?;
for source in parse_symbol_sources(&symbol_paths) {
session.target.symbols.append_symbol_source(source);
}
for mapping in parse_source_paths(&source_paths) {
session.target.symbols.append_source_path(mapping);
}
if symbol_paths.is_empty() {
return Ok(());
}
let report = session
.target
.reload_module_symbols(None)
.map_err(|error| error.to_string())?;
let message = format!(
"ntoseye: symbols reloaded from the configured path: {}/{} modules loaded, {} failed\n",
report.loaded, report.total, report.failed
);
self.emit_output("console", message);
Ok(())
}
fn on_configuration_done(&mut self, request: &Request) -> Handled {
self.configured = true;
let status = self.session()?.run_status();
let running = status.running;
let rip = status.rip.unwrap_or(0);
self.respond(request, Ok(None));
if running {
self.state = RunState::Running;
} else {
self.state = RunState::Halted;
self.report_stop(ContinueOutcome::Halted { rip });
}
Ok(None)
}
fn on_continue(&mut self, request: &Request) -> Handled {
let result = self.session()?.resume().map_err(|error| error.to_string());
match result {
Ok(()) => {
self.invalidate_stop_state();
self.state = RunState::Running;
self.respond(request, Ok(Some(json!({"allThreadsContinued": true}))));
self.send_event(
"continued",
json!({"threadId": 1, "allThreadsContinued": true}),
);
}
Err(message) => self.respond(request, Err(message)),
}
Ok(None)
}
fn on_pause(&mut self, request: &Request) -> Handled {
self.cancel.store(false, Ordering::Relaxed);
if matches!(self.state, RunState::Halted) {
self.respond(request, Ok(None));
return Ok(None);
}
let result = self
.session()?
.interrupt_outcome()
.map_err(|error| error.to_string());
match result {
Ok(outcome) => {
self.respond(request, Ok(None));
let forced = matches!(
outcome,
ContinueOutcome::Stopped { .. } | ContinueOutcome::Halted { .. }
)
.then_some("pause");
self.report_stop_as(outcome, forced);
}
Err(message) => self.respond(request, Err(message)),
}
Ok(None)
}
fn on_step(&mut self, request: &Request, mode: StepMode) -> Handled {
let instruction_granularity =
arg_str(&request.arguments, "granularity").as_deref() == Some("instruction");
if let Some(thread) = arg_i64(&request.arguments, "threadId")
&& let Err(message) = self.select_thread(thread)
{
self.respond(request, Err(message));
return Ok(None);
}
self.invalidate_stop_state();
let outcome = self.run_step(mode, instruction_granularity);
match outcome {
Ok(outcome) => {
self.respond(request, Ok(None));
self.report_stop(outcome);
}
Err(message) => {
self.respond(request, Err(message));
if let Some(rip) = self
.session
.as_mut()
.and_then(|session| session.run_status().rip)
{
self.report_stop(ContinueOutcome::Halted { rip });
}
}
}
Ok(None)
}
fn run_step(
&mut self,
mode: StepMode,
instruction_granularity: bool,
) -> result::Result<ContinueOutcome, String> {
let cancel = Arc::clone(&self.cancel);
if mode == StepMode::Out {
let session = self.session()?;
return session.step_out(&cancel).map_err(|error| error.to_string());
}
let (mut rip, start) = {
let session = self.session()?;
let rip = session.run_status().rip;
let start = rip
.and_then(|rip| session.target.source_location(VirtAddr(rip)))
.map(|location| (location.file, location.line));
(rip.unwrap_or(0), start)
};
if instruction_granularity || start.is_none() {
return self.step_once(mode);
}
for _ in 0..STEP_LINE_BUDGET {
let outcome = self.advance_within_line(mode, rip)?;
if !matches!(outcome, ContinueOutcome::Step { .. }) {
return Ok(outcome);
}
if self.cancel.load(Ordering::Relaxed) {
return Ok(outcome);
}
let session = self.session()?;
rip = session.run_status().rip.unwrap_or(0);
match session.target.source_location(VirtAddr(rip)) {
None => return Ok(outcome),
Some(location) => {
if start
.as_ref()
.is_none_or(|(file, line)| location.line != *line || location.file != *file)
{
return Ok(outcome);
}
}
}
}
Ok(ContinueOutcome::Step { rip })
}
fn advance_within_line(
&mut self,
mode: StepMode,
rip: u64,
) -> result::Result<ContinueOutcome, String> {
let Some(end) = self.coalescible_line_end(rip)? else {
return self.step_once(mode);
};
let cancel = Arc::clone(&self.cancel);
match self.session()?.run_to(end, &cancel) {
Ok(outcome) => Ok(outcome),
Err(_) => self.step_once(mode),
}
}
fn coalescible_line_end(&mut self, rip: u64) -> result::Result<Option<VirtAddr>, String> {
let session = self.session()?;
let Some(end) = session
.target
.source_line_extent(VirtAddr(rip))
.and_then(|extent| extent.end)
.filter(|end| end.0 > rip)
else {
return Ok(None);
};
let Some(span) = usize::try_from(end.0 - rip)
.ok()
.filter(|span| *span <= MAX_LINE_SPAN)
else {
return Ok(None);
};
let mut bytes = vec![0u8; span];
if session.read_masked(VirtAddr(rip), &mut bytes).is_err() {
return Ok(None);
}
let arch = session.target.arch();
let Some(run_end) = fallthrough_run_end(&bytes, rip, end.0, arch) else {
return Ok(None);
};
if instruction_length(&bytes, arch) == Some(run_end.saturating_sub(rip) as usize) {
return Ok(None);
}
Ok(Some(VirtAddr(run_end)))
}
fn step_once(&mut self, mode: StepMode) -> result::Result<ContinueOutcome, String> {
let cancel = Arc::clone(&self.cancel);
let session = self.session()?;
match mode {
StepMode::Into => {
session.step().map_err(|error| error.to_string())?;
Ok(ContinueOutcome::Step {
rip: session.run_status().rip.unwrap_or(0),
})
}
StepMode::Over => session
.step_over(&cancel)
.map_err(|error| error.to_string()),
StepMode::Out => session.step_out(&cancel).map_err(|error| error.to_string()),
}
}
fn select_thread(&mut self, thread: i64) -> result::Result<(), String> {
self.session()?;
if self.threads.is_empty() {
self.sync_threads();
}
let Some(backend_id) = self.backend_thread_id(thread) else {
return Err(format!("unknown thread id {thread}"));
};
let session = self.session()?;
if session.current_thread == backend_id {
return Ok(());
}
session
.set_current_thread(&backend_id)
.map_err(|error| error.to_string())
}
fn backend_thread_id(&self, thread: i64) -> Option<String> {
usize::try_from(thread)
.ok()
.and_then(|index| index.checked_sub(1))
.and_then(|index| self.threads.get(index))
.cloned()
}
fn dap_thread_id(&self, backend_id: &str) -> i64 {
self.threads
.iter()
.position(|id| id == backend_id)
.map(|index| index as i64 + 1)
.unwrap_or(1)
}
fn invalidate_stop_state(&mut self) {
self.frames.clear();
self.vars.clear();
self.var_refs.clear();
if let Some(session) = self.session.as_mut() {
session.target.selected_frame = None;
}
}
fn report_stop(&mut self, outcome: ContinueOutcome) {
self.report_stop_as(outcome, None);
}
fn reconcile_breakpoints(&mut self) {
let Some(session) = self.session.as_mut() else {
return;
};
session.refresh_modules_on_stop();
let tracked: Vec<u32> = self
.source_breakpoints
.values()
.flatten()
.flatten()
.copied()
.chain(self.function_breakpoints.iter().copied())
.chain(self.instruction_breakpoints.iter().copied())
.chain(self.data_breakpoints.iter().copied())
.collect();
for id in tracked {
let Some(session) = self.session.as_ref() else {
return;
};
let Some(breakpoint) = session.breakpoint(id) else {
self.verified.remove(&id);
continue;
};
let state = BreakpointState::from(breakpoint);
if self.verified.insert(id, state) == Some(state) {
continue;
}
let Some(breakpoint) = self.session.as_ref().and_then(|s| s.breakpoint(id)) else {
continue;
};
let body = json!({"reason": "changed", "breakpoint": breakpoint_json(breakpoint, 1)});
self.send_event("breakpoint", body);
}
}
fn report_stop_as(&mut self, outcome: ContinueOutcome, forced_reason: Option<&'static str>) {
let (outcome, forced_reason) = match outcome {
ContinueOutcome::Running => {
let halted_rip = self
.session
.as_mut()
.filter(|session| !session.backend.is_running())
.and_then(|session| session.run_status().rip);
match halted_rip {
Some(rip) => (ContinueOutcome::Halted { rip }, Some("pause")),
None => {
self.invalidate_stop_state();
self.state = RunState::Running;
return;
}
}
}
outcome => (outcome, forced_reason),
};
self.invalidate_stop_state();
self.state = RunState::Halted;
self.cancel.store(false, Ordering::Relaxed);
self.drain_debug_output();
self.reconcile_breakpoints();
let mut action = None;
let stop = match outcome {
ContinueOutcome::Running => unreachable!("a running target was reported above"),
ContinueOutcome::Breakpoint {
id,
address,
symbol,
rip,
condition_error,
action: breakpoint_action,
..
} => {
action = breakpoint_action;
let where_ = symbol.unwrap_or_else(|| format!("{rip:#x}"));
if let Some(error) = condition_error {
self.emit_output(
"important",
format!("breakpoint {id} condition failed: {error}\n"),
);
}
StopInfo {
reason: "breakpoint",
description: format!("breakpoint {id} at {where_}"),
exception_id: None,
detail: Some(format!("address {address:#x}")),
breakpoint_id: Some(id),
}
}
ContinueOutcome::Step { rip } => StopInfo {
reason: "step",
description: format!("step to {rip:#x}"),
exception_id: None,
detail: None,
breakpoint_id: None,
},
ContinueOutcome::Halted { rip } => StopInfo {
reason: "entry",
description: format!("halted at {rip:#x}"),
exception_id: None,
detail: None,
breakpoint_id: None,
},
ContinueOutcome::Stopped {
rip,
exception_code,
first_chance,
exception_address,
} => match exception_code {
Some(code) => {
let name = exception_code_name(code);
let chance = match first_chance {
Some(true) => " (first chance)",
Some(false) => " (second chance)",
None => "",
};
StopInfo {
reason: "exception",
description: format!("{name} ({code:#x}){chance} at {rip:#x}"),
exception_id: Some(format!("{code:#010x}")),
detail: exception_address
.map(|address| format!("exception record address {address:#x}")),
breakpoint_id: None,
}
}
None => StopInfo {
reason: "pause",
description: format!("halted at {rip:#x}"),
exception_id: None,
detail: None,
breakpoint_id: None,
},
},
ContinueOutcome::Bugcheck { rip, info } => {
let detail = match &info {
Some(info) => format!(
"bugcheck {:#x} ({:#x}, {:#x}, {:#x}, {:#x})",
info.code,
info.parameters[0],
info.parameters[1],
info.parameters[2],
info.parameters[3]
),
None => "bugcheck (code unavailable from the transport)".to_string(),
};
self.emit_output("important", format!("{detail}\n"));
self.emit_output(
"console",
"run `!analyze -v` in the debug console for the full triage\n",
);
StopInfo {
reason: "exception",
description: match rip {
Some(rip) => format!("{detail} at {rip:#x}"),
None => detail.clone(),
},
exception_id: Some("bugcheck".to_string()),
detail: Some(detail),
breakpoint_id: None,
}
}
ContinueOutcome::TargetReloaded {
kernel_base,
coherent,
} => {
let base = kernel_base
.map(|base| format!("{base:#x}"))
.unwrap_or_else(|| "unknown".to_string());
self.emit_output(
"important",
format!(
"target rebooted; nt base {base}{}\n",
if coherent {
""
} else {
" (early boot: module and process enumeration not yet available)"
}
),
);
StopInfo {
reason: "entry",
description: format!("target rebooted (nt {base})"),
exception_id: None,
detail: None,
breakpoint_id: None,
}
}
};
if let Some(action) = action {
self.run_breakpoint_action(&action);
if self
.session
.as_ref()
.is_some_and(|session| session.backend.is_running())
{
self.state = RunState::Running;
self.last_stop = Some(stop);
return;
}
}
self.sync_threads();
let thread_id = self
.session
.as_ref()
.map(|session| session.current_thread.clone())
.map(|id| self.dap_thread_id(&id))
.unwrap_or(1);
let mut body = json!({
"reason": forced_reason.unwrap_or(stop.reason),
"threadId": thread_id,
"allThreadsStopped": true,
"description": stop.description,
"preserveFocusHint": false,
});
if stop.reason == "breakpoint"
&& let Some(detail) = &stop.detail
{
body["text"] = json!(detail);
}
if let Some(id) = stop.breakpoint_id {
body["hitBreakpointIds"] = json!([id]);
}
self.last_stop = Some(stop);
self.send_event("stopped", body);
}
fn run_breakpoint_action(&mut self, action: &str) {
let Some(session) = self.session.as_mut() else {
return;
};
let store = ReplStore::new(session, DispatchContext::BreakpointAction);
let mut state = ReplState::attach(session, store);
state.line = action.to_string();
let (result, text) = output::capture(|| state.dispatch_breakpoint_action(action));
drop(state.detach());
if !text.is_empty() {
self.emit_output("stdout", text);
}
match result {
Ok(true) => {
if let Some(session) = self.session.as_mut()
&& let Err(error) = session.resume()
{
self.emit_output(
"stderr",
format!("breakpoint action could not resume the target: {error}\n"),
);
}
}
Ok(false) => {}
Err(error) => {
self.emit_output("stderr", format!("breakpoint action failed: {error}\n"));
}
}
}
fn sync_threads(&mut self) {
let Some(session) = self.session.as_mut() else {
return;
};
let ids: Vec<String> = match session.vcpus() {
Ok(vcpus) => vcpus.into_iter().map(|vcpu| vcpu.id).collect(),
Err(_) => vec![session.current_thread.clone()],
};
self.set_threads(ids);
}
fn set_threads(&mut self, ids: Vec<String>) {
if ids == self.threads {
return;
}
let previous = replace(&mut self.threads, ids);
for index in previous.len()..self.threads.len() {
self.send_event(
"thread",
json!({"reason": "started", "threadId": index as i64 + 1}),
);
}
for index in (self.threads.len()..previous.len()).rev() {
self.send_event(
"thread",
json!({"reason": "exited", "threadId": index as i64 + 1}),
);
}
}
fn on_threads(&mut self) -> Handled {
if matches!(self.state, RunState::Running) {
let threads = self
.threads
.iter()
.enumerate()
.map(|(index, id)| json!({"id": index as i64 + 1, "name": format!("{id} (running)")}))
.collect::<Vec<_>>();
return Ok(Some(json!({"threads": threads})));
}
let session = self.session()?;
let vcpus = session.vcpus().map_err(|error| error.to_string())?;
self.set_threads(vcpus.iter().map(|vcpu| vcpu.id.clone()).collect());
let threads = vcpus
.iter()
.enumerate()
.map(|(index, vcpu)| {
let mut name = vcpu.id.clone();
if !vcpu.context.is_empty() {
name.push_str(&format!(" [{}]", vcpu.context));
}
match (&vcpu.symbol, vcpu.rip, &vcpu.error) {
(Some(symbol), _, _) => name.push_str(&format!(" {symbol}")),
(None, Some(rip), _) => name.push_str(&format!(" {rip:#x}")),
(None, None, Some(error)) => name.push_str(&format!(" <{error}>")),
_ => {}
}
json!({"id": index as i64 + 1, "name": name})
})
.collect::<Vec<_>>();
Ok(Some(json!({"threads": threads})))
}
fn on_stack_trace(&mut self, args: &Value) -> Handled {
let thread = arg_i64(args, "threadId").unwrap_or(1);
self.select_thread(thread)?;
if !self.frames.iter().any(|frame| frame.thread == thread) {
self.build_frames(thread)?;
}
let start = arg_i64(args, "startFrame").unwrap_or(0).max(0) as usize;
let levels = arg_i64(args, "levels").unwrap_or(0).max(0) as usize;
let own: Vec<usize> = self
.frames
.iter()
.enumerate()
.filter(|(_, frame)| frame.thread == thread)
.map(|(handle, _)| handle)
.collect();
let total = own.len();
let end = if levels == 0 {
total
} else {
(start + levels).min(total)
};
let mut frames = Vec::new();
for handle in own.iter().skip(start).take(end.saturating_sub(start)) {
frames.push(self.frame_value(*handle));
}
Ok(Some(json!({"stackFrames": frames, "totalFrames": total})))
}
fn build_frames(&mut self, thread: i64) -> result::Result<(), String> {
let session = self.session()?;
let (recovered, seed) = session
.recovered_backtrace(STACK_FRAME_LIMIT)
.map_err(|error| error.to_string())?;
for (index, frame) in recovered.frames.iter().enumerate() {
self.frames.push(FrameRef {
thread,
index,
ip: frame.frame.ip,
sp: frame.frame.sp,
frame_base: frame.frame_base,
registers: frame.registers.clone(),
seed_registers: seed.clone(),
});
}
Ok(())
}
fn frame_value(&mut self, handle: usize) -> Value {
let (ip, name) = {
let session = self.session.as_ref();
let frame = &self.frames[handle];
let name = session
.map(|session| {
session
.target
.closest_symbol_current_context(VirtAddr(frame.ip))
})
.unwrap_or(None)
.unwrap_or_else(|| format!("{:#x}", frame.ip));
(frame.ip, name)
};
let location = self
.session
.as_ref()
.and_then(|session| session.target.source_location(VirtAddr(ip)));
let mut value = json!({
"id": handle as i64 + 1,
"name": name,
"line": 0,
"column": 0,
"instructionPointerReference": format!("{ip:#x}"),
});
if let Some(location) = &location {
value["line"] = json!(self.to_client_line(location.line as i64));
value["column"] = json!(
location
.column
.map(|column| self.to_client_column(column as i64))
.unwrap_or(0)
);
value["source"] = source_value(location);
}
value
}
fn on_scopes(&mut self, args: &Value) -> Handled {
let handle = self.frame_handle(args, "frameId")?;
let locals = self.var_ref(VarRef::Locals(handle));
let registers = self.var_ref(VarRef::Registers(handle));
Ok(Some(json!({"scopes": [
{
"name": "Locals",
"presentationHint": "locals",
"variablesReference": locals,
"expensive": false,
},
{
"name": "Registers",
"presentationHint": "registers",
"variablesReference": registers,
"expensive": false,
}
]})))
}
fn frame_handle(&self, args: &Value, key: &str) -> result::Result<usize, String> {
let id = arg_i64(args, key).ok_or_else(|| format!("missing {key}"))?;
usize::try_from(id)
.ok()
.and_then(|id| id.checked_sub(1))
.filter(|handle| *handle < self.frames.len())
.ok_or_else(|| format!("stale frame id {id}; re-request the stack trace"))
}
fn select_named_frame(&mut self, args: &Value, key: &str) -> result::Result<(), String> {
if args.get(key).is_none() {
return Ok(());
}
let handle = self.frame_handle(args, key)?;
self.select_frame(handle)
}
fn var_ref(&mut self, reference: VarRef) -> i64 {
if let Some(existing) = self.var_refs.get(&reference) {
return *existing;
}
self.vars.push(reference.clone());
let id = VARIABLES_BASE + self.vars.len() as i64 - 1;
self.var_refs.insert(reference, id);
id
}
fn var_index(&self, reference: i64) -> result::Result<usize, String> {
reference
.checked_sub(VARIABLES_BASE)
.and_then(|index| usize::try_from(index).ok())
.filter(|index| *index < self.vars.len())
.ok_or_else(|| format!("stale variablesReference {reference}"))
}
fn on_variables(&mut self, args: &Value) -> Handled {
let reference = arg_i64(args, "variablesReference")
.ok_or_else(|| "missing variablesReference".to_string())?;
let index = self.var_index(reference)?;
let filter = arg_str(args, "filter").unwrap_or_default();
let start = arg_i64(args, "start").unwrap_or(0).max(0) as usize;
let requested = arg_i64(args, "count").unwrap_or(0).max(0) as usize;
match self.vars[index].clone() {
VarRef::Registers(handle) if filter != "indexed" => {
let rows = self.register_variables(handle)?;
Self::page_rows(rows, start, requested)
}
VarRef::Locals(handle) if filter != "indexed" => {
let rows = self.local_variables(handle)?;
Self::page_rows(rows, start, requested)
}
VarRef::Fields { type_name, address } if filter != "indexed" => {
let rows = self.field_variables(&type_name, address)?;
Self::page_rows(rows, start, requested)
}
VarRef::Elements {
element,
count,
element_size,
address,
} if filter != "named" => {
let window = match requested {
0 => MAX_VARIABLE_PAGE,
requested => requested.min(MAX_VARIABLE_PAGE),
};
self.element_variables(&element, count, element_size, address, start, window)
}
_ => Ok(Some(json!({"variables": []}))),
}
}
fn page_rows(body: Option<Value>, start: usize, count: usize) -> Handled {
if start == 0 && count == 0 {
return Ok(body);
}
let Some(mut body) = body else {
return Ok(None);
};
let Some(rows) = body["variables"].as_array() else {
return Ok(Some(body));
};
let windowed: Vec<Value> = match count {
0 => rows.iter().skip(start).cloned().collect(),
count => rows.iter().skip(start).take(count).cloned().collect(),
};
body["variables"] = Value::Array(windowed);
Ok(Some(body))
}
fn register_variables(&mut self, handle: usize) -> Handled {
self.select_frame(handle)?;
let frame_index = self.frames[handle].index;
let snapshot = self.frames[handle].registers.clone();
let session = self.session()?;
let live_context = frame_index == 0 && session.parked_windows_thread().is_none();
let values = match live_context.then(|| session.read_registers()) {
Some(Ok(registers)) => session.register_map.to_hashmap(®isters),
_ => snapshot,
};
let mut variables = Vec::new();
for name in session.register_map.names().iter() {
let Some(value) = values.get(name.as_str()) else {
continue;
};
variables.push(json!({
"name": name,
"value": format!("{value:#018x}"),
"variablesReference": 0,
"presentationHint": {"kind": "data", "attributes": ["rawString"]},
}));
}
Ok(Some(json!({"variables": variables})))
}
fn local_variables(&mut self, handle: usize) -> Handled {
let ip = self.frames[handle].ip;
self.select_frame(handle)?;
let views = {
let session = self.session()?;
let locals = session
.target
.procedure_locals(VirtAddr(ip))
.map_err(|error| error.to_string())?;
let Some(locals) = locals else {
return Ok(Some(json!({"variables": []})));
};
let view = TypeView::new(session);
locals
.iter()
.map(|local| {
let address = session.target.procedure_local_address(local).map(VirtAddr);
let (value, expand) = match address {
Some(address) => {
let field = FieldInfo {
offset: 0,
size: local.byte_size.unwrap_or_default(),
type_data: local.type_data.clone(),
};
let (text, raw) = view.value_and_raw(address, &field);
(text, view.expand_for(&local.type_data, Some(address), raw))
}
None => {
let value = session
.target
.resolve_procedure_local_value(VirtAddr(ip), local);
let expand = view.expand_for(&local.type_data, None, value);
(local_value_text(local, value, expand.as_ref()), expand)
}
};
let field = FieldView {
name: local.name.clone(),
type_name: local.type_name.clone(),
address,
value,
expand,
};
(field, local.is_parameter)
})
.collect::<Vec<_>>()
};
let variables = self.variable_rows(views);
Ok(Some(json!({"variables": variables})))
}
fn field_variables(&mut self, type_name: &str, address: VirtAddr) -> Handled {
let views = {
let session = self.session()?;
let view = TypeView::new(session);
let type_info = view
.lookup_type(type_name)
.ok_or_else(|| format!("type '{type_name}' is not in the loaded symbols"))?;
view.fields(type_info.as_ref(), address)
.into_iter()
.map(|field| (field, false))
.collect::<Vec<_>>()
};
let variables = self.variable_rows(views);
Ok(Some(json!({"variables": variables})))
}
fn element_variables(
&mut self,
element: &ParsedType,
count: u32,
element_size: usize,
address: VirtAddr,
start: usize,
window: usize,
) -> Handled {
let views = {
let session = self.session()?;
let view = TypeView::new(session);
view.elements_from(address, element, count, element_size, start, window)
.into_iter()
.map(|field| (field, false))
.collect::<Vec<_>>()
};
let variables = self.variable_rows(views);
Ok(Some(json!({"variables": variables})))
}
fn variable_rows(&mut self, views: Vec<(FieldView, bool)>) -> Vec<Value> {
let mut variables = Vec::with_capacity(views.len());
for (field, parameter) in views {
let value = if field.value.is_empty() {
match &field.expand {
Some(Expand::Fields { .. }) => "{...}".to_string(),
Some(Expand::Elements { count, .. }) => format!("[{count}]"),
None => String::new(),
}
} else {
field.value
};
let indexed = match &field.expand {
Some(Expand::Elements { count, .. }) => Some(*count),
_ => None,
};
let reference = match field.expand {
Some(expand) => self.var_ref(VarRef::from(expand)),
None => 0,
};
let mut variable = json!({
"name": field.name,
"type": field.type_name,
"value": value,
"variablesReference": reference,
"presentationHint": {
"kind": if parameter { "property" } else { "data" },
},
});
if let Some(address) = field.address {
variable["memoryReference"] = json!(format!("{:#x}", address.0));
}
if let Some(count) = indexed {
variable["indexedVariables"] = json!(count);
}
variables.push(variable);
}
variables
}
fn refresh_live_frame(&mut self, handle: usize) {
if self.frames[handle].index != 0 {
return;
}
let Some(session) = self.session.as_mut() else {
return;
};
let Ok(registers) = session.read_registers() else {
return;
};
let values = session.register_map.to_hashmap(®isters);
self.frames[handle].registers = values.clone();
self.frames[handle].seed_registers = values;
}
fn select_frame(&mut self, handle: usize) -> result::Result<(), String> {
self.select_thread(self.frames[handle].thread)?;
self.refresh_live_frame(handle);
let seed_live = self
.session
.as_ref()
.is_some_and(|session| session.parked_windows_thread().is_none());
let frame = &self.frames[handle];
let selected = SelectedFrame {
index: frame.index,
ip: frame.ip,
sp: frame.sp,
frame_base: frame.frame_base,
registers: frame.registers.clone(),
seed_registers: frame.seed_registers.clone(),
seed_live,
};
if let Some(session) = self.session.as_mut() {
session.select_frame(selected);
}
Ok(())
}
fn on_set_variable(&mut self, args: &Value) -> Handled {
let reference = arg_i64(args, "variablesReference")
.ok_or_else(|| "missing variablesReference".to_string())?;
let name = arg_str(args, "name").ok_or_else(|| "missing name".to_string())?;
let expression = arg_str(args, "value").ok_or_else(|| "missing value".to_string())?;
let index = self.var_index(reference)?;
let target = self.vars[index].clone();
if let VarRef::Locals(handle) | VarRef::Registers(handle) = target {
self.select_frame(handle)?;
}
let value = self.evaluate_expression(&expression)?;
match target {
VarRef::Registers(handle) => {
if self.frames[handle].index != 0 {
return Err(
"caller-frame registers are recovered from unwind metadata and are not writable"
.to_string(),
);
}
let session = self.session()?;
if session.parked_windows_thread().is_some() {
return Err(
"a parked Windows thread's registers are recovered from its saved context \
and are not writable; select a vCPU with `.thread` first"
.to_string(),
);
}
session
.write_register(&name, value)
.map_err(|error| error.to_string())?;
self.refresh_live_frame(handle);
Ok(Some(json!({"value": format!("{value:#018x}")})))
}
VarRef::Locals(handle) => {
let ip = self.frames[handle].ip;
let live_frame = self.frames[handle].index == 0;
let session = self.session()?;
let locals = session
.target
.procedure_locals(VirtAddr(ip))
.map_err(|error| error.to_string())?
.unwrap_or_default();
let local = locals
.iter()
.find(|local| local.name == name)
.ok_or_else(|| format!("no local named '{name}' in scope"))?;
let size = local
.byte_size
.and_then(|size| usize::try_from(size).ok())
.filter(|size| *size > 0 && *size <= 8)
.ok_or_else(|| {
format!("'{name}' is not a scalar this adapter can write in place")
})?;
match &local.location {
LocalVariableLocation::Register { register } => {
if !live_frame {
return Err(format!(
"'{name}' lives in a register recovered from unwind metadata for a \
caller frame; writing it would change the live register instead"
));
}
let register = register.clone();
session
.write_register(®ister, value)
.map_err(|error| error.to_string())?;
}
_ => {
let address = session
.target
.procedure_local_address(local)
.ok_or_else(|| format!("'{name}' has no resolvable address"))?;
let bytes = value.to_le_bytes();
session
.target
.current_process()
.map_err(|error| error.to_string())?
.memory()
.write_bytes(VirtAddr(address), &bytes[..size])
.map_err(|error| error.to_string())?;
}
}
self.refresh_live_frame(handle);
let session = self.session()?;
let refreshed = session
.target
.procedure_locals(VirtAddr(ip))
.ok()
.flatten()
.unwrap_or_default();
let view = TypeView::new(session);
let text = refreshed
.iter()
.find(|local| local.name == name)
.map(|local| {
match session.target.procedure_local_address(local).map(VirtAddr) {
Some(address) => {
let field = FieldInfo {
offset: 0,
size: local.byte_size.unwrap_or_default(),
type_data: local.type_data.clone(),
};
view.value_text(address, &field)
}
None => {
let value = session
.target
.resolve_procedure_local_value(VirtAddr(ip), local);
let expand = view.expand_for(&local.type_data, None, value);
local_value_text(local, value, expand.as_ref())
}
}
})
.unwrap_or_else(|| format!("{value:#x}"));
Ok(Some(json!({"value": text})))
}
VarRef::Fields { type_name, address } => {
let (field_address, size, field) = {
let session = self.session()?;
let view = TypeView::new(session);
let type_info = view.lookup_type(&type_name).ok_or_else(|| {
format!("type '{type_name}' is not in the loaded symbols")
})?;
let (field_name, field) = find_field(type_info.as_ref(), &name)
.ok_or_else(|| format!("no field named '{name}' in {type_name}"))?;
if let ParsedType::Bitfield { .. } = field.type_data {
return Err(format!(
"'{field_name}' is a bitfield; set it with 'eb'/'ed' on the containing \
value in the console"
));
}
let size = view.field_size(field);
if !(1..=8).contains(&size) {
return Err(format!(
"'{field_name}' is {size} bytes; write it with 'eb' in the console"
));
}
let field_address = address + u64::from(field.offset);
(field_address, size, field.clone())
};
self.write_scalar(field_address, size, value)?;
let session = self.session()?;
let view = TypeView::new(session);
Ok(Some(
json!({"value": view.value_text(field_address, &field)}),
))
}
VarRef::Elements {
element,
count,
element_size,
address,
} => {
let index = element_index(&name)?;
if index >= count {
return Err(format!(
"element {index} is past the end of a [{count}] array"
));
}
if !(1..=8).contains(&element_size) {
return Err(format!(
"elements are {element_size} bytes; write them with 'eb' in the console"
));
}
let element_address =
address + u64::from(index) * u64::try_from(element_size).unwrap_or(1);
self.write_scalar(element_address, element_size, value)?;
let field = FieldInfo {
offset: 0,
size: element_size as u64,
type_data: element,
};
let session = self.session()?;
let view = TypeView::new(session);
Ok(Some(
json!({"value": view.value_text(element_address, &field)}),
))
}
}
}
fn write_scalar(
&mut self,
address: VirtAddr,
size: usize,
value: u64,
) -> result::Result<(), String> {
let bytes = value.to_le_bytes();
self.session()?
.target
.current_process()
.map_err(|error| error.to_string())?
.memory()
.write_bytes(address, &bytes[..size])
.map_err(|error| error.to_string())
}
fn on_evaluate(&mut self, args: &Value) -> Handled {
let expression = arg_str(args, "expression").unwrap_or_default();
if expression.trim().is_empty() {
return Ok(Some(json!({"result": "", "variablesReference": 0})));
}
let context = arg_str(args, "context").unwrap_or_else(|| "repl".to_string());
self.select_named_frame(args, "frameId")?;
if context == "repl" {
let before = self.inspection_context();
let text = self.run_console_command(&expression)?;
if self.inspection_context() != before {
self.invalidate_context();
}
return Ok(Some(json!({
"result": text,
"variablesReference": 0,
})));
}
let (expr, value) = self.parse_and_evaluate(&expression)?;
let (result, type_name, memory_reference, expansion, indexed_variables) = {
let session = self.session()?;
let type_data = value.type_data().cloned();
let byte_size = value.byte_size();
let scalar = value.scalar(&session.target);
let storage = value.address().ok();
let register = direct_register_expression(&expr, &session.target);
let pointer_value = matches!(type_data, Some(ParsedType::Pointer(_)));
let view = TypeView::new(session);
let expansion = type_data.as_ref().and_then(|type_data| {
view.expand_for_with_size(
type_data,
storage,
scalar.as_ref().ok().map(|value| value.0),
byte_size,
)
});
let indexed_variables = match expansion.as_ref() {
Some(Expand::Elements { count, .. }) => Some(*count),
_ => None,
};
let result = match (&type_data, scalar.as_ref(), storage) {
(Some(type_data), Ok(value), _) => view.scalar_text(value.0, type_data, byte_size),
(Some(type_data), Err(_), Some(address)) => {
let field = FieldInfo {
offset: 0,
size: byte_size.unwrap_or_default(),
type_data: type_data.clone(),
};
view.value_text(address, &field)
}
(Some(_), Err(error), None) => format!("<unavailable: {error}>"),
(None, Ok(value), _) => format!("{:#x} ({})", value.0, value.0),
(None, Err(error), _) => return Err(error.to_string()),
};
let result = if result.is_empty() {
match expansion.as_ref() {
Some(Expand::Fields { .. }) => "{...}".to_string(),
Some(Expand::Elements { count, .. }) => format!("[{count}]"),
None => result,
}
} else {
result
};
let memory_reference = storage.or_else(|| {
(!register && (type_data.is_none() || pointer_value))
.then(|| scalar.as_ref().ok().copied())
.flatten()
});
(
result,
type_data.map(|type_data| type_data.to_string()),
memory_reference,
expansion,
indexed_variables,
)
};
let reference = expansion
.map(|expansion| self.var_ref(VarRef::from(expansion)))
.unwrap_or(0);
let mut response = json!({
"result": result,
"variablesReference": reference,
});
if let Some(type_name) = type_name {
response["type"] = json!(type_name);
}
if let Some(memory_reference) = memory_reference {
response["memoryReference"] = json!(format!("{:#x}", memory_reference.0));
}
if let Some(indexed_variables) = indexed_variables {
response["indexedVariables"] = json!(indexed_variables);
}
Ok(Some(response))
}
fn parse_and_evaluate(
&mut self,
expression: &str,
) -> result::Result<(Expr, ExprValue), String> {
let radix = self.repl_radix();
let expr = Expr::parse_with_radix(expression, radix).map_err(|error| error.to_string())?;
let session = self.session()?;
let value = expr
.evaluate(&session.target)
.map_err(|error| error.to_string())?;
Ok((expr, value))
}
fn evaluate_expression(&mut self, expression: &str) -> result::Result<u64, String> {
let radix = self.repl_radix();
let session = self.session()?;
Expr::eval_with_radix(expression, &session.target, radix)
.map(|address| address.0)
.map_err(|error| error.to_string())
}
fn repl_radix(&mut self) -> NumberRadix {
self.repl
.as_ref()
.map(ReplStore::radix)
.unwrap_or(NumberRadix::Hexadecimal)
}
fn inspection_context(&mut self) -> (Option<u64>, u64, String) {
let Some(session) = self.session.as_mut() else {
return (None, 0, String::new());
};
(
session
.parked_windows_thread()
.map(|thread| thread.ethread.0),
session.target.current_dtb(),
session.current_thread.clone(),
)
}
fn invalidate_context(&mut self) {
self.invalidate_stop_state();
if self.supports_invalidated {
self.send_event(
"invalidated",
json!({"areas": ["stacks", "variables", "registers"]}),
);
return;
}
self.emit_output(
"console",
"ntoseye: inspection context changed; this client does not support the invalidated \
event, so the call stack and variables panes refresh at the next stop\n",
);
}
fn run_console_command(&mut self, line: &str) -> result::Result<String, String> {
let session = self
.session
.as_mut()
.ok_or_else(|| "no target is attached".to_string())?;
let store = self
.repl
.take()
.unwrap_or_else(|| ReplStore::new(session, DispatchContext::Remote(RemoteClient::Dap)));
let mut state = ReplState::attach(session, store);
state.line = line.trim().to_string();
let (result, text) = output::capture(|| state.dispatch_line(line));
self.repl = Some(state.detach());
match result {
Ok(Flow::Continue | Flow::Quit) => Ok(text),
Ok(Flow::Denied) => Err(if text.is_empty() {
"the debug console cannot move the target; use the client's run controls"
.to_string()
} else {
text
}),
Err(error) => Err(if text.is_empty() {
error.to_string()
} else {
format!("{text}{error}")
}),
}
}
fn on_set_breakpoints(&mut self, args: &Value) -> Handled {
let source = args.get("source").cloned().unwrap_or_else(|| json!({}));
let key = arg_str(&source, "path")
.or_else(|| arg_str(&source, "name"))
.ok_or_else(|| "source breakpoints need a path or name".to_string())?;
let requested = args
.get("breakpoints")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let lines: Vec<Option<i64>> = requested
.iter()
.map(|entry| arg_i64(entry, "line"))
.collect();
let plan = requested
.iter()
.zip(&lines)
.map(|(entry, line)| {
let line = line.ok_or_else(|| "breakpoint without a line".to_string())?;
let config = breakpoint_config(entry)?;
let spec = self.source_spec(&key, self.to_source_line(line));
Ok(Install::Source(spec, config))
})
.collect();
let previous: Vec<u32> = self
.source_breakpoints
.get(&key)
.into_iter()
.flatten()
.flatten()
.copied()
.collect();
let results = self.install_breakpoints(previous, plan)?;
self.source_breakpoints.remove(&key);
let mut installed = Vec::with_capacity(results.len());
let mut response = Vec::with_capacity(results.len());
for (result, line) in results.into_iter().zip(lines) {
match result {
Ok(ids) => {
response.push(self.breakpoint_value(&ids, line));
installed.push(ids);
}
Err(message) => response.push(refused_breakpoint(message, line)),
}
}
self.source_breakpoints.insert(key, installed);
Ok(Some(json!({"breakpoints": response})))
}
fn source_spec(&mut self, path: &str, line: u32) -> String {
let matched_full = self
.session
.as_ref()
.is_some_and(|session| !session.target.source_addresses(path, line).is_empty());
if matched_full {
return format!("{path}:{line}");
}
let base = file_stem_of(path);
format!("{base}:{line}")
}
fn on_set_function_breakpoints(&mut self, args: &Value) -> Handled {
let requested = args
.get("breakpoints")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let plan = requested
.iter()
.map(|entry| {
let name = arg_str(entry, "name")
.ok_or_else(|| "breakpoint without a name".to_string())?;
let config = BreakpointConfig {
skip_prologue: true,
..breakpoint_config(entry)?
};
Ok(Install::Symbol(name, config))
})
.collect();
self.replace_owned_set(OwnedSet::Function, plan)
}
fn on_set_instruction_breakpoints(&mut self, args: &Value) -> Handled {
let requested = args
.get("breakpoints")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let plan = requested
.iter()
.map(|entry| {
let address = arg_str(entry, "instructionReference")
.ok_or_else(|| "breakpoint without an instructionReference".to_string())
.and_then(|reference| parse_address(&reference))?
.wrapping_add_signed(arg_i64(entry, "offset").unwrap_or(0));
let config = breakpoint_config(entry)?;
Ok(Install::Address(address, config))
})
.collect();
self.replace_owned_set(OwnedSet::Instruction, plan)
}
fn on_data_breakpoint_info(&mut self, args: &Value) -> Handled {
let name = arg_str(args, "name").ok_or_else(|| "missing name".to_string())?;
let watchpoints = {
let session = self.session()?;
let capabilities = session.capabilities();
supports_capability(&capabilities, DebugCapability::Watchpoints)
};
if !watchpoints {
return Ok(Some(json!({
"dataId": Value::Null,
"description": "this backend does not support data watchpoints",
})));
}
self.select_named_frame(args, "frameId")?;
let resolved = match arg_i64(args, "variablesReference") {
Some(reference) if reference >= VARIABLES_BASE => self.data_target(reference, &name),
_ => self.data_expression_target(&name),
};
let (address, size) = match resolved {
Ok(resolved) => resolved,
Err(message) => {
return Ok(Some(json!({
"dataId": Value::Null,
"description": message,
})));
}
};
let len = watch_length(size);
if let Err(error) = validate_hw_breakpoint(WatchpointAccess::Write.into(), len, address) {
return Ok(Some(json!({
"dataId": Value::Null,
"description": error.to_string(),
})));
}
Ok(Some(json!({
"dataId": format!("{address:#x}:{len}"),
"description": format!("{len} byte(s) at {address:#x}"),
"accessTypes": ["write", "readWrite"],
"canPersist": false,
})))
}
fn data_expression_target(&mut self, expression: &str) -> result::Result<(u64, usize), String> {
let (expr, value) = self.parse_and_evaluate(expression)?;
let session = self.session()?;
match value.address() {
Ok(address) => expression_watch_size(&value, session).map(|size| (address.0, size)),
Err(_error) if direct_register_expression(&expr, &session.target) => {
Err("registers cannot be watched; watch the memory they point at".to_string())
}
Err(error) => match value.type_data() {
Some(ParsedType::Pointer(pointee)) => {
let size = TypeView::new(session).parsed_type_size(pointee);
if size == 0 {
return Err("typed pointer has unknown pointee size".to_string());
}
value
.scalar(&session.target)
.map(|address| (address.0, size))
.map_err(|error| error.to_string())
}
Some(_) => Err(error.to_string()),
None => {
let address = value
.scalar(&session.target)
.map_err(|error| error.to_string())?;
let size = expression_watch_size(&value, session)?;
Ok((address.0, size))
}
},
}
}
fn data_target(&mut self, reference: i64, name: &str) -> result::Result<(u64, usize), String> {
let index = self.var_index(reference)?;
match self.vars[index].clone() {
VarRef::Locals(handle) => {
self.select_frame(handle)?;
let expression = Expr::Local(name.to_string());
let session = self.session()?;
let value = expression
.evaluate(&session.target)
.map_err(|error| error.to_string())?;
let address = value.address().map_err(|error| error.to_string())?;
let size = expression_watch_size(&value, session)?;
Ok((address.0, size))
}
VarRef::Registers(_) => {
Err("registers cannot be watched; watch the memory they point at".into())
}
VarRef::Fields { type_name, address } => {
let field_name = {
let session = self.session()?;
let view = TypeView::new(session);
let type_info = view.lookup_type(&type_name).ok_or_else(|| {
format!("type '{type_name}' is not in the loaded symbols")
})?;
find_field(type_info.as_ref(), name)
.map(|(field_name, _)| field_name.clone())
.ok_or_else(|| format!("no field named '{name}' in {type_name}"))?
};
let expression = Expr::FieldAccess(
Box::new(Expr::Cast(
Box::new(Expr::Literal(address)),
ExprType::Pointer(Box::new(ExprType::Struct(type_name))),
)),
field_name,
);
self.data_value_target(&expression)
}
VarRef::Elements {
count,
element_size,
address,
element: element_type,
..
} => {
let index = element_index(name)?;
if index >= count {
Err(format!(
"element {index} is past the end of a [{count}] array"
))
} else if matches!(&element_type, ParsedType::Bitfield { .. }) {
Err("bitfields have no independently addressable storage".to_string())
} else if element_size == 0 {
Err("array element has no storage size".to_string())
} else {
let offset = u64::from(index) * element_size as u64;
Ok((address.0.wrapping_add(offset), element_size))
}
}
}
}
fn data_value_target(&mut self, expression: &Expr) -> result::Result<(u64, usize), String> {
let value = {
let session = self.session()?;
expression
.evaluate(&session.target)
.map_err(|error| error.to_string())?
};
let session = self.session()?;
let address = value.address().map_err(|error| error.to_string())?;
let size = expression_watch_size(&value, session)?;
Ok((address.0, size))
}
fn on_set_data_breakpoints(&mut self, args: &Value) -> Handled {
let requested = args
.get("breakpoints")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let plan = requested
.iter()
.map(|entry| {
let (address, len) = arg_str(entry, "dataId")
.ok_or_else(|| "breakpoint without a dataId".to_string())
.and_then(|id| parse_data_id(&id))?;
let access = match arg_str(entry, "accessType").as_deref() {
Some("read") | Some("readWrite") => WatchpointAccess::ReadWrite,
_ => WatchpointAccess::Write,
};
let config = breakpoint_config(entry)?;
Ok(Install::Watch {
address,
access,
len,
config,
})
})
.collect();
self.replace_owned_set(OwnedSet::Data, plan)
}
fn owned_set(&mut self, set: OwnedSet) -> &mut Vec<u32> {
match set {
OwnedSet::Function => &mut self.function_breakpoints,
OwnedSet::Instruction => &mut self.instruction_breakpoints,
OwnedSet::Data => &mut self.data_breakpoints,
}
}
fn replace_owned_set(
&mut self,
set: OwnedSet,
plan: Vec<result::Result<Install, String>>,
) -> Handled {
let previous = self.owned_set(set).clone();
let results = self.install_breakpoints(previous, plan)?;
self.owned_set(set).clear();
let mut response = Vec::with_capacity(results.len());
for result in results {
match result {
Ok(ids) => {
response.push(self.breakpoint_value(&ids, None));
self.owned_set(set).extend(ids);
}
Err(message) => response.push(refused_breakpoint(message, None)),
}
}
Ok(Some(json!({"breakpoints": response})))
}
fn install_breakpoints(
&mut self,
remove: Vec<u32>,
mut plan: Vec<result::Result<Install, String>>,
) -> result::Result<Vec<result::Result<Vec<u32>, String>>, String> {
let session = self.session()?;
let mut results = Vec::with_capacity(plan.len());
let mut edited = false;
let outcome = session.with_target_halted(|session| {
edited = true;
for id in &remove {
let _ = session.remove_breakpoint(*id);
}
results.extend(plan.drain(..).map(|slot| {
slot.and_then(|install| install.apply(session).map_err(|error| error.to_string()))
}));
Ok(())
});
match outcome {
Ok(()) => {}
Err(error) if !edited => {
return Err(format!(
"breakpoints unchanged; the target could not be halted: {error}"
));
}
Err(error) => self.emit_output(
"important",
format!("ntoseye: breakpoints changed but the target did not resume: {error}\n"),
),
}
for id in &remove {
self.verified.remove(id);
}
Ok(results)
}
fn breakpoint_value(&mut self, ids: &[u32], line: Option<i64>) -> Value {
let Some(session) = self.session.as_ref() else {
return json!({"verified": false, "message": "no target attached"});
};
let first = ids.first().and_then(|id| session.breakpoint(*id));
let states: Vec<(u32, BreakpointState)> = ids
.iter()
.filter_map(|id| {
session
.breakpoint(*id)
.map(|breakpoint| (*id, BreakpointState::from(breakpoint)))
})
.collect();
for (id, state) in states {
self.verified.insert(id, state);
}
let mut value = match first {
Some(breakpoint) => breakpoint_json(breakpoint, ids.len()),
None => json!({"verified": false, "message": "breakpoint was not installed"}),
};
if let Some(line) = line {
value["line"] = json!(line);
}
value
}
fn on_set_exception_breakpoints(args: &Value) -> Handled {
for field in ["filters", "filterOptions", "exceptionOptions"] {
let requested = args
.get(field)
.and_then(Value::as_array)
.is_some_and(|entries| !entries.is_empty());
if requested {
return Err(format!(
"exception breakpoints are not supported; '{field}' cannot be honored. \
Use the REPL's `sx` commands for exception policy"
));
}
}
Ok(Some(json!({"breakpoints": []})))
}
fn on_exception_info(&mut self) -> Handled {
let Some(stop) = self.last_stop.as_ref() else {
return Err("no stop has been reported yet".to_string());
};
let Some(exception_id) = stop.exception_id.clone() else {
return Err("the last stop was not an exception".to_string());
};
let description = stop.description.clone();
let detail = stop.detail.clone().unwrap_or_else(|| description.clone());
Ok(Some(json!({
"exceptionId": exception_id,
"description": description,
"breakMode": "always",
"details": {"message": detail},
})))
}
fn on_read_memory(&mut self, args: &Value) -> Handled {
let reference = arg_str(args, "memoryReference")
.ok_or_else(|| "missing memoryReference".to_string())?;
let base = parse_address(&reference)?;
let offset = arg_i64(args, "offset").unwrap_or(0);
let address = base.wrapping_add_signed(offset);
let count = arg_i64(args, "count").unwrap_or(0).max(0) as usize;
let count = count.min(MAX_READ_MEMORY);
if count == 0 {
return Ok(Some(
json!({"address": format!("{address:#x}"), "data": ""}),
));
}
let session = self.session()?;
let mut buffer = vec![0u8; count];
let read = match session.read_masked(VirtAddr(address), &mut buffer) {
Ok(()) => count,
Err(_) => partial_read(session, address, &mut buffer),
};
buffer.truncate(read);
let mut body = json!({
"address": format!("{address:#x}"),
"data": BASE64.encode(&buffer),
});
if read < count {
body["unreadableBytes"] = json!((count - read) as i64);
}
Ok(Some(body))
}
fn on_write_memory(&mut self, args: &Value) -> Handled {
let reference = arg_str(args, "memoryReference")
.ok_or_else(|| "missing memoryReference".to_string())?;
let base = parse_address(&reference)?;
let offset = arg_i64(args, "offset").unwrap_or(0);
let address = base.wrapping_add_signed(offset);
let data = arg_str(args, "data").ok_or_else(|| "missing data".to_string())?;
let bytes = BASE64
.decode(data.as_bytes())
.map_err(|error| format!("invalid base64 payload: {error}"))?;
if bytes.is_empty() {
return Ok(Some(json!({"bytesWritten": 0})));
}
let allow_partial = arg_bool(args, "allowPartial").unwrap_or(false);
let session = self.session()?;
let written = session
.target
.current_process()
.map_err(|error| error.to_string())?
.memory()
.write_bytes(VirtAddr(address), &bytes);
let written = match written {
Ok(()) => bytes.len(),
Err(Error::PartialWrite(committed)) if allow_partial => committed,
Err(error) => return Err(error.to_string()),
};
Ok(Some(json!({"bytesWritten": written as i64})))
}
fn on_disassemble(&mut self, args: &Value) -> Handled {
let reference = arg_str(args, "memoryReference")
.ok_or_else(|| "missing memoryReference".to_string())?;
let base = parse_address(&reference)?;
let offset = arg_i64(args, "offset").unwrap_or(0);
let address = base.wrapping_add_signed(offset);
let instruction_offset = arg_i64(args, "instructionOffset").unwrap_or(0);
let count = (arg_i64(args, "instructionCount").unwrap_or(0).max(0) as usize)
.min(MAX_DISASSEMBLE_INSTRUCTIONS);
if count == 0 {
return Ok(Some(json!({"instructions": []})));
}
let mut rows: Vec<Option<DisassembledRow>> = Vec::with_capacity(count);
let backwards = usize::try_from(instruction_offset.min(0).saturating_neg())
.unwrap_or(0)
.min(MAX_DISASSEMBLE_INSTRUCTIONS);
if backwards > 0 {
let preceding = self.disassemble_preceding(address, backwards)?;
rows.resize_with(backwards.saturating_sub(preceding.len()), || None);
rows.extend(preceding.into_iter().map(Some));
}
let forward_start = if instruction_offset > 0 {
let skip = (instruction_offset as usize).min(MAX_DISASSEMBLE_INSTRUCTIONS);
let session = self.session()?;
match session.disassemble(VirtAddr(address), skip + 1) {
Ok(decoded) => decoded.get(skip).map(|row| row.ip).unwrap_or(address),
Err(_) => address,
}
} else {
address
};
let remaining = count.saturating_sub(rows.len());
if remaining > 0 {
let session = self.session()?;
match session.disassemble(VirtAddr(forward_start), remaining) {
Ok(decoded) => rows.extend(decoded.into_iter().map(|row| {
Some(DisassembledRow {
address: row.ip,
bytes: Some(row.hex.clone()),
text: row.asm(),
})
})),
Err(error) => rows.push(Some(DisassembledRow {
address: forward_start,
bytes: None,
text: format!("<{error}>"),
})),
}
}
rows.resize_with(count, || None);
let instructions = self.disassembly_values(rows);
Ok(Some(json!({"instructions": instructions})))
}
fn disassemble_preceding(
&mut self,
address: u64,
count: usize,
) -> result::Result<Vec<DisassembledRow>, String> {
let session = self.session()?;
let arch = session.target.arch();
let bitness = session.target.code_bitness(VirtAddr(address));
let window = count.saturating_mul(max_instruction_bytes(arch));
let start = address.saturating_sub(window as u64);
let mut bytes = vec![0u8; (address - start) as usize];
if bytes.is_empty() || session.read_masked(VirtAddr(start), &mut bytes).is_err() {
return Ok(Vec::new());
}
let rows = decode_preceding(arch, &bytes, start, address, count, bitness, |target| {
format!("{target:#x}")
});
Ok(rows
.unwrap_or_default()
.into_iter()
.map(|row| DisassembledRow {
address: row.ip,
bytes: Some(row.hex.clone()),
text: row.asm(),
})
.collect())
}
fn disassembly_values(&mut self, rows: Vec<Option<DisassembledRow>>) -> Vec<Value> {
let mut instructions = Vec::with_capacity(rows.len());
for row in rows {
let Some(row) = row else {
instructions.push(json!({
"address": "-1",
"instruction": "(unreadable)",
"presentationHint": "invalid",
}));
continue;
};
let symbol = self.session.as_ref().and_then(|session| {
session
.target
.closest_symbol_current_context(VirtAddr(row.address))
});
let location = self
.session
.as_ref()
.and_then(|session| session.target.source_location(VirtAddr(row.address)));
let mut value = json!({
"address": format!("{:#x}", row.address),
"instruction": row.text,
});
if let Some(bytes) = row.bytes {
value["instructionBytes"] = json!(bytes);
}
if let Some(symbol) = symbol {
value["symbol"] = json!(symbol);
}
if let Some(location) = &location {
value["location"] = source_value(location);
value["line"] = json!(self.to_client_line(location.line as i64));
}
instructions.push(value);
}
instructions
}
fn on_modules(&mut self, args: &Value) -> Handled {
let start = arg_i64(args, "startModule").unwrap_or(0).max(0) as usize;
let requested = arg_i64(args, "moduleCount").unwrap_or(0).max(0) as usize;
let session = self.session()?;
let modules = session
.target
.modules()
.map_err(|error| error.to_string())?;
let total = modules.len();
let end = if requested == 0 {
total
} else {
(start + requested).min(total)
};
let values = modules
.get(start..end)
.unwrap_or_default()
.iter()
.map(|module| {
let mut value = json!({
"id": module.name,
"name": module.short_name,
"path": module.name,
"addressRange": format!("{:#x}", module.base_address.0),
});
if let Some(version) = &module.file_version {
value["version"] = json!(version);
}
value
})
.collect::<Vec<_>>();
Ok(Some(
json!({"modules": values, "totalModules": total as i64}),
))
}
fn to_client_line(&self, line: i64) -> i64 {
if self.lines_start_at_1 {
line
} else {
line.saturating_sub(1)
}
}
fn to_source_line(&self, line: i64) -> u32 {
let line = if self.lines_start_at_1 {
line
} else {
line + 1
};
u32::try_from(line.max(0)).unwrap_or(0)
}
fn to_client_column(&self, column: i64) -> i64 {
if self.columns_start_at_1 {
column
} else {
column.saturating_sub(1)
}
}
}
struct DisassembledRow {
address: u64,
bytes: Option<String>,
text: String,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum StepMode {
Over,
Into,
Out,
}
type Handled = result::Result<Option<Value>, String>;
fn capabilities() -> Value {
json!({
"supportsConfigurationDoneRequest": true,
"supportsFunctionBreakpoints": true,
"supportsConditionalBreakpoints": true,
"supportsHitConditionalBreakpoints": true,
"supportsLogPoints": true,
"supportsInstructionBreakpoints": true,
"supportsDataBreakpoints": true,
"supportsEvaluateForHovers": true,
"supportsVariablePaging": true,
"supportsDelayedStackTraceLoading": true,
"supportsSetVariable": true,
"supportsSteppingGranularity": true,
"supportsDisassembleRequest": true,
"supportsReadMemoryRequest": true,
"supportsWriteMemoryRequest": true,
"supportsModulesRequest": true,
"supportsExceptionInfoRequest": true,
"supportsTerminateRequest": true,
"supportTerminateDebuggee": false,
"supportsRestartRequest": false,
"supportsStepBack": false,
})
}
fn target_spec(args: &Value) -> result::Result<TargetSpec, String> {
if let Some(dump) = arg_str(args, "dump") {
return Ok(TargetSpec::Dump(PathBuf::from(dump)));
}
let backend = match arg_str(args, "backend") {
Some(name) => name.parse::<Backend>().map_err(|error| error.to_string())?,
None => Backend::Kd,
};
let memory_source = match arg_str(args, "memorySource") {
Some(source) => source.parse::<KdMemorySource>()?,
None => KdMemorySource::Auto,
};
let spec = TargetSpec::Live {
backend,
connect: arg_str(args, "connect"),
kdnet_key: arg_str(args, "kdnetKey"),
memory_source,
};
spec.validate()?;
Ok(spec)
}
fn breakpoint_config(entry: &Value) -> result::Result<BreakpointConfig, String> {
let pass_count = match arg_str(entry, "hitCondition") {
Some(text) => {
let trimmed = text.trim();
trimmed
.parse::<u64>()
.map_err(|_| format!("hit condition '{trimmed}' must be a decimal pass count"))?
}
None => 0,
};
Ok(BreakpointConfig {
condition: arg_str(entry, "condition").filter(|text| !text.trim().is_empty()),
pass_count,
action: match arg_str(entry, "logMessage") {
Some(message) => Some(log_point_action(&message)?),
None => None,
},
..BreakpointConfig::default()
})
}
enum Install {
Source(String, BreakpointConfig),
Symbol(String, BreakpointConfig),
Address(u64, BreakpointConfig),
Watch {
address: u64,
access: WatchpointAccess,
len: u8,
config: BreakpointConfig,
},
}
impl Install {
fn apply(self, session: &mut Session) -> Result<Vec<u32>> {
match self {
Self::Source(spec, config) => session.add_source_breakpoint(spec, config),
Self::Symbol(name, config) => session
.add_symbol_breakpoint(name, config)
.map(|id| vec![id]),
Self::Address(address, config) => session
.add_breakpoint(VirtAddr(address), None, config)
.map(|id| vec![id]),
Self::Watch {
address,
access,
len,
config,
} => session
.add_watchpoint(VirtAddr(address), access, len, None, config)
.map(|id| vec![id]),
}
}
}
#[derive(Clone, Copy)]
enum OwnedSet {
Function,
Instruction,
Data,
}
fn refused_breakpoint(message: String, line: Option<i64>) -> Value {
let mut row = json!({"verified": false, "message": message});
if let Some(line) = line {
row["line"] = json!(line);
}
row
}
fn log_point_action(message: &str) -> result::Result<String, String> {
let mut format = String::new();
let mut arguments: Vec<String> = Vec::new();
let mut rest = message;
while let Some(open) = rest.find('{') {
format.push_str(&escape_printf(&rest[..open]));
let tail = &rest[open + 1..];
let close = tail
.find('}')
.ok_or_else(|| format!("log message has an unclosed '{{': {message}"))?;
let expression: String = tail[..close]
.chars()
.filter(|character| !character.is_whitespace())
.collect();
if expression.is_empty() {
return Err(format!("log message has an empty '{{}}': {message}"));
}
if expression.contains(['"', ';']) {
return Err(format!(
"log message expression '{expression}' cannot contain a quote or a semicolon"
));
}
format.push_str("%p");
arguments.push(expression);
rest = &tail[close + 1..];
}
format.push_str(&escape_printf(rest));
let mut action = format!("\"{format}\"");
for argument in arguments {
action.push(' ');
action.push_str(&argument);
}
Ok(format!(".printf {action}; gc"))
}
fn escape_printf(text: &str) -> String {
text.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('%', "%%")
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct BreakpointState {
resolved: bool,
address: VirtAddr,
}
impl From<&Breakpoint> for BreakpointState {
fn from(breakpoint: &Breakpoint) -> Self {
Self {
resolved: breakpoint.resolved,
address: breakpoint.address,
}
}
}
fn breakpoint_json(breakpoint: &Breakpoint, matches: usize) -> Value {
let mut value = json!({
"id": breakpoint.id as i64,
"verified": breakpoint.resolved,
});
if breakpoint.resolved {
value["instructionReference"] = json!(format!("{:#x}", breakpoint.address.0));
}
let mut notes = Vec::new();
if breakpoint.deferred() {
notes.push(format!(
"deferred: '{}' is not resolvable yet (it will arm when its module loads)",
breakpoint.specification().unwrap_or("symbol")
));
}
if matches > 1 {
notes.push(format!("{matches} addresses matched"));
}
if !notes.is_empty() {
value["message"] = json!(notes.join("; "));
}
value
}
fn source_value(location: &SourceLocation) -> Value {
let name = file_stem_of(&location.file);
let mut source = json!({"name": name});
match location
.local_path
.as_ref()
.filter(|_| location.local_exists)
{
Some(path) => source["path"] = json!(path.to_string_lossy()),
None => {
source["origin"] = json!(format!("recorded as {}", location.file));
source["presentationHint"] = json!("deemphasize");
}
}
source
}
fn local_value_text(local: &ProcedureLocal, value: Option<u64>, expand: Option<&Expand>) -> String {
if let Some(value) = value {
return match local.byte_size {
Some(size) if size <= 8 => format!("{value:#x} ({value})"),
_ => format!("{value:#x}"),
};
}
if expand.is_some() {
return String::new();
}
match &local.location {
LocalVariableLocation::Unavailable { reason } => format!("<unavailable: {reason}>"),
LocalVariableLocation::Register { register } => {
format!("<in {register}, unavailable in this context>")
}
location => format!("<at {}, unreadable>", location.describe()),
}
}
fn direct_register_expression(expr: &Expr, target: &Target) -> bool {
match expr {
Expr::Register(name) => target.register_value(name).is_some(),
Expr::Symbol(name) => {
target
.symbols
.find_symbol_across_modules(target.current_dtb(), name)
.ok()
.flatten()
.is_none()
&& target.register_value(name).is_some()
}
_ => false,
}
}
fn expression_watch_size(value: &ExprValue, session: &Session) -> result::Result<usize, String> {
if let Some(size) = value
.byte_size()
.and_then(|size| usize::try_from(size).ok())
.filter(|size| *size != 0)
{
return Ok(size);
}
if let Some(type_data) = value.type_data() {
let size = TypeView::new(session).parsed_type_size(type_data);
if size != 0 {
return Ok(size);
}
return Err(format!(
"typed value '{type_data}' has unknown storage size"
));
}
Ok(8)
}
fn file_stem_of(path: &str) -> String {
path.rsplit(['/', '\\'])
.next()
.filter(|name| !name.is_empty())
.unwrap_or(path)
.to_string()
}
fn partial_read(session: &Session, address: u64, buffer: &mut [u8]) -> usize {
const CHUNK: usize = 0x1000;
let mut read = 0;
while read < buffer.len() {
let end = (read + CHUNK).min(buffer.len());
let chunk_address = VirtAddr(address.wrapping_add(read as u64));
if session
.read_masked(chunk_address, &mut buffer[read..end])
.is_err()
{
break;
}
read = end;
}
read
}
fn watch_length(size: usize) -> u8 {
match size {
0 | 1 => 1,
2 | 3 => 2,
4..=7 => 4,
_ => 8,
}
}
fn element_index(name: &str) -> result::Result<u32, String> {
name.trim()
.strip_prefix('[')
.and_then(|rest| rest.strip_suffix(']'))
.and_then(|index| index.parse().ok())
.ok_or_else(|| format!("'{name}' is not an array element"))
}
fn parse_data_id(id: &str) -> result::Result<(u64, u8), String> {
let (address, len) = id
.split_once(':')
.ok_or_else(|| format!("malformed dataId '{id}'"))?;
let address = parse_address(address)?;
let len = len
.parse::<u8>()
.map_err(|_| format!("malformed dataId '{id}'"))?;
Ok((address, len))
}
fn parse_address(text: &str) -> result::Result<u64, String> {
let trimmed = text.trim();
let parsed = match trimmed
.strip_prefix("0x")
.or_else(|| trimmed.strip_prefix("0X"))
{
Some(hex) => u64::from_str_radix(hex, 16),
None => trimmed.parse::<u64>(),
};
parsed.map_err(|_| format!("malformed address '{text}'"))
}
fn arg_str(args: &Value, key: &str) -> Option<String> {
args.get(key)
.and_then(Value::as_str)
.map(str::to_string)
.filter(|text| !text.is_empty())
}
fn arg_i64(args: &Value, key: &str) -> Option<i64> {
args.get(key).and_then(Value::as_i64)
}
fn arg_bool(args: &Value, key: &str) -> Option<bool> {
args.get(key).and_then(Value::as_bool)
}
fn arg_strings(args: &Value, key: &str) -> Vec<String> {
match args.get(key) {
Some(Value::String(single)) => vec![single.clone()],
Some(Value::Array(entries)) => entries
.iter()
.filter_map(|entry| entry.as_str().map(str::to_string))
.collect(),
_ => Vec::new(),
}
}
#[cfg(test)]
mod tests;