Skip to main content

qcode_vm/
flat.rs

1//! Flat storage for the spaces a guest does not address.
2//!
3//! Register, unique and temporary spaces are small, dense and hit constantly:
4//! every operand of every p-code operation is a read or a write here. The
5//! emulator's general-purpose backing for them is `FxHashMap<u64, u8>`, which
6//! costs *one hash lookup per byte* — reading `RAX` is eight lookups, and a
7//! `Vec` allocation for the result.
8//!
9//! These spaces are nothing like guest memory: their addresses are assigned by
10//! the specification, start at zero, and span a few hundred bytes. A plain
11//! `Vec<u8>` indexed by address is the right shape, turning each access into a
12//! bounds check and a copy.
13//!
14//! # This is not, by itself, faster
15//!
16//! Measured against the hash-map backing on a hot loop, this is neutral to
17//! about 5% *slower*. Profiling says why: register access is roughly 4% of run
18//! time, so removing per-byte hashing from it cannot matter much. The cost is
19//! interpreter dispatch and allocation churn instead.
20//!
21//! It is kept because a JIT needs it. Compiled code has to reach the register
22//! file by address, and it cannot call into a hash map per operand and stay
23//! worth compiling. This is groundwork for that, not a speedup in its own
24//! right.
25//!
26//! Guest RAM deliberately does *not* live here — it is sparse across a 64-bit
27//! space and needs permissions, which is what [`Mmu`](crate::Mmu) is for.
28
29use qcode::{
30    context::Context,
31    space::{MemorySpaceId, Space, SpaceId, SpaceType},
32};
33use qcode_emulator::EmulatorErrorKind;
34use rustc_hash::FxHashMap;
35
36/// Upper bound on how large a flat space may grow.
37///
38/// These spaces hold registers and lifter scratch, so a few kilobytes is
39/// typical. The cap exists so that a malformed address cannot turn into an
40/// enormous allocation; it is far above any real specification's needs.
41const MAX_FLAT_SPACE: usize = 1 << 24;
42
43/// One densely-addressed space.
44#[derive(Debug, Default, Clone)]
45pub struct FlatSpace {
46    bytes: Vec<u8>,
47    /// Which bytes have been written. Only consulted for spaces that are not
48    /// zero-filled, where reading an unwritten byte is a lifter error worth
49    /// reporting rather than a silent zero.
50    written: Vec<bool>,
51    /// Whether an unwritten byte reads as zero.
52    ///
53    /// Register space is architectural state that exists whether or not a
54    /// harness seeded it; a unique is scratch that must be written before it is
55    /// read, so reading one that was not is a defect.
56    zero_filled: bool,
57}
58
59impl FlatSpace {
60    fn new(zero_filled: bool) -> Self {
61        Self {
62            bytes: Vec::new(),
63            written: Vec::new(),
64            zero_filled,
65        }
66    }
67
68    /// Grows the space so that `end` bytes are addressable.
69    fn reserve_to(&mut self, end: usize) -> Result<(), EmulatorErrorKind> {
70        if end > MAX_FLAT_SPACE {
71            return Err(EmulatorErrorKind::AddressOverflow(end as u64, 0));
72        }
73        if self.bytes.len() < end {
74            self.bytes.resize(end, 0);
75            // Tracked only where it is consulted. For a zero-filled space an
76            // unwritten byte legitimately reads as zero, so maintaining this
77            // would be a second array touched on every write for nothing.
78            if !self.zero_filled {
79                self.written.resize(end, false);
80            }
81        }
82        Ok(())
83    }
84
85    /// The byte range an access covers, or `None` if it would overflow.
86    fn range(addr: u64, size: usize) -> Option<(usize, usize)> {
87        let start = usize::try_from(addr).ok()?;
88        let end = start.checked_add(size)?;
89        (end <= MAX_FLAT_SPACE).then_some((start, end))
90    }
91
92    /// Grows the space so `len` bytes are addressable, and returns the base
93    /// pointer of its storage.
94    ///
95    /// For compiled code, which addresses this storage directly rather than
96    /// through the accessors. The caller must not cause the space to grow while
97    /// holding the pointer — growing may reallocate — which is why the required
98    /// size is requested up front.
99    pub fn base_ptr(&mut self, len: usize) -> Result<*mut u8, EmulatorErrorKind> {
100        self.reserve_to(len)?;
101        Ok(self.bytes.as_mut_ptr())
102    }
103
104    pub fn read_bytes(&self, addr: u64, size: usize) -> Result<Vec<u8>, EmulatorErrorKind> {
105        let Some((start, end)) = Self::range(addr, size) else {
106            return Err(EmulatorErrorKind::AddressOverflow(addr, size));
107        };
108        // Past the end of what has been written: zero-filled spaces read zero,
109        // others report the first missing byte.
110        if end > self.bytes.len() {
111            if !self.zero_filled {
112                return Err(EmulatorErrorKind::MemoryReadError(
113                    self.bytes.len().max(start) as u64,
114                ));
115            }
116            let mut out = vec![0; size];
117            let available = self.bytes.len().saturating_sub(start);
118            if available > 0 {
119                out[..available].copy_from_slice(&self.bytes[start..self.bytes.len()]);
120            }
121            return Ok(out);
122        }
123        if !self.zero_filled
124            && let Some(offset) = self.written[start..end].iter().position(|written| !written)
125        {
126            return Err(EmulatorErrorKind::MemoryReadError((start + offset) as u64));
127        }
128        Ok(self.bytes[start..end].to_vec())
129    }
130
131    /// Reads a little-endian unsigned integer, matching the emulator's value
132    /// domain (which is at most 16 bytes wide).
133    pub fn read_u128(&self, addr: u64, size: usize) -> Result<u128, EmulatorErrorKind> {
134        let width = size.min(16);
135        let Some((start, end)) = Self::range(addr, width) else {
136            return Err(EmulatorErrorKind::AddressOverflow(addr, width));
137        };
138        if end > self.bytes.len()
139            || (!self.zero_filled && self.written[start..end].contains(&false))
140        {
141            // Fall back to the checked path, which reports the exact byte.
142            let bytes = self.read_bytes(addr, width)?;
143            let mut bits = 0u128;
144            for (index, byte) in bytes.iter().enumerate() {
145                bits |= u128::from(*byte) << (index * 8);
146            }
147            return Ok(bits);
148        }
149        let mut bits = 0u128;
150        for (index, byte) in self.bytes[start..end].iter().enumerate() {
151            bits |= u128::from(*byte) << (index * 8);
152        }
153        Ok(bits)
154    }
155
156    pub fn write_bytes(&mut self, addr: u64, bytes: &[u8]) -> Result<(), EmulatorErrorKind> {
157        let Some((start, end)) = Self::range(addr, bytes.len()) else {
158            return Err(EmulatorErrorKind::AddressOverflow(addr, bytes.len()));
159        };
160        self.reserve_to(end)?;
161        self.bytes[start..end].copy_from_slice(bytes);
162        if !self.zero_filled {
163            self.written[start..end].fill(true);
164        }
165        Ok(())
166    }
167
168    /// Writes the low `size` bytes of a little-endian integer.
169    pub fn write_u128(
170        &mut self,
171        addr: u64,
172        size: usize,
173        bits: u128,
174    ) -> Result<(), EmulatorErrorKind> {
175        let width = size.min(16);
176        let Some((start, end)) = Self::range(addr, width) else {
177            return Err(EmulatorErrorKind::AddressOverflow(addr, width));
178        };
179        self.reserve_to(end)?;
180        for index in 0..width {
181            self.bytes[start + index] = (bits >> (index * 8)) as u8;
182        }
183        if !self.zero_filled {
184            self.written[start..end].fill(true);
185        }
186        Ok(())
187    }
188}
189
190/// The set of flat spaces for a module.
191#[derive(Debug, Default, Clone)]
192pub struct FlatSpaces {
193    /// Storage, appended to as spaces are first touched.
194    ///
195    /// Held in a `Vec` rather than keyed directly by id so that an index is
196    /// *stable*: a caller that resolves a space once can address it forever
197    /// without hashing again. Compiled code re-enters through here on every
198    /// block execution, and two map lookups per space per entry was a
199    /// measurable share of the JIT's run time.
200    spaces: Vec<FlatSpace>,
201    /// Where each space's storage lives in `spaces`. Append-only.
202    slots: FxHashMap<MemorySpaceId, usize>,
203    /// Shared spaces whose unwritten bytes read as zero, learned from the
204    /// context. Rebuilt only when the module gains spaces.
205    zero_filled: FxHashMap<SpaceId, bool>,
206    configured_space_count: Option<usize>,
207}
208
209impl FlatSpaces {
210    /// Learns which spaces are zero-filled. Cheap to call repeatedly: spaces are
211    /// append-only, so an unchanged count means nothing to redo.
212    pub fn configure(&mut self, ctx: &Context<'_>) {
213        let count = ctx.space_count();
214        if self.configured_space_count == Some(count) {
215            return;
216        }
217        self.zero_filled.clear();
218        for index in 0..count {
219            let id = SpaceId::from(index);
220            let space = Space::from_id(ctx, id);
221            // Register space is architectural state, and x86's private x87 file
222            // is too — FXSAVE can read a slot before a harness seeds it.
223            let zero =
224                matches!(space.ty, SpaceType::Register) || space.name.as_deref() == Some("x87");
225            self.zero_filled.insert(id, zero);
226        }
227        self.configured_space_count = Some(count);
228    }
229
230    /// The stable index of `space`'s storage, creating it on first use.
231    ///
232    /// Resolve this once and address the space with [`base_ptr_at`](Self::base_ptr_at)
233    /// thereafter; the index stays valid for the life of these spaces.
234    pub fn slot(&mut self, space: MemorySpaceId) -> usize {
235        if let Some(&slot) = self.slots.get(&space) {
236            return slot;
237        }
238        let zero_filled = self.is_zero_filled(space);
239        self.spaces.push(FlatSpace::new(zero_filled));
240        let slot = self.spaces.len() - 1;
241        self.slots.insert(space, slot);
242        slot
243    }
244
245    /// The base pointer of the storage at `slot`, grown to hold `len` bytes.
246    ///
247    /// Panics if `slot` did not come from [`slot`](Self::slot) on these spaces.
248    pub fn base_ptr_at(&mut self, slot: usize, len: usize) -> Result<*mut u8, EmulatorErrorKind> {
249        self.spaces[slot].base_ptr(len)
250    }
251
252    fn is_zero_filled(&self, space: MemorySpaceId) -> bool {
253        match space {
254            // Lifter scratch is created per function and read after writing.
255            MemorySpaceId::Temp(_) => true,
256            MemorySpaceId::Shared(id) => self.zero_filled.get(&id).copied().unwrap_or(false),
257        }
258    }
259
260    /// The base pointer of `space`'s storage, grown to hold `len` bytes.
261    pub fn base_ptr(
262        &mut self,
263        space: MemorySpaceId,
264        len: usize,
265    ) -> Result<*mut u8, EmulatorErrorKind> {
266        self.entry(space).base_ptr(len)
267    }
268
269    pub fn get(&self, space: MemorySpaceId) -> Option<&FlatSpace> {
270        self.slots.get(&space).map(|&slot| &self.spaces[slot])
271    }
272
273    /// The space's storage, created on first use.
274    pub fn entry(&mut self, space: MemorySpaceId) -> &mut FlatSpace {
275        let slot = self.slot(space);
276        &mut self.spaces[slot]
277    }
278
279    pub fn read_u128(
280        &self,
281        space: MemorySpaceId,
282        addr: u64,
283        size: usize,
284    ) -> Result<u128, EmulatorErrorKind> {
285        match self.get(space) {
286            Some(flat) => flat.read_u128(addr, size),
287            None if self.is_zero_filled(space) => Ok(0),
288            None => Err(EmulatorErrorKind::UnknownSpace(space)),
289        }
290    }
291
292    pub fn read_bytes(
293        &self,
294        space: MemorySpaceId,
295        addr: u64,
296        size: usize,
297    ) -> Result<Vec<u8>, EmulatorErrorKind> {
298        match self.get(space) {
299            Some(flat) => flat.read_bytes(addr, size),
300            None if self.is_zero_filled(space) => Ok(vec![0; size]),
301            None => Err(EmulatorErrorKind::UnknownSpace(space)),
302        }
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn round_trips_a_little_endian_value() {
312        let mut flat = FlatSpace::new(true);
313        flat.write_u128(8, 4, 0xdead_beef).unwrap();
314        assert_eq!(flat.read_u128(8, 4).unwrap(), 0xdead_beef);
315        assert_eq!(flat.read_bytes(8, 4).unwrap(), vec![0xef, 0xbe, 0xad, 0xde]);
316    }
317
318    #[test]
319    fn a_zero_filled_space_reads_unwritten_bytes_as_zero() {
320        let flat = FlatSpace::new(true);
321        assert_eq!(flat.read_u128(0, 8).unwrap(), 0);
322        assert_eq!(flat.read_bytes(0, 4).unwrap(), vec![0; 4]);
323    }
324
325    #[test]
326    fn a_scratch_space_reports_an_unwritten_read() {
327        // Reading a unique that was never written is a lifter defect, and the
328        // flat store must keep reporting it rather than inventing a zero.
329        let mut flat = FlatSpace::new(false);
330        flat.write_bytes(0, &[1, 2]).unwrap();
331        assert!(flat.read_bytes(0, 2).is_ok());
332        assert!(matches!(
333            flat.read_bytes(0, 4),
334            Err(EmulatorErrorKind::MemoryReadError(2))
335        ));
336    }
337
338    #[test]
339    fn a_partially_written_scratch_read_names_the_missing_byte() {
340        let mut flat = FlatSpace::new(false);
341        flat.write_bytes(0, &[0; 8]).unwrap();
342        let mut flat2 = FlatSpace::new(false);
343        flat2.write_bytes(4, &[1, 2, 3, 4]).unwrap();
344        // Bytes 0..4 were never written even though the space is long enough.
345        assert!(matches!(
346            flat2.read_bytes(0, 8),
347            Err(EmulatorErrorKind::MemoryReadError(0))
348        ));
349        assert!(flat.read_bytes(0, 8).is_ok());
350    }
351
352    #[test]
353    fn writes_grow_the_space_and_preserve_neighbours() {
354        let mut flat = FlatSpace::new(true);
355        flat.write_bytes(0, &[9; 4]).unwrap();
356        flat.write_bytes(64, &[7; 4]).unwrap();
357        assert_eq!(flat.read_bytes(0, 4).unwrap(), vec![9; 4]);
358        assert_eq!(flat.read_bytes(64, 4).unwrap(), vec![7; 4]);
359        assert_eq!(flat.read_bytes(32, 4).unwrap(), vec![0; 4]);
360    }
361
362    #[test]
363    fn an_absurd_address_is_refused_rather_than_allocated() {
364        let mut flat = FlatSpace::new(true);
365        assert!(matches!(
366            flat.write_bytes(u64::MAX - 8, &[1; 4]),
367            Err(EmulatorErrorKind::AddressOverflow(..))
368        ));
369    }
370
371    #[test]
372    fn a_wide_value_is_truncated_to_the_domain_width() {
373        let mut flat = FlatSpace::new(true);
374        flat.write_u128(0, 32, u128::MAX).unwrap();
375        // Only the domain's 16 bytes are stored.
376        assert_eq!(flat.read_u128(0, 16).unwrap(), u128::MAX);
377        assert_eq!(flat.read_bytes(16, 4).unwrap(), vec![0; 4]);
378    }
379}