rsemu 0.0.4

A multiplatform emulator in pure Rust, built bottom-up on a generic framework.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
//! The GDB remote serial protocol, over TCP (`ROADMAP.md` §8).
//!
//! ```console
//! $ rsemu debug apple1 --gdb :1234
//! $ gdb -ex 'target remote :1234'
//! ```
//!
//! This is the highest-leverage tool for every later phase: building x86
//! protected mode or bringing up a kernel without a debugger is a self-inflicted
//! wound. It is also the code path that violates `ROADMAP.md` §15's invariant 5
//! first if nobody is careful, so that invariant has a section of its own below.
//!
//! # Layout
//!
//! | Module | Covers |
//! | --- | --- |
//! | [`packet`] | framing, checksums, `+`/`-`, escapes, run-length encoding |
//! | [`arch`] | per-CPU register maps and the `qXfer:features:read` XML |
//! | [`target`] | the [`DebugTarget`] seam, and a [`Machine`] behind it |
//! | [`stub`] | the protocol: one packet in, one reply out |
//! | this module | the listener, the session loop, and [`serve`] |
//!
//! The split is deliberate: [`stub::Stub`] owns no socket and no machine, so the
//! whole protocol is tested against a fake target with neither, and
//! [`target::MachineTarget`] is tested against a real machine with no socket.
//! Only this module needs both.
//!
//! # Invariant 5: every debugger access is a debug access
//!
//! Reading a device register from GDB must not acknowledge an interrupt, pop a
//! FIFO or advance a pointer. There is exactly one constructor for access
//! attributes in this subsystem — [`target::debug_attrs`] — and it starts from
//! [`MemAttrs::DEBUG`](crate::core::space::MemAttrs::DEBUG). Nothing else here
//! builds a `MemAttrs`, so honouring the invariant is a property of the code's
//! shape rather than of everyone remembering.
//!
//! It matters more here than anywhere else because watchpoints are *polled*:
//! the watched bytes are re-read after every clock tick, so one non-debug read
//! would be a side effect a million times a second.
//!
//! # Stopping the world
//!
//! A debugger must not race the scheduler (`ROADMAP.md` §4.7). It does not have
//! to here, because the session loop and the machine share one thread: virtual
//! time advances only inside [`DebugTarget::resume`] and
//! [`DebugTarget::step`], both called from [`GdbServer::poll`] and never while a
//! packet is being answered.
//!
//! **That holds under `parallel` too**, which is worth stating because it is
//! the mode where two cores really are on host threads of their own. A round
//! joins every job it submitted before it returns, and that join *is* §4.7's
//! rendezvous — so `Machine::run_until` and `Machine::step_until` both come back
//! with every runnable unwound to the scheduler whatever the threading mode is,
//! and there is no instant at which a packet is answered while a guest core is
//! mid-instruction. The stub therefore never has to raise a stop-the-world
//! barrier of its own; if `Machine` ever grew a way to leave a round in flight
//! across a return, [`Machine::stop_the_world`](crate::machine::Machine::stop_the_world)
//! is what would go around each of those two calls. `tests/gdb_multicpu.rs`
//! debugs a two-core machine in that mode rather than leaving this a claim.
//!
//! What `parallel` does cost the debugger is *speed*, not correctness: a
//! breakpoint-checking run advances one clock tick at a time, and under
//! `parallel` each of those ticks is a job dispatch per core.
//!
//! # What is not here
//!
//! * **Read and access watchpoints.** `Z3` and `Z4` need to observe a guest
//!   *read*, which needs a hook on the access path; `core::space` has none, and
//!   a debug read cannot see one. They are refused, not faked. `Z2` — stop when
//!   the watched bytes change — is polled and does work.
//! * **A register view on `Device`.** There is no route from a `dyn Device` to a
//!   concrete CPU, so registers are read and written through the device's
//!   snapshot chunk with a per-class byte map. See [`arch`].
//! * **A gdbarch for every core.** A target description gives a client the
//!   register file; upstream GDB additionally insists on knowing the machine,
//!   and has no 6502. `rsemu debug` says so at startup rather than letting the
//!   user find out from GDB's error. [`arch`] has the long form.
//!
//! # Sources
//!
//! The GDB manual's "Remote Protocol" and "Target Descriptions" appendices;
//! `docs/system/debug-protocols.md` for why implementing a published wire
//! protocol from its specification is not a provenance problem.

pub mod arch;
pub mod packet;
pub mod stub;
pub mod target;

use std::io::{ErrorKind, Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::time::Duration;

use crate::machine::Machine;

pub use stub::Outcome;
pub use target::{DebugTarget, MachineTarget, Stop, StopKind, TargetError, TargetResult};

/// How long an idle poll sleeps rather than spinning on a socket that has
/// nothing to say.
///
/// One millisecond: below a person's reaction time, and it keeps a halted
/// session off the CPU entirely.
const IDLE_SLEEP: Duration = Duration::from_millis(1);

/// Where a session has got to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Progress {
    /// No client has ever connected. The machine is held stopped, so that
    /// attaching lands on the reset vector rather than wherever the guest had
    /// got to.
    Waiting,
    /// A client is connected and has the machine stopped.
    Halted,
    /// A client is connected and has asked the machine to run.
    Running,
    /// A client attached and detached. The machine is free to run.
    Detached,
    /// The client sent `k`: shut the machine down.
    Kill,
}

