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
//! The firmware socket: where a PC's BIOS lives, and how a user's own binary
//! gets there.
//!
//! rsemu ships no BIOS. A PC's firmware is whatever the person running the
//! machine points at — their copy of SeaBIOS, a vendor image dumped off a real
//! board, a hand-written test stub — and it is bound to a **media slot**, the
//! same mechanism the RISC-V board loads its firmware through:
//!
//! ```console
//! $ rsemu run pc-at --bios /usr/share/qemu/bios.bin
//! $ rsemu run pc-at --media bios=/path/to/bios.bin --media vgabios=/path/to/vgabios.bin
//! ```
//!
//! Nothing is vendored, and nothing is downloaded: the bytes come from the
//! caller (`machine::realize`'s media table), the emulation core never opens a
//! file, and a slot nothing is bound to is an error naming the slot rather than
//! a machine that quietly comes up with no firmware.
//!
//! # Why this is a ROM socket and not a loader
//!
//! `riscv.loader` writes its image into DRAM at reset, because a RISC-V board
//! has no mask ROM and its firmware genuinely lives in RAM. A PC's does not: the
//! BIOS is in a ROM the chipset decodes at the top of the address space, the
//! guest can read it forever, and — this is the part that matters — **a PC's
//! firmware writes to its own address range on purpose**. Shadowing, the
//! chipset registers that make `0xe0000-0xfffff` writable so the BIOS can copy
//! itself into RAM, and the option-ROM checksum walk all touch it. A device that
//! had written bytes into RAM and gone away could model none of that.
//!
//! So this is a [`Region::rom`](crate::core::space::Region::rom): reads come
//! straight from the store, and a write is **ignored, not faulted**. A PC with
//! no chipset shadow control has ROM in those sockets and a write to ROM does
//! nothing at all — which is exactly what firmware that probes for shadow RAM
//! expects to find when there is none.
//!
//! The shadow, where there is one, is not this device's: `pc-at` carries a
//! [`pc.pmc`](super::pmc) host bridge whose PAM registers map main memory over
//! these sockets, per 16 KiB window and separately for reads and for writes.
//! The socket goes on behaving as a socket underneath it — a cycle the bridge
//! does not claim still reaches the ROM, and a write it does not claim still
//! goes nowhere. That division is the hardware's: a ROM chip has no idea it is
//! being shadowed.
//!
//! # The image, the socket, and which end it sits at
//!
//! A socket has a fixed size, set by `size`, and `align` says which end a
//! shorter image lands at. The two answers are not a preference — they are two
//! different kinds of ROM:
//!
//! ```text
//!   align = "top"     size = 128K, image = 64K
//!       0x00000-0x0ffff erased, 0x10000-0x1ffff image
//!   align = "bottom"  size = 64K,  image = 38K
//!       0x00000-0x097ff image, 0x09800-0x0ffff erased
//! ```
//!
//! **The system BIOS is top-aligned**, because an x86 starts executing at the
//! *top* of its address space (`0xffff:0x0000` on an 8086, `0xfffffff0` on a
//! 386): whatever is in the last bytes of the socket is what runs first. A 64
//! KiB image bottom-aligned in a 128 KiB window would put erased bytes under
//! the reset vector, and the machine would execute `0xff` — `INC BYTE PTR
//! [BX+DI]` — forever, with nothing to say why.
//!
//! **An option ROM is bottom-aligned**, because firmware finds one by scanning
//! for the `0x55 0xaa` signature on a 2 KiB boundary and then trusting the
//! length byte that follows it. A video BIOS top-aligned in its window would
//! put that signature somewhere the scan never looks.
//!
//! `top` is the default, because getting the *system* BIOS wrong is the failure
//! with no diagnostic.
//!
//! The machine file then maps the socket wherever the board decodes it, and may
//! map it **twice**: a 386 fetches its first instruction from `0xfffffff0`,
//! sixteen bytes below 4 GiB, while every real-mode `far jmp` afterwards lands
//! at `0x000f0000-0x000fffff`. Both windows are the same chip, which is why a
//! `map` statement can name this region more than once.

use alloc::boxed::Box;
use alloc::format;
use alloc::sync::Arc;

use crate::core::device::{Device, DeviceClass, PropertySpec, RealizeCtx, ResetKind};
use crate::core::error::{Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::space::{Region, RegionRef, RomStore, RomWrite};
use crate::machine::realize::Instance;
use crate::machine::validate::{ClassSchema, PropSchema};

/// The class name a machine description writes.
pub const CLASS_NAME: &str = "pc.rom";

/// What an unprogrammed cell reads as, and what a short image is padded with.
///
/// `0xff` because that is an erased EPROM byte, and because firmware that
/// checksums an option ROM header looks for `0x55 0xaa` — which erased flash
/// never accidentally is.
const ERASED: u8 = 0xff;

/// The socket size a machine file gets if it names none: 128 KiB, the AT's
/// `0xe0000-0xfffff` system ROM window.
const DEFAULT_SIZE: u64 = 128 * 1024;

/// The largest socket this class will allocate.
///
/// Not a hardware limit — it is a sanity bound, so that `size = 4G` in a
/// machine file is an error naming the property rather than an allocation that
/// takes the host down.
const MAX_SIZE: u64 = 16 * 1024 * 1024;

/// Which end of the socket a short image sits at.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Align {
    /// Against the top, under the reset vector. What a system BIOS needs.
    Top,
    /// Against the bottom, where an option-ROM scan looks for `0x55 0xaa`.
    Bottom,
}

