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}