//! An incredibly simple emulator to run elf binaries compiled with //! `arm-none-eabi-cc -march=armv4t`. Uses a dual-core architecture to show off //! `gdbstub`'s multi-process support. It's not modeled after any real-world //! system. use gdbstub::common::Signal; use gdbstub::conn::Connection; use gdbstub::conn::ConnectionExt; use gdbstub::stub::run_blocking; use gdbstub::stub::DisconnectReason; use gdbstub::stub::GdbStub; use gdbstub::stub::MultiThreadStopReason; use gdbstub::target::Target; use std::net::TcpListener; use std::net::TcpStream; #[cfg(unix)] use std::os::unix::net::UnixListener; #[cfg(unix)] use std::os::unix::net::UnixStream; type DynResult = Result>; static TEST_PROGRAM_ELF: &[u8] = include_bytes!("test_bin/test.elf"); mod emu; mod gdb; mod mem_sniffer; fn wait_for_tcp(port: u16) -> DynResult { let sockaddr = format!("127.0.0.1:{}", port); eprintln!("Waiting for a GDB connection on {:?}...", sockaddr); let sock = TcpListener::bind(sockaddr)?; let (stream, addr) = sock.accept()?; eprintln!("Debugger connected from {}", addr); Ok(stream) } #[cfg(unix)] fn wait_for_uds(path: &str) -> DynResult { match std::fs::remove_file(path) { Ok(_) => {} Err(e) => match e.kind() { std::io::ErrorKind::NotFound => {} _ => return Err(e.into()), }, } eprintln!("Waiting for a GDB connection on {}...", path); let sock = UnixListener::bind(path)?; let (stream, addr) = sock.accept()?; eprintln!("Debugger connected from {:?}", addr); Ok(stream) } enum EmuGdbEventLoop {} impl run_blocking::BlockingEventLoop for EmuGdbEventLoop { type Target = emu::Emu; type Connection = Box>; type StopReason = MultiThreadStopReason; #[allow(clippy::type_complexity)] fn wait_for_stop_reason( target: &mut emu::Emu, conn: &mut Self::Connection, ) -> Result< run_blocking::Event, run_blocking::WaitForStopReasonError< ::Error, ::Error, >, > { // The `armv4t_multicore` example runs the emulator in the same thread as the // GDB state machine loop. As such, it uses a simple poll-based model to // check for interrupt events, whereby the emulator will check if there // is any incoming data over the connection, and pause execution with a // synthetic `RunEvent::IncomingData` event. // // In more complex integrations, the target will probably be running in a // separate thread, and instead of using a poll-based model to check for // incoming data, you'll want to use some kind of "select" based model to // simultaneously wait for incoming GDB data coming over the connection, along // with any target-reported stop events. // // The specifics of how this "select" mechanism work + how the target reports // stop events will entirely depend on your project's architecture. // // Some ideas on how to implement this `select` mechanism: // // - A mpsc channel // - epoll/kqueue // - Running the target + stopping every so often to peek the connection // - Driving `GdbStub` from various interrupt handlers let poll_incoming_data = || { // gdbstub takes ownership of the underlying connection, so the `borrow_conn` // method is used to borrow the underlying connection back from the stub to // check for incoming data. conn.peek().map(|b| b.is_some()).unwrap_or(true) }; match target.run(poll_incoming_data) { emu::RunEvent::IncomingData => { let byte = conn .read() .map_err(run_blocking::WaitForStopReasonError::Connection)?; Ok(run_blocking::Event::IncomingData(byte)) } emu::RunEvent::Event(event, cpuid) => { use gdbstub::target::ext::breakpoints::WatchKind; // translate emulator stop reason into GDB stop reason let tid = gdb::cpuid_to_tid(cpuid); let stop_reason = match event { emu::Event::DoneStep => MultiThreadStopReason::DoneStep, emu::Event::Halted => MultiThreadStopReason::Terminated(Signal::SIGSTOP), emu::Event::Break => MultiThreadStopReason::SwBreak(tid), emu::Event::WatchWrite(addr) => MultiThreadStopReason::Watch { tid, kind: WatchKind::Write, addr, }, emu::Event::WatchRead(addr) => MultiThreadStopReason::Watch { tid, kind: WatchKind::Read, addr, }, }; Ok(run_blocking::Event::TargetStopped(stop_reason)) } } } fn on_interrupt( _target: &mut emu::Emu, ) -> Result>, ::Error> { // Because this emulator runs as part of the GDB stub loop, there isn't any // special action that needs to be taken to interrupt the underlying target. It // is implicitly paused whenever the stub isn't within the // `wait_for_stop_reason` callback. Ok(Some(MultiThreadStopReason::Signal(Signal::SIGINT))) } } fn main() -> DynResult<()> { pretty_env_logger::init(); let mut emu = emu::Emu::new(TEST_PROGRAM_ELF)?; let connection: Box> = { if std::env::args().nth(1) == Some("--uds".to_string()) { #[cfg(not(unix))] { return Err("Unix Domain Sockets can only be used on Unix".into()); } #[cfg(unix)] { Box::new(wait_for_uds("/tmp/armv4t_gdb")?) } } else { Box::new(wait_for_tcp(9001)?) } }; let gdb = GdbStub::new(connection); match gdb.run_blocking::(&mut emu) { Ok(disconnect_reason) => match disconnect_reason { DisconnectReason::Disconnect => { println!("GDB client has disconnected. Running to completion..."); while emu.step() != Some((emu::Event::Halted, emu::CpuId::Cpu)) {} } DisconnectReason::TargetExited(code) => { println!("Target exited with code {}!", code) } DisconnectReason::TargetTerminated(sig) => { println!("Target terminated with signal {}!", sig) } DisconnectReason::Kill => println!("GDB sent a kill command!"), }, Err(e) => { if e.is_target_error() { println!( "target encountered a fatal error: {}", e.into_target_error().unwrap() ) } else if e.is_connection_error() { let (e, kind) = e.into_connection_error().unwrap(); println!("connection error: {:?} - {}", kind, e,) } else { println!("gdbstub encountered a fatal error: {}", e) } } } let ret = emu.cpu.reg_get(armv4t_emu::Mode::User, 0); println!("Program completed. Return value: {}", ret); Ok(()) }