/// A firmware ROM socket.
#[derive(Debug)]
pub struct FirmwareRom {
    store: Arc<RomStore>,
    region: RegionRef,
    /// How many bytes of the socket the image actually filled, for diagnostics
    /// and for `rsemu`'s summary line.
    image_len: u64,
}

impl FirmwareRom {
    /// Validate `props` and build the device.
    ///
    /// # Errors
    ///
    /// [`Error::Property`] if the `image` media slot is missing, if its image
    /// does not fit the socket, or if a property this class does not know was
    /// given.
    pub fn new(props: &Props) -> Result<FirmwareRom> {
        let mut r = props.reader();
        let image = r.require_media("image")?.to_bytes();
        let size = r.or_size("size", DEFAULT_SIZE)?;
        let align = r.or_enum("align", "top", &["top", "bottom"])?;
        let align = if align == "bottom" {
            Align::Bottom
        } else {
            Align::Top
        };
        r.finish()?;
        FirmwareRom::from_image(&image, size, align)
    }

    /// Build one from bytes directly.
    ///
    /// # Errors
    ///
    /// [`Error::Property`] if `size` is zero or implausibly large, or if the
    /// image does not fit.
    pub fn from_image(image: &[u8], size: u64, align: Align) -> Result<FirmwareRom> {
        if size == 0 || size > MAX_SIZE {
            return Err(Error::Property(format!(
                "property `size`: a firmware socket holds between 1 and {MAX_SIZE} bytes, not \
                 {size}"
            )));
        }
        let len = image.len() as u64;
        if len > size {
            return Err(Error::Property(format!(
                "property `image`: this socket is {size} bytes and the image is {len}; give the \
                 object a larger `size`, and map it over a larger window"
            )));
        }
        let mut bytes = alloc::vec![ERASED; size as usize];
        let at = match align {
            // An x86 fetches its first instruction from the top of the socket,
            // so a short system BIOS must end where the reset vector is.
            Align::Top => (size - len) as usize,
            // An option ROM is found by a scan for `0x55 0xaa` from the bottom
            // of its window upward, so it must start there.
            Align::Bottom => 0,
        };
        bytes[at..at + len as usize].copy_from_slice(image);
        let store = Arc::new(RomStore::new(bytes));
        let region: RegionRef = Arc::new(Region::rom(
            CLASS_NAME,
            Arc::clone(&store),
            // Ignored, not faulted: a write to a ROM socket that has no shadow
            // control behind it does nothing on real hardware, and firmware
            // probing for shadow RAM writes to itself to find out.
            RomWrite::Ignore,
        ));
        Ok(FirmwareRom {
            store,
            region,
            image_len: len,
        })
    }

    /// How many bytes the socket holds.
    #[must_use]
    pub fn len(&self) -> u64 {
        self.store.len()
    }

    /// Whether the socket holds no bytes — it never does; `new` refuses a zero
    /// size.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.store.len() == 0
    }

    /// How many bytes of the socket the image filled.
    #[must_use]
    pub fn image_len(&self) -> u64 {
        self.image_len
    }

    /// The backing store, for a test or a debugger that wants the bytes.
    #[must_use]
    pub fn store(&self) -> &Arc<RomStore> {
        &self.store
    }
}

/// The `pc.rom` device class.
pub static CLASS: DeviceClass = DeviceClass {
    name: CLASS_NAME,
    version: 1,
    summary: "a firmware ROM socket: a user-supplied BIOS or option ROM image",
    properties: &[
        PropertySpec {
            name: "image",
            kind: ValueKind::Media,
            required: true,
            summary: "the media slot the image is bound to (`--bios`, `--media vgabios=…`)",
        },
        PropertySpec {
            name: "size",
            kind: ValueKind::Size,
            required: false,
            summary: "how many bytes the socket decodes (default 128K)",
        },
        PropertySpec {
            name: "align",
            kind: ValueKind::Str,
            required: false,
            summary: "\"top\" for a system BIOS under the reset vector, \"bottom\" for an option ROM",
        },
    ],
    construct: |props| Ok(Box::new(FirmwareRom::new(props)?)),
};

impl Device for FirmwareRom {
    fn class(&self) -> &'static DeviceClass {
        &CLASS
    }

    fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
        // Nothing outward: a `map` statement places the region, and the
        // realizer does that after every device has realized.
        Ok(())
    }

    fn reset(&self, _kind: ResetKind) {
        // A ROM is a ROM. There is no state here to return to — the image was
        // fixed at construction, and a cold reset does not re-read a socket.
    }

    fn region(&self, name: &str) -> Option<RegionRef> {
        matches!(name, "" | "rom").then(|| Arc::clone(&self.region))
    }

    // No `save`/`load`. The contents cannot change, and writing 128 KiB of
    // unchanging bytes into every snapshot would be a cost with no benefit —
    // `ROADMAP.md` §4.5 says devices serialize architectural state, and a mask
    // ROM's architectural state is the image the machine was built with.
}

