qcode_vm/inject.rs
1//! Rewriting lifted code before it runs: the mechanism every hook is built on.
2//!
3//! An injector is handed each block once it has been lifted and cleaned, and
4//! again whenever the block grows, and may edit it through the ordinary
5//! builder: insert a [`VM_INTERRUPT`] where the host wants control, add loads
6//! and stores that count or log, rewrite an operation. The interpreter and
7//! the JIT both run whatever the block then contains, so instrumentation is
8//! compiled along with the code it instruments and costs nothing where none
9//! was injected.
10//!
11//! # Idempotence
12//!
13//! A block is offered to an injector more than once: at discovery, after
14//! absorption has folded more guest instructions into it, and after a new
15//! injector is registered. The injector must therefore recognise its own
16//! earlier edits and not repeat them. [`is_interrupt`] is what the built-in
17//! injectors use for that.
18//!
19//! Most hooks are better written against [`crate::hook`], which chooses
20//! sites, hides the builder and handles idempotence; this is the layer
21//! underneath it.
22
23use qcode::{
24 context::Context,
25 value::{
26 BasicBlock, BlockId, InstructionId, ValueId, ValueRef,
27 insn::{Mnemonic, VM_INTERRUPT},
28 },
29};
30
31/// Edits a block before it runs. See the [module documentation](self).
32pub trait CodeInjector {
33 /// Rewrites `block`, which is lifted, cleaned and about to be entered.
34 /// Must leave the block terminated, and must be idempotent.
35 fn inject(&mut self, ctx: &mut Context<'static>, block: BlockId);
36}
37
38/// The first instruction of `block` lifted from guest address `addr`, if the
39/// block covers it.
40pub fn instruction_at_address(
41 ctx: &Context<'static>,
42 block: BlockId,
43 addr: u64,
44) -> Option<InstructionId> {
45 BasicBlock::from_id(ctx, block)
46 .instructions()
47 .find(|insn| insn.address() == Some(addr))
48 .map(|insn| insn.id)
49}
50
51/// Whether `insn` is a [`VM_INTERRUPT`] whose first operand is the literal
52/// `code` and, when `arg` is given, whose second is the literal `arg`.
53pub fn is_interrupt(
54 ctx: &Context<'static>,
55 insn: InstructionId,
56 code: u64,
57 arg: Option<u64>,
58) -> bool {
59 let insn = qcode::value::Instruction::from_id(ctx, insn);
60 let Mnemonic::PCodeOp(op) = insn.mnemonic() else {
61 return false;
62 };
63 if ctx.shared.pcode_ops[op.id].as_ref() != VM_INTERRUPT {
64 return false;
65 }
66 let literal = |index: usize| -> Option<u64> {
67 let value = op.args.get(index)?.qualify(insn.id.func);
68 match ValueRef::new(value, ctx) {
69 ValueRef::Literal(literal) => Some(literal.value()),
70 _ => None,
71 }
72 };
73 literal(0) == Some(code) && arg.is_none_or(|arg| literal(1) == Some(arg))
74}
75
76/// Inserts `vm.interrupt(code, args...)` into `block`, before `before` or at
77/// the block's start when `before` is `None`. The op declares no result and
78/// is stamped with guest address `address`, which is what the interrupt then
79/// reports as its `pc`. `args` are any values visible at that point.
80pub fn insert_interrupt(
81 ctx: &mut Context<'static>,
82 block: BlockId,
83 before: Option<InstructionId>,
84 address: Option<u64>,
85 code: u64,
86 args: &[ValueId],
87) -> InstructionId {
88 let op = ctx.shared.vm_interrupt_op();
89 let operands: Vec<ValueId> = std::iter::once(ctx.shared.get_const(code, 8))
90 .chain(args.iter().copied())
91 .collect();
92 let mut builder = ctx.builder(block);
93 match before {
94 Some(before) => builder.set_insert_point_before(before),
95 None => builder.set_insert_point_to_start(),
96 }
97 if let Some(address) = address {
98 builder.set_address(address);
99 }
100 builder.push_pcode_op(op, operands, None, 0).id
101}