qcode_vm/jit_abi.rs
1//! The calling interface between compiled code and this VM's memory.
2//!
3//! Compiled code reaches guest RAM through an inlined translation and an
4//! inlined permission check (see [`crate::tlb`]). Neither can answer every
5//! access: the page may not be cached yet, the access may straddle a page
6//! boundary, the permissions may refuse it, or a dynamic check may be in force
7//! that the inline test cannot express. All of those land here, on the same
8//! [`Mmu`](crate::mmu::Mmu) the interpreter uses.
9//!
10//! That is the point of the split. There is one implementation of what an
11//! access *means* — mapping, permissions, initializedness, watchpoints, fault
12//! addresses — and the inline path is a cache in front of it, never a second
13//! opinion. A miss costs a call; a wrong answer would cost correctness.
14//!
15//! # Faults
16//!
17//! These return a status rather than taking a fault themselves, because
18//! compiled code has to unwind on its own: it stops at the faulting access,
19//! leaving the guest state its earlier stores produced, and returns the status
20//! to the runtime. The fault itself is left on the [`VmMemory`] for the caller
21//! to take, exactly as an interpreted fault would be.
22
23use crate::VmMemory;
24
25/// The four integer divisions, at a width the host has no instruction for.
26///
27/// x86-64 divides a 128-bit dividend by a 64-bit divisor, so SLEIGH lifts every
28/// `div` and `idiv` as a 128-bit operation — and neither the machine nor
29/// Cranelift can do one. Compiled code calls out to these rather than declining
30/// the block: a call is far cheaper than interpreting every instruction around
31/// the division as well.
32///
33/// Each states QCode's rule directly, rather than the caller guarding the
34/// operands: a zero divisor yields zero, and signed division wraps where the
35/// machine would fault.
36macro_rules! wide_division {
37 ($name:ident, $int:ty, $checked:ident, $wrapping:ident) => {
38 /// # Safety
39 ///
40 /// `out` must point to two writable, consecutive `u64`s, which receive
41 /// the low and high halves of the result. Called only from code this
42 /// crate's JIT backend generated.
43 pub unsafe extern "C" fn $name(
44 a_low: u64,
45 a_high: u64,
46 b_low: u64,
47 b_high: u64,
48 out: *mut u64,
49 ) {
50 let a = (u128::from(a_high) << 64 | u128::from(a_low)) as $int;
51 let b = (u128::from(b_high) << 64 | u128::from(b_low)) as $int;
52 // `checked_*` covers both rules at once: it declines a zero divisor
53 // and the signed overflow, and QCode's answer for each is zero and
54 // the wrapped value respectively.
55 let result = match a.$checked(b) {
56 Some(value) => value,
57 None if b == 0 => 0,
58 // The only other refusal is the most negative value over -1,
59 // where wrapping gives exactly what QCode specifies: that same
60 // value as the quotient, and zero as the remainder.
61 None => a.$wrapping(b),
62 } as u128;
63 // SAFETY: the caller guarantees two writable `u64`s.
64 unsafe {
65 out.write(result as u64);
66 out.add(1).write((result >> 64) as u64);
67 }
68 }
69 };
70}
71
72wide_division!(qcode_jit_udiv128, u128, checked_div, wrapping_div);
73wide_division!(qcode_jit_urem128, u128, checked_rem, wrapping_rem);
74wide_division!(qcode_jit_sdiv128, i128, checked_div, wrapping_div);
75wide_division!(qcode_jit_srem128, i128, checked_rem, wrapping_rem);
76
77/// The access succeeded.
78pub const ACCESS_OK: u32 = 0;
79/// The access faulted; the fault is on the [`VmMemory`].
80pub const ACCESS_FAULT: u32 = 1;
81
82/// Performs a guest RAM read that compiled code could not do inline.
83///
84/// # Safety
85///
86/// `memory` must point to a live [`VmMemory`] that nothing else is borrowing,
87/// `out` to a writable `u64`, and `size` must be at most 8. Called only from
88/// code this crate's JIT backend generated.
89pub unsafe extern "C" fn qcode_jit_load(
90 memory: *mut VmMemory,
91 addr: u64,
92 size: u32,
93 out: *mut u64,
94) -> u32 {
95 // SAFETY: the caller guarantees an exclusive, live pointer.
96 let memory = unsafe { &mut *memory };
97 let size = size as usize;
98 debug_assert!(size <= 8);
99
100 let mut bytes = [0u8; 8];
101 if let Err(fault) = memory.mmu.read(addr, &mut bytes[..size]) {
102 memory.record_read_fault(fault);
103 return ACCESS_FAULT;
104 }
105 // SAFETY: the caller guarantees `out` is a writable `u64`.
106 unsafe { out.write(u64::from_le_bytes(bytes)) };
107
108 // The access worked, so the page is resident and reachable: cache it so
109 // the next execution of this instruction stays inline. An access that
110 // straddled two pages caches the first, which the inline path's same-page
111 // test will decline again — that is a slow instruction, not a wrong one.
112 memory.mmu.cache_translation(addr);
113 ACCESS_OK
114}
115
116/// Performs a guest RAM write that compiled code could not do inline.
117///
118/// # Safety
119///
120/// As [`qcode_jit_load`], and `size` must be at most 8.
121pub unsafe extern "C" fn qcode_jit_store(
122 memory: *mut VmMemory,
123 addr: u64,
124 size: u32,
125 value: u64,
126) -> u32 {
127 // SAFETY: the caller guarantees an exclusive, live pointer.
128 let memory = unsafe { &mut *memory };
129 let size = size as usize;
130 debug_assert!(size <= 8);
131
132 if let Err(fault) = memory.mmu.write(addr, &value.to_le_bytes()[..size]) {
133 memory.record_write_fault(fault);
134 return ACCESS_FAULT;
135 }
136 memory.mmu.cache_translation(addr);
137 ACCESS_OK
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::mmu::{FaultKind, PAGE_SIZE, perm};
144
145 fn memory() -> VmMemory {
146 let mut memory = VmMemory::new();
147 memory.mmu.map(0x1000, PAGE_SIZE, perm::RW_INIT).unwrap();
148 memory
149 }
150
151 #[test]
152 fn a_served_access_leaves_the_page_cached() {
153 let mut memory = memory();
154 let mut out = 0u64;
155 let status = unsafe { qcode_jit_store(&raw mut memory, 0x1008, 4, 0xdead_beef) };
156 assert_eq!(status, ACCESS_OK);
157 let status = unsafe { qcode_jit_load(&raw mut memory, 0x1008, 4, &raw mut out) };
158 assert_eq!(status, ACCESS_OK);
159 assert_eq!(out, 0xdead_beef);
160 // Having served it the slow way once, the next one can be inline.
161 assert!(memory.mmu.cache_translation(0x1008));
162 }
163
164 #[test]
165 fn a_refused_access_reports_a_fault_and_caches_nothing() {
166 let mut memory = memory();
167 let mut out = 0u64;
168 let status = unsafe { qcode_jit_load(&raw mut memory, 0x9000, 1, &raw mut out) };
169 assert_eq!(status, ACCESS_FAULT);
170 assert_eq!(
171 memory.take_fault().map(|fault| fault.kind),
172 Some(FaultKind::ReadUnmapped)
173 );
174 }
175
176 #[test]
177 fn a_write_to_read_only_memory_faults_at_the_byte() {
178 let mut memory = VmMemory::new();
179 memory.mmu.map(0x1000, PAGE_SIZE, perm::RX_INIT).unwrap();
180 let status = unsafe { qcode_jit_store(&raw mut memory, 0x1000, 1, 0xff) };
181 assert_eq!(status, ACCESS_FAULT);
182 assert_eq!(memory.take_fault().map(|fault| fault.addr), Some(0x1000));
183 }
184
185 #[test]
186 fn an_access_straddling_two_pages_is_served_whole() {
187 let mut memory = VmMemory::new();
188 memory
189 .mmu
190 .map(0x1000, 2 * PAGE_SIZE, perm::RW_INIT)
191 .unwrap();
192 let mut out = 0u64;
193 // The inline path declines this shape outright; the slow path is the
194 // only thing that ever sees it, so it has to be right here.
195 unsafe { qcode_jit_store(&raw mut memory, 0x1ffe, 8, 0x0102_0304_0506_0708) };
196 unsafe { qcode_jit_load(&raw mut memory, 0x1ffe, 8, &raw mut out) };
197 assert_eq!(out, 0x0102_0304_0506_0708);
198 }
199}