extern crate byteorder; use std::io; use std::io::{Read, Write}; use std::net::TcpStream; use super::riscv::{RiscvCpu, RiscvCpuError}; use wishbone_bridge::{Bridge, BridgeError}; use log::{debug, error, info}; use crate::gdb::byteorder::ByteOrder; use byteorder::{BigEndian, NativeEndian}; const SUPPORTED_QUERIES: &[u8] = b"PacketSize=3fff;qXfer:features:read+;qXfer:threads:read+;qXfer:memory-map:read-;QStartNoAckMode+;vContSupported+"; pub struct GdbController { connection: TcpStream, } impl Write for GdbController { fn write(&mut self, buf: &[u8]) -> io::Result { self.connection.write(buf) } fn flush(&mut self) -> io::Result<()> { self.connection.flush() } } impl GdbController { pub fn gdb_send(&mut self, inp: &[u8]) -> io::Result<()> { let mut buffer = [0; 16388]; let mut checksum: u8 = 0; buffer[0] = b'$'; for i in 0..inp.len() { buffer[i + 1] = inp[i]; checksum = checksum.wrapping_add(inp[i]); } let checksum_str = &format!("{:02x}", checksum); let checksum_bytes = checksum_str.as_bytes(); buffer[inp.len() + 1] = b'#'; buffer[inp.len() + 2] = checksum_bytes[0]; buffer[inp.len() + 3] = checksum_bytes[1]; let (to_write, _rest) = buffer.split_at(inp.len() + 4); debug!( " > Writing {} bytes: {}", to_write.len(), String::from_utf8_lossy(&to_write) ); self.connection.write_all(&to_write)?; Ok(()) } pub fn print_string(&mut self, msg: &str) -> io::Result<()> { debug!("Printing string {} to GDB", msg); let mut strs: Vec = msg .as_bytes() .iter() .map(|b| format!("{:02X}", b)) .collect(); strs.insert(0, "O".to_string()); let joined = strs.join(""); self.gdb_send(joined.as_bytes()) } } pub struct GdbServer { connection: TcpStream, no_ack_mode: bool, is_alive: bool, last_signal: u8, } fn swab(src: u32) -> u32 { (src << 24) & 0xff00_0000 | (src << 8) & 0x00ff_0000 | (src >> 8) & 0x0000_ff00 | (src >> 24) & 0x0000_00ff } pub fn parse_u32(value: &str) -> Result { match u32::from_str_radix(value, 16) { Ok(o) => Ok(o), Err(e) => Err(GdbServerError::NumberParseError(value.to_owned(), e)), } } pub fn parse_i32(value: &str) -> Result { match i32::from_str_radix(value, 16) { Ok(o) => Ok(o), Err(e) => Err(GdbServerError::NumberParseError(value.to_owned(), e)), } } fn gdb_unescape(input: &[u8]) -> Vec { let mut out = input.to_vec(); out.iter_mut().fold(&mut Vec::new(), |vec_acc, this_u8| { if vec_acc.last() == Some(&(b'}')) { let len = vec_acc.len(); vec_acc[len - 1] = *this_u8 ^ 0x20; } else { vec_acc.push(*this_u8); } vec_acc }); out } pub fn parse_u64(value: &str) -> Result { match u64::from_str_radix(value, 16) { Ok(o) => Ok(o), Err(e) => Err(GdbServerError::NumberParseError(value.to_owned(), e)), } } #[derive(Debug)] pub enum GdbServerError { /// Rust standard IO error IoError(io::Error), /// The network connection has closed ConnectionClosed, /// We were unable to parse an integer NumberParseError(String, std::num::ParseIntError), /// Something happened with the CPU CpuError(RiscvCpuError), /// The bridge failed somehow BridgeError(BridgeError), /// Something strange was received ProtocolError, /// Client tried to give us a breakpoint we didn't recognize UnknownBreakpointType(String), } impl std::convert::From for GdbServerError { fn from(e: BridgeError) -> Self { GdbServerError::BridgeError(e) } } impl std::convert::From for GdbServerError { fn from(e: RiscvCpuError) -> Self { GdbServerError::CpuError(e) } } impl std::convert::From for GdbServerError { fn from(e: io::Error) -> Self { GdbServerError::IoError(e) } } #[derive(Debug, PartialEq)] pub enum BreakPointType { BreakSoft, BreakHard, WatchWrite, WatchRead, WatchAccess, } impl BreakPointType { fn from_str(r: &str) -> Result { match r { "0" => Ok(BreakPointType::BreakSoft), "1" => Ok(BreakPointType::BreakHard), "2" => Ok(BreakPointType::WatchWrite), "3" => Ok(BreakPointType::WatchRead), "4" => Ok(BreakPointType::WatchAccess), c => Err(GdbServerError::UnknownBreakpointType(c.to_string())), } } } #[derive(Debug, PartialEq)] pub enum GdbCommand { /// Server gave an unrecognized command Unknown(String), /// This should be responded to in the same way as Unknown(String), /// sent by the server to test how it responds to unknown packets. MustReplyEmpty, /// qSupported SupportedQueries(String), /// QStartNoAckMode StartNoAckMode, /// D Disconnect, /// Hg# SetCurrentThread(u64), /// Hc# (# may be -1) ContinueThread(i32), /// ? LastSignalPacket, /// qfThreadInfo GetThreadInfo, /// qC GetCurrentThreadId, /// qAttached CheckIsAttached, /// g GetRegisters, /// p# GetRegister(u32), /// P#=# SetRegister(u32, u32), /// qSymbol:: SymbolsReady, /// m#,# ReadMemory(u32 /* addr */, u32 /* length */), /// M#,#:# WriteMemory( u32, /* addr */ u32, /* length */ Vec, /* value */ ), /// vCont? VContQuery, /// vCont;c VContContinue, /// vCont;C04:0;c VContContinueFromSignal(String), /// vCont;s:0;c VContStepFromSignal(String), /// c Continue, /// s Step, /// Ctrl-C Interrupt, /// qRcmd, MonitorCommand(String), /// Z0,###,2 AddBreakpoint( BreakPointType, u32, /* address */ u32, /* length */ ), /// z0,###,2 RemoveBreakpoint( BreakPointType, u32, /* address */ u32, /* length */ ), /// qOffsets GetOffsets, /// qXfer:memory-map:read:: ReadMemoryMap(u32 /* offset */, u32 /* len */), /// qXfer:features:read:target.xml:0,1000 ReadFeature( String, /* filename */ u32, /* offset */ u32, /* len */ ), /// qTStatus TraceStatusQuery, /// qXfer:threads:read::0,1000 ReadThreads(u32 /* offset */, u32 /* len */), } impl GdbServer { pub fn new(connection: TcpStream) -> Result { Ok(GdbServer { connection, no_ack_mode: false, is_alive: true, last_signal: 0, }) } #[allow(clippy::cognitive_complexity)] fn packet_to_command(&self, raw_pkt: &[u8]) -> Result { let pkt = String::from_utf8_lossy(raw_pkt).to_string(); debug!("Raw GDB packet: {}", pkt); if pkt == "qSupported" || pkt.starts_with("qSupported:") { Ok(GdbCommand::SupportedQueries(pkt)) } else if pkt == "D" { Ok(GdbCommand::Disconnect) } else if pkt == "QStartNoAckMode" { Ok(GdbCommand::StartNoAckMode) } else if pkt == "qAttached" { Ok(GdbCommand::CheckIsAttached) } else if pkt == "qOffsets" { Ok(GdbCommand::GetOffsets) } else if pkt == "qTStatus" { Ok(GdbCommand::TraceStatusQuery) } else if pkt.starts_with("qXfer:memory-map:read::") { let pkt = pkt.trim_start_matches("qXfer:memory-map:read::"); let offsets: Vec<&str> = pkt.split(',').collect(); let offset = parse_u32(offsets[0])?; let len = parse_u32(offsets[1])?; Ok(GdbCommand::ReadMemoryMap(offset, len)) } else if pkt.starts_with("qXfer:features:read:") { let pkt = pkt.trim_start_matches("qXfer:features:read:"); let fields: Vec<&str> = pkt.split(':').collect(); let offsets: Vec<&str> = fields[1].split(',').collect(); let offset = parse_u32(offsets[0])?; let len = parse_u32(offsets[1])?; Ok(GdbCommand::ReadFeature(fields[0].to_string(), offset, len)) } else if pkt.starts_with("qXfer:threads:read::") { let pkt = pkt.trim_start_matches("qXfer:threads:read::"); let offsets: Vec<&str> = pkt.split(',').collect(); let offset = parse_u32(offsets[0])?; let len = parse_u32(offsets[1])?; Ok(GdbCommand::ReadThreads(offset, len)) } else if pkt.starts_with('Z') { let pkt = pkt.trim_start_matches('Z'); let fields: Vec<&str> = pkt.split(',').collect(); let bptype = BreakPointType::from_str(fields[0])?; let address = parse_u32(fields[1])?; let size = parse_u32(fields[2])?; Ok(GdbCommand::AddBreakpoint(bptype, address, size)) } else if pkt.starts_with('z') { let pkt = pkt.trim_start_matches('z'); let fields: Vec<&str> = pkt.split(',').collect(); let bptype = BreakPointType::from_str(fields[0])?; let address = parse_u32(fields[1])?; let size = parse_u32(fields[2])?; Ok(GdbCommand::RemoveBreakpoint(bptype, address, size)) } else if pkt.starts_with("qRcmd,") { let pkt = pkt.trim_start_matches("qRcmd,"); let pkt_bytes = pkt.as_bytes(); let mut tmp1 = Vec::new(); let mut acc = 0; for (i, pkt_byte) in pkt_bytes.iter().enumerate() { let nybble = if *pkt_byte >= 0x30 && *pkt_byte <= 0x39 { *pkt_byte - 0x30 } else if *pkt_byte >= 0x61 && *pkt_byte <= 0x66 { *pkt_byte + 10 - 0x61 } else if *pkt_byte >= 0x41 && *pkt_byte <= 0x46 { *pkt_byte + 10 - 0x41 } else { 0 }; if i & 1 == 1 { tmp1.push((acc << 4) | nybble); acc = 0; } else { acc = nybble; } } Ok(GdbCommand::MonitorCommand( String::from_utf8_lossy(&tmp1).to_string(), )) } else if pkt == "g" { Ok(GdbCommand::GetRegisters) } else if pkt.starts_with('P') { let pkt = pkt.trim_start_matches('P').to_string(); let v: Vec<&str> = pkt.split('=').collect(); let addr = parse_u32(v[0])?; let value = swab(parse_u32(v[1])?); Ok(GdbCommand::SetRegister(addr, value)) } else if pkt == "c" { Ok(GdbCommand::Continue) } else if pkt == "s" { Ok(GdbCommand::Step) } else if pkt.starts_with('m') { let pkt = pkt.trim_start_matches('m').to_string(); let v: Vec<&str> = pkt.split(',').collect(); let addr = parse_u32(v[0])?; let length = parse_u32(v[1])?; Ok(GdbCommand::ReadMemory(addr, length)) } else if pkt.starts_with('M') { let pkt = pkt.trim_start_matches('M').to_string(); let d: Vec<&str> = pkt.split(':').collect(); let v: Vec<&str> = d[0].split(',').collect(); let addr = parse_u32(v[0])?; let length = parse_u32(v[1])?; let value = swab(parse_u32(d[1])?); Ok(GdbCommand::WriteMemory(addr, length, vec![value])) } else if pkt.starts_with('X') { let (_opcode, data) = match raw_pkt.split_first() { None => return Err(GdbServerError::ProtocolError), Some(s) => s, }; // Packet format: Xaddr,count:data // Look for ":" let mut delimiter_offset = None; for (idx, c) in data.iter().enumerate() { if *c == b':' { delimiter_offset = Some(idx); break; } } let delimiter_offset = match delimiter_offset { Some(s) => s, None => return Err(GdbServerError::ProtocolError), }; // warn!("X command: Not doing GDB unescaping"); let (description, bin_data_plus) = data.split_at(delimiter_offset); let bin_data_plus = bin_data_plus.split_first(); let description = String::from_utf8_lossy(&description).to_string(); let v: Vec<&str> = description.split(',').collect(); let addr = parse_u32(v[0])?; let length = parse_u32(v[1])?; let mut values = vec![]; if let Some((_delimiter, bin_data)) = bin_data_plus { let bin_data = gdb_unescape(bin_data); for value in bin_data.chunks_exact(4) { values.push(swab(BigEndian::read_u32(&value))); } let remainder = bin_data.chunks_exact(4).remainder(); if !remainder.is_empty() { let mut remainder = remainder.to_vec(); while remainder.len() < 4 { remainder.insert(0, 0); } // remainder.resize(4, 0); values.push(swab(BigEndian::read_u32(&remainder))); } } Ok(GdbCommand::WriteMemory(addr, length, values)) } else if pkt.starts_with('p') { Ok(GdbCommand::GetRegister(parse_u32( pkt.trim_start_matches('p'), )?)) } else if pkt.starts_with("Hg") { Ok(GdbCommand::SetCurrentThread(parse_u64( pkt.trim_start_matches("Hg"), )?)) } else if pkt.starts_with("Hc") { Ok(GdbCommand::ContinueThread(parse_i32( pkt.trim_start_matches("Hc"), )?)) } else if pkt == "qC" { Ok(GdbCommand::GetCurrentThreadId) } else if pkt == "?" { Ok(GdbCommand::LastSignalPacket) } else if pkt == "qfThreadInfo" { Ok(GdbCommand::GetThreadInfo) } else if pkt == "vCont?" { Ok(GdbCommand::VContQuery) } else if pkt == "vCont;c" || pkt == "vCont;c:0" { Ok(GdbCommand::VContContinue) } else if pkt.starts_with("vCont;C") { //vCont;C04:0;c let pkt = pkt.trim_start_matches("vCont;C").to_string(); // let v: Vec<&str> = pkt.split(',').collect(); Ok(GdbCommand::VContContinueFromSignal(pkt)) } else if pkt.starts_with("vCont;s") { let pkt = pkt.trim_start_matches("vCont;s").to_string(); Ok(GdbCommand::VContStepFromSignal(pkt)) } else if pkt == "qSymbol::" { Ok(GdbCommand::SymbolsReady) } else if pkt == "vMustReplyEmpty" { Ok(GdbCommand::MustReplyEmpty) } else { info!("unrecognized GDB command: {}", pkt); Ok(GdbCommand::Unknown(pkt)) } } pub fn get_controller(&self) -> GdbController { GdbController { connection: self.connection.try_clone().unwrap(), } } pub fn get_command(&mut self) -> Result { let cmd = self.do_get_command()?; debug!("< GDB packet: {:?}", cmd); Ok(cmd) } fn do_get_command(&mut self) -> Result { let mut buffer = [0; 16384]; let mut byte = [0; 1]; let mut remote_checksum = [0; 2]; let mut buffer_offset = 0; // XXX Replace this with a BufReader for performance loop { let len = self.connection.read(&mut byte)?; if len == 0 { return Err(GdbServerError::ConnectionClosed); } match byte[0] { 0x24 /*'$'*/ => { let mut checksum: u8 = 0; loop { let len = self.connection.read(&mut byte)?; if len == 0 { return Err(GdbServerError::ConnectionClosed); } match byte[0] as char { '#' => { // There's got to be a better way to compare the checksum self.connection.read_exact(&mut remote_checksum)?; let checksum_str = format!("{:02x}", checksum); if checksum_str != String::from_utf8_lossy(&remote_checksum) { info!( "Checksum mismatch: Calculated {:?} vs {}", checksum_str, String::from_utf8_lossy(&remote_checksum) ); self.gdb_send_nak()?; } else if !self.no_ack_mode { self.gdb_send_ack()?; } let (buffer, _remainder) = buffer.split_at(buffer_offset); // debug!("< Read packet ${:?}#{:#?}", String::from_utf8_lossy(buffer), String::from_utf8_lossy(&remote_checksum)); return self.packet_to_command(&buffer); } other => { buffer[buffer_offset] = other as u8; buffer_offset += 1; checksum = checksum.wrapping_add(other as u8); } } } } 0x2b /*'+'*/ => {} 0x2d /*'-'*/ => {} 0x3 => return Ok(GdbCommand::Interrupt), other => error!("Warning: unrecognied byte received: {}", other), } } } #[allow(clippy::cognitive_complexity)] pub fn process( &mut self, cmd: GdbCommand, cpu: &RiscvCpu, bridge: &Bridge, ) -> Result<(), GdbServerError> { match cmd { GdbCommand::SupportedQueries(_) => self.gdb_send(SUPPORTED_QUERIES)?, GdbCommand::StartNoAckMode => { self.no_ack_mode = true; self.gdb_send(b"OK")? } GdbCommand::SetCurrentThread(_) => self.gdb_send(b"OK")?, GdbCommand::ContinueThread(_) => self.gdb_send(b"OK")?, GdbCommand::AddBreakpoint(_bptype, address, _size) => { let response = match cpu.add_breakpoint(bridge, address) { Ok(_) => "OK", Err(RiscvCpuError::BreakpointExhausted) => { error!("No available breakpoint found"); "E0E" } Err(e) => { error!( "An error occurred while trying to add the breakpoint: {:?}", e ); "E0E" } }; self.gdb_send(response.as_bytes())?; } GdbCommand::TraceStatusQuery => self.gdb_send(b"")?, GdbCommand::RemoveBreakpoint(_bptype, address, _size) => { cpu.remove_breakpoint(bridge, address)?; self.gdb_send(b"OK")? } GdbCommand::LastSignalPacket => { let sig_str = format!("S{:02x}", self.last_signal); self.gdb_send(if self.is_alive { sig_str.as_bytes() } else { b"W00" })? } GdbCommand::GetThreadInfo => self.gdb_send(b"l")?, GdbCommand::GetCurrentThreadId => self.gdb_send(b"QC0")?, GdbCommand::CheckIsAttached => self.gdb_send(b"1")?, GdbCommand::Disconnect => { cpu.resume(bridge)?; self.gdb_send(b"OK")? } GdbCommand::GetRegisters => { let mut register_list = String::new(); for i in cpu.all_cpu_registers() { register_list .push_str(format!("{:08x}", swab(cpu.read_register(bridge, i)?)).as_str()); } self.gdb_send(register_list.as_bytes())? } GdbCommand::GetRegister(reg) => { let response = match cpu.read_register(bridge, reg) { Ok(val) => format!("{:08x}", swab(val)), Err(e) => { error!("Error reading register: {}", e); "E01".to_string() } }; self.gdb_send(response.as_bytes())? } GdbCommand::SetRegister(reg, val) => { let response = match cpu.write_register(bridge, reg, val) { Ok(()) => "OK", Err(_) => "E01", }; self.gdb_send(response.as_bytes())? } GdbCommand::SymbolsReady => self.gdb_send(b"OK")?, GdbCommand::ReadMemory(addr, len) => { debug!("Reading memory {:08x}", addr); let mut values = vec![]; let mut out_str = String::new(); if len == 1 { let val = cpu.read_memory(bridge, addr, 1)? as u8; out_str.push_str(&format!("{:02x}", val)); self.gdb_send(out_str.as_bytes())? } else if len == 2 { let val = cpu.read_memory(bridge, addr, 2)? as u16; let mut buf = [0; 2]; BigEndian::write_u16(&mut buf, val); out_str.push_str(&format!("{:04x}", NativeEndian::read_u16(&buf))); self.gdb_send(out_str.as_bytes())? } else if len == 4 { values.push(cpu.read_memory(bridge, addr, 4)?); self.gdb_send_u32(values)? } else { for offset in (0..len).step_by(4) { values.push(cpu.read_memory(bridge, addr + offset, 4)?); if addr + offset >= 0xffff_fffc { break; } } self.gdb_send_u32(values)? } } GdbCommand::WriteMemory(addr, len, values) => { if len == 1 { debug!("Writing memory {:08x} -> {:08x}", addr, values[0] >> 24); cpu.write_memory(bridge, addr, 1, values[0] >> 24)?; } else if len == 2 { debug!("Writing memory {:08x} -> {:08x}", addr, values[0] >> 16); cpu.write_memory(bridge, addr, 2, values[0] >> 16)?; } else if len == 4 { debug!("Writing memory {:08x} -> {:08x}", addr, values[0]); cpu.write_memory(bridge, addr, 4, values[0])?; } else { for (offset, value) in values.iter().enumerate() { debug!("Writing memory {:08x} -> {:08x}", addr, values[offset]); cpu.write_memory(bridge, addr + (offset as u32 * 4), 4, *value)?; } } self.gdb_send(b"OK")? } GdbCommand::VContQuery => self.gdb_send(b"vCont;c;C;s;S")?, GdbCommand::VContContinue => { if let Some(s) = cpu.resume(bridge)? { self.print_string(&format!("Note: CPU is currently in a trap: {}\n", s))? } } GdbCommand::VContContinueFromSignal(_) => { if let Some(s) = cpu.resume(bridge)? { self.print_string(&format!("Note: CPU is currently in a trap: {}\n", s))? } } GdbCommand::VContStepFromSignal(_) => { if let Some(s) = cpu.step(bridge)? { self.print_string(&format!("Note: CPU is currently in a trap: {}\n", s))?; } self.last_signal = 5; self.gdb_send(format!("S{:02x}", self.last_signal).as_bytes())?; } GdbCommand::GetOffsets => self.gdb_send(b"Text=0;Data=0;Bss=0")?, GdbCommand::Continue => { if let Some(s) = cpu.resume(bridge)? { self.print_string(&format!("Note: CPU is currently in a trap: {}\n", s))? } } GdbCommand::Step => { if let Some(s) = cpu.step(bridge)? { self.print_string(&format!("Note: CPU is currently in a trap: {}\n", s))? } } GdbCommand::MonitorCommand(cmd) => { match cmd.as_str() { "reset" => { self.print_string("Resetting CPU...\n")?; cpu.reset(&bridge)?; } "about" => { self.print_string("VexRiscv GDB bridge\n")?; } "explain" => { self.print_string(&cpu.explain(&bridge)?)?; } _ => { self.print_string("Unrecognized monitor command. Available commands:\n")?; self.print_string(" about - Information about the bridge\n")?; self.print_string(" explain - Explain what the CPU is doing\n")?; self.print_string(" reset - Reset the CPU\n")?; } } self.gdb_send(b"OK")? } GdbCommand::ReadFeature(filename, offset, len) => { self.gdb_send_file(cpu.get_feature(&filename)?, offset, len)? } GdbCommand::ReadMemoryMap(_offset, _len) => { // self.gdb_send_file(cpu.get_memory_map()?, offset, len)? self.gdb_send(b"")? } GdbCommand::ReadThreads(offset, len) => { self.gdb_send_file(cpu.get_threads()?, offset, len)? } GdbCommand::Interrupt => { self.last_signal = 2; cpu.halt(bridge)?; self.gdb_send(format!("S{:02x}", self.last_signal).as_bytes())?; } GdbCommand::MustReplyEmpty => self.gdb_send(b"")?, GdbCommand::Unknown(_) => self.gdb_send(b"")?, }; Ok(()) } fn gdb_send_ack(&mut self) -> io::Result { self.connection.write(&[b'+']) } fn gdb_send_nak(&mut self) -> io::Result { self.connection.write(&[b'-']) } fn gdb_send_u32(&mut self, vals: Vec) -> io::Result<()> { let mut out_str = String::new(); for val in vals { let mut buf = [0; 4]; BigEndian::write_u32(&mut buf, val); out_str.push_str(&format!("{:08x}", NativeEndian::read_u32(&buf))); } self.gdb_send(out_str.as_bytes()) } fn gdb_send(&mut self, inp: &[u8]) -> io::Result<()> { let mut buffer = [0; 16388]; let mut checksum: u8 = 0; buffer[0] = b'$'; for i in 0..inp.len() { buffer[i + 1] = inp[i]; checksum = checksum.wrapping_add(inp[i]); } let checksum_str = &format!("{:02x}", checksum); let checksum_bytes = checksum_str.as_bytes(); buffer[inp.len() + 1] = b'#'; buffer[inp.len() + 2] = checksum_bytes[0]; buffer[inp.len() + 3] = checksum_bytes[1]; let (to_write, _rest) = buffer.split_at(inp.len() + 4); // debug!( // " > Writing {} bytes: {}", // to_write.len(), // String::from_utf8_lossy(&to_write) // ); self.connection.write_all(&to_write)?; Ok(()) } pub fn print_string(&mut self, msg: &str) -> io::Result<()> { debug!("Printing string {} to GDB", msg); let mut strs: Vec = msg .as_bytes() .iter() .map(|b| format!("{:02X}", b)) .collect(); strs.insert(0, "O".to_string()); let joined = strs.join(""); self.gdb_send(joined.as_bytes()) } fn gdb_send_file(&mut self, mut data: Vec, offset: u32, len: u32) -> io::Result<()> { let offset = offset as usize; let len = len as usize; let mut end = offset + len; if offset > data.len() { self.gdb_send(b"l")?; } else { if end > data.len() { end = data.len(); } let mut trimmed_data: Vec = data.drain(offset..end).collect(); if trimmed_data.len() >= len { // XXX should this be <= or < ? trimmed_data.insert(0, b'm'); } else { trimmed_data.insert(0, b'l'); } self.gdb_send(&trimmed_data)?; } Ok(()) } }