Skip to main content

qcode_vm/
tlb.rs

1//! A software TLB: the shape compiled code needs to reach guest memory without
2//! calling back into Rust.
3//!
4//! The [`Mmu`](crate::mmu::Mmu) answers an access with a hash lookup, a
5//! per-page bounds walk and a per-byte permission scan. That is the right shape
6//! for an interpreter, which pays a dispatch per operation anyway, and the wrong
7//! one for compiled code, where it is the *only* thing standing between a guest
8//! load and a machine load. This cache is the part a compiled block can inline:
9//! an index, a tag compare, and an add.
10//!
11//! # What an entry promises, and what it does not
12//!
13//! An entry says only *where the page lives*: the host address of a resident
14//! [`PageData`](crate::mmu::PageData) for one guest page. It says nothing about
15//! permissions. Compiled code checks those itself, per byte, out of the same
16//! allocation — which is why [`PageData`](crate::mmu::PageData) keeps the
17//! permission bytes next to the data bytes at a fixed offset rather than in a
18//! second allocation.
19//!
20//! Splitting reads from writes would let the table itself carry the coarse
21//! permission, as icicle's does. It would buy nothing here: this MMU's
22//! permissions are per *byte*, so the byte scan happens either way, and one
23//! table means one entry to fill and invalidate.
24//!
25//! # Invalidation
26//!
27//! An entry holds a raw pointer into a page allocation, so it outlives its page
28//! only if nobody drops one. Every operation that can add, drop or replace a
29//! page flushes the whole table; permission edits do not, because permissions
30//! are read live from the page rather than cached here.
31
32use crate::mmu::PAGE_SIZE;
33
34/// log2 of the number of entries. 64 entries covers the working set of the code
35/// this runs — a stack page, a couple of data pages, the code page — with room
36/// for a few more before it thrashes, and the whole table stays inside a few
37/// cache lines.
38pub const TLB_INDEX_BITS: u32 = 6;
39
40/// Number of entries in the table. A power of two, so indexing is a mask.
41pub const TLB_ENTRIES: usize = 1 << TLB_INDEX_BITS;
42
43/// The address bits an entry's tag holds: the guest page base.
44const PAGE_MASK: u64 = PAGE_SIZE - 1;
45
46/// A tag that no address produces, because every real tag is page-aligned.
47const INVALID_TAG: u64 = u64::MAX;
48
49/// One cached translation.
50///
51/// `#[repr(C)]` and 16 bytes wide on purpose: compiled code computes an entry's
52/// address arithmetically from the guest address, so both the field order and
53/// the size are part of the interface.
54#[repr(C)]
55#[derive(Debug, Clone, Copy)]
56pub struct TlbEntry {
57    /// The guest page base this entry translates, or `INVALID_TAG`.
58    pub tag: u64,
59    /// Added to a guest address to get the host address of that byte.
60    pub guest_to_host_offset: u64,
61}
62
63impl TlbEntry {
64    const fn invalid() -> Self {
65        Self {
66            tag: INVALID_TAG,
67            guest_to_host_offset: 0,
68        }
69    }
70
71    /// The tag an address belongs under.
72    pub const fn tag_of(addr: u64) -> u64 {
73        addr & !PAGE_MASK
74    }
75}
76
77/// The table compiled code indexes.
78#[repr(C)]
79#[derive(Debug, Clone)]
80pub struct TranslationCache {
81    pub entries: [TlbEntry; TLB_ENTRIES],
82}
83
84impl Default for TranslationCache {
85    fn default() -> Self {
86        Self {
87            entries: [TlbEntry::invalid(); TLB_ENTRIES],
88        }
89    }
90}
91
92impl TranslationCache {
93    /// The slot an address maps to.
94    pub const fn index(addr: u64) -> usize {
95        ((addr >> PAGE_SIZE.trailing_zeros()) as usize) & (TLB_ENTRIES - 1)
96    }
97
98    /// Drops every cached translation.
99    ///
100    /// Called for any change to which pages exist or where they live. It is
101    /// deliberately the blunt instrument: mapping is a setup-time operation and
102    /// a mis-scoped invalidation here would be a use-after-free in compiled
103    /// code.
104    pub fn flush(&mut self) {
105        self.entries.fill(TlbEntry::invalid());
106    }
107
108    /// Caches `host` as the address of the page holding `addr`.
109    ///
110    /// # Safety
111    ///
112    /// `host` must be the start of a page allocation that stays live and in
113    /// place until the next [`flush`](Self::flush).
114    pub fn insert(&mut self, addr: u64, host: *mut u8) {
115        let tag = TlbEntry::tag_of(addr);
116        self.entries[Self::index(addr)] = TlbEntry {
117            tag,
118            // Stored relative to the guest address so the hot path is one add
119            // rather than a mask and an add.
120            guest_to_host_offset: (host as u64).wrapping_sub(tag),
121        };
122    }
123
124    /// The host address of `addr`, if it is cached.
125    pub fn lookup(&self, addr: u64) -> Option<*mut u8> {
126        let entry = self.entries[Self::index(addr)];
127        (entry.tag == TlbEntry::tag_of(addr))
128            .then(|| addr.wrapping_add(entry.guest_to_host_offset) as *mut u8)
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn a_fresh_table_translates_nothing() {
138        let tlb = TranslationCache::default();
139        assert!(tlb.lookup(0).is_none());
140        // The last page in the address space, whose base is the one bit
141        // pattern the invalid tag could be confused with.
142        assert!(tlb.lookup(!PAGE_MASK).is_none());
143    }
144
145    #[test]
146    fn an_inserted_page_translates_every_byte_in_it() {
147        let mut page = vec![0u8; PAGE_SIZE as usize];
148        let host = page.as_mut_ptr();
149        let mut tlb = TranslationCache::default();
150        tlb.insert(0x1234, host);
151
152        assert_eq!(tlb.lookup(0x1000), Some(host));
153        assert_eq!(tlb.lookup(0x1fff), Some(unsafe { host.add(0xfff) }));
154        // The next page is a different tag, and shares no entry with this one.
155        assert!(tlb.lookup(0x2000).is_none());
156    }
157
158    #[test]
159    fn a_flush_forgets_everything() {
160        let mut page = vec![0u8; PAGE_SIZE as usize];
161        let mut tlb = TranslationCache::default();
162        tlb.insert(0x1000, page.as_mut_ptr());
163        tlb.flush();
164        assert!(tlb.lookup(0x1000).is_none());
165    }
166
167    #[test]
168    fn pages_a_multiple_of_the_table_size_apart_share_a_slot() {
169        let stride = PAGE_SIZE * TLB_ENTRIES as u64;
170        assert_eq!(
171            TranslationCache::index(0x1000),
172            TranslationCache::index(0x1000 + stride)
173        );
174        let mut page = vec![0u8; PAGE_SIZE as usize];
175        let mut tlb = TranslationCache::default();
176        tlb.insert(0x1000, page.as_mut_ptr());
177        tlb.insert(0x1000 + stride, page.as_mut_ptr());
178        // The later insert evicted the earlier one rather than answering for it.
179        assert!(tlb.lookup(0x1000).is_none());
180        assert!(tlb.lookup(0x1000 + stride).is_some());
181    }
182}