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