use anyhow::{bail, Result}; use registers::REGISTERS; use request::{Request, RequestKind, RequestSource}; use response::Response; use std::{ sync::{Arc, Mutex}, thread, }; use tokio::{ io::{AsyncWriteExt as _, BufReader}, net::{TcpListener, TcpStream}, select, sync::{mpsc, oneshot}, }; use crate::emulator::{DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId}; mod registers; mod request; mod response; pub struct GdbServer { sim_id: SimId, client: EmulatorClient, status: Arc>, killer: Option>, } impl GdbServer { pub fn new(sim_id: SimId, client: EmulatorClient) -> Self { Self { sim_id, client, status: Arc::new(Mutex::new(GdbServerStatus::Stopped)), killer: None, } } pub fn status(&self) -> GdbServerStatus { self.status.lock().unwrap().clone() } pub fn start(&mut self, port: u16) { *self.status.lock().unwrap() = GdbServerStatus::Connecting; let sim_id = self.sim_id; let client = self.client.clone(); let status = self.status.clone(); let (tx, rx) = oneshot::channel(); self.killer = Some(tx); thread::spawn(move || { tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap() .block_on(async move { select! { _ = run_server(sim_id, client, port, &status) => {} _ = rx => { *status.lock().unwrap() = GdbServerStatus::Stopped; } } }) }); } pub fn stop(&mut self) { if let Some(killer) = self.killer.take() { let _ = killer.send(()); } } } async fn run_server( sim_id: SimId, client: EmulatorClient, port: u16, status: &Mutex, ) { let Some(stream) = try_connect(port, status).await else { return; }; let mut connection = GdbConnection::new(sim_id, client); match connection.run(stream).await { Ok(()) => { *status.lock().unwrap() = GdbServerStatus::Stopped; } Err(error) => { *status.lock().unwrap() = GdbServerStatus::Error(error.to_string()); } } } async fn try_connect(port: u16, status: &Mutex) -> Option { *status.lock().unwrap() = GdbServerStatus::Connecting; let listener = match TcpListener::bind(("127.0.0.1", port)).await { Ok(l) => l, Err(err) => { *status.lock().unwrap() = GdbServerStatus::Error(err.to_string()); return None; } }; match listener.accept().await { Ok((stream, _)) => { *status.lock().unwrap() = GdbServerStatus::Running; Some(stream) } Err(err) => { *status.lock().unwrap() = GdbServerStatus::Error(err.to_string()); None } } } #[derive(Clone)] pub enum GdbServerStatus { Stopped, Connecting, Running, Error(String), } impl GdbServerStatus { pub fn running(&self) -> bool { matches!(self, Self::Connecting | Self::Running) } } struct GdbConnection { sim_id: SimId, client: EmulatorClient, ack_messages: bool, stop_reason: Option, response_buf: Option>, memory_buf: Option>, } impl GdbConnection { fn new(sim_id: SimId, client: EmulatorClient) -> Self { Self { sim_id, client, ack_messages: true, stop_reason: None, response_buf: None, memory_buf: None, } } async fn run(&mut self, stream: TcpStream) -> Result<()> { let (debug_sink, mut debug_source) = mpsc::unbounded_channel(); let (rx, mut tx) = stream.into_split(); let mut request_source = RequestSource::new(BufReader::new(rx)); self.client .send_command(EmulatorCommand::StartDebugging(self.sim_id, debug_sink)); loop { let response = select! { maybe_event = debug_source.recv() => { let Some(event) = maybe_event else { // debugger has stopped running break; }; self.handle_event(event) } maybe_request = request_source.recv() => { let req = maybe_request?; self.handle_request(req)? } }; if let Some(res) = response { let buffer = res.finish(); match std::str::from_utf8(&buffer) { Ok(text) => println!("response: {text}"), Err(_) => println!("response: {buffer:02x?}"), } tx.write_all(&buffer).await?; self.response_buf = Some(buffer); tx.flush().await?; } } Ok(()) } fn handle_event(&mut self, event: DebugEvent) -> Option { let res = match event { DebugEvent::Stopped(reason) => { if self.stop_reason.is_some_and(|r| r == reason) { return None; } self.stop_reason = Some(reason); self.response() .write_str(debug_stop_reason_string(self.stop_reason)) } }; Some(res) } fn handle_request(&mut self, mut req: Request<'_>) -> Result> { println!("received {:02x?}", req); if req.kind == RequestKind::Signal { self.client .send_command(EmulatorCommand::DebugInterrupt(self.sim_id)); return Ok(None); // we'll send a message when the emulator reports it has stopped } let res = if req.match_str("QStartNoAckMode") { let res = self.response().write_str("OK"); self.ack_messages = false; res } else if req.match_str("qSupported:") { self.response() .write_str("multiprocess+;swbreak+;vContSupported+;PacketSize=10000") } else if req .match_some_str([ "QThreadSuffixSupported", "QListThreadsInStopReply", "QEnableErrorStrings", ]) .is_some() { self.response().write_str("OK") } else if req.match_str("qHostInfo") { self.response().write_str(&format!( "triple:{};endian:little;ptrsize:4;", hex::encode("v810-unknown-vb") )) } else if req.match_str("qProcessInfo") { self.response().write_str(&format!( "pid:1;triple:{};endian:little;ptrsize:4;", hex::encode("v810-unknown-vb") )) } else if req.match_str("qRegisterInfo") { let mut get_reg_info = || { let register = req.match_hex::()?; REGISTERS.get(register) }; if let Some(reg_info) = get_reg_info() { self.response().write_str(®_info.to_description()) } else { self.response() } } else if req.match_str("vCont?") { self.response().write_str("vCont;c;") } else if req.match_str("qC") { // The v810 has no threads, so report that the "current thread" is 1. self.response().write_str("QCp1.t1") } else if req.match_str("qfThreadInfo") { self.response().write_str("mp1.t1") } else if req.match_str("qsThreadInfo") { self.response().write_str("l") } else if req.match_str("k") { bail!("debug process was killed"); } else if req.match_str("?") { self.response() .write_str(debug_stop_reason_string(self.stop_reason)) } else if req.match_some_str(["c", "vCont;c:"]).is_some() { self.client .send_command(EmulatorCommand::DebugContinue(self.sim_id)); self.stop_reason = None; // Don't send a response until we hit a breakpoint or get interrupted return Ok(None); } else if req.match_str("s") { self.client .send_command(EmulatorCommand::DebugStep(self.sim_id)); self.stop_reason = None; // Don't send a response until we hit a breakpoint or get interrupted return Ok(None); } else if req.match_str("p") { let mut read_register = || { let register_index = req.match_hex::()?; let register = REGISTERS.get(register_index)?.to_vb_register(); let (tx, rx) = ::oneshot::channel(); self.client .send_command(EmulatorCommand::ReadRegister(self.sim_id, register, tx)); rx.recv().ok() }; if let Some(value) = read_register() { self.response().write_hex(value) } else { self.response() } } else if let Some(op) = req.match_some_str(["m", "x"]) { let mut read_memory = || { let start = req.match_hex::()? as u32; if !req.match_str(",") { return None; }; let length = req.match_hex::()?; let mut buf = self.memory_buf.take().unwrap_or_default(); buf.clear(); let (tx, rx) = ::oneshot::channel(); self.client.send_command(EmulatorCommand::ReadMemory( self.sim_id, start, length, buf, tx, )); rx.recv().ok() }; if let Some(memory) = read_memory() { let mut res = self.response(); if op == "m" { // send the hex-encoded byte stream for byte in &memory { res = res.write_hex(*byte); } } else { // send the raw byte stream for byte in &memory { res = res.write_byte(*byte); } } self.memory_buf = Some(memory); res } else { self.response() } } else if req.match_str("Z0,") { if let Some(address) = req.match_hex() { self.client .send_command(EmulatorCommand::AddBreakpoint(self.sim_id, address)); self.response().write_str("OK") } else { self.response() } } else if req.match_str("z0,") { if let Some(address) = req.match_hex() { self.client .send_command(EmulatorCommand::RemoveBreakpoint(self.sim_id, address)); self.response().write_str("OK") } else { self.response() } } else { // unrecognized command self.response() }; Ok(Some(res)) } fn response(&mut self) -> Response { Response::new( self.response_buf.take().unwrap_or_default(), self.ack_messages, ) } } impl Drop for GdbConnection { fn drop(&mut self) { self.client .send_command(EmulatorCommand::StopDebugging(self.sim_id)); } } fn debug_stop_reason_string(reason: Option) -> &'static str { match reason { Some(DebugStopReason::Trace) => "T05;thread:p1.t1;threads:p1.t1;reason:trace;", Some(DebugStopReason::Breakpoint) => "T05;thread:p1.t1;threads:p1.t1;reason:breakpoint;", Some(DebugStopReason::Trapped) => "T05;swbreak;thread:p1.t1;threads:p1.t1;reason:trap;", None => "T00;thread:p1.t1;threads:p1.t1;", } }