use std::collections::HashMap;
use crate::arch::x86::{
DecodedInsn, Instruction, InstructionInfoFactory, OpAccess, OpKind, Register,
};
use crate::ir::Function;
pub use crate::ir::ssa::{DefSite, Liveness, SsaInfo, Var};
#[must_use]
pub fn compute_liveness(f: &Function<DecodedInsn>, sp_delta_at: &HashMap<u64, i64>) -> Liveness {
crate::ir::ssa::compute_liveness(f, |i| insn_reads_writes(i, sp_delta_at))
}
#[must_use]
pub fn build_ssa(f: &Function<DecodedInsn>, sp_delta_at: &HashMap<u64, i64>) -> SsaInfo {
crate::ir::ssa::build_ssa(f, |i| insn_reads_writes(i, sp_delta_at))
}
pub fn insn_reads_writes(
insn: &DecodedInsn,
sp_delta_at: &HashMap<u64, i64>,
) -> (Vec<Var>, Vec<Var>) {
let mut reads: Vec<Var> = Vec::new();
let mut writes: Vec<Var> = Vec::new();
let i = &insn.iced;
let sp = sp_delta_at.get(&i.ip()).copied();
let mut factory = InstructionInfoFactory::new();
let info = factory.info(i);
for used in info.used_registers() {
let Some(name) = canonical_reg_name(used.register()) else {
continue;
};
let var = Var::Reg(name);
match used.access() {
OpAccess::Read | OpAccess::CondRead => push_unique(&mut reads, var),
OpAccess::Write | OpAccess::CondWrite => push_unique(&mut writes, var),
OpAccess::ReadWrite | OpAccess::ReadCondWrite => {
push_unique(&mut reads, var.clone());
push_unique(&mut writes, var);
}
_ => {}
}
}
for op in 0..i.op_count() {
if i.op_kind(op) == OpKind::Memory {
if i.mnemonic() == crate::arch::x86::Mnemonic::Lea {
continue;
}
let mem_var = memory_var(i, sp);
if op == 0 && is_write_to_op0(i.mnemonic()) {
push_unique(&mut writes, mem_var.clone());
}
if is_read_of_op(i.mnemonic(), op) {
push_unique(&mut reads, mem_var);
}
}
}
(reads, writes)
}
fn is_write_to_op0(m: crate::arch::x86::Mnemonic) -> bool {
use crate::arch::x86::Mnemonic;
!matches!(
m,
Mnemonic::Cmp
| Mnemonic::Test
| Mnemonic::Push
| Mnemonic::Jmp
| Mnemonic::Call
| Mnemonic::Bt
)
}
fn is_read_of_op(m: crate::arch::x86::Mnemonic, op: u32) -> bool {
use crate::arch::x86::Mnemonic;
if op > 0 {
return true; }
matches!(
m,
Mnemonic::Cmp
| Mnemonic::Test
| Mnemonic::Push
| Mnemonic::Jmp
| Mnemonic::Call
| Mnemonic::Bt
| Mnemonic::Add
| Mnemonic::Sub
| Mnemonic::And
| Mnemonic::Or
| Mnemonic::Xor
| Mnemonic::Adc
| Mnemonic::Sbb
| Mnemonic::Inc
| Mnemonic::Dec
| Mnemonic::Neg
| Mnemonic::Not
| Mnemonic::Shl
| Mnemonic::Shr
| Mnemonic::Sar
| Mnemonic::Rol
| Mnemonic::Ror
| Mnemonic::Sal
| Mnemonic::Rcl
| Mnemonic::Rcr
| Mnemonic::Xadd
| Mnemonic::Xchg
| Mnemonic::Cmpxchg
)
}
fn push_unique(v: &mut Vec<Var>, x: Var) {
if !v.contains(&x) {
v.push(x);
}
}
#[must_use]
pub fn canonical_reg_name(reg: Register) -> Option<String> {
let full = reg.full_register();
let name = match full {
Register::EAX | Register::RAX => "eax",
Register::EBX | Register::RBX => "ebx",
Register::ECX | Register::RCX => "ecx",
Register::EDX | Register::RDX => "edx",
Register::ESI | Register::RSI => "esi",
Register::EDI | Register::RDI => "edi",
Register::EBP | Register::RBP => "ebp",
Register::ESP | Register::RSP => "esp",
Register::R8 => "r8",
Register::R9 => "r9",
Register::R10 => "r10",
Register::R11 => "r11",
Register::R12 => "r12",
Register::R13 => "r13",
Register::R14 => "r14",
Register::R15 => "r15",
_ => return None,
};
Some(name.to_string())
}
fn memory_var(i: &Instruction, sp: Option<i64>) -> Var {
let base = i.memory_base();
let has_index = i.memory_index() != Register::None;
if (base == Register::EBP || base == Register::RBP) && !has_index {
#[allow(clippy::cast_possible_wrap)]
let disp = i.memory_displacement64() as i64;
return Var::Stack(disp);
}
if (base == Register::ESP || base == Register::RSP)
&& !has_index
&& let Some(delta) = sp
{
#[allow(clippy::cast_possible_wrap)]
let disp = i.memory_displacement64() as i64;
return Var::Stack(disp.wrapping_add(delta));
}
Var::Memory
}
#[cfg(test)]
mod tests {
use super::*;
use crate::arch::x86::lift_function;
use crate::arch::x86::{Bitness, decode};
fn lift(bytes: &[u8]) -> Function<DecodedInsn> {
let insns = decode(Bitness::Bits32, bytes, 0x1000).unwrap();
lift_function("test".into(), &insns).unwrap()
}
#[test]
fn linear_two_defs_keep_separate_versions() {
let bytes = [
0xb8, 0x01, 0x00, 0x00, 0x00, 0xb8, 0x02, 0x00, 0x00, 0x00, 0xc3, ];
let f = lift(&bytes);
let sp: HashMap<u64, i64> = HashMap::new();
let ssa = build_ssa(&f, &sp);
let eax = Var::Reg("eax".into());
let eax_defs: Vec<_> = ssa
.defs
.iter()
.enumerate()
.filter(|(_, r)| r.var == eax)
.collect();
assert!(
eax_defs.len() >= 2,
"expected ≥2 eax defs (entry + 2 mov writes), got {}",
eax_defs.len()
);
let ret_ip = 0x1000 + 10;
if let Some(reaching) = ssa.use_at.get(&(ret_ip, eax.clone())) {
let site = &ssa.defs[reaching.0 as usize].site;
assert!(
matches!(site, DefSite::Insn(ip) if *ip == 0x1005),
"ret's eax read should reach the 2nd mov (ip=0x1005), got {site:?}"
);
}
}
#[test]
fn liveness_kills_overwritten_register() {
let bytes = [
0xb8, 0x01, 0x00, 0x00, 0x00, 0xb8, 0x02, 0x00, 0x00, 0x00, 0x89, 0xc3, 0xc3, ];
let f = lift(&bytes);
let sp: HashMap<u64, i64> = HashMap::new();
let live = compute_liveness(&f, &sp);
let eax = Var::Reg("eax".into());
let after_first = live.live_after_insn.get(&0x1000).expect("after first mov");
assert!(
!after_first.contains(&eax),
"eax should be DEAD after first mov: {after_first:?}"
);
let after_second = live.live_after_insn.get(&0x1005).expect("after second mov");
assert!(
after_second.contains(&eax),
"eax should be LIVE after second mov (next reads it): {after_second:?}"
);
}
#[test]
fn diamond_cfg_places_phi_at_merge() {
let bytes = [
0x83, 0xf9, 0x00, 0x74, 0x06, 0xb8, 0x01, 0x00, 0x00, 0x00, 0xeb, 0x05, 0xb8, 0x02, 0x00, 0x00, 0x00, 0xc3, ];
let f = lift(&bytes);
assert!(
f.blocks.len() >= 3,
"expected diamond CFG, got {} blocks",
f.blocks.len()
);
let sp: HashMap<u64, i64> = HashMap::new();
let ssa = build_ssa(&f, &sp);
let eax = Var::Reg("eax".into());
let has_phi = ssa
.defs
.iter()
.any(|r| r.var == eax && matches!(r.site, DefSite::Phi { .. }));
assert!(has_phi, "expected a phi for eax at the diamond merge");
}
}