use qcode::{
context::Context,
space::MemorySpaceId,
value::{
BasicBlock, BlockId, InstructionId, ValueId,
insn::{Binop, IntBinop, Mnemonic, Store, VM_INTERRUPT},
},
};
use rustc_hash::FxHashSet;
use crate::inject::CodeInjector;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Site {
BlockEntry { address: u64, anchor: InstructionId },
Address { address: u64, anchor: InstructionId },
Store { insn: InstructionId },
Load { insn: InstructionId },
Compare { insn: InstructionId },
}
impl Site {
pub fn anchor(&self) -> InstructionId {
match self {
Self::BlockEntry { anchor, .. } | Self::Address { anchor, .. } => *anchor,
Self::Store { insn } | Self::Load { insn } | Self::Compare { insn } => *insn,
}
}
}
pub struct BlockView<'a> {
pub ctx: &'a Context<'static>,
pub block: BlockId,
}
impl BlockView<'_> {
pub fn address(&self) -> Option<u64> {
BasicBlock::from_id(self.ctx, self.block).address()
}
pub fn entry(&self) -> Option<Site> {
let address = self.address()?;
let anchor = BasicBlock::from_id(self.ctx, self.block)
.instructions()
.find(|insn| !is_interrupt_op(self.ctx, insn.id))
.map(|insn| insn.id)?;
Some(Site::BlockEntry { address, anchor })
}
pub fn addresses(&self) -> Vec<Site> {
let block = BasicBlock::from_id(self.ctx, self.block);
let continued = if block.address().is_none() {
block
.predecessors()
.filter_map(|(_, pred)| {
BasicBlock::from_id(self.ctx, pred)
.instructions()
.last()
.and_then(|insn| insn.address())
})
.collect::<Vec<u64>>()
} else {
Vec::new()
};
let mut sites = Vec::new();
let mut seen = None;
for insn in block.instructions() {
if is_interrupt_op(self.ctx, insn.id) {
continue;
}
let at = insn.address();
if at.is_some() && at != seen {
let first_run = seen.is_none();
seen = at;
let address = at.unwrap_or_default();
if first_run && continued.contains(&address) {
continue;
}
sites.push(Site::Address {
address,
anchor: insn.id,
});
}
}
sites
}
pub fn at(&self, address: u64) -> Option<Site> {
self.addresses()
.into_iter()
.find(|site| matches!(site, Site::Address { address: at, .. } if *at == address))
}
fn is_ram(&self, space: qcode::space::LocalMemorySpaceId) -> bool {
space.qualify(self.block.func) == MemorySpaceId::Shared(self.ctx.shared.default_space)
}
pub fn stores(&self) -> Vec<Site> {
BasicBlock::from_id(self.ctx, self.block)
.instructions()
.filter(|insn| matches!(insn.mnemonic(), Mnemonic::Store(store) if self.is_ram(store.space)))
.map(|insn| Site::Store { insn: insn.id })
.collect()
}
pub fn loads(&self) -> Vec<Site> {
BasicBlock::from_id(self.ctx, self.block)
.instructions()
.filter(
|insn| matches!(insn.mnemonic(), Mnemonic::Load(load) if self.is_ram(load.space)),
)
.map(|insn| Site::Load { insn: insn.id })
.collect()
}
pub fn compares(&self) -> Vec<Site> {
BasicBlock::from_id(self.ctx, self.block)
.instructions()
.filter(|insn| {
matches!(insn.mnemonic(), Mnemonic::Binop(binary) if binary.op.is_comparison()
&& matches!(binary.op, Binop::Int(_)))
})
.map(|insn| Site::Compare { insn: insn.id })
.collect()
}
}
pub trait Hook {
fn sites(&mut self, block: &BlockView<'_>) -> Vec<Site>;
fn instrument(&mut self, site: &Site, emit: &mut Emitter<'_>);
}
pub struct Emitter<'a> {
ctx: &'a mut Context<'static>,
block: BlockId,
anchor: InstructionId,
address: Option<u64>,
}
impl<'a> Emitter<'a> {
pub fn new(ctx: &'a mut Context<'static>, site: &Site) -> Self {
let anchor = site.anchor();
let block = qcode::value::Instruction::from_id(ctx, anchor)
.parent()
.map(|block| block.id)
.expect("a site's anchor is in a block");
let address = match site {
Site::BlockEntry { address, .. } | Site::Address { address, .. } => Some(*address),
_ => qcode::value::Instruction::from_id(ctx, anchor).address(),
};
Self {
ctx,
block,
anchor,
address,
}
}
pub fn ctx(&self) -> &Context<'static> {
self.ctx
}
pub fn address(&self) -> Option<u64> {
self.address
}
pub fn constant(&self, value: u64, size: usize) -> ValueId {
self.ctx.shared.get_const(value, size)
}
pub fn size_of(&self, value: ValueId) -> usize {
self.ctx
.stored_type_of(value)
.map(|ty| self.ctx.shared.types.size_of(ty))
.unwrap_or(0)
}
pub fn store_operands(&self) -> Option<(ValueId, usize, ValueId)> {
let insn = self.ctx.instruction(self.anchor);
let Mnemonic::Store(Store { ptr, size, src, .. }) = insn.mnemonic() else {
return None;
};
let func = self.anchor.func;
Some((ptr.qualify(func), *size, src.qualify(func)))
}
pub fn load_operands(&self) -> Option<(ValueId, usize)> {
let insn = self.ctx.instruction(self.anchor);
let Mnemonic::Load(load) = insn.mnemonic() else {
return None;
};
Some((load.ptr.qualify(self.anchor.func), load.size))
}
pub fn binop_operands(&self) -> Option<(Binop, ValueId, ValueId)> {
let insn = self.ctx.instruction(self.anchor);
let Mnemonic::Binop(binary) = insn.mnemonic() else {
return None;
};
let func = self.anchor.func;
Some((
binary.op,
binary.lhs.qualify(func),
binary.rhs.qualify(func),
))
}
pub fn binop(&mut self, op: IntBinop, lhs: ValueId, rhs: ValueId) -> ValueId {
let (block, anchor) = (self.block, self.anchor);
let mut builder = self.ctx.builder(block);
builder.set_insert_point_before(anchor);
builder.push_binop(Binop::Int(op), lhs, rhs).id()
}
pub fn zext(&mut self, value: ValueId, size: usize) -> ValueId {
if self.size_of(value) == size {
return value;
}
let (block, anchor) = (self.block, self.anchor);
let mut builder = self.ctx.builder(block);
builder.set_insert_point_before(anchor);
builder.push_zext(value, size).id()
}
pub fn in_range(&mut self, value: ValueId, begin: u64, end: u64) -> ValueId {
let value = self.zext(value, 8);
let offset = self.binop(IntBinop::Sub, value, self.constant(begin, 8));
let length = self.constant(end.wrapping_sub(begin).wrapping_add(1), 8);
self.binop(IntBinop::Less, offset, length)
}
pub fn interrupt(&mut self, code: u64, args: &[ValueId]) -> InstructionId {
let (block, anchor, address) = (self.block, self.anchor, self.address);
crate::inject::insert_interrupt(self.ctx, block, Some(anchor), address, code, args)
}
pub fn interrupt_if(&mut self, cond: ValueId, code: u64, args: &[ValueId]) -> InstructionId {
let (block, anchor, address) = (self.block, self.anchor, self.address);
let func = block.func;
let rest = self.ctx.split_block_before(block, anchor);
let hook = self.ctx.body_mut(func).make_block();
let interrupt = crate::inject::insert_interrupt(self.ctx, hook, None, address, code, args);
{
let mut builder = self.ctx.builder(hook);
if let Some(address) = address {
builder.set_address(address);
}
builder.finalize(rest);
}
{
let mut builder = self.ctx.builder(block);
builder.push_cbranch(cond, hook, rest);
}
self.block = rest;
interrupt
}
}
pub struct HookInjector<H> {
pub hook: H,
done: FxHashSet<InstructionId>,
}
impl<H: Hook> HookInjector<H> {
pub fn new(hook: H) -> Self {
Self {
hook,
done: FxHashSet::default(),
}
}
}
impl<H: Hook> CodeInjector for HookInjector<H> {
fn inject(&mut self, ctx: &mut Context<'static>, block: BlockId) {
let sites = self.hook.sites(&BlockView { ctx, block });
for site in sites {
if !self.done.insert(site.anchor()) {
continue;
}
let mut emit = Emitter::new(ctx, &site);
self.hook.instrument(&site, &mut emit);
}
}
}
pub fn is_interrupt_op(ctx: &Context<'static>, insn: InstructionId) -> bool {
match ctx.instruction(insn).mnemonic() {
Mnemonic::PCodeOp(op) => ctx.shared.pcode_ops[op.id].as_ref() == VM_INTERRUPT,
_ => false,
}
}
fn in_range(begin: u64, end: u64, addr: u64) -> bool {
begin > end || (begin..=end).contains(&addr)
}
#[derive(Debug, Clone)]
pub struct BlockEntryHook {
pub begin: u64,
pub end: u64,
pub code: u64,
}
impl Hook for BlockEntryHook {
fn sites(&mut self, block: &BlockView<'_>) -> Vec<Site> {
block
.entry()
.filter(|site| matches!(site, Site::BlockEntry { address, .. } if in_range(self.begin, self.end, *address)))
.into_iter()
.collect()
}
fn instrument(&mut self, _site: &Site, emit: &mut Emitter<'_>) {
let address = emit.constant(emit.address().unwrap_or_default(), 8);
emit.interrupt(self.code, &[address]);
}
}
#[derive(Debug, Clone)]
pub struct AddressHook {
pub addresses: FxHashSet<u64>,
pub code: u64,
}
impl AddressHook {
pub fn new(addresses: impl IntoIterator<Item = u64>, code: u64) -> Self {
Self {
addresses: addresses.into_iter().collect(),
code,
}
}
}
impl Hook for AddressHook {
fn sites(&mut self, block: &BlockView<'_>) -> Vec<Site> {
block
.addresses()
.into_iter()
.filter(|site| matches!(site, Site::Address { address, .. } if self.addresses.contains(address)))
.collect()
}
fn instrument(&mut self, _site: &Site, emit: &mut Emitter<'_>) {
let address = emit.constant(emit.address().unwrap_or_default(), 8);
emit.interrupt(self.code, &[address]);
}
}
#[derive(Debug, Clone)]
pub struct WriteWatch {
pub begin: u64,
pub end: u64,
pub code: u64,
}
impl Hook for WriteWatch {
fn sites(&mut self, block: &BlockView<'_>) -> Vec<Site> {
block.stores()
}
fn instrument(&mut self, _site: &Site, emit: &mut Emitter<'_>) {
let Some((ptr, size, value)) = emit.store_operands() else {
return;
};
let cond = emit.in_range(ptr, self.begin, self.end);
let mut args = vec![emit.zext(ptr, 8), emit.constant(size as u64, 8)];
if size <= 8 {
args.push(emit.zext(value, 8));
}
emit.interrupt_if(cond, self.code, &args);
}
}
#[derive(Debug, Clone)]
pub struct CompareHook {
pub code: u64,
}
impl Hook for CompareHook {
fn sites(&mut self, block: &BlockView<'_>) -> Vec<Site> {
block.compares()
}
fn instrument(&mut self, _site: &Site, emit: &mut Emitter<'_>) {
let Some((_, lhs, rhs)) = emit.binop_operands() else {
return;
};
emit.interrupt(self.code, &[lhs, rhs]);
}
}