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