qcode_vm/vm.rs
1//! The run loop: a machine that owns its code, discovers more of it as the
2//! guest reaches it, and stops with a reason instead of an error.
3//!
4//! Two things separate this from driving [`StandaloneEmulator`] directly.
5//!
6//! **The context is owned, not borrowed.** Lifting new code mutates the module,
7//! which a `&Context` cannot allow. This is why the VM is built on
8//! [`StandaloneEmulator`] — the lifetime-free emulator that takes `&Context` per
9//! call — rather than on `Emulator<'ctx>`, which would pin the module for as
10//! long as the machine exists.
11//!
12//! **Stopping is a value.** A guest that reads unmapped memory has not broken
13//! the emulator; it has taken a fault, which a harness may want to report,
14//! resume from, or count as a crash. So the loop returns [`VmExit`] and leaves
15//! the machine intact and inspectable.
16
17use qcode::value::LocalInsnId;
18use qcode::{
19 address_index::{AddressIndex, AddressTarget},
20 context::Context,
21 value::{BasicBlock, BlockId},
22};
23use qcode_emulator::{EmulatorErrorKind, EmulatorMemory, StandaloneEmulator};
24use rustc_hash::FxHashSet;
25
26use crate::{memory::VmMemory, mmu::MemFault, stats::Stats};
27
28/// Why a lifting attempt failed.
29#[derive(Debug, Clone)]
30pub enum CodeError {
31 /// The instruction bytes could not be fetched.
32 Fault(MemFault),
33 /// The bytes were fetched but did not decode, or did not lift.
34 Decode(Box<str>),
35}
36
37/// Supplies code the machine has not seen yet.
38///
39/// Kept as a trait so the VM does not depend on SLEIGH: a decoder is a policy
40/// choice (which specification, which variant), and a test wants to hand over
41/// blocks without compiling one. An implementation reads instruction bytes from
42/// the [`Mmu`](crate::Mmu) — via [`read_code`](crate::Mmu::read_code), so that
43/// executing a non-executable page faults at the fetch — and lowers them into
44/// `ctx`.
45pub trait CodeSource {
46 /// Lifts the code at `addr` into `ctx`.
47 ///
48 /// Returning `Ok(())` asserts that a block starting at `addr` now exists;
49 /// the VM re-resolves the address itself rather than trusting a returned id,
50 /// so a source is free to lift a whole run of instructions at once.
51 ///
52 /// `index` is the machine's live address lookup, and the implementation must
53 /// keep it current as it adds blocks — every lifting entry point takes one
54 /// for exactly this reason. Rebuilding it per instruction instead is
55 /// quadratic in the size of the module discovered so far.
56 ///
57 /// `stats` is the machine's own counters: an implementation records the
58 /// time it spends fetching and decoding there, so a benchmark can separate
59 /// translation cost from interpretation cost.
60 fn lift(
61 &mut self,
62 ctx: &mut Context<'static>,
63 memory: &VmMemory,
64 index: &mut AddressIndex,
65 addr: u64,
66 stats: &mut Stats,
67 ) -> Result<(), CodeError>;
68}
69
70/// An alternative way to execute a block's body.
71///
72/// The interpreter is always present and always correct; an executor is an
73/// *optimisation* that may decline any block for any reason, in which case the
74/// interpreter runs it unchanged. That is what lets a backend be partial: a JIT
75/// need only handle the shapes it handles well.
76///
77/// An executor runs the block's body, not its terminator. Control flow, block
78/// parameters and call semantics stay in one implementation.
79///
80/// It is handed the whole emulator rather than just its memory because the
81/// interpreter still has to run the terminator, and a terminator reads
82/// operands — a `cbranch` condition, a branch's block arguments. Those are
83/// values the body produced, so an executor that keeps them somewhere other
84/// than the interpreter's value table must put them back before returning.
85pub trait BlockExecutor {
86 /// Runs everything in `block` except its terminator.
87 ///
88 /// `Ok(None)` means "not mine" and is not an error — the caller falls back
89 /// to the interpreter.
90 ///
91 /// On `Ok(Some(_))` every value the terminator of [`Executed::block`] reads
92 /// must be readable from `emu.insn_values`, exactly as if the interpreter
93 /// had run that body.
94 ///
95 /// `chain` lets the executor run on past `block` into successors it also
96 /// handles, instead of handing control back after one. Deciding a branch
97 /// itself is how an executor keeps control inside its own code rather than
98 /// paying a round trip per block. The caller withholds it when something
99 /// needs to observe every block — a breakpoint is set, say — because blocks
100 /// crossed this way are never offered to the interpreter.
101 fn run_block(
102 &mut self,
103 ctx: &Context<'_>,
104 emu: &mut StandaloneEmulator<VmMemory>,
105 block: BlockId,
106 chain: bool,
107 ) -> Result<Option<Executed>, EmulatorErrorKind>;
108}
109
110/// Where an executor left the machine.
111#[derive(Debug, Clone, Copy)]
112pub struct Executed {
113 /// The block whose terminator the interpreter still has to run. With
114 /// chaining this is the last of several, not the one that was asked for.
115 pub block: BlockId,
116 /// How many instructions of that block were retired: its body.
117 pub body: usize,
118 /// Operations retired across every block run, for accounting.
119 pub retired: u64,
120}
121
122/// Why the machine stopped.
123#[derive(Debug, Clone)]
124pub enum VmExit {
125 /// The step budget ran out. The machine is resumable.
126 InstructionLimit,
127 /// Execution reached an address with a breakpoint on it. The breakpoint
128 /// instruction has *not* been executed.
129 Breakpoint(u64),
130 /// A memory access failed.
131 Fault(MemFault),
132 /// Code at this address could not be lifted.
133 Unlifted { addr: u64, error: CodeError },
134 /// The interpreter reported something the VM does not model as a guest
135 /// event — an unsupported p-code op, a malformed block.
136 Error(Box<str>),
137}
138
139/// A machine: an owned module, a memory, and a position in the code.
140pub struct Vm<S> {
141 ctx: Context<'static>,
142 emu: StandaloneEmulator<VmMemory>,
143 source: S,
144 /// Counters and phase timings for this run.
145 pub stats: Stats,
146 /// An optional faster path for block bodies. `None` means the interpreter
147 /// executes everything, which is always a valid way to run.
148 executor: Option<Box<dyn BlockExecutor>>,
149 /// Set by a lift that folded the block it filled into a predecessor, which
150 /// leaves the machine already positioned. Taken by the step that asked for
151 /// the lift.
152 absorbed_into: Option<BlockId>,
153 /// Whether freshly lifted blocks get a cleanup round.
154 ///
155 /// Lifting one machine instruction emits every side effect the
156 /// specification describes, including flag computations the surrounding
157 /// code never reads. Removing the ones with no users at all is sound
158 /// block-locally — an instruction with no users cannot be observed — and is
159 /// paid once per block instead of on every execution of it.
160 pub optimize: bool,
161 /// The block that has grown by absorption and not been cleaned since.
162 ///
163 /// Absorption folds a straight-line run one guest instruction at a time,
164 /// and cleaning the whole enlarged block after each one is quadratic in the
165 /// length of the run — which on unrolled code is the dominant cost of
166 /// translation. The cleanup is deferred to the point the block is next
167 /// entered at its first instruction, by which time the run has stopped
168 /// growing and one pass does the work of all of them.
169 ///
170 /// At most one: absorption extends one run at a time, so a *different*
171 /// block being absorbed into means the previous run has stopped growing
172 /// and can be cleaned right there. Waiting for it to be entered again
173 /// instead would let compiled code be built from the uncleaned form — and
174 /// worse, would clean it in an interpreted run but not in a chained
175 /// compiled one, leaving the two strategies running different QCode.
176 dirty: Option<BlockId>,
177 breakpoints: FxHashSet<u64>,
178}
179
180impl<S: CodeSource> Vm<S> {
181 /// Builds a machine positioned at `entry`.
182 pub fn new(ctx: Context<'static>, entry: BlockId, source: S) -> Self {
183 let mut emu = StandaloneEmulator::<VmMemory>::new_in(entry);
184 emu.memory.configure_spaces(&ctx);
185 Self {
186 ctx,
187 emu,
188 source,
189 optimize: true,
190 stats: Stats::default(),
191 executor: None,
192 absorbed_into: None,
193 dirty: None,
194 breakpoints: FxHashSet::default(),
195 }
196 }
197
198 /// Builds a machine positioned at a guest address, lifting the entry block
199 /// if the module does not already contain it.
200 pub fn at_address(
201 mut ctx: Context<'static>,
202 addr: u64,
203 mut source: S,
204 memory: VmMemory,
205 ) -> Result<Self, CodeError> {
206 // Built once here and handed to the machine, which keeps it current
207 // from then on.
208 let mut index = AddressIndex::analyze(&ctx);
209 let mut stats = Stats::default();
210 if resolve(&ctx, &index, addr).is_none() {
211 stats.lifts += 1;
212 source.lift(&mut ctx, &memory, &mut index, addr, &mut stats)?;
213 }
214 let entry = resolve(&ctx, &index, addr).ok_or_else(|| {
215 CodeError::Decode(format!("no block at {addr:#x} after lifting").into())
216 })?;
217 let mut vm = Self::new(ctx, entry, source);
218 vm.emu.memory = memory;
219 vm.emu.memory.configure_spaces(&vm.ctx);
220 vm.emu.set_address_index(index);
221 vm.stats = stats;
222 Ok(vm)
223 }
224
225 /// Installs an alternative executor for block bodies, replacing any
226 /// previous one. Purely an optimisation: removing it changes speed, not
227 /// behaviour.
228 pub fn set_block_executor(&mut self, executor: Box<dyn BlockExecutor>) {
229 self.executor = Some(executor);
230 }
231
232 pub fn clear_block_executor(&mut self) {
233 self.executor = None;
234 }
235
236 pub fn context(&self) -> &Context<'static> {
237 &self.ctx
238 }
239
240 pub fn memory(&self) -> &VmMemory {
241 &self.emu.memory
242 }
243
244 pub fn memory_mut(&mut self) -> &mut VmMemory {
245 &mut self.emu.memory
246 }
247
248 /// The emulator underneath, for register access and harness seeding.
249 pub fn emulator(&mut self) -> &mut StandaloneEmulator<VmMemory> {
250 &mut self.emu
251 }
252
253 /// The guest address of the block about to execute, if it has one.
254 pub fn pc(&self) -> Option<u64> {
255 BasicBlock::from_id(&self.ctx, self.emu.block).address()
256 }
257
258 pub fn add_breakpoint(&mut self, addr: u64) -> bool {
259 self.breakpoints.insert(addr)
260 }
261
262 pub fn remove_breakpoint(&mut self, addr: u64) -> bool {
263 self.breakpoints.remove(&addr)
264 }
265
266 /// Executes one instruction, lifting code on demand if control leaves the
267 /// part of the module already known.
268 ///
269 /// Returns `None` when the step was ordinary, and `Some(exit)` when the
270 /// machine stopped for a reason worth reporting.
271 pub fn step(&mut self) -> Option<VmExit> {
272 // A branch to unlifted code fails *before* the emulator moves, so the
273 // address can be lifted and the same step retried. One retry is enough:
274 // the second failure means the source did not produce the block it
275 // claimed to, which is a source bug rather than a discovery step.
276 for attempt in 0..2 {
277 // At its first instruction a block is between runs, which is the
278 // one moment a deferred cleanup can be taken without disturbing a
279 // position inside it — and it has to happen before the executor
280 // looks, or compiled code gets built from uncleaned QCode.
281 if self.emu.idx == 0 {
282 let block = self.emu.block;
283 self.clean_before_entering(block);
284 }
285
286 // At a block's first instruction, an installed executor may run the
287 // whole body at once, leaving the interpreter only the terminator.
288 if self.emu.idx == 0
289 && let Some(executor) = self.executor.as_mut()
290 {
291 let block = self.emu.block;
292 // Blocks the executor runs are never offered to the interpreter, so
293 // it may only run past the first when nothing needs to see them.
294 let chain = self.breakpoints.is_empty();
295 match executor.run_block(&self.ctx, &mut self.emu, block, chain) {
296 Ok(Some(run)) => {
297 // The operations were retired by the executor; they are
298 // counted so throughput stays comparable between strategies.
299 self.stats.steps += run.retired;
300 self.stats.native_bodies += 1;
301 // Positioning inside a block the interpreter has not walked
302 // into invalidates its cached instruction list.
303 self.emu.invalidate_block_cache();
304 self.emu.block = run.block;
305 self.emu.idx = run.body;
306 }
307 Ok(None) => {}
308 Err(kind) => {
309 let fault = self.emu.memory.take_fault();
310 return Some(match fault {
311 Some(fault) => VmExit::Fault(fault),
312 None => VmExit::Error(kind.to_string().into()),
313 });
314 }
315 }
316 }
317
318 match self.emu.step(&self.ctx) {
319 Ok(()) => {
320 self.stats.steps += 1;
321 return None;
322 }
323 Err(error) => match error.kind {
324 EmulatorErrorKind::InvalidBlockAddress(addr)
325 | EmulatorErrorKind::UnknownAddress(addr)
326 if attempt == 0 =>
327 {
328 if let Some(exit) = self.discover(addr) {
329 return Some(exit);
330 }
331 }
332 // A direct branch to code that has not been lifted does not
333 // fail to resolve: the lifter materializes the target as an
334 // *empty* block at that address, and execution walks into
335 // it. So an empty block carrying an address is a request to
336 // discover it, not a malformed-IR error.
337 EmulatorErrorKind::EmptyBlock(block) if attempt == 0 => {
338 let Some(addr) = BasicBlock::from_id(&self.ctx, block).address() else {
339 return Some(VmExit::Error(
340 EmulatorErrorKind::EmptyBlock(block).to_string().into(),
341 ));
342 };
343 // The address may already be lifted: a branch back into
344 // known code still gets a fresh placeholder block in the
345 // branching instruction's own function, and lifting it
346 // again would collide with the function that owns it.
347 // Resolving first is what makes loops work.
348 let before = self.emu.block;
349 self.reposition(addr);
350 if self.emu.block != before {
351 // Already lifted: a translation-cache hit.
352 self.stats.resolves += 1;
353 continue;
354 }
355 if let Some(exit) = self.discover(addr) {
356 return Some(exit);
357 }
358 // Lifting may place the instruction in a *new* block
359 // rather than filling the placeholder the emulator is
360 // sitting in, so the machine has to be moved onto
361 // whatever now covers the address — unless the lift
362 // folded that block into its predecessor, which
363 // positions the machine itself. `addr` is interior to
364 // the absorbing block then, and resolving it by address
365 // would land at that block's *start*.
366 if self.absorbed_into.take().is_none() {
367 self.reposition(addr);
368 }
369 }
370 EmulatorErrorKind::MemoryReadError(addr)
371 | EmulatorErrorKind::MemoryWriteError(addr) => {
372 // The backend records the precise cause; the error alone
373 // could only say that an access at this address failed.
374 let fault = self.emu.memory.take_fault().unwrap_or(MemFault {
375 kind: crate::mmu::FaultKind::ReadUnmapped,
376 addr,
377 });
378 return Some(VmExit::Fault(fault));
379 }
380 kind => return Some(VmExit::Error(kind.to_string().into())),
381 },
382 }
383 }
384 None
385 }
386
387 /// Points the emulator at whatever block now covers `addr`, reusing the
388 /// emulator's own cached index rather than building one.
389 fn reposition(&mut self, addr: u64) {
390 if let Some(block) = self.emu.block_at_address(&self.ctx, addr)
391 && block != self.emu.block
392 {
393 self.emu.block = block;
394 self.emu.idx = 0;
395 }
396 }
397
398 /// Lifts `addr` and makes it visible to the emulator. Returns an exit only
399 /// if the address could not be supplied.
400 fn discover(&mut self, addr: u64) -> Option<VmExit> {
401 // Moved out, updated in place by the lift, and moved back: the index
402 // stays current without ever being rebuilt, and the borrow checker is
403 // satisfied because nothing borrows the emulator across the lift.
404 let mut index = self
405 .emu
406 .take_address_index()
407 .unwrap_or_else(|| AddressIndex::analyze(&self.ctx));
408 self.stats.lifts += 1;
409 let result = self.source.lift(
410 &mut self.ctx,
411 &self.emu.memory,
412 &mut index,
413 addr,
414 &mut self.stats,
415 );
416 self.emu.set_address_index(index);
417 if let Err(error) = result {
418 return Some(VmExit::Unlifted { addr, error });
419 }
420 if self.optimize
421 && let Some(block) = self.emu.block_at_address(&self.ctx, addr)
422 {
423 // Forwarding first: it turns the temp round trips into direct value
424 // uses, which is what leaves the surrounding computation dead.
425 let started = std::time::Instant::now();
426 let cleanup = crate::optimize::forward_temp_stores(&mut self.ctx, block);
427 qcode_passes::remove_dead_insns(&mut self.ctx, block);
428 self.stats.optimize += started.elapsed();
429 self.stats.forwarded_loads += cleanup.forwarded_loads as u64;
430 self.stats.removed_stores += cleanup.removed_stores as u64;
431 }
432 self.absorbed_into = self.absorb_into_basic_block(addr);
433 None
434 }
435
436 /// Points every address `block` has absorbed back at `block`.
437 ///
438 /// Absorption deletes the blocks it takes in, and each of them was what the
439 /// index named for its address. Left alone the index hands out ids of
440 /// deleted blocks — and a run may absorb a whole chain, not just the block
441 /// that was being discovered, so every address the absorber now covers has
442 /// to be repointed, not only the one that prompted this.
443 fn reindex_absorbed(&mut self, block: BlockId) {
444 let covered = self.ctx.block(block).extra_addresses.clone();
445 if covered.is_empty() {
446 return;
447 }
448 let mut index = self
449 .emu
450 .take_address_index()
451 .unwrap_or_else(|| AddressIndex::analyze(&self.ctx));
452 for addr in covered {
453 index.set_block(addr, block);
454 }
455 self.emu.set_address_index(index);
456 }
457
458 /// Re-runs the block cleanup over a block that has just grown.
459 ///
460 /// The cleanup at discovery saw a single guest instruction, where every
461 /// register it writes is still live at the block's edge. Absorption puts a
462 /// whole run in one block, and that is the first point at which a write
463 /// nothing goes on to read is visible as dead: the flags an arithmetic
464 /// instruction sets, when the next instruction overwrites all of them
465 /// before the branch reads any.
466 ///
467 /// Deliberately block-local (no alias result): at discovery the rest of the
468 /// CFG is still unknown, so only a store this block itself overwrites can
469 /// be proven dead. Anything live at the exit stays.
470 /// Records that `block` has grown and owes a cleanup, cleaning whatever
471 /// run was growing before it.
472 fn mark_dirty(&mut self, block: BlockId) {
473 if !self.optimize {
474 return;
475 }
476 let previous = self.dirty.replace(block);
477 if let Some(previous) = previous
478 && previous != block
479 // Discovery retires blocks — splitting an absorbed run empties
480 // both halves — so the one that was growing may be gone.
481 && self.ctx.contains_block(previous)
482 {
483 self.reoptimize(previous);
484 }
485 }
486
487 /// Cleans the block the machine is about to run, if it has grown since it
488 /// was last cleaned.
489 ///
490 /// Only ever the block being entered, which is why a stale id cannot be
491 /// reached here: absorption and splitting retire blocks that may still be
492 /// listed, but the machine can only be about to run a live one. A leftover
493 /// entry for a retired id is harmless — at worst it cleans a block whose id
494 /// was reused, which is always safe.
495 fn clean_before_entering(&mut self, block: BlockId) {
496 if self.dirty == Some(block) {
497 self.dirty = None;
498 self.reoptimize(block);
499 }
500 }
501
502 fn reoptimize(&mut self, block: BlockId) {
503 if !self.optimize {
504 return;
505 }
506 let started = std::time::Instant::now();
507 let cleanup = crate::optimize::forward_temp_stores(&mut self.ctx, block);
508 qcode_passes::remove_dead_insns(&mut self.ctx, block);
509 self.stats.optimize += started.elapsed();
510 self.stats.forwarded_loads += cleanup.forwarded_loads as u64;
511 self.stats.removed_stores += cleanup.removed_stores as u64;
512 // The interpreter may hold this block's instruction list, and some of
513 // those instructions are gone.
514 self.emu.invalidate_block_cache();
515 }
516
517 /// Folds the block just lifted at `addr` into its predecessor, when the two
518 /// are a straight-line pair.
519 ///
520 /// Lifting is per guest instruction, so a run of straight-line guest code
521 /// arrives as a chain of one-instruction blocks joined by unconditional
522 /// branches. Left that way, every guest instruction is its own unit of
523 /// execution: a separate compilation, a separate entry into compiled code
524 /// and a separate return to the interpreter for its terminator. Folding the
525 /// chain as it is discovered rebuilds the guest's *basic block*, which is
526 /// the unit worth compiling.
527 ///
528 /// Absorbing an address does not settle that it belongs here — code
529 /// discovered later may branch into the middle of the run, and
530 /// [`Context::split_block_at_address`] breaks it apart again when it does.
531 fn absorb_into_basic_block(&mut self, addr: u64) -> Option<BlockId> {
532 let filled = self.emu.block_at_address(&self.ctx, addr)?;
533
534 // Forward: the rest of this run may already be known. That is the shape
535 // a split leaves behind — it re-establishes a block's *start* while
536 // everything after it is still lifted — and the shape a back-edge into
537 // the middle of a run creates generally.
538 let forward = qcode_passes::absorb_straight_line(&mut self.ctx, filled);
539 self.stats.absorbed += forward as u64;
540 if forward > 0 {
541 self.reindex_absorbed(filled);
542 self.mark_dirty(filled);
543 }
544
545 // Backward: the straight-line predecessor that branched here, for the
546 // ordinary case of a run discovered one guest instruction at a time.
547 //
548 // Not if something branches to this address: it has to keep *starting*
549 // a block, or the split that established that would be undone here.
550 // Extending it forward, above, stays fine — that moves its end, not its
551 // start.
552 if self
553 .emu
554 .address_index()
555 .is_some_and(|index| index.is_boundary(addr))
556 {
557 return (forward > 0).then_some(filled);
558 }
559 // Exactly one predecessor, or absorbing would strand the others.
560 // Collected eagerly so the module is free to be mutated below.
561 let preds: Vec<BlockId> = BasicBlock::from_id(&self.ctx, filled)
562 .predecessors()
563 .map(|(_, block)| block)
564 .take(2)
565 .collect();
566 let [head] = preds[..] else {
567 return None;
568 };
569 if head == filled {
570 return None;
571 }
572 // Only into a block that starts at a machine address. Absorbing makes
573 // the head responsible for the absorbed addresses, and a later branch
574 // to one of them splits the head apart again — which works by emptying
575 // it and lifting it afresh. A block with no address of its own (the
576 // fallthrough arm of a branch *inside* one instruction's p-code) has
577 // nowhere to be lifted from, so emptying it leaves a hole nothing can
578 // fill.
579 if self.ctx.block(head).address.is_none() {
580 return (forward > 0).then_some(filled);
581 }
582 // Where `filled`'s instructions land: the head's own, less the
583 // terminator that absorption drops.
584 let offset = self
585 .ctx
586 .block(head)
587 .instruction_ids()
588 .len()
589 .saturating_sub(1);
590 if qcode_passes::absorb_straight_line(&mut self.ctx, head) == 0 {
591 return (forward > 0).then_some(filled);
592 }
593 self.stats.absorbed += 1;
594
595 // Where the machine has to resume, named by instruction rather than by
596 // index: cleaning the enlarged block deletes instructions ahead of that
597 // point, and every index after a deletion shifts. The first of these
598 // still standing afterwards is the one to resume at.
599 let resume: Vec<LocalInsnId> = self.ctx.block(head).instruction_ids()[offset..].to_vec();
600 self.mark_dirty(head);
601
602 self.reindex_absorbed(head);
603
604 // The machine stopped at the empty placeholder this lift filled, which
605 // absorption has just deleted; its instructions are in the head now.
606 if self.emu.block == filled {
607 let now = self.ctx.block(head).instruction_ids();
608 let resumed = resume
609 .iter()
610 .find_map(|wanted| now.iter().position(|have| have == wanted))
611 .unwrap_or(now.len().saturating_sub(1));
612 self.emu.block = head;
613 self.emu.idx = resumed;
614 self.emu.invalidate_block_cache();
615 }
616 Some(head)
617 }
618
619 /// Runs until the machine stops, or until `budget` p-code operations have
620 /// been retired.
621 pub fn run(&mut self, budget: u64) -> VmExit {
622 let deadline = self.stats.steps + budget;
623 while self.stats.steps < deadline {
624 // Checked before the step so a breakpoint reports the instruction
625 // about to run, not the one after it, and so resuming from a
626 // breakpoint is possible without immediately re-triggering it.
627 //
628 // Guarded on there being any breakpoint at all: `pc()` resolves the
629 // block through the module arena, and paying that on every step to
630 // consult an empty set cost about 6% of run time.
631 if !self.breakpoints.is_empty()
632 && let Some(pc) = self.pc()
633 && self.breakpoints.contains(&pc)
634 && self.stats.steps > 0
635 {
636 return VmExit::Breakpoint(pc);
637 }
638 if let Some(exit) = self.step() {
639 return exit;
640 }
641 }
642 VmExit::InstructionLimit
643 }
644}
645
646/// Resolves a guest address to a block against an existing index.
647fn resolve(ctx: &Context<'_>, index: &AddressIndex, addr: u64) -> Option<BlockId> {
648 match index.get(addr) {
649 Some(AddressTarget::Block(block)) => Some(block),
650 Some(AddressTarget::Function(function)) => {
651 qcode::value::FunctionBody::from_id(ctx, function)
652 .root()
653 .map(|root| root.id)
654 }
655 None => None,
656 }
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662 use crate::mmu::{PAGE_SIZE, perm};
663 use qcode::value::FunctionBody;
664
665 /// A source that hands over one pre-planned block per address, so the
666 /// discovery path can be exercised without a decoder.
667 #[derive(Default)]
668 struct Planned {
669 /// Addresses this source is willing to supply, and how many times it was
670 /// actually asked.
671 available: Vec<u64>,
672 pub calls: Vec<u64>,
673 }
674
675 impl CodeSource for Planned {
676 fn lift(
677 &mut self,
678 ctx: &mut Context<'static>,
679 memory: &VmMemory,
680 index: &mut AddressIndex,
681 addr: u64,
682 _stats: &mut Stats,
683 ) -> Result<(), CodeError> {
684 self.calls.push(addr);
685 // A real source fetches through the MMU, so executing unmapped or
686 // non-executable memory faults at the fetch. Mirrored here.
687 let mut byte = [0u8; 1];
688 memory
689 .mmu
690 .read_code(addr, &mut byte)
691 .map_err(CodeError::Fault)?;
692 if !self.available.contains(&addr) {
693 return Err(CodeError::Decode("no plan for this address".into()));
694 }
695 let function = FunctionBody::make_at_addr(ctx, addr, None).id;
696 let block = BasicBlock::make(ctx, function).with_address(addr).id;
697 index
698 .register(ctx, addr, AddressTarget::Block(block))
699 .map_err(|error| CodeError::Decode(format!("{error:?}").into()))?;
700 Ok(())
701 }
702 }
703
704 /// A module with a single empty block at `addr`.
705 fn module(addr: u64) -> (Context<'static>, BlockId) {
706 let mut ctx = Context::new();
707 let function = FunctionBody::make_at_addr(&mut ctx, addr, None).id;
708 let block = BasicBlock::make(&mut ctx, function).with_address(addr).id;
709 (ctx, block)
710 }
711
712 fn executable_memory() -> VmMemory {
713 let mut memory = VmMemory::new();
714 memory.mmu.map(0x1000, PAGE_SIZE, perm::RX_INIT).unwrap();
715 memory
716 }
717
718 #[test]
719 fn an_empty_addressed_block_asks_the_source_for_code() {
720 let (ctx, block) = module(0x1000);
721 let mut vm = Vm::new(ctx, block, Planned::default());
722 // An empty block carrying an address is how the lifter represents a
723 // branch target it has not reached yet, so it is a discovery request.
724 // This source cannot supply it, which is what makes the exit reportable.
725 let exit = vm.run(16);
726 assert!(
727 matches!(exit, VmExit::Unlifted { addr: 0x1000, .. }),
728 "expected a discovery attempt, got {exit:?}"
729 );
730 assert_eq!(vm.source.calls, vec![0x1000]);
731 }
732
733 #[test]
734 fn an_empty_block_with_no_address_is_an_error() {
735 // Nothing to discover: without an address there is no code to fetch.
736 let mut ctx = Context::new();
737 let function = FunctionBody::make_at_addr(&mut ctx, 0x1000, None).id;
738 let block = BasicBlock::make(&mut ctx, function).id;
739 let mut vm = Vm::new(ctx, block, Planned::default());
740 assert!(matches!(vm.run(16), VmExit::Error(_)));
741 }
742
743 #[test]
744 fn pc_reports_the_block_about_to_run() {
745 let (ctx, block) = module(0x1000);
746 let vm = Vm::new(ctx, block, Planned::default());
747 assert_eq!(vm.pc(), Some(0x1000));
748 }
749
750 #[test]
751 fn at_address_lifts_an_entry_the_module_lacks() {
752 let ctx = Context::new();
753 let source = Planned {
754 available: vec![0x1000],
755 calls: Vec::new(),
756 };
757 let vm = Vm::at_address(ctx, 0x1000, source, executable_memory())
758 .expect("the source can supply this address");
759 assert_eq!(vm.pc(), Some(0x1000));
760 }
761
762 #[test]
763 fn at_address_reuses_a_block_the_module_already_has() {
764 let (ctx, _) = module(0x1000);
765 let vm = Vm::at_address(ctx, 0x1000, Planned::default(), executable_memory())
766 .expect("no lifting is needed");
767 assert_eq!(vm.pc(), Some(0x1000));
768 // The source was never consulted.
769 assert!(vm.source.calls.is_empty());
770 }
771
772 #[test]
773 fn fetching_from_non_executable_memory_reports_the_fault() {
774 let ctx = Context::new();
775 let mut memory = VmMemory::new();
776 memory.mmu.map(0x1000, PAGE_SIZE, perm::RW_INIT).unwrap();
777 let source = Planned {
778 available: vec![0x1000],
779 calls: Vec::new(),
780 };
781 let error = Vm::at_address(ctx, 0x1000, source, memory)
782 .err()
783 .expect("the page is not executable");
784 assert!(matches!(
785 error,
786 CodeError::Fault(MemFault {
787 kind: crate::mmu::FaultKind::ExecViolation,
788 addr: 0x1000
789 })
790 ));
791 }
792
793 #[test]
794 fn an_unsuppliable_address_reports_where_it_stopped() {
795 let ctx = Context::new();
796 let error = Vm::at_address(ctx, 0x2000, Planned::default(), executable_memory())
797 .err()
798 .expect("nothing is mapped or planned at 0x2000");
799 assert!(matches!(error, CodeError::Fault(_)));
800 }
801
802 #[test]
803 fn breakpoints_are_recorded_and_removable() {
804 let (ctx, block) = module(0x1000);
805 let mut vm = Vm::new(ctx, block, Planned::default());
806 assert!(vm.add_breakpoint(0x2000));
807 assert!(!vm.add_breakpoint(0x2000));
808 assert!(vm.remove_breakpoint(0x2000));
809 assert!(!vm.remove_breakpoint(0x2000));
810 }
811
812 #[test]
813 fn memory_is_reachable_and_backed_by_the_mmu() {
814 let (ctx, block) = module(0x1000);
815 let mut vm = Vm::new(ctx, block, Planned::default());
816 vm.memory_mut()
817 .mmu
818 .map(0x4000, PAGE_SIZE, perm::RW_INIT)
819 .unwrap();
820 vm.memory_mut().mmu.write(0x4000, &[1, 2, 3]).unwrap();
821 let mut out = [0; 3];
822 vm.memory().mmu.read(0x4000, &mut out).unwrap();
823 assert_eq!(out, [1, 2, 3]);
824 }
825}