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::{
22 BasicBlock, BlockId, InstructionId, ValueId,
23 insn::{Mnemonic, PCodeOpId, VM_INTERRUPT},
24 },
25};
26use qcode_emulator::{EmulatorErrorKind, EmulatorMemory, SizedValue, StandaloneEmulator};
27use rustc_hash::{FxHashMap, FxHashSet};
28
29use crate::{
30 hook::{AddressHook, BlockEntryHook, WriteWatch},
31 inject::CodeInjector,
32 memory::VmMemory,
33 mmu::MemFault,
34 stats::Stats,
35 table::{
36 Callback, CodeRangeHook, HookAction, HookId, HookTable, InsnAction, MemAccess, ReadWatch,
37 },
38};
39
40/// Why a lifting attempt failed.
41#[derive(Debug, Clone)]
42pub enum CodeError {
43 /// The instruction bytes could not be fetched.
44 Fault(MemFault),
45 /// The bytes were fetched but did not decode, or did not lift.
46 Decode(Box<str>),
47}
48
49/// Supplies code the machine has not seen yet.
50///
51/// Kept as a trait so the VM does not depend on SLEIGH: a decoder is a policy
52/// choice (which specification, which variant), and a test wants to hand over
53/// blocks without compiling one. An implementation reads instruction bytes from
54/// the [`Mmu`](crate::Mmu) — via [`read_code`](crate::Mmu::read_code), so that
55/// executing a non-executable page faults at the fetch — and lowers them into
56/// `ctx`.
57pub trait CodeSource {
58 /// Lifts the code at `addr` into `ctx`.
59 ///
60 /// Returning `Ok(())` asserts that a block starting at `addr` now exists;
61 /// the VM re-resolves the address itself rather than trusting a returned id,
62 /// so a source is free to lift a whole run of instructions at once.
63 ///
64 /// `index` is the machine's live address lookup, and the implementation must
65 /// keep it current as it adds blocks — every lifting entry point takes one
66 /// for exactly this reason. Rebuilding it per instruction instead is
67 /// quadratic in the size of the module discovered so far.
68 ///
69 /// `stats` is the machine's own counters: an implementation records the
70 /// time it spends fetching and decoding there, so a benchmark can separate
71 /// translation cost from interpretation cost.
72 fn lift(
73 &mut self,
74 ctx: &mut Context<'static>,
75 memory: &VmMemory,
76 index: &mut AddressIndex,
77 addr: u64,
78 stats: &mut Stats,
79 ) -> Result<(), CodeError>;
80}
81
82/// An alternative way to execute a block's body.
83///
84/// The interpreter is always present and always correct; an executor is an
85/// *optimisation* that may decline any block for any reason, in which case the
86/// interpreter runs it unchanged. That is what lets a backend be partial: a JIT
87/// need only handle the shapes it handles well.
88///
89/// An executor runs the block's body, not its terminator. Control flow, block
90/// parameters and call semantics stay in one implementation.
91///
92/// It is handed the whole emulator rather than just its memory because the
93/// interpreter still has to run the terminator, and a terminator reads
94/// operands — a `cbranch` condition, a branch's block arguments. Those are
95/// values the body produced, so an executor that keeps them somewhere other
96/// than the interpreter's value table must put them back before returning.
97pub trait BlockExecutor {
98 /// Runs everything in `block` except its terminator.
99 ///
100 /// `Ok(None)` means "not mine" and is not an error — the caller falls back
101 /// to the interpreter.
102 ///
103 /// On `Ok(Some(_))` every value the terminator of [`Executed::block`] reads
104 /// must be readable from `emu.insn_values`, exactly as if the interpreter
105 /// had run that body.
106 ///
107 /// `chain` lets the executor run on past `block` into successors it also
108 /// handles, instead of handing control back after one. Deciding a branch
109 /// itself is how an executor keeps control inside its own code rather than
110 /// paying a round trip per block. The caller withholds it when something
111 /// needs to observe every block — a breakpoint is set, say — because blocks
112 /// crossed this way are never offered to the interpreter.
113 ///
114 /// `start` is the body index to begin at. It is 0 when a block is
115 /// entered, and the instruction after an interrupting op when the
116 /// interpreter has run the block up to there and hands the rest over.
117 /// Results the interpreter already computed for the block are in
118 /// `emu.insn_values`; an executor may read them or decline.
119 fn run_block(
120 &mut self,
121 ctx: &Context<'_>,
122 emu: &mut StandaloneEmulator<VmMemory>,
123 block: BlockId,
124 start: usize,
125 chain: bool,
126 ) -> Result<Option<Executed>, EmulatorErrorKind>;
127}
128
129/// Where an executor left the machine.
130#[derive(Debug, Clone, Copy)]
131pub struct Executed {
132 /// The block the interpreter continues in. With chaining this is the
133 /// last of several, not the one that was asked for.
134 pub block: BlockId,
135 /// The body index of that block the interpreter continues from: its
136 /// terminator, or an interrupting op the executor stopped short of.
137 pub body: usize,
138 /// Operations retired across every block run, for accounting.
139 pub retired: u64,
140}
141
142/// Why the machine stopped.
143#[derive(Debug, Clone)]
144pub enum VmExit {
145 /// The step budget ran out. The machine is resumable.
146 InstructionLimit,
147 /// Execution reached an address with a breakpoint on it. The breakpoint
148 /// instruction has *not* been executed.
149 Breakpoint(u64),
150 /// A memory access failed.
151 Fault(MemFault),
152 /// Code at this address could not be lifted.
153 Unlifted { addr: u64, error: CodeError },
154 /// The machine stopped *at* a user p-code operation for the host to act:
155 /// an explicit `vm.interrupt`, or an op the interpreter has no semantics
156 /// for (`syscall`, `cpuid`, `rdtsc`, ...). Everything before the op in
157 /// its block has retired and nothing after it has run. The machine stays
158 /// there until [`Vm::resume`] supplies the op's effect; running again
159 /// without resuming reports the same interrupt.
160 Interrupt(Interrupt),
161 /// A hook registered through the [table](crate::table) asked the run to
162 /// stop. For a code or memory hook the machine is before the hooked
163 /// guest instruction, and running again executes it without notifying
164 /// the hook again. For an instruction hook the user op is still pending,
165 /// as [`Interrupt`] describes, until the caller resumes it.
166 HookStop(HookId),
167 /// The interpreter reported something the VM does not model as a guest
168 /// event — a malformed block, an internal failure.
169 Error(Box<str>),
170}
171
172/// What kind of operation stopped the machine.
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub enum InterruptKind {
175 /// An explicit [`VM_INTERRUPT`] placed in the IR by a hook or an injector.
176 /// `code` is its first operand.
177 Explicit { code: u64 },
178 /// An architecture user op with no interpreter semantics, named as in the
179 /// SLEIGH specification.
180 Intrinsic { op: PCodeOpId, name: Box<str> },
181}
182
183/// A stop at a user p-code operation, with what the host needs to act on it.
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct Interrupt {
186 pub kind: InterruptKind,
187 /// The operation's instruction: where [`Vm::resume`] files its result.
188 pub insn: InstructionId,
189 /// The width in bytes of the result the operation declares, or 0 when it
190 /// produces nothing.
191 pub size: usize,
192 /// The operation's operands as read at the stop — after `code` for an
193 /// explicit interrupt. `None` where an operand is wider than 64 bits or
194 /// could not be read.
195 pub args: Vec<Option<u64>>,
196 /// The guest address of the instruction that lifted to the operation.
197 pub pc: Option<u64>,
198}
199
200/// Why [`Vm::resume`] refused.
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub enum ResumeError {
203 /// The machine is not stopped at an interrupt.
204 NotInterrupted,
205 /// The operation declares a result of this many bytes and none was given.
206 ResultRequired { size: usize },
207}
208
209impl std::fmt::Display for ResumeError {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 match self {
212 Self::NotInterrupted => write!(f, "the machine is not stopped at an interrupt"),
213 Self::ResultRequired { size } => {
214 write!(f, "the interrupted operation needs a {size}-byte result")
215 }
216 }
217 }
218}
219
220impl std::error::Error for ResumeError {}
221
222/// A machine: an owned module, a memory, and a position in the code.
223pub struct Vm<S> {
224 ctx: Context<'static>,
225 emu: StandaloneEmulator<VmMemory>,
226 source: S,
227 /// Counters and phase timings for this run.
228 pub stats: Stats,
229 /// An optional faster path for block bodies. `None` means the interpreter
230 /// executes everything, which is always a valid way to run.
231 executor: Option<Box<dyn BlockExecutor>>,
232 /// Set by a lift that folded the block it filled into a predecessor, which
233 /// leaves the machine already positioned. Taken by the step that asked for
234 /// the lift.
235 absorbed_into: Option<BlockId>,
236 /// Whether freshly lifted blocks get a cleanup round.
237 ///
238 /// Lifting one machine instruction emits every side effect the
239 /// specification describes, including flag computations the surrounding
240 /// code never reads. Removing the ones with no users at all is sound
241 /// block-locally — an instruction with no users cannot be observed — and is
242 /// paid once per block instead of on every execution of it.
243 pub optimize: bool,
244 /// The block that has grown by absorption and not been cleaned since.
245 ///
246 /// Absorption folds a straight-line run one guest instruction at a time,
247 /// and cleaning the whole enlarged block after each one is quadratic in the
248 /// length of the run — which on unrolled code is the dominant cost of
249 /// translation. The cleanup is deferred to the point the block is next
250 /// entered at its first instruction, by which time the run has stopped
251 /// growing and one pass does the work of all of them.
252 ///
253 /// At most one: absorption extends one run at a time, so a *different*
254 /// block being absorbed into means the previous run has stopped growing
255 /// and can be cleaned right there. Waiting for it to be entered again
256 /// instead would let compiled code be built from the uncleaned form — and
257 /// worse, would clean it in an interpreted run but not in a chained
258 /// compiled one, leaving the two strategies running different QCode.
259 dirty: Option<BlockId>,
260 breakpoints: FxHashSet<u64>,
261 /// The interrupt the machine is stopped at, until it is resumed.
262 pending: Option<Interrupt>,
263 /// Set by a resume: the machine is part-way into a block, and the
264 /// executor gets one chance to take the rest of it.
265 offer_rest: bool,
266 /// Code rewriters, run over each block before it is first entered and
267 /// again when it grows. See [`crate::inject`].
268 injectors: Vec<Box<dyn CodeInjector>>,
269 /// The injector set each block was last rewritten by, so registering an
270 /// injector reaches blocks already lifted the next time they are entered.
271 injected: FxHashMap<BlockId, u64>,
272 /// Bumped by every registration.
273 generation: u64,
274 /// Callbacks the run calls itself. See [`crate::table`].
275 table: HookTable<S>,
276}
277
278impl<S: CodeSource> Vm<S> {
279 /// Builds a machine positioned at `entry`.
280 pub fn new(ctx: Context<'static>, entry: BlockId, source: S) -> Self {
281 let mut emu = StandaloneEmulator::<VmMemory>::new_in(entry);
282 emu.memory.configure_spaces(&ctx);
283 Self {
284 ctx,
285 emu,
286 source,
287 optimize: true,
288 stats: Stats::default(),
289 executor: None,
290 absorbed_into: None,
291 dirty: None,
292 breakpoints: FxHashSet::default(),
293 pending: None,
294 offer_rest: false,
295 injectors: Vec::new(),
296 injected: FxHashMap::default(),
297 generation: 0,
298 table: HookTable::default(),
299 }
300 }
301
302 /// Builds a machine positioned at a guest address, lifting the entry block
303 /// if the module does not already contain it.
304 pub fn at_address(
305 mut ctx: Context<'static>,
306 addr: u64,
307 mut source: S,
308 memory: VmMemory,
309 ) -> Result<Self, CodeError> {
310 // Built once here and handed to the machine, which keeps it current
311 // from then on.
312 let mut index = AddressIndex::analyze(&ctx);
313 let mut stats = Stats::default();
314 if resolve(&ctx, &index, addr).is_none() {
315 stats.lifts += 1;
316 source.lift(&mut ctx, &memory, &mut index, addr, &mut stats)?;
317 }
318 let entry = resolve(&ctx, &index, addr).ok_or_else(|| {
319 CodeError::Decode(format!("no block at {addr:#x} after lifting").into())
320 })?;
321 let mut vm = Self::new(ctx, entry, source);
322 vm.emu.memory = memory;
323 vm.emu.memory.configure_spaces(&vm.ctx);
324 vm.emu.set_address_index(index);
325 vm.stats = stats;
326 Ok(vm)
327 }
328
329 /// Installs an alternative executor for block bodies, replacing any
330 /// previous one. Purely an optimisation: removing it changes speed, not
331 /// behaviour.
332 pub fn set_block_executor(&mut self, executor: Box<dyn BlockExecutor>) {
333 self.executor = Some(executor);
334 }
335
336 pub fn clear_block_executor(&mut self) {
337 self.executor = None;
338 }
339
340 pub fn context(&self) -> &Context<'static> {
341 &self.ctx
342 }
343
344 pub fn memory(&self) -> &VmMemory {
345 &self.emu.memory
346 }
347
348 pub fn memory_mut(&mut self) -> &mut VmMemory {
349 &mut self.emu.memory
350 }
351
352 /// The emulator underneath, for register access and harness seeding.
353 pub fn emulator(&mut self) -> &mut StandaloneEmulator<VmMemory> {
354 &mut self.emu
355 }
356
357 /// The guest address of the block about to execute, if it has one.
358 pub fn pc(&self) -> Option<u64> {
359 BasicBlock::from_id(&self.ctx, self.emu.block).address()
360 }
361
362 pub fn add_breakpoint(&mut self, addr: u64) -> bool {
363 self.breakpoints.insert(addr)
364 }
365
366 pub fn remove_breakpoint(&mut self, addr: u64) -> bool {
367 self.breakpoints.remove(&addr)
368 }
369
370 /// Registers a code rewriter. It runs over every block the machine enters
371 /// from now on, including blocks lifted before this call, before the
372 /// block executes or is compiled.
373 pub fn add_injector(&mut self, injector: Box<dyn CodeInjector>) {
374 self.injectors.push(injector);
375 self.generation += 1;
376 }
377
378 /// Registers a [`Hook`](crate::hook::Hook): a code rewriter that picks
379 /// its sites and emits through an [`Emitter`](crate::hook::Emitter).
380 pub fn add_hook(&mut self, hook: impl crate::hook::Hook + 'static) {
381 self.add_injector(Box::new(crate::hook::HookInjector::new(hook)));
382 }
383
384 // ---- Unicorn-shaped hooks. See [`crate::table`].
385
386 /// Calls `callback` with the address at the entry of every block in
387 /// `begin..=end` (anywhere when `begin > end`).
388 pub fn hook_block(
389 &mut self,
390 begin: u64,
391 end: u64,
392 callback: impl FnMut(&mut Self, u64) -> HookAction + 'static,
393 ) -> HookId {
394 let id = self.table.register(Callback::Code(Box::new(callback)));
395 self.add_hook(BlockEntryHook {
396 begin,
397 end,
398 code: HookTable::<S>::code(id),
399 });
400 id
401 }
402
403 /// Calls `callback` before every guest instruction in `begin..=end`.
404 pub fn hook_code(
405 &mut self,
406 begin: u64,
407 end: u64,
408 callback: impl FnMut(&mut Self, u64) -> HookAction + 'static,
409 ) -> HookId {
410 let id = self.table.register(Callback::Code(Box::new(callback)));
411 self.add_hook(CodeRangeHook {
412 begin,
413 end,
414 code: HookTable::<S>::code(id),
415 });
416 id
417 }
418
419 /// Calls `callback` before the guest instruction at `addr`.
420 pub fn hook_address(
421 &mut self,
422 addr: u64,
423 callback: impl FnMut(&mut Self, u64) -> HookAction + 'static,
424 ) -> HookId {
425 let id = self.table.register(Callback::Code(Box::new(callback)));
426 self.add_hook(AddressHook::new([addr], HookTable::<S>::code(id)));
427 id
428 }
429
430 /// Calls `callback` before every store to guest memory in `begin..=end`,
431 /// with the address, width and value.
432 pub fn hook_mem_write(
433 &mut self,
434 begin: u64,
435 end: u64,
436 callback: impl FnMut(&mut Self, &MemAccess) -> HookAction + 'static,
437 ) -> HookId {
438 let id = self.table.register(Callback::Mem(Box::new(callback)));
439 self.add_hook(WriteWatch {
440 begin,
441 end,
442 code: HookTable::<S>::code(id),
443 });
444 id
445 }
446
447 /// Calls `callback` before every load from guest memory in `begin..=end`,
448 /// with the address and width.
449 pub fn hook_mem_read(
450 &mut self,
451 begin: u64,
452 end: u64,
453 callback: impl FnMut(&mut Self, &MemAccess) -> HookAction + 'static,
454 ) -> HookId {
455 let id = self.table.register(Callback::Mem(Box::new(callback)));
456 self.add_hook(ReadWatch {
457 begin,
458 end,
459 code: HookTable::<S>::code(id),
460 });
461 id
462 }
463
464 /// Calls `callback` when the guest reaches the user op called `name` —
465 /// `syscall`, `rdtsc`, `cpuid_basic`, ... — to supply its effect.
466 pub fn hook_insn(
467 &mut self,
468 name: &str,
469 callback: impl FnMut(&mut Self, &Interrupt) -> InsnAction + 'static,
470 ) -> HookId {
471 self.table.register(Callback::Insn {
472 name: Some(Box::from(name)),
473 callback: Box::new(callback),
474 })
475 }
476
477 /// Calls `callback` for every user op the interpreter cannot run, whatever
478 /// its name.
479 pub fn hook_intr(
480 &mut self,
481 callback: impl FnMut(&mut Self, &Interrupt) -> InsnAction + 'static,
482 ) -> HookId {
483 self.table.register(Callback::Insn {
484 name: None,
485 callback: Box::new(callback),
486 })
487 }
488
489 /// Removes a hook. Interrupts it injected stay in the code and resume
490 /// silently; they cost a stop and nothing more.
491 pub fn hook_del(&mut self, id: HookId) -> bool {
492 self.table.remove(id)
493 }
494
495 /// Handles an interrupt the table owns, or an intrinsic a hook answers.
496 ///
497 /// Returns `None` when the run should carry on, and the exit to return
498 /// otherwise. The callbacks are moved out for the duration so they can be
499 /// handed the machine; ones registered meanwhile are kept.
500 fn dispatch(&mut self, interrupt: &Interrupt) -> Option<VmExit> {
501 let owner = HookTable::<S>::owner(interrupt);
502 let intrinsic = match &interrupt.kind {
503 InterruptKind::Intrinsic { name, .. } => Some(name.clone()),
504 InterruptKind::Explicit { .. } => None,
505 };
506 if owner.is_none() && intrinsic.is_none() {
507 return Some(VmExit::Interrupt(interrupt.clone()));
508 }
509 let mut callbacks = std::mem::take(&mut self.table.callbacks);
510 let mut outcome: Option<VmExit> = None;
511 let mut handled = false;
512 for (id, callback) in &mut callbacks {
513 match (callback, owner, &intrinsic) {
514 (Callback::Code(callback), Some(owner), _) if *id == owner => {
515 let pc = interrupt
516 .args
517 .first()
518 .copied()
519 .flatten()
520 .unwrap_or_default();
521 if callback(self, pc) == HookAction::Stop {
522 outcome = Some(VmExit::HookStop(*id));
523 }
524 handled = true;
525 }
526 (Callback::Mem(callback), Some(owner), _) if *id == owner => {
527 let arg = |index: usize| interrupt.args.get(index).copied().flatten();
528 let access = MemAccess {
529 pc: interrupt.pc,
530 addr: arg(0).unwrap_or_default(),
531 size: arg(1).unwrap_or_default(),
532 value: arg(2),
533 };
534 if callback(self, &access) == HookAction::Stop {
535 outcome = Some(VmExit::HookStop(*id));
536 }
537 handled = true;
538 }
539 (Callback::Insn { name, callback }, None, Some(op))
540 if name.as_ref().is_none_or(|name| name == op) =>
541 {
542 match callback(self, interrupt) {
543 InsnAction::Handled(value) => {
544 if let Err(error) = self.resume(value) {
545 outcome = Some(VmExit::Error(error.to_string().into()));
546 }
547 handled = true;
548 }
549 InsnAction::Stop => {
550 outcome = Some(VmExit::HookStop(*id));
551 handled = true;
552 }
553 InsnAction::Unhandled => continue,
554 }
555 }
556 _ => continue,
557 }
558 if outcome.is_some() {
559 break;
560 }
561 if intrinsic.is_some() && handled {
562 // One answer per op.
563 break;
564 }
565 }
566 // Hooks registered by a callback were pushed onto the empty table.
567 callbacks.append(&mut self.table.callbacks);
568 self.table.callbacks = callbacks;
569
570 if owner.is_some() {
571 // The table's interrupt is a notification, delivered now (or
572 // owed to a hook since deleted). Whether the run goes on or
573 // stops, the machine is left before the guest instruction, not
574 // at the notification: a later run must not deliver it again.
575 if let Err(error) = self.resume(None) {
576 return Some(VmExit::Error(error.to_string().into()));
577 }
578 return outcome;
579 }
580 if let Some(exit) = outcome {
581 return Some(exit);
582 }
583 if !handled {
584 // An intrinsic no hook answered is the caller's.
585 return Some(VmExit::Interrupt(interrupt.clone()));
586 }
587 None
588 }
589
590 /// Runs the injectors over `block` unless the current set already has.
591 ///
592 /// Only over lifted code: an empty block carrying an address is a request
593 /// to lift it, and its instructions arrive — and get rewritten — once it
594 /// is discovered.
595 fn inject(&mut self, block: BlockId) {
596 if self.injectors.is_empty()
597 || self.injected.get(&block) == Some(&self.generation)
598 || !BasicBlock::from_id(&self.ctx, block).is_terminated()
599 {
600 return;
601 }
602 // Moved out for the duration, so the injectors can be handed the
603 // module without borrowing the machine twice.
604 let mut injectors = std::mem::take(&mut self.injectors);
605 for injector in &mut injectors {
606 injector.inject(&mut self.ctx, block);
607 }
608 self.injectors = injectors;
609 self.injected.insert(block, self.generation);
610 // The interpreter may hold this block's instruction list, and it has
611 // changed.
612 self.emu.invalidate_block_cache();
613 }
614
615 /// Executes one instruction, lifting code on demand if control leaves the
616 /// part of the module already known.
617 ///
618 /// Returns `None` when the step was ordinary, and `Some(exit)` when the
619 /// machine stopped for a reason worth reporting.
620 pub fn step(&mut self) -> Option<VmExit> {
621 // Stopped at an operation nobody has resumed: the machine has not
622 // moved, and stepping it would run the op again without its effect.
623 if let Some(interrupt) = &self.pending {
624 return Some(VmExit::Interrupt(interrupt.clone()));
625 }
626 // A branch to unlifted code fails *before* the emulator moves, so the
627 // address can be lifted and the same step retried. One retry is enough:
628 // the second failure means the source did not produce the block it
629 // claimed to, which is a source bug rather than a discovery step.
630 for attempt in 0..2 {
631 // At its first instruction a block is between runs, which is the
632 // one moment a deferred cleanup can be taken without disturbing a
633 // position inside it — and it has to happen before the executor
634 // looks, or compiled code gets built from uncleaned QCode.
635 if self.emu.idx == 0 {
636 let block = self.emu.block;
637 self.clean_before_entering(block);
638 self.inject(block);
639 }
640
641 // At a block's first instruction — or just past an interrupt it
642 // resumed from — an installed executor may run the rest of the
643 // body at once, leaving the interpreter only the terminator.
644 if (self.emu.idx == 0 || self.offer_rest)
645 && let Some(executor) = self.executor.as_mut()
646 {
647 self.offer_rest = false;
648 let block = self.emu.block;
649 let start = self.emu.idx;
650 // Blocks the executor runs are never offered to the interpreter, so
651 // it may only run past the first when nothing needs to see them.
652 let chain = self.breakpoints.is_empty();
653 match executor.run_block(&self.ctx, &mut self.emu, block, start, chain) {
654 Ok(Some(run)) => {
655 // The operations were retired by the executor; they are
656 // counted so throughput stays comparable between strategies.
657 self.stats.steps += run.retired;
658 self.stats.native_bodies += 1;
659 // Positioning inside a block the interpreter has not walked
660 // into invalidates its cached instruction list.
661 self.emu.invalidate_block_cache();
662 self.emu.block = run.block;
663 self.emu.idx = run.body;
664 }
665 Ok(None) => {}
666 Err(kind) => {
667 let fault = self.emu.memory.take_fault();
668 return Some(match fault {
669 Some(fault) => VmExit::Fault(fault),
670 None => self.exit_for(kind),
671 });
672 }
673 }
674 }
675
676 match self.emu.step(&self.ctx) {
677 Ok(()) => {
678 self.stats.steps += 1;
679 return None;
680 }
681 Err(error) => match error.kind {
682 EmulatorErrorKind::InvalidBlockAddress(addr)
683 | EmulatorErrorKind::UnknownAddress(addr)
684 if attempt == 0 =>
685 {
686 if let Some(exit) = self.discover(addr) {
687 return Some(exit);
688 }
689 }
690 // A direct branch to code that has not been lifted does not
691 // fail to resolve: the lifter materializes the target as an
692 // *empty* block at that address, and execution walks into
693 // it. So an empty block carrying an address is a request to
694 // discover it, not a malformed-IR error.
695 EmulatorErrorKind::EmptyBlock(block) if attempt == 0 => {
696 let Some(addr) = BasicBlock::from_id(&self.ctx, block).address() else {
697 return Some(VmExit::Error(
698 EmulatorErrorKind::EmptyBlock(block).to_string().into(),
699 ));
700 };
701 // The address may already be lifted: a branch back into
702 // known code still gets a fresh placeholder block in the
703 // branching instruction's own function, and lifting it
704 // again would collide with the function that owns it.
705 // Resolving first is what makes loops work.
706 let before = self.emu.block;
707 self.reposition(addr);
708 if self.emu.block != before {
709 // Already lifted: a translation-cache hit.
710 self.stats.resolves += 1;
711 continue;
712 }
713 if let Some(exit) = self.discover(addr) {
714 return Some(exit);
715 }
716 // Lifting may place the instruction in a *new* block
717 // rather than filling the placeholder the emulator is
718 // sitting in, so the machine has to be moved onto
719 // whatever now covers the address — unless the lift
720 // folded that block into its predecessor, which
721 // positions the machine itself. `addr` is interior to
722 // the absorbing block then, and resolving it by address
723 // would land at that block's *start*.
724 if self.absorbed_into.take().is_none() {
725 self.reposition(addr);
726 }
727 }
728 EmulatorErrorKind::MemoryReadError(addr)
729 | EmulatorErrorKind::MemoryWriteError(addr) => {
730 // The backend records the precise cause; the error alone
731 // could only say that an access at this address failed.
732 let fault = self.emu.memory.take_fault().unwrap_or(MemFault {
733 kind: crate::mmu::FaultKind::ReadUnmapped,
734 addr,
735 });
736 return Some(VmExit::Fault(fault));
737 }
738 kind => return Some(self.exit_for(kind)),
739 },
740 }
741 }
742 None
743 }
744
745 /// The exit for an interpreter error that is not a memory fault or a
746 /// discovery request.
747 ///
748 /// A stop at a user operation is a guest event with a typed exit; anything
749 /// else is reported as the error it is.
750 fn exit_for(&mut self, kind: EmulatorErrorKind) -> VmExit {
751 match kind {
752 EmulatorErrorKind::Interrupt | EmulatorErrorKind::UnsupportedPCodeOp(_) => {
753 match self.interrupt_at_position() {
754 Some(interrupt) => {
755 self.pending = Some(interrupt.clone());
756 VmExit::Interrupt(interrupt)
757 }
758 // The error named an op, but the machine is not at one:
759 // an executor stopped somewhere it should not have.
760 None => VmExit::Error(kind.to_string().into()),
761 }
762 }
763 other => VmExit::Error(other.to_string().into()),
764 }
765 }
766
767 /// Describes the user operation at the machine's position, if that is
768 /// what it is stopped at.
769 fn interrupt_at_position(&mut self) -> Option<Interrupt> {
770 let block = self.emu.block;
771 let idx = self.emu.idx;
772 if !self.ctx.contains_block(block) {
773 return None;
774 }
775 let (insn, size, pc, op, operands) = {
776 let insn = BasicBlock::from_id(&self.ctx, block)
777 .instructions()
778 .nth(idx)?;
779 let Mnemonic::PCodeOp(op) = insn.mnemonic() else {
780 return None;
781 };
782 let operands: Vec<ValueId> =
783 op.args.iter().map(|arg| arg.qualify(block.func)).collect();
784 // An instruction carries the address of the guest instruction it
785 // was lifted from; one built by hand may not, in which case the
786 // block's own address is the best that can be said.
787 let pc = insn
788 .address()
789 .or_else(|| BasicBlock::from_id(&self.ctx, block).address());
790 (insn.id, insn.size(), pc, op.id, operands)
791 };
792 let name = self.ctx.shared.pcode_ops[op].clone();
793 let mut args: Vec<Option<u64>> = operands
794 .into_iter()
795 .map(|value| self.emu.get_value(&self.ctx, value))
796 .collect();
797 let kind = if name.as_ref() == VM_INTERRUPT {
798 let code = if args.is_empty() {
799 0
800 } else {
801 args.remove(0).unwrap_or(0)
802 };
803 InterruptKind::Explicit { code }
804 } else {
805 InterruptKind::Intrinsic { op, name }
806 };
807 Some(Interrupt {
808 kind,
809 insn,
810 size,
811 args,
812 pc,
813 })
814 }
815
816 /// The interrupt the machine is stopped at, if any.
817 pub fn pending_interrupt(&self) -> Option<&Interrupt> {
818 self.pending.as_ref()
819 }
820
821 /// Supplies the effect of the operation the machine is stopped at and
822 /// steps past it.
823 ///
824 /// `value` is the operation's result, required when it declares one
825 /// ([`Interrupt::size`] is non-zero) and ignored otherwise. Any other
826 /// effect — a register written by a system call, memory filled by a host
827 /// service — the caller applies through [`Vm::emulator`] and
828 /// [`Vm::memory_mut`] before resuming. The operation's own instruction is
829 /// not run; the one after it is next.
830 pub fn resume(&mut self, value: Option<u128>) -> Result<(), ResumeError> {
831 let Some(interrupt) = self.pending.as_ref() else {
832 return Err(ResumeError::NotInterrupted);
833 };
834 if interrupt.size > 0 {
835 let Some(value) = value else {
836 return Err(ResumeError::ResultRequired {
837 size: interrupt.size,
838 });
839 };
840 self.emu
841 .insn_values
842 .insert(interrupt.insn, SizedValue::from_bits(value, interrupt.size));
843 }
844 self.pending = None;
845 self.stats.steps += 1;
846 self.emu.idx += 1;
847 self.offer_rest = true;
848 Ok(())
849 }
850
851 /// Points the emulator at whatever block now covers `addr`, reusing the
852 /// emulator's own cached index rather than building one.
853 fn reposition(&mut self, addr: u64) {
854 if let Some(block) = self.emu.block_at_address(&self.ctx, addr)
855 && block != self.emu.block
856 {
857 self.emu.block = block;
858 self.emu.idx = 0;
859 }
860 }
861
862 /// Lifts `addr` and makes it visible to the emulator. Returns an exit only
863 /// if the address could not be supplied.
864 fn discover(&mut self, addr: u64) -> Option<VmExit> {
865 // Moved out, updated in place by the lift, and moved back: the index
866 // stays current without ever being rebuilt, and the borrow checker is
867 // satisfied because nothing borrows the emulator across the lift.
868 let mut index = self
869 .emu
870 .take_address_index()
871 .unwrap_or_else(|| AddressIndex::analyze(&self.ctx));
872 self.stats.lifts += 1;
873 let result = self.source.lift(
874 &mut self.ctx,
875 &self.emu.memory,
876 &mut index,
877 addr,
878 &mut self.stats,
879 );
880 self.emu.set_address_index(index);
881 if let Err(error) = result {
882 return Some(VmExit::Unlifted { addr, error });
883 }
884 if self.optimize
885 && let Some(block) = self.emu.block_at_address(&self.ctx, addr)
886 {
887 // Forwarding first: it turns the temp round trips into direct value
888 // uses, which is what leaves the surrounding computation dead.
889 let started = std::time::Instant::now();
890 let cleanup = crate::optimize::forward_temp_stores(&mut self.ctx, block);
891 qcode_passes::remove_dead_insns(&mut self.ctx, block);
892 self.stats.optimize += started.elapsed();
893 self.stats.forwarded_loads += cleanup.forwarded_loads as u64;
894 self.stats.removed_stores += cleanup.removed_stores as u64;
895 }
896 self.absorbed_into = self.absorb_into_basic_block(addr);
897 None
898 }
899
900 /// Points every address `block` has absorbed back at `block`.
901 ///
902 /// Absorption deletes the blocks it takes in, and each of them was what the
903 /// index named for its address. Left alone the index hands out ids of
904 /// deleted blocks — and a run may absorb a whole chain, not just the block
905 /// that was being discovered, so every address the absorber now covers has
906 /// to be repointed, not only the one that prompted this.
907 fn reindex_absorbed(&mut self, block: BlockId) {
908 let covered = self.ctx.block(block).extra_addresses.clone();
909 if covered.is_empty() {
910 return;
911 }
912 let mut index = self
913 .emu
914 .take_address_index()
915 .unwrap_or_else(|| AddressIndex::analyze(&self.ctx));
916 for addr in covered {
917 index.set_block(addr, block);
918 }
919 self.emu.set_address_index(index);
920 }
921
922 /// Re-runs the block cleanup over a block that has just grown.
923 ///
924 /// The cleanup at discovery saw a single guest instruction, where every
925 /// register it writes is still live at the block's edge. Absorption puts a
926 /// whole run in one block, and that is the first point at which a write
927 /// nothing goes on to read is visible as dead: the flags an arithmetic
928 /// instruction sets, when the next instruction overwrites all of them
929 /// before the branch reads any.
930 ///
931 /// Deliberately block-local (no alias result): at discovery the rest of the
932 /// CFG is still unknown, so only a store this block itself overwrites can
933 /// be proven dead. Anything live at the exit stays.
934 /// Records that `block` has grown and owes a cleanup, cleaning whatever
935 /// run was growing before it.
936 fn mark_dirty(&mut self, block: BlockId) {
937 // Grown, so it holds guest instructions the injectors have not seen.
938 self.injected.remove(&block);
939 if !self.optimize {
940 return;
941 }
942 let previous = self.dirty.replace(block);
943 if let Some(previous) = previous
944 && previous != block
945 // Discovery retires blocks — splitting an absorbed run empties
946 // both halves — so the one that was growing may be gone.
947 && self.ctx.contains_block(previous)
948 {
949 self.reoptimize(previous);
950 }
951 }
952
953 /// Cleans the block the machine is about to run, if it has grown since it
954 /// was last cleaned.
955 ///
956 /// Only ever the block being entered, which is why a stale id cannot be
957 /// reached here: absorption and splitting retire blocks that may still be
958 /// listed, but the machine can only be about to run a live one. A leftover
959 /// entry for a retired id is harmless — at worst it cleans a block whose id
960 /// was reused, which is always safe.
961 fn clean_before_entering(&mut self, block: BlockId) {
962 if self.dirty == Some(block) {
963 self.dirty = None;
964 self.reoptimize(block);
965 }
966 }
967
968 fn reoptimize(&mut self, block: BlockId) {
969 if !self.optimize {
970 return;
971 }
972 let started = std::time::Instant::now();
973 let cleanup = crate::optimize::forward_temp_stores(&mut self.ctx, block);
974 qcode_passes::remove_dead_insns(&mut self.ctx, block);
975 self.stats.optimize += started.elapsed();
976 self.stats.forwarded_loads += cleanup.forwarded_loads as u64;
977 self.stats.removed_stores += cleanup.removed_stores as u64;
978 // The interpreter may hold this block's instruction list, and some of
979 // those instructions are gone.
980 self.emu.invalidate_block_cache();
981 }
982
983 /// Folds the block just lifted at `addr` into its predecessor, when the two
984 /// are a straight-line pair.
985 ///
986 /// Lifting is per guest instruction, so a run of straight-line guest code
987 /// arrives as a chain of one-instruction blocks joined by unconditional
988 /// branches. Left that way, every guest instruction is its own unit of
989 /// execution: a separate compilation, a separate entry into compiled code
990 /// and a separate return to the interpreter for its terminator. Folding the
991 /// chain as it is discovered rebuilds the guest's *basic block*, which is
992 /// the unit worth compiling.
993 ///
994 /// Absorbing an address does not settle that it belongs here — code
995 /// discovered later may branch into the middle of the run, and
996 /// [`Context::split_block_at_address`] breaks it apart again when it does.
997 fn absorb_into_basic_block(&mut self, addr: u64) -> Option<BlockId> {
998 let filled = self.emu.block_at_address(&self.ctx, addr)?;
999
1000 // Forward: the rest of this run may already be known. That is the shape
1001 // a split leaves behind — it re-establishes a block's *start* while
1002 // everything after it is still lifted — and the shape a back-edge into
1003 // the middle of a run creates generally.
1004 let forward = qcode_passes::absorb_straight_line(&mut self.ctx, filled);
1005 self.stats.absorbed += forward as u64;
1006 if forward > 0 {
1007 self.reindex_absorbed(filled);
1008 self.mark_dirty(filled);
1009 }
1010
1011 // Backward: the straight-line predecessor that branched here, for the
1012 // ordinary case of a run discovered one guest instruction at a time.
1013 //
1014 // Not if something branches to this address: it has to keep *starting*
1015 // a block, or the split that established that would be undone here.
1016 // Extending it forward, above, stays fine — that moves its end, not its
1017 // start.
1018 if self
1019 .emu
1020 .address_index()
1021 .is_some_and(|index| index.is_boundary(addr))
1022 {
1023 return (forward > 0).then_some(filled);
1024 }
1025 // Exactly one predecessor, or absorbing would strand the others.
1026 // Collected eagerly so the module is free to be mutated below.
1027 let preds: Vec<BlockId> = BasicBlock::from_id(&self.ctx, filled)
1028 .predecessors()
1029 .map(|(_, block)| block)
1030 .take(2)
1031 .collect();
1032 let [head] = preds[..] else {
1033 return None;
1034 };
1035 if head == filled {
1036 return None;
1037 }
1038 // Only into a block that starts at a machine address. Absorbing makes
1039 // the head responsible for the absorbed addresses, and a later branch
1040 // to one of them splits the head apart again — which works by emptying
1041 // it and lifting it afresh. A block with no address of its own (the
1042 // fallthrough arm of a branch *inside* one instruction's p-code) has
1043 // nowhere to be lifted from, so emptying it leaves a hole nothing can
1044 // fill.
1045 if self.ctx.block(head).address.is_none() {
1046 return (forward > 0).then_some(filled);
1047 }
1048 // Where `filled`'s instructions land: the head's own, less the
1049 // terminator that absorption drops.
1050 let offset = self
1051 .ctx
1052 .block(head)
1053 .instruction_ids()
1054 .len()
1055 .saturating_sub(1);
1056 if qcode_passes::absorb_straight_line(&mut self.ctx, head) == 0 {
1057 return (forward > 0).then_some(filled);
1058 }
1059 self.stats.absorbed += 1;
1060
1061 // What the head has already run: everything it held before the
1062 // absorbed instructions were appended. The machine resumes right
1063 // after the last of these still standing, whatever cleanup deletes
1064 // ahead of that point or injection inserts after it — an interrupt an
1065 // injector places before the first absorbed instruction, say, which
1066 // has to run before it.
1067 let executed: FxHashSet<LocalInsnId> = self.ctx.block(head).instruction_ids()[..offset]
1068 .iter()
1069 .copied()
1070 .collect();
1071 self.mark_dirty(head);
1072
1073 self.reindex_absorbed(head);
1074
1075 // The machine stopped at the empty placeholder this lift filled, which
1076 // absorption has just deleted; its instructions are in the head now.
1077 if self.emu.block == filled {
1078 // The absorbed instructions have not run, and the injectors have
1079 // not seen them: rewrite now, so a hook on the instruction about
1080 // to execute is not missed the first time.
1081 self.inject(head);
1082 let now = self.ctx.block(head).instruction_ids();
1083 let resumed = now
1084 .iter()
1085 .rposition(|local| executed.contains(local))
1086 .map_or(0, |last| last + 1);
1087 self.emu.block = head;
1088 self.emu.idx = resumed;
1089 self.emu.invalidate_block_cache();
1090 }
1091 Some(head)
1092 }
1093
1094 /// Runs until the machine stops, or until `budget` p-code operations have
1095 /// been retired.
1096 ///
1097 /// Hooks registered through the [table](crate::table) are called from
1098 /// here and the machine resumes past them, so the run only returns for
1099 /// an exit the caller has to see.
1100 pub fn run(&mut self, budget: u64) -> VmExit {
1101 let deadline = self.stats.steps + budget;
1102 while self.stats.steps < deadline {
1103 // Checked before the step so a breakpoint reports the instruction
1104 // about to run, not the one after it, and so resuming from a
1105 // breakpoint is possible without immediately re-triggering it.
1106 //
1107 // Guarded on there being any breakpoint at all: `pc()` resolves the
1108 // block through the module arena, and paying that on every step to
1109 // consult an empty set cost about 6% of run time.
1110 if !self.breakpoints.is_empty()
1111 && let Some(pc) = self.pc()
1112 && self.breakpoints.contains(&pc)
1113 && self.stats.steps > 0
1114 {
1115 return VmExit::Breakpoint(pc);
1116 }
1117 match self.step() {
1118 None => {}
1119 Some(VmExit::Interrupt(interrupt)) => {
1120 if let Some(exit) = self.dispatch(&interrupt) {
1121 return exit;
1122 }
1123 }
1124 Some(exit) => return exit,
1125 }
1126 }
1127 VmExit::InstructionLimit
1128 }
1129}
1130
1131/// Resolves a guest address to a block against an existing index.
1132fn resolve(ctx: &Context<'_>, index: &AddressIndex, addr: u64) -> Option<BlockId> {
1133 match index.get(addr) {
1134 Some(AddressTarget::Block(block)) => Some(block),
1135 Some(AddressTarget::Function(function)) => {
1136 qcode::value::FunctionBody::from_id(ctx, function)
1137 .root()
1138 .map(|root| root.id)
1139 }
1140 None => None,
1141 }
1142}
1143
1144#[cfg(test)]
1145mod tests {
1146 use super::*;
1147 use crate::mmu::{PAGE_SIZE, perm};
1148 use qcode::value::FunctionBody;
1149
1150 /// A source that hands over one pre-planned block per address, so the
1151 /// discovery path can be exercised without a decoder.
1152 #[derive(Default)]
1153 struct Planned {
1154 /// Addresses this source is willing to supply, and how many times it was
1155 /// actually asked.
1156 available: Vec<u64>,
1157 pub calls: Vec<u64>,
1158 }
1159
1160 impl CodeSource for Planned {
1161 fn lift(
1162 &mut self,
1163 ctx: &mut Context<'static>,
1164 memory: &VmMemory,
1165 index: &mut AddressIndex,
1166 addr: u64,
1167 _stats: &mut Stats,
1168 ) -> Result<(), CodeError> {
1169 self.calls.push(addr);
1170 // A real source fetches through the MMU, so executing unmapped or
1171 // non-executable memory faults at the fetch. Mirrored here.
1172 let mut byte = [0u8; 1];
1173 memory
1174 .mmu
1175 .read_code(addr, &mut byte)
1176 .map_err(CodeError::Fault)?;
1177 if !self.available.contains(&addr) {
1178 return Err(CodeError::Decode("no plan for this address".into()));
1179 }
1180 let function = FunctionBody::make_at_addr(ctx, addr, None).id;
1181 let block = BasicBlock::make(ctx, function).with_address(addr).id;
1182 index
1183 .register(ctx, addr, AddressTarget::Block(block))
1184 .map_err(|error| CodeError::Decode(format!("{error:?}").into()))?;
1185 Ok(())
1186 }
1187 }
1188
1189 /// A module with a single empty block at `addr`.
1190 fn module(addr: u64) -> (Context<'static>, BlockId) {
1191 let mut ctx = Context::new();
1192 let function = FunctionBody::make_at_addr(&mut ctx, addr, None).id;
1193 let block = BasicBlock::make(&mut ctx, function).with_address(addr).id;
1194 (ctx, block)
1195 }
1196
1197 fn executable_memory() -> VmMemory {
1198 let mut memory = VmMemory::new();
1199 memory.mmu.map(0x1000, PAGE_SIZE, perm::RX_INIT).unwrap();
1200 memory
1201 }
1202
1203 #[test]
1204 fn an_empty_addressed_block_asks_the_source_for_code() {
1205 let (ctx, block) = module(0x1000);
1206 let mut vm = Vm::new(ctx, block, Planned::default());
1207 // An empty block carrying an address is how the lifter represents a
1208 // branch target it has not reached yet, so it is a discovery request.
1209 // This source cannot supply it, which is what makes the exit reportable.
1210 let exit = vm.run(16);
1211 assert!(
1212 matches!(exit, VmExit::Unlifted { addr: 0x1000, .. }),
1213 "expected a discovery attempt, got {exit:?}"
1214 );
1215 assert_eq!(vm.source.calls, vec![0x1000]);
1216 }
1217
1218 #[test]
1219 fn an_empty_block_with_no_address_is_an_error() {
1220 // Nothing to discover: without an address there is no code to fetch.
1221 let mut ctx = Context::new();
1222 let function = FunctionBody::make_at_addr(&mut ctx, 0x1000, None).id;
1223 let block = BasicBlock::make(&mut ctx, function).id;
1224 let mut vm = Vm::new(ctx, block, Planned::default());
1225 assert!(matches!(vm.run(16), VmExit::Error(_)));
1226 }
1227
1228 #[test]
1229 fn pc_reports_the_block_about_to_run() {
1230 let (ctx, block) = module(0x1000);
1231 let vm = Vm::new(ctx, block, Planned::default());
1232 assert_eq!(vm.pc(), Some(0x1000));
1233 }
1234
1235 #[test]
1236 fn at_address_lifts_an_entry_the_module_lacks() {
1237 let ctx = Context::new();
1238 let source = Planned {
1239 available: vec![0x1000],
1240 calls: Vec::new(),
1241 };
1242 let vm = Vm::at_address(ctx, 0x1000, source, executable_memory())
1243 .expect("the source can supply this address");
1244 assert_eq!(vm.pc(), Some(0x1000));
1245 }
1246
1247 #[test]
1248 fn at_address_reuses_a_block_the_module_already_has() {
1249 let (ctx, _) = module(0x1000);
1250 let vm = Vm::at_address(ctx, 0x1000, Planned::default(), executable_memory())
1251 .expect("no lifting is needed");
1252 assert_eq!(vm.pc(), Some(0x1000));
1253 // The source was never consulted.
1254 assert!(vm.source.calls.is_empty());
1255 }
1256
1257 #[test]
1258 fn fetching_from_non_executable_memory_reports_the_fault() {
1259 let ctx = Context::new();
1260 let mut memory = VmMemory::new();
1261 memory.mmu.map(0x1000, PAGE_SIZE, perm::RW_INIT).unwrap();
1262 let source = Planned {
1263 available: vec![0x1000],
1264 calls: Vec::new(),
1265 };
1266 let error = Vm::at_address(ctx, 0x1000, source, memory)
1267 .err()
1268 .expect("the page is not executable");
1269 assert!(matches!(
1270 error,
1271 CodeError::Fault(MemFault {
1272 kind: crate::mmu::FaultKind::ExecViolation,
1273 addr: 0x1000
1274 })
1275 ));
1276 }
1277
1278 #[test]
1279 fn an_unsuppliable_address_reports_where_it_stopped() {
1280 let ctx = Context::new();
1281 let error = Vm::at_address(ctx, 0x2000, Planned::default(), executable_memory())
1282 .err()
1283 .expect("nothing is mapped or planned at 0x2000");
1284 assert!(matches!(error, CodeError::Fault(_)));
1285 }
1286
1287 #[test]
1288 fn breakpoints_are_recorded_and_removable() {
1289 let (ctx, block) = module(0x1000);
1290 let mut vm = Vm::new(ctx, block, Planned::default());
1291 assert!(vm.add_breakpoint(0x2000));
1292 assert!(!vm.add_breakpoint(0x2000));
1293 assert!(vm.remove_breakpoint(0x2000));
1294 assert!(!vm.remove_breakpoint(0x2000));
1295 }
1296
1297 #[test]
1298 fn memory_is_reachable_and_backed_by_the_mmu() {
1299 let (ctx, block) = module(0x1000);
1300 let mut vm = Vm::new(ctx, block, Planned::default());
1301 vm.memory_mut()
1302 .mmu
1303 .map(0x4000, PAGE_SIZE, perm::RW_INIT)
1304 .unwrap();
1305 vm.memory_mut().mmu.write(0x4000, &[1, 2, 3]).unwrap();
1306 let mut out = [0; 3];
1307 vm.memory().mmu.read(0x4000, &mut out).unwrap();
1308 assert_eq!(out, [1, 2, 3]);
1309 }
1310
1311 /// A block whose body is one user op, followed by a branch to an empty
1312 /// block at the next address, so continuing past the op is observable as
1313 /// a discovery request for that address.
1314 fn module_with_op(
1315 op_name: &str,
1316 args: Vec<u64>,
1317 size: usize,
1318 ) -> (Context<'static>, BlockId, InstructionId) {
1319 let (mut ctx, block) = module(0x1000);
1320 let op = ctx.shared.pcode_op(op_name);
1321 let target = BasicBlock::make(&mut ctx, block.func)
1322 .with_address(0x1001)
1323 .id;
1324 let args = args
1325 .into_iter()
1326 .map(|value| ctx.shared.get_const(value, 8))
1327 .collect();
1328 let insn = {
1329 let mut builder = ctx.builder(block);
1330 let insn = builder.push_pcode_op(op, args, None, size).id;
1331 builder.finalize(target);
1332 insn
1333 };
1334 (ctx, block, insn)
1335 }
1336
1337 #[test]
1338 fn an_explicit_interrupt_stops_at_the_op_and_reports_its_operands() {
1339 let (ctx, block, insn) = module_with_op(VM_INTERRUPT, vec![7, 99], 8);
1340 let mut vm = Vm::new(ctx, block, Planned::default());
1341 let exit = vm.run(16);
1342 let VmExit::Interrupt(interrupt) = exit else {
1343 panic!("expected an interrupt, got {exit:?}");
1344 };
1345 assert_eq!(interrupt.kind, InterruptKind::Explicit { code: 7 });
1346 assert_eq!(interrupt.args, vec![Some(99)]);
1347 assert_eq!(interrupt.insn, insn);
1348 assert_eq!(interrupt.size, 8);
1349 assert_eq!(interrupt.pc, Some(0x1000));
1350 // The machine is at the op, not past it.
1351 assert_eq!(vm.emulator().idx, 0);
1352 assert_eq!(vm.pending_interrupt(), Some(&interrupt));
1353 }
1354
1355 #[test]
1356 fn running_again_without_resuming_reports_the_same_interrupt() {
1357 let (ctx, block, _) = module_with_op(VM_INTERRUPT, vec![1], 0);
1358 let mut vm = Vm::new(ctx, block, Planned::default());
1359 let first = vm.run(16);
1360 let again = vm.run(16);
1361 assert!(
1362 matches!(&first, VmExit::Interrupt(i) if i.kind == InterruptKind::Explicit { code: 1 })
1363 );
1364 assert!(
1365 matches!(&again, VmExit::Interrupt(i) if i.kind == InterruptKind::Explicit { code: 1 })
1366 );
1367 assert_eq!(vm.emulator().idx, 0);
1368 }
1369
1370 #[test]
1371 fn resume_files_the_result_and_continues_after_the_op() {
1372 let (ctx, block, insn) = module_with_op(VM_INTERRUPT, vec![7], 8);
1373 let mut vm = Vm::new(ctx, block, Planned::default());
1374 assert!(matches!(vm.run(16), VmExit::Interrupt(_)));
1375 // The op declares an 8-byte result, so resuming needs one.
1376 assert_eq!(
1377 vm.resume(None),
1378 Err(ResumeError::ResultRequired { size: 8 })
1379 );
1380 vm.resume(Some(42)).unwrap();
1381 assert_eq!(vm.pending_interrupt(), None);
1382 assert_eq!(
1383 vm.emulator().insn_values.get(&insn).map(|v| v.as_bits()),
1384 Some(42)
1385 );
1386 // Past the op, the branch runs and reaches the next address.
1387 assert!(matches!(vm.run(16), VmExit::Unlifted { addr: 0x1001, .. }));
1388 }
1389
1390 #[test]
1391 fn resume_needs_an_interrupt() {
1392 let (ctx, block) = module(0x1000);
1393 let mut vm = Vm::new(ctx, block, Planned::default());
1394 assert_eq!(vm.resume(None), Err(ResumeError::NotInterrupted));
1395 }
1396
1397 #[test]
1398 fn an_unmodelled_user_op_is_an_intrinsic_interrupt() {
1399 let (ctx, block, _) = module_with_op("rdpmc", vec![], 0);
1400 let mut vm = Vm::new(ctx, block, Planned::default());
1401 let exit = vm.run(16);
1402 let VmExit::Interrupt(interrupt) = exit else {
1403 panic!("expected an interrupt, got {exit:?}");
1404 };
1405 assert!(
1406 matches!(&interrupt.kind, InterruptKind::Intrinsic { name, .. } if name.as_ref() == "rdpmc")
1407 );
1408 assert_eq!(interrupt.size, 0);
1409 // Nothing to supply for an op with no result.
1410 vm.resume(None).unwrap();
1411 assert!(matches!(vm.run(16), VmExit::Unlifted { addr: 0x1001, .. }));
1412 }
1413}