/// One accepted connection.
#[derive(Debug)]
struct Conn {
    stream: TcpStream,
    peer: Option<SocketAddr>,
    framer: packet::Framer,
    stub: stub::Stub,
    /// Bytes written but not yet accepted by the socket.
    pending: Vec<u8>,
}

/// A GDB remote-protocol server listening on a TCP port.
///
/// Single connection at a time, on purpose: two debuggers driving one machine's
/// execution state would fight, and the second one's `continue` would be the
/// first one's mystery.
#[derive(Debug)]
pub struct GdbServer {
    listener: TcpListener,
    conn: Option<Conn>,
    /// Whether the machine is held stopped until the first client attaches.
    wait_for_attach: bool,
    /// Set once a client has attached and gone.
    detached: bool,
}

impl GdbServer {
    /// Bind a listener.
    ///
    /// `addr` may be `1234`, `:1234`, `host:1234` or `[::1]:1234`. **A bare
    /// port or a leading colon binds the loopback interface only**: the far end
    /// of this socket can read and write every byte of guest memory and change
    /// the program counter, so exposing it to the network is a decision someone
    /// has to make explicitly by naming an address (`0.0.0.0:1234`).
    ///
    /// # Errors
    ///
    /// An address that does not parse or resolve, or a port that cannot be
    /// bound.
    pub fn bind(addr: &str) -> std::io::Result<GdbServer> {
        let resolved = resolve(addr)?;
        let listener = TcpListener::bind(&resolved[..])?;
        listener.set_nonblocking(true)?;
        Ok(GdbServer {
            listener,
            conn: None,
            wait_for_attach: true,
            detached: false,
        })
    }

    /// Let the machine run before anyone attaches.
    ///
    /// The default is to hold it stopped, which is what `rsemu debug` wants:
    /// attaching then lands on the reset vector rather than wherever a
    /// free-running guest happened to be.
    #[must_use]
    pub fn without_waiting(mut self) -> GdbServer {
        self.wait_for_attach = false;
        self
    }

    /// The address actually bound, which is how a test finds the ephemeral port
    /// it asked for with `:0`.
    ///
    /// # Errors
    ///
    /// Whatever the operating system says about the socket.
    pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
        self.listener.local_addr()
    }

    /// Whether a client is connected.
    #[must_use]
    pub fn is_attached(&self) -> bool {
        self.conn.is_some()
    }

    /// Service the debugger, and let the machine run if it has been told to.
    ///
    /// One call is one turn of the session loop: accept a connection if one is
    /// waiting, read and answer whatever packets have arrived, and advance the
    /// machine by one slice if the client asked for that. It never blocks for
    /// longer than a millisecond, so a caller can pump a console between calls.
    ///
    /// # Errors
    ///
    /// A socket error other than "would block" and other than a peer that hung
    /// up — that one closes the connection and is reported as
    /// [`Progress::Detached`].
    pub fn poll(&mut self, target: &mut dyn DebugTarget) -> std::io::Result<Progress> {
        self.accept()?;
        let Some(conn) = self.conn.as_mut() else {
            std::thread::sleep(IDLE_SLEEP);
            return Ok(if self.wait_for_attach && !self.detached {
                Progress::Waiting
            } else {
                Progress::Detached
            });
        };

        let mut out = Vec::new();
        let mut outcome = Outcome::Continue;
        let mut closed = false;

        let mut buf = [0u8; 1024];
        match conn.stream.read(&mut buf) {
            Ok(0) => closed = true,
            Ok(n) => {
                for byte in buf.get(..n).unwrap_or(&[]) {
                    if let Some(event) = conn.framer.push(*byte) {
                        match conn.stub.on_event(event, target, &mut out) {
                            Outcome::Continue => {}
                            other => outcome = other,
                        }
                    }
                }
            }
            Err(e) if e.kind() == ErrorKind::WouldBlock => {}
            Err(e) if e.kind() == ErrorKind::Interrupted => {}
            Err(_) => closed = true,
        }

        if !closed && outcome == Outcome::Continue {
            conn.stub.drive(target, &mut out);
        }

        conn.pending.extend_from_slice(&out);
        if !closed && !Self::flush(conn) {
            closed = true;
        }

        let running = conn.stub.is_running();
        match outcome {
            Outcome::Kill => {
                self.conn = None;
                return Ok(Progress::Kill);
            }
            Outcome::Detach => {
                // Flush the `OK` before hanging up, or GDB reports the detach
                // as a protocol error.
                let _ = conn.stream.flush();
                closed = true;
            }
            Outcome::Continue => {}
        }

        if closed {
            self.conn = None;
            self.detached = true;
            return Ok(Progress::Detached);
        }
        if running {
            Ok(Progress::Running)
        } else {
            // Nothing to do but wait for the next packet.
            std::thread::sleep(IDLE_SLEEP);
            Ok(Progress::Halted)
        }
    }

    /// Take a waiting connection, if there is one.
    fn accept(&mut self) -> std::io::Result<()> {
        if self.conn.is_some() {
            return Ok(());
        }
        match self.listener.accept() {
            Ok((stream, peer)) => {
                stream.set_nonblocking(true)?;
                // Debug traffic is small and latency-sensitive: a stop reply
                // held back by Nagle's algorithm is a debugger that feels
                // broken.
                let _ = stream.set_nodelay(true);
                self.conn = Some(Conn {
                    stream,
                    peer: Some(peer),
                    framer: packet::Framer::new(),
                    stub: stub::Stub::new(),
                    pending: Vec::new(),
                });
                Ok(())
            }
            Err(e) if e.kind() == ErrorKind::WouldBlock => Ok(()),
            Err(e) if e.kind() == ErrorKind::Interrupted => Ok(()),
            Err(e) => Err(e),
        }
    }

    /// Push as much of the pending output as the socket will take.
    ///
    /// Returns false when the peer has gone. A short write is normal on a
    /// non-blocking socket and simply leaves the rest queued for the next poll.
    fn flush(conn: &mut Conn) -> bool {
        while !conn.pending.is_empty() {
            match conn.stream.write(&conn.pending) {
                Ok(0) => return false,
                Ok(n) => {
                    conn.pending.drain(..n);
                }
                Err(e) if e.kind() == ErrorKind::WouldBlock => return true,
                Err(e) if e.kind() == ErrorKind::Interrupted => {}
                Err(_) => return false,
            }
        }
        let _ = conn.stream.flush();
        true
    }

    /// The connected client's address, for a status line.
    #[must_use]
    pub fn peer(&self) -> Option<SocketAddr> {
        self.conn.as_ref().and_then(|c| c.peer)
    }
}

