Implement GDB/LLDB compatible server #3

Merged
SonicSwordcane merged 33 commits from debugger into main 2025-01-19 00:13:43 +00:00
3 changed files with 201 additions and 179 deletions
Showing only changes of commit 9519897711 - Show all commits

View File

@ -1,10 +1,12 @@
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use request::{Request, RequestKind};
use response::Response;
use std::{ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread, thread,
}; };
use tokio::{ use tokio::{
io::{AsyncReadExt, AsyncWriteExt as _, BufReader, BufWriter}, io::{AsyncWriteExt as _, BufReader, BufWriter},
net::{ net::{
tcp::{OwnedReadHalf, OwnedWriteHalf}, tcp::{OwnedReadHalf, OwnedWriteHalf},
TcpListener, TcpStream, TcpListener, TcpStream,
@ -15,6 +17,9 @@ use tokio::{
use crate::emulator::{EmulatorClient, EmulatorCommand, SimId}; use crate::emulator::{EmulatorClient, EmulatorCommand, SimId};
mod request;
mod response;
pub struct GdbServer { pub struct GdbServer {
sim_id: SimId, sim_id: SimId,
client: EmulatorClient, client: EmulatorClient,
@ -126,6 +131,8 @@ struct GdbConnection {
stream_in: BufReader<OwnedReadHalf>, stream_in: BufReader<OwnedReadHalf>,
stream_out: BufWriter<OwnedWriteHalf>, stream_out: BufWriter<OwnedWriteHalf>,
ack_messages: bool, ack_messages: bool,
request_buf: Vec<u8>,
response_buf: Option<Vec<u8>>,
} }
impl GdbConnection { impl GdbConnection {
@ -137,208 +144,94 @@ impl GdbConnection {
stream_in: BufReader::new(rx), stream_in: BufReader::new(rx),
stream_out: BufWriter::new(tx), stream_out: BufWriter::new(tx),
ack_messages: true, ack_messages: true,
request_buf: vec![],
response_buf: None,
} }
} }
async fn run(mut self) -> Result<()> { async fn run(mut self) -> Result<()> {
println!("Connected for {}", self.sim_id); println!("Connected for {}", self.sim_id);
self.client.send_command(EmulatorCommand::Pause); self.client.send_command(EmulatorCommand::Pause);
loop { loop {
let message = self.read_message().await?; let mut req = Request::read(&mut self.stream_in, &mut self.request_buf).await?;
println!("received {:?}", message); println!("received {:?}", req);
//let mut res = ResponseWriter::new(&mut self.stream_out); if req.kind == RequestKind::Signal {
//res.init(self.ack_messages).await?;
let body = match &message {
Message::String(str) => str.as_str(),
Message::Signal => {
self.client.send_command(EmulatorCommand::Pause); self.client.send_command(EmulatorCommand::Pause);
let mut res = self.respond().await?; let res = self
res.write_str("T05;thread:p1.t1;threads:p1.t1;reason:trap;") .response()
.await?; .write_str("T05;thread:p1.t1;threads:p1.t1;reason:trap;");
res.send().await?; self.send(res).await?;
continue; continue;
} }
};
if body == "QStartNoAckMode" { if req.match_str("QStartNoAckMode") {
let mut res = ResponseWriter::new(&mut self.stream_out); let res = self.response().write_str("OK");
res.init(self.ack_messages).await?; self.send(res).await?;
self.ack_messages = false; self.ack_messages = false;
res.send_ok().await?; } else if req.match_str("qSupported:") {
} else if body.starts_with("qSupported:") { let res = self
let mut res = self.respond().await?; .response()
res.write_str("multiprocess+;swbreak+;vContSupported+") .write_str("multiprocess+;swbreak+;vContSupported+");
.await?; self.send(res).await?;
res.send().await?; } else if req.match_str("QThreadSuffixSupported")
} else if body == "QThreadSuffixSupported" || req.match_str("QListThreadsInStopReply")
|| body == "QListThreadsInStopReply" || req.match_str("QEnableErrorStrings")
|| body == "QEnableErrorStrings"
{ {
let res = self.respond().await?; let res = self.response().write_str("OK");
res.send_ok().await?; self.send(res).await?;
} else if body == "qHostInfo" { } else if req.match_str("qHostInfo") {
let mut res = self.respond().await?; let res = self.response().write_str(&format!(
res.write_str(&format!(
"triple:{};endian:little;ptrsize:4;", "triple:{};endian:little;ptrsize:4;",
hex::encode("v810-unknown-vb") hex::encode("v810-unknown-vb")
)) ));
.await?; self.send(res).await?;
res.send().await?; } else if req.match_str("qProcessInfo") {
} else if body == "qProcessInfo" { let res = self.response().write_str(&format!(
let mut res = self.respond().await?;
res.write_str(&format!(
"pid:1;triple:{};endian:little;ptrsize:4;", "pid:1;triple:{};endian:little;ptrsize:4;",
hex::encode("v810-unknown-vb") hex::encode("v810-unknown-vb")
)) ));
.await?; self.send(res).await?;
res.send().await?; } else if req.match_str("vCont?") {
} else if body == "vCont?" { let res = self.response().write_str("vCont;c;");
let mut res = self.respond().await?; self.send(res).await?;
res.write_str("vCont;c;").await?; } else if req.match_str("qC") {
res.send().await?;
} else if body == "qC" {
// The v810 has no threads, so report that the "current thread" is 1. // The v810 has no threads, so report that the "current thread" is 1.
let mut res = self.respond().await?; let res = self.response().write_str("QCp1.t1");
res.write_str("QCp1.t1").await?; self.send(res).await?;
res.send().await?; } else if req.match_str("qfThreadInfo") {
} else if body == "qfThreadInfo" { let res = self.response().write_str("mp1.t1");
let mut res = self.respond().await?; self.send(res).await?;
res.write_str("mp1.t1").await?; } else if req.match_str("qsThreadInfo") {
res.send().await?; let res = self.response().write_str("l");
} else if body == "qsThreadInfo" { self.send(res).await?;
let mut res = self.respond().await?; } else if req.match_str("k") {
res.write_str("l").await?;
res.send().await?;
} else if body == "k" {
bail!("debug process was killed"); bail!("debug process was killed");
} else if body == "?" { } else if req.match_str("?") {
let mut res = self.respond().await?; let res = self.response().write_str("T00;thread:p1.t1;threads:p1.t1;");
res.write_str("T00;thread:p1.t1;threads:p1.t1;").await?; self.send(res).await?;
res.send().await?; } else if req.match_str("c") || req.match_str("vCont;c:") {
} else if body == "c" || body.starts_with("vCont;c:") {
// Continue running the game until we're interrupted again
self.client.send_command(EmulatorCommand::Resume); self.client.send_command(EmulatorCommand::Resume);
continue; // Don't send a response until we hit a breakpoint or get interrupted
} else { } else {
// unrecognized command // unrecognized command
let res = self.respond().await?; let res = self.response();
res.send().await?; self.send(res).await?;
} }
} }
} }
async fn read_message(&mut self) -> Result<Message> { fn response(&mut self) -> Response {
let mut char = self.read_byte().await?; Response::new(
while char == b'+' { self.response_buf.take().unwrap_or_default(),
// just ignore positive acks self.ack_messages,
char = self.read_byte().await?; )
}
if char == b'-' {
bail!("no support for negative acks");
}
if char == 0x03 {
// This is how the client "cancels an in-flight request"
return Ok(Message::Signal);
}
if char != b'$' {
// Messages are supposed to start with a dollar sign
bail!("malformed message");
} }
// now read the body async fn send(&mut self, res: Response) -> std::io::Result<()> {
let mut checksum = 0u8; let buffer = res.finish();
let mut body = vec![]; println!("{:?}", std::str::from_utf8(&buffer));
char = self.read_byte().await?; self.stream_out.write_all(&buffer).await?;
while char != b'#' { self.response_buf = Some(buffer);
if char == b'}' { self.stream_out.flush().await
// escape character
checksum = checksum.wrapping_add(char);
char = self.read_byte().await?;
checksum = checksum.wrapping_add(char);
body.push(char ^ 0x20);
} else {
checksum = checksum.wrapping_add(char);
body.push(char);
}
char = self.read_byte().await?;
}
let mut checksum_bytes = [b'0'; 2];
self.stream_in.read_exact(&mut checksum_bytes).await?;
let checksum_str = std::str::from_utf8(&checksum_bytes)?;
let real_checksum = u8::from_str_radix(checksum_str, 16)?;
if checksum != real_checksum {
bail!("invalid checksum");
}
let string = String::from_utf8(body)?;
Ok(Message::String(string))
}
async fn read_byte(&mut self) -> std::io::Result<u8> {
self.stream_in.read_u8().await
}
async fn respond(&mut self) -> std::io::Result<ResponseWriter<'_>> {
let mut res = ResponseWriter::new(&mut self.stream_out);
res.init(self.ack_messages).await?;
Ok(res)
} }
} }
struct ResponseWriter<'a> {
inner: &'a mut BufWriter<OwnedWriteHalf>,
checksum: u8,
}
impl<'a> ResponseWriter<'a> {
fn new(inner: &'a mut BufWriter<OwnedWriteHalf>) -> Self {
Self { inner, checksum: 0 }
}
async fn init(&mut self, ack: bool) -> std::io::Result<()> {
if ack {
self.inner.write_u8(b'+').await?;
}
self.inner.write_u8(b'$').await
}
async fn write_str(&mut self, str: &str) -> std::io::Result<()> {
for byte in str.bytes() {
self.checksum = self.checksum.wrapping_add(byte);
}
self.inner.write_all(str.as_bytes()).await
}
async fn write_hex_u8(&mut self, value: u8) -> std::io::Result<()> {
for digit in [(value >> 4), (value & 0xf)] {
let char = if digit > 9 {
b'a' + digit - 10
} else {
b'0' + digit
};
self.checksum = self.checksum.wrapping_add(char);
self.inner.write_u8(char).await?;
}
Ok(())
}
async fn send_ok(mut self) -> std::io::Result<()> {
self.write_str("OK").await?;
self.send().await
}
async fn send(mut self) -> std::io::Result<()> {
let final_checksum = self.checksum;
self.inner.write_u8(b'#').await?;
self.write_hex_u8(final_checksum).await?;
println!("{:?}", std::str::from_utf8(self.inner.buffer()));
self.inner.flush().await
}
}
#[derive(Debug)]
enum Message {
String(String),
Signal,
}

