Skip to main content

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}