1 //! gdbstub protocol implementation in Wasmtime's debug-main world. 2 3 mod addr; 4 mod api; 5 mod target; 6 7 use crate::{ 8 addr::AddrSpace, 9 api::{WasmType, WasmValue}, 10 }; 11 use anyhow::Result; 12 use clap::Parser; 13 use futures::{FutureExt, select}; 14 use gdbstub::{ 15 common::{Signal, Tid}, 16 conn::Connection, 17 stub::{ 18 MultiThreadStopReason, 19 state_machine::{GdbStubStateMachine, GdbStubStateMachineInner, state::Running}, 20 }, 21 }; 22 use gdbstub_arch::wasm::addr::WasmAddr; 23 use log::trace; 24 use wstd::{ 25 io::{AsyncRead, AsyncWrite}, 26 iter::AsyncIterator, 27 net::{TcpListener, TcpStream}, 28 }; 29 30 /// Command-line options. 31 #[derive(Parser)] 32 struct Options { 33 /// The TCP address to listen on, in `<addr>:<port>` format. 34 tcp_address: String, 35 /// Verbose logging. 36 #[clap(short = 'v')] 37 verbose: bool, 38 } 39 40 struct Component; 41 api::export!(Component with_types_in api); 42 43 impl api::exports::bytecodealliance::wasmtime::debugger::Guest for Component { debug(d: &api::Debuggee, args: Vec<String>)44 fn debug(d: &api::Debuggee, args: Vec<String>) { 45 let options = Options::parse_from(args); 46 if options.verbose { 47 env_logger::Builder::new() 48 .filter_level(log::LevelFilter::Trace) 49 .init(); 50 } 51 let mut debugger = Debugger { 52 debuggee: d, 53 tid: Tid::new(1).unwrap(), 54 options, 55 running: None, 56 current_pc: WasmAddr::from_raw(0).unwrap(), 57 interrupt: false, 58 single_stepping: false, 59 frame_cache: vec![], 60 addr_space: AddrSpace::new(), 61 }; 62 wstd::runtime::block_on(async { 63 if let Err(e) = debugger.run().await { 64 trace!("debugger exited with error: {e}"); 65 } 66 }); 67 } 68 } 69 70 struct Debugger<'a> { 71 debuggee: &'a api::Debuggee, 72 tid: Tid, 73 options: Options, 74 running: Option<api::Resumption>, 75 addr_space: AddrSpace, 76 interrupt: bool, 77 single_stepping: bool, 78 current_pc: WasmAddr, 79 frame_cache: Vec<api::Frame>, 80 } 81 82 impl<'a> Debugger<'a> { run(&mut self) -> Result<()>83 async fn run(&mut self) -> Result<()> { 84 // Load module info from the debuggee. Modules are 85 // pre-registered on the debuggee store before the Debuggee is 86 // created, so they are visible here without needing to 87 // execute any Wasm. 88 self.update_on_stop(); 89 90 let listener = TcpListener::bind(&self.options.tcp_address) 91 .await 92 .expect("Could not bind to TCP port"); 93 94 api::print_debugger_info(&format!( 95 "Debugger listening on {}", 96 self.options.tcp_address, 97 )); 98 api::print_debugger_info(&format!( 99 "In LLDB, attach with: process connect --plugin wasm connect://{}", 100 self.options.tcp_address, 101 )); 102 103 // Only accept one connection for the run; once the debugger 104 // disconnects, we'll just continue. 105 let Some(connection) = listener.incoming().next().await else { 106 return Ok(()); 107 }; 108 109 let gdbconn = Conn::new(connection?); 110 let mut stub = gdbstub::stub::GdbStub::new(gdbconn).run_state_machine(&mut *self)?; 111 112 // Main loop. 113 'mainloop: loop { 114 match stub { 115 GdbStubStateMachine::Idle(mut inner) => { 116 if inner.borrow_conn().flush().await.is_err() { 117 // Connection closed or other outbound error. 118 break 'mainloop; 119 } 120 121 // Wait for an inbound network byte. 122 let Some(byte) = inner.borrow_conn().read_byte().await? else { 123 inner.borrow_conn().flush().await?; 124 log::info!("Connection closed; debugger detached."); 125 break 'mainloop; 126 }; 127 128 stub = inner.incoming_data(self, byte)?; 129 } 130 131 GdbStubStateMachine::Running(mut inner) => { 132 if inner.borrow_conn().flush().await.is_err() { 133 // Connection closed or other outbound error. 134 break 'mainloop; 135 } 136 137 // Wait for either a resumption or a byte from the 138 // connection. 139 let resumption = self 140 .running 141 .as_mut() 142 .expect("In Running state, we must have a resumption future"); 143 select! { 144 _ = resumption.wait().fuse() => { 145 let resumption = self.running.take().unwrap(); 146 let event = resumption.result(self.debuggee)?; 147 stub = self.handle_event(event, inner).await?; 148 } 149 byte = inner.borrow_conn().read_byte().fuse() => { 150 let Some(byte) = byte? else { 151 // Eat any connection-closed errors on 152 // the outbound flush. 153 let _ = inner.borrow_conn().flush().await; 154 // Connection closed. 155 log::info!("Connection closed; debugger detached."); 156 break 'mainloop; 157 }; 158 stub = inner.incoming_data(&mut *self, byte)?; 159 } 160 } 161 } 162 GdbStubStateMachine::CtrlCInterrupt(mut inner) => { 163 if inner.borrow_conn().flush().await.is_err() { 164 // Connection error: break. 165 break 'mainloop; 166 } 167 stub = inner.interrupt_handled(self, None::<MultiThreadStopReason<u64>>)?; 168 } 169 GdbStubStateMachine::Disconnected(mut inner) => { 170 // Eat any connection-closed errors -- we are 171 // already in Disconnected state. 172 let _ = inner.borrow_conn().flush().await; 173 break 'mainloop; 174 } 175 } 176 } 177 178 Ok(()) 179 } 180 start_continue(&mut self, resumption: api::ResumptionValue)181 fn start_continue(&mut self, resumption: api::ResumptionValue) { 182 assert!(self.running.is_none()); 183 trace!("continuing"); 184 self.single_stepping = false; 185 self.running = Some(api::Resumption::continue_(self.debuggee, resumption)); 186 } 187 start_single_step(&mut self, resumption: api::ResumptionValue)188 fn start_single_step(&mut self, resumption: api::ResumptionValue) { 189 assert!(self.running.is_none()); 190 trace!("single-stepping"); 191 self.single_stepping = true; 192 self.running = Some(api::Resumption::single_step(self.debuggee, resumption)); 193 } 194 update_on_stop(&mut self)195 fn update_on_stop(&mut self) { 196 self.addr_space.update(self.debuggee).unwrap(); 197 198 // Cache all frame handles for the duration of this stop. 199 // The Wasm trait methods take `&self` and need access to 200 // frames by depth, so we eagerly walk the full stack here. 201 self.frame_cache.clear(); 202 let mut next = self.debuggee.exit_frames().into_iter().next(); 203 while let Some(f) = next { 204 next = f.parent_frame(self.debuggee).unwrap(); 205 self.frame_cache.push(f); 206 } 207 208 if let Some(f) = self.frame_cache.first() { 209 self.current_pc = self.addr_space.frame_to_pc(f, self.debuggee); 210 } else { 211 self.current_pc = WasmAddr::from_raw(0).unwrap(); 212 } 213 } 214 handle_event<'b>( &mut self, event: api::Event, inner: GdbStubStateMachineInner<'b, Running, Self, Conn>, ) -> Result<GdbStubStateMachine<'b, Self, Conn>>215 async fn handle_event<'b>( 216 &mut self, 217 event: api::Event, 218 inner: GdbStubStateMachineInner<'b, Running, Self, Conn>, 219 ) -> Result<GdbStubStateMachine<'b, Self, Conn>> { 220 match event { 221 api::Event::Complete => { 222 trace!("Event::Complete"); 223 let pc_bytes = self.current_pc.as_raw().to_le_bytes(); 224 let mut regs = core::iter::once(( 225 gdbstub_arch::wasm::reg::id::WasmRegId::Pc, 226 pc_bytes.as_slice(), 227 )); 228 Ok(inner.report_stop_with_regs( 229 self, 230 MultiThreadStopReason::Exited(0), 231 &mut regs, 232 )?) 233 } 234 api::Event::Breakpoint => { 235 trace!( 236 "Event::Breakpoint; single_stepping = {}", 237 self.single_stepping 238 ); 239 self.update_on_stop(); 240 let stop_reason = if self.single_stepping { 241 MultiThreadStopReason::SignalWithThread { 242 tid: self.tid, 243 signal: Signal::SIGTRAP, 244 } 245 } else { 246 MultiThreadStopReason::SwBreak(self.tid) 247 }; 248 let pc_bytes = self.current_pc.as_raw().to_le_bytes(); 249 let mut regs = core::iter::once(( 250 gdbstub_arch::wasm::reg::id::WasmRegId::Pc, 251 pc_bytes.as_slice(), 252 )); 253 Ok(inner.report_stop_with_regs(self, stop_reason, &mut regs)?) 254 } 255 api::Event::Trap => { 256 trace!("Event::Trap"); 257 self.update_on_stop(); 258 let pc_bytes = self.current_pc.as_raw().to_le_bytes(); 259 let mut regs = core::iter::once(( 260 gdbstub_arch::wasm::reg::id::WasmRegId::Pc, 261 pc_bytes.as_slice(), 262 )); 263 Ok(inner.report_stop_with_regs( 264 self, 265 // We report all traps as SIGSEGV for now. 266 MultiThreadStopReason::SignalWithThread { 267 tid: self.tid, 268 signal: Signal::SIGSEGV, 269 }, 270 &mut regs, 271 )?) 272 } 273 _ => { 274 trace!("other event: {event:?}"); 275 if self.interrupt { 276 self.interrupt = false; 277 self.update_on_stop(); 278 let pc_bytes = self.current_pc.as_raw().to_le_bytes(); 279 let mut regs = core::iter::once(( 280 gdbstub_arch::wasm::reg::id::WasmRegId::Pc, 281 pc_bytes.as_slice(), 282 )); 283 Ok(inner.report_stop_with_regs( 284 self, 285 MultiThreadStopReason::Signal(Signal::SIGINT), 286 &mut regs, 287 )?) 288 } else { 289 if self.single_stepping { 290 self.start_single_step(api::ResumptionValue::Normal); 291 } else { 292 self.start_continue(api::ResumptionValue::Normal); 293 } 294 Ok(GdbStubStateMachine::Running(inner)) 295 } 296 } 297 } 298 } 299 value_to_bytes(&self, value: WasmValue) -> Vec<u8>300 fn value_to_bytes(&self, value: WasmValue) -> Vec<u8> { 301 match value.get_type() { 302 WasmType::WasmI32 => value.unwrap_i32().to_le_bytes().to_vec(), 303 WasmType::WasmI64 => value.unwrap_i64().to_le_bytes().to_vec(), 304 WasmType::WasmF32 => value.unwrap_f32().to_le_bytes().to_vec(), 305 WasmType::WasmF64 => value.unwrap_f64().to_le_bytes().to_vec(), 306 WasmType::WasmV128 => value.unwrap_v128(), 307 WasmType::WasmFuncref => 0u32.to_le_bytes().to_vec(), 308 WasmType::WasmExnref => 0u32.to_le_bytes().to_vec(), 309 } 310 } 311 } 312 313 struct Conn { 314 buf: Vec<u8>, 315 conn: TcpStream, 316 } 317 318 impl Conn { new(conn: TcpStream) -> Self319 fn new(conn: TcpStream) -> Self { 320 Conn { buf: vec![], conn } 321 } 322 flush(&mut self) -> anyhow::Result<()>323 async fn flush(&mut self) -> anyhow::Result<()> { 324 self.conn.write_all(&self.buf).await?; 325 self.buf.clear(); 326 Ok(()) 327 } 328 read_byte(&mut self) -> Result<Option<u8>>329 async fn read_byte(&mut self) -> Result<Option<u8>> { 330 let mut buf = [0u8]; 331 let len = self.conn.read(&mut buf).await?; 332 if len == 1 { Ok(Some(buf[0])) } else { Ok(None) } 333 } 334 } 335 336 impl Drop for Conn { drop(&mut self)337 fn drop(&mut self) { 338 assert!( 339 self.buf.is_empty(), 340 "failed to async-flush before dropping connection write buffer" 341 ); 342 } 343 } 344 345 impl Connection for Conn { 346 type Error = anyhow::Error; 347 write(&mut self, byte: u8) -> std::result::Result<(), Self::Error>348 fn write(&mut self, byte: u8) -> std::result::Result<(), Self::Error> { 349 self.buf.push(byte); 350 Ok(()) 351 } 352 flush(&mut self) -> std::result::Result<(), Self::Error>353 fn flush(&mut self) -> std::result::Result<(), Self::Error> { 354 // We cannot flush synchronously; we leave this to the `async 355 // fn flush` method called within the main loop. Fortunately 356 // the gdbstub cannot wait for a response before returning to 357 // the main loop, so we cannot introduce any deadlocks by 358 // failing to flush synchronously here. 359 Ok(()) 360 } 361 } 362