1 //! An incredibly simple emulator to run elf binaries compiled with
2 //! `arm-none-eabi-cc -march=armv4t`. It's not modeled after any real-world
3 //! system.
4 
5 use gdbstub::common::Signal;
6 use gdbstub::conn::Connection;
7 use gdbstub::conn::ConnectionExt;
8 use gdbstub::stub::run_blocking;
9 use gdbstub::stub::DisconnectReason;
10 use gdbstub::stub::GdbStub;
11 use gdbstub::stub::SingleThreadStopReason;
12 use gdbstub::target::Target;
13 use std::net::TcpListener;
14 use std::net::TcpStream;
15 #[cfg(unix)]
16 use std::os::unix::net::UnixListener;
17 #[cfg(unix)]
18 use std::os::unix::net::UnixStream;
19 
20 type DynResult<T> = Result<T, Box<dyn std::error::Error>>;
21 
22 const TEST_PROGRAM_ELF: &[u8] = include_bytes!("test_bin/test.elf");
23 
24 mod emu;
25 mod gdb;
26 mod mem_sniffer;
27 
wait_for_tcp(port: u16) -> DynResult<TcpStream>28 fn wait_for_tcp(port: u16) -> DynResult<TcpStream> {
29     let sockaddr = format!("127.0.0.1:{}", port);
30     eprintln!("Waiting for a GDB connection on {:?}...", sockaddr);
31 
32     let sock = TcpListener::bind(sockaddr)?;
33     let (stream, addr) = sock.accept()?;
34     eprintln!("Debugger connected from {}", addr);
35 
36     Ok(stream)
37 }
38 
39 #[cfg(unix)]
wait_for_uds(path: &str) -> DynResult<UnixStream>40 fn wait_for_uds(path: &str) -> DynResult<UnixStream> {
41     match std::fs::remove_file(path) {
42         Ok(_) => {}
43         Err(e) => match e.kind() {
44             std::io::ErrorKind::NotFound => {}
45             _ => return Err(e.into()),
46         },
47     }
48 
49     eprintln!("Waiting for a GDB connection on {}...", path);
50 
51     let sock = UnixListener::bind(path)?;
52     let (stream, addr) = sock.accept()?;
53     eprintln!("Debugger connected from {:?}", addr);
54 
55     Ok(stream)
56 }
57 
58 enum EmuGdbEventLoop {}
59 
60 impl run_blocking::BlockingEventLoop for EmuGdbEventLoop {
61     type Target = emu::Emu;
62     type Connection = Box<dyn ConnectionExt<Error = std::io::Error>>;
63     type StopReason = SingleThreadStopReason<u32>;
64 
65     #[allow(clippy::type_complexity)]
wait_for_stop_reason( target: &mut emu::Emu, conn: &mut Self::Connection, ) -> Result< run_blocking::Event<SingleThreadStopReason<u32>>, run_blocking::WaitForStopReasonError< <Self::Target as Target>::Error, <Self::Connection as Connection>::Error, >, >66     fn wait_for_stop_reason(
67         target: &mut emu::Emu,
68         conn: &mut Self::Connection,
69     ) -> Result<
70         run_blocking::Event<SingleThreadStopReason<u32>>,
71         run_blocking::WaitForStopReasonError<
72             <Self::Target as Target>::Error,
73             <Self::Connection as Connection>::Error,
74         >,
75     > {
76         // The `armv4t` example runs the emulator in the same thread as the GDB state
77         // machine loop. As such, it uses a simple poll-based model to check for
78         // interrupt events, whereby the emulator will check if there is any incoming
79         // data over the connection, and pause execution with a synthetic
80         // `RunEvent::IncomingData` event.
81         //
82         // In more complex integrations, the target will probably be running in a
83         // separate thread, and instead of using a poll-based model to check for
84         // incoming data, you'll want to use some kind of "select" based model to
85         // simultaneously wait for incoming GDB data coming over the connection, along
86         // with any target-reported stop events.
87         //
88         // The specifics of how this "select" mechanism work + how the target reports
89         // stop events will entirely depend on your project's architecture.
90         //
91         // Some ideas on how to implement this `select` mechanism:
92         //
93         // - A mpsc channel
94         // - epoll/kqueue
95         // - Running the target + stopping every so often to peek the connection
96         // - Driving `GdbStub` from various interrupt handlers
97 
98         let poll_incoming_data = || {
99             // gdbstub takes ownership of the underlying connection, so the `borrow_conn`
100             // method is used to borrow the underlying connection back from the stub to
101             // check for incoming data.
102             conn.peek().map(|b| b.is_some()).unwrap_or(true)
103         };
104 
105         match target.run(poll_incoming_data) {
106             emu::RunEvent::IncomingData => {
107                 let byte = conn
108                     .read()
109                     .map_err(run_blocking::WaitForStopReasonError::Connection)?;
110                 Ok(run_blocking::Event::IncomingData(byte))
111             }
112             emu::RunEvent::Event(event) => {
113                 use gdbstub::target::ext::breakpoints::WatchKind;
114 
115                 // translate emulator stop reason into GDB stop reason
116                 let stop_reason = match event {
117                     emu::Event::DoneStep => SingleThreadStopReason::DoneStep,
118                     emu::Event::Halted => SingleThreadStopReason::Terminated(Signal::SIGSTOP),
119                     emu::Event::Break => SingleThreadStopReason::SwBreak(()),
120                     emu::Event::WatchWrite(addr) => SingleThreadStopReason::Watch {
121                         tid: (),
122                         kind: WatchKind::Write,
123                         addr,
124                     },
125                     emu::Event::WatchRead(addr) => SingleThreadStopReason::Watch {
126                         tid: (),
127                         kind: WatchKind::Read,
128                         addr,
129                     },
130                 };
131 
132                 Ok(run_blocking::Event::TargetStopped(stop_reason))
133             }
134         }
135     }
136 
on_interrupt( _target: &mut emu::Emu, ) -> Result<Option<SingleThreadStopReason<u32>>, <emu::Emu as Target>::Error>137     fn on_interrupt(
138         _target: &mut emu::Emu,
139     ) -> Result<Option<SingleThreadStopReason<u32>>, <emu::Emu as Target>::Error> {
140         // Because this emulator runs as part of the GDB stub loop, there isn't any
141         // special action that needs to be taken to interrupt the underlying target. It
142         // is implicitly paused whenever the stub isn't within the
143         // `wait_for_stop_reason` callback.
144         Ok(Some(SingleThreadStopReason::Signal(Signal::SIGINT)))
145     }
146 }
147 
main() -> DynResult<()>148 fn main() -> DynResult<()> {
149     pretty_env_logger::init();
150 
151     let mut emu = emu::Emu::new(TEST_PROGRAM_ELF)?;
152 
153     let connection: Box<dyn ConnectionExt<Error = std::io::Error>> = {
154         if std::env::args().nth(1) == Some("--uds".to_string()) {
155             #[cfg(not(unix))]
156             {
157                 return Err("Unix Domain Sockets can only be used on Unix".into());
158             }
159             #[cfg(unix)]
160             {
161                 Box::new(wait_for_uds("/tmp/armv4t_gdb")?)
162             }
163         } else {
164             Box::new(wait_for_tcp(9001)?)
165         }
166     };
167 
168     let gdb = GdbStub::new(connection);
169 
170     match gdb.run_blocking::<EmuGdbEventLoop>(&mut emu) {
171         Ok(disconnect_reason) => match disconnect_reason {
172             DisconnectReason::Disconnect => {
173                 println!("GDB client has disconnected. Running to completion...");
174                 while emu.step() != Some(emu::Event::Halted) {}
175             }
176             DisconnectReason::TargetExited(code) => {
177                 println!("Target exited with code {}!", code)
178             }
179             DisconnectReason::TargetTerminated(sig) => {
180                 println!("Target terminated with signal {}!", sig)
181             }
182             DisconnectReason::Kill => println!("GDB sent a kill command!"),
183         },
184         Err(e) => {
185             if e.is_target_error() {
186                 println!(
187                     "target encountered a fatal error: {}",
188                     e.into_target_error().unwrap()
189                 )
190             } else if e.is_connection_error() {
191                 let (e, kind) = e.into_connection_error().unwrap();
192                 println!("connection error: {:?} - {}", kind, e,)
193             } else {
194                 println!("gdbstub encountered a fatal error: {}", e)
195             }
196         }
197     }
198 
199     let ret = emu.cpu.reg_get(armv4t_emu::Mode::User, 0);
200     println!("Program completed. Return value: {}", ret);
201 
202     Ok(())
203 }
204