impl Instance for FirmwareRom {}

/// Add [`CLASS`] to a registry.
///
/// # Errors
///
/// [`Error::Config`] if something already claimed the name.
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
    registry.add(&CLASS)
}

/// Bind [`CLASS`] into the machine graph.
///
/// # Errors
///
/// [`Error::Config`] if the class is already bound.
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
    bindings.bind(CLASS_NAME, |props| Ok(Arc::new(FirmwareRom::new(props)?)))
}

/// What the validator should know about `pc.rom`.
#[must_use]
pub fn schema() -> ClassSchema {
    ClassSchema::new(CLASS_NAME)
        .prop(PropSchema::new("image", ValueKind::Media).required())
        .prop(PropSchema::new("size", ValueKind::Size))
        .prop(PropSchema::new("align", ValueKind::Str).values(&["top", "bottom"]))
        .region("")
        .region("rom")
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloc::string::ToString;
    use alloc::vec::Vec;

    /// A socket's worth of bytes, for a test that wants to know what is where.
    fn socket_bytes(rom: &FirmwareRom) -> Vec<u8> {
        let mut out = alloc::vec![0u8; rom.len() as usize];
        rom.store.read_at(0, &mut out).expect("the whole socket");
        out
    }
    use crate::core::space::{AddressSpace, MemAttrs};
    use crate::core::value::Width;

    #[test]
    fn a_full_image_fills_the_socket() {
        let image: Vec<u8> = (0..256u32).map(|i| i as u8).collect();
        let rom = FirmwareRom::from_image(&image, 256, Align::Top).expect("it fits exactly");
        assert_eq!(socket_bytes(&rom), image);
        assert_eq!(rom.image_len(), 256);
    }

    #[test]
    fn a_short_image_lands_at_the_top_where_the_reset_vector_is() {
        // The property the whole class turns on: an x86 fetches from the top
        // of the socket, so a 16-byte image in a 64-byte socket must end at
        // offset 63 and not at offset 15.
        let image = [0xeau8; 16];
        let rom = FirmwareRom::from_image(&image, 64, Align::Top).expect("it fits");
        let bytes = socket_bytes(&rom);
        assert_eq!(&bytes[..48], &[ERASED; 48], "the unprogrammed half");
        assert_eq!(&bytes[48..], &image, "the image, ending at the top");
    }

    #[test]
    fn a_bottom_aligned_image_starts_where_an_option_rom_scan_looks() {
        // A video BIOS is found by a scan for `0x55 0xaa` from the bottom of
        // its window; top-aligning one would hide the signature.
        let image = [0x55u8, 0xaa, 0x4c];
        let rom = FirmwareRom::from_image(&image, 64, Align::Bottom).expect("it fits");
        let bytes = socket_bytes(&rom);
        assert_eq!(&bytes[..3], &image, "the signature is at the bottom");
        assert_eq!(&bytes[3..], &[ERASED; 61], "the rest is unprogrammed");
    }

    #[test]
    fn an_image_larger_than_the_socket_is_refused_by_name() {
        let e = FirmwareRom::from_image(&[0u8; 128], 64, Align::Top)
            .expect_err("128 bytes do not fit in 64")
            .to_string();
        assert!(e.contains("image"), "{e}");
        assert!(e.contains("64"), "{e}");
    }

    #[test]
    fn an_implausible_socket_is_refused_by_name() {
        let e = FirmwareRom::from_image(&[], 1 << 40, Align::Top)
            .expect_err("a terabyte of ROM")
            .to_string();
        assert!(e.contains("size"), "{e}");
    }

    #[test]
    fn a_write_to_rom_is_ignored_rather_than_faulted() {
        // Firmware probing for shadow RAM writes to its own address range and
        // reads it back. On a board with no shadow control the write does
        // nothing and the read still returns the ROM — a bus fault here would
        // send that probe down a path no real machine takes.
        let rom = FirmwareRom::from_image(&[0x55, 0xaa], 2, Align::Top).expect("it fits");
        let space = AddressSpace::new("mem", 20);
        space
            .topology()
            .map(rom.region("").expect("the socket's region"), 0)
            .expect("nothing else is mapped");
        space
            .write(0, Width::U8, 0x00, MemAttrs::DEFAULT)
            .expect("a write to ROM is swallowed, not refused");
        assert_eq!(
            space.read(0, Width::U8, MemAttrs::DEFAULT),
            Ok(0x55),
            "the ROM is unchanged"
        );
    }

    #[test]
    fn the_class_constructs_from_properties() {
        let mut props = Props::new();
        props.insert(
            "image",
            crate::core::props::Media::new("bios", alloc::vec![0x90u8; 4]),
        );
        props.insert("size", crate::core::props::Value::Size(16));
        let dev = (CLASS.construct)(&props).expect("a socket");
        assert_eq!(dev.class().name, CLASS_NAME);
    }
}