84
src/gdbserver/request.rs Normal file
View File

@ -0,0 +1,84 @@
use anyhow::{bail, Result};
use tokio::io::{AsyncRead, AsyncReadExt as _};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RequestKind {
Signal,
Command,
}
#[derive(Debug)]
pub struct Request<'a> {
pub kind: RequestKind,
body: &'a str,
}
impl<'a> Request<'a> {
pub async fn read<R: AsyncRead + Unpin>(
reader: &mut R,
buffer: &'a mut Vec<u8>,
) -> Result<Self> {
buffer.clear();
let mut char = reader.read_u8().await?;
while char == b'+' {
// just ignore positive acks
char = reader.read_u8().await?;
}
if char == b'-' {
bail!("no support for negative acks");
}
if char == 0x03 {
// This is how the client "cancels an in-flight request"
buffer.push(char);
let body = std::str::from_utf8(buffer)?;
return Ok(Self {
kind: RequestKind::Signal,
body,
});
}
if char != b'$' {
// Messages are supposed to start with a dollar sign
bail!("malformed message");
}
// now read the body
let mut checksum = 0u8;
char = reader.read_u8().await?;
while char != b'#' {
if char == b'}' {
// escape character
checksum = checksum.wrapping_add(char);
char = reader.read_u8().await?;
checksum = checksum.wrapping_add(char);
buffer.push(char ^ 0x20);
} else {
checksum = checksum.wrapping_add(char);
buffer.push(char);
}
char = reader.read_u8().await?;
}
let mut checksum_bytes = [b'0'; 2];
reader.read_exact(&mut checksum_bytes).await?;
let checksum_str = std::str::from_utf8(&checksum_bytes)?;
let real_checksum = u8::from_str_radix(checksum_str, 16)?;
if checksum != real_checksum {
bail!("invalid checksum");
}
let body = std::str::from_utf8(buffer)?;
Ok(Self {
kind: RequestKind::Command,
body,
})
}
pub fn match_str(&mut self, prefix: &str) -> bool {
if let Some(new_body) = self.body.strip_prefix(prefix) {
self.body = new_body;
return true;
}
false
}
}

45
src/gdbserver/response.rs Normal file
View File

@ -0,0 +1,45 @@
pub struct Response {
buffer: Vec<u8>,
checksum: u8,
}
impl Response {
pub fn new(mut buffer: Vec<u8>, ack: bool) -> Self {
buffer.clear();
if ack {
buffer.push(b'+');
}
buffer.push(b'$');
Self {
buffer,
checksum: 0,
}
}
pub fn write_str(mut self, str: &str) -> Self {
for char in str.as_bytes() {
self.buffer.push(*char);
self.checksum = self.checksum.wrapping_add(*char);
}
self
}
pub fn write_hex_u8(mut self, value: u8) -> Self {
for digit in [(value >> 4), (value & 0xf)] {
let char = if digit > 9 {
b'a' + digit - 10
} else {
b'0' + digit
};
self.buffer.push(char);
self.checksum = self.checksum.wrapping_add(char);
}
self
}
pub fn finish(mut self) -> Vec<u8> {
let checksum = self.checksum;
self.buffer.push(b'#');
self.write_hex_u8(checksum).buffer
}
}