/// Turn `1234`, `:1234` or `host:1234` into addresses to bind.
///
/// The rule — a bare port or a leading colon binds the loopback interface only
/// — and the reason for it live in [`crate::host::listen`], so that the two
/// frontends which hand a stranger something dangerous cannot drift apart about
/// what an address means.
fn resolve(addr: &str) -> std::io::Result<Vec<SocketAddr>> {
    crate::host::listen::resolve(addr)
}

/// Why [`serve`] returned.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitReason {
    /// The client sent `k`.
    Killed,
    /// The caller's stop condition fired — a deadline, or a console interrupt.
    Stopped,
}

/// Run `machine` under a debugger until it is killed or `keep_going` says stop.
///
/// The whole session loop in one call, so a front end is three lines. Between
/// turns it calls `keep_going`, which is where a console is pumped and a
/// Ctrl-C on the emulator's own terminal is noticed; returning `false` from it
/// ends the session.
///
/// # Errors
///
/// A socket error the session cannot continue past.
pub fn serve(
    machine: &mut Machine,
    server: &mut GdbServer,
    mut keep_going: impl FnMut(&mut Machine) -> bool,
) -> std::io::Result<ExitReason> {
    let mut target = MachineTarget::new(machine);
    loop {
        let progress = server.poll(&mut target)?;
        if progress == Progress::Kill {
            return Ok(ExitReason::Killed);
        }
        if progress == Progress::Detached {
            // Nobody is watching: let it run rather than freezing a guest whose
            // debugger has gone home.
            if let Err(e) = target.resume() {
                return Err(std::io::Error::other(e.to_string()));
            }
        }
        if !keep_going(target.machine_mut()) {
            return Ok(ExitReason::Stopped);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_bare_port_binds_the_loopback_only() {
        let addrs = resolve("1234").expect("a bare port");
        assert!(addrs.iter().all(|a| a.ip().is_loopback()), "{addrs:?}");
        let addrs = resolve(":1234").expect("a leading colon");
        assert!(addrs.iter().all(|a| a.ip().is_loopback()), "{addrs:?}");
        // An explicit address is honoured as written, which is the only way to
        // expose the port.
        let addrs = resolve("0.0.0.0:1234").expect("an explicit address");
        assert!(addrs.iter().any(|a| a.ip().is_unspecified()), "{addrs:?}");
    }

    #[test]
    fn a_nonsense_address_is_an_error_not_a_panic() {
        assert!(resolve("").is_err());
        assert!(resolve("not a host name at all:1").is_err());
    }

    #[test]
    fn an_ephemeral_port_reports_where_it_landed() {
        let server = GdbServer::bind(":0").expect("bind");
        let addr = server.local_addr().expect("local_addr");
        assert!(addr.port() != 0);
        assert!(addr.ip().is_loopback());
        assert!(!server.is_attached());
    }
}