use std::collections::HashMap;
use iced_x86::{FlowControl, Instruction, Mnemonic, OpKind, Register};
use crate::DecodedInsn;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArgValue {
Const(i64),
Lea { addr: u64 },
GlobalLoad { addr: u64 },
StackLoad { displacement: i64 },
PrevCallResult,
Reg(String),
Raw(String),
}
#[derive(Debug, Clone)]
pub struct CallSite {
pub call_target: u64,
pub args: Vec<ArgValue>,
pub setup_start: usize,
pub call_idx: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PostCallSpill {
pub displacement: i64,
pub insns_consumed: usize,
}
#[must_use]
pub fn detect_post_call_spill(insns: &[DecodedInsn], after_idx: usize) -> Option<PostCallSpill> {
let first = insns.get(after_idx)?;
let m = first.iced.mnemonic();
if m == Mnemonic::Movq && is_movq_rax_from_xmm0(&first.iced) {
let second = insns.get(after_idx + 1)?;
let disp = parse_rbp_store(&second.iced, &[Register::RAX])?;
return Some(PostCallSpill {
displacement: disp,
insns_consumed: 2,
});
}
if m == Mnemonic::Mov {
let disp = parse_rbp_store(&first.iced, &[Register::RAX, Register::EAX])?;
return Some(PostCallSpill {
displacement: disp,
insns_consumed: 1,
});
}
None
}
fn is_movq_rax_from_xmm0(insn: &Instruction) -> bool {
insn.op_count() == 2
&& insn.op0_kind() == OpKind::Register
&& insn.op0_register() == Register::RAX
&& insn.op1_kind() == OpKind::Register
&& insn.op1_register() == Register::XMM0
}
fn parse_rbp_store(insn: &Instruction, allowed_src: &[Register]) -> Option<i64> {
if insn.op_count() != 2 {
return None;
}
if insn.op0_kind() != OpKind::Memory
|| insn.memory_base() != Register::RBP
|| insn.memory_index() != Register::None
{
return None;
}
if insn.op1_kind() != OpKind::Register {
return None;
}
if !allowed_src.contains(&insn.op1_register()) {
return None;
}
#[allow(clippy::cast_possible_wrap)]
Some(insn.memory_displacement64() as i64)
}
#[must_use]
pub fn identify_call_sites(insns: &[DecodedInsn]) -> Vec<CallSite> {
let mut analyzer = Analyzer::default();
let mut out = Vec::new();
for (i, insn) in insns.iter().enumerate() {
analyzer.step(i, &insn.iced, &mut out);
}
out
}
#[derive(Default)]
struct Analyzer {
regs: HashMap<Register, ArgValue>,
stack_args: HashMap<i32, ArgValue>,
fpu_top: Option<ArgValue>,
setup_start: usize,
saw_any_call: bool,
push_chain: Vec<ArgValue>,
}
impl Analyzer {
fn step(&mut self, idx: usize, insn: &Instruction, out: &mut Vec<CallSite>) {
match insn.mnemonic() {
Mnemonic::Call => self.handle_call(idx, insn, out),
Mnemonic::Mov
| Mnemonic::Movq
| Mnemonic::Movd
| Mnemonic::Movss
| Mnemonic::Movsd
| Mnemonic::Movaps
| Mnemonic::Movapd => {
if !self.handle_mov(insn) {
self.break_window(idx + 1);
}
}
Mnemonic::Lea => {
if !self.handle_lea(insn) {
self.break_window(idx + 1);
}
}
Mnemonic::Fld => {
if !self.handle_fld(insn) {
self.break_window(idx + 1);
}
}
Mnemonic::Fstp => {
if !self.handle_fstp(insn) {
self.break_window(idx + 1);
}
}
Mnemonic::Nop | Mnemonic::Endbr64 | Mnemonic::Endbr32 => {
}
Mnemonic::Push => {
self.handle_push(insn);
}
_ => self.break_window(idx + 1),
}
}
fn handle_call(&mut self, idx: usize, insn: &Instruction, out: &mut Vec<CallSite>) {
if insn.flow_control() != FlowControl::Call {
self.break_window(idx + 1);
return;
}
let target = insn.near_branch_target();
if target == 0 {
self.break_window(idx + 1);
return;
}
let int_arg_regs = [
Register::RDI,
Register::RSI,
Register::RDX,
Register::RCX,
Register::R8,
Register::R9,
];
let xmm_arg_regs = [
Register::XMM0,
Register::XMM1,
Register::XMM2,
Register::XMM3,
Register::XMM4,
Register::XMM5,
Register::XMM6,
Register::XMM7,
];
let mut args = Vec::new();
for r in int_arg_regs {
match self.regs.get(&full_reg(r)).cloned() {
Some(v) => args.push(v),
None => break,
}
}
if args.is_empty() && !self.push_chain.is_empty() {
let mut chain = std::mem::take(&mut self.push_chain);
chain.reverse();
args.extend(chain);
}
if args.is_empty() && !self.stack_args.is_empty() {
let mut offsets: Vec<i32> = self.stack_args.keys().copied().collect();
offsets.sort_unstable();
for off in offsets {
if off < 0 {
continue;
}
if let Some(v) = self.stack_args.get(&off).cloned() {
args.push(v);
}
}
}
for r in xmm_arg_regs {
match self.regs.get(&full_reg(r)).cloned() {
Some(v) => args.push(v),
None => break,
}
}
out.push(CallSite {
call_target: target,
args,
setup_start: self.setup_start,
call_idx: idx,
});
self.regs.clear();
self.regs.insert(Register::RAX, ArgValue::PrevCallResult);
self.regs.insert(Register::XMM0, ArgValue::PrevCallResult);
self.fpu_top = Some(ArgValue::PrevCallResult);
self.stack_args.clear();
self.push_chain.clear();
self.saw_any_call = true;
self.setup_start = idx + 1;
}
fn handle_push(&mut self, insn: &Instruction) {
if insn.op_count() != 1 {
self.push_chain.push(self.classify_push_operand(insn));
return;
}
self.push_chain.push(self.classify_push_operand(insn));
}
fn classify_push_operand(&self, insn: &Instruction) -> ArgValue {
match insn.op0_kind() {
OpKind::Immediate8
| OpKind::Immediate16
| OpKind::Immediate32
| OpKind::Immediate64
| OpKind::Immediate8to16
| OpKind::Immediate8to32
| OpKind::Immediate8to64
| OpKind::Immediate32to64 => ArgValue::Const(read_signed_immediate(insn)),
OpKind::Register => {
let reg = insn.op0_register();
let full = full_reg(reg);
if let Some(v) = self.regs.get(&full) {
return v.clone();
}
ArgValue::Reg(format!("{reg:?}").to_lowercase())
}
OpKind::Memory => {
if insn.memory_index() == Register::None {
match insn.memory_base() {
Register::RIP => {
return ArgValue::GlobalLoad {
addr: insn.memory_displacement64(),
};
}
Register::RBP | Register::EBP => {
return ArgValue::StackLoad {
displacement: signed_displacement(insn),
};
}
_ => {}
}
}
let full = crate::format_intel(insn);
let operand = full
.strip_prefix("push ")
.map_or_else(|| full.clone(), str::to_string);
let trimmed = operand
.trim_start_matches("dword ptr ")
.trim_start_matches("qword ptr ")
.trim_start_matches("word ptr ")
.to_string();
ArgValue::Raw(trimmed)
}
_ => {
let full = crate::format_intel(insn);
ArgValue::Raw(
full.strip_prefix("push ")
.map_or_else(|| full.clone(), str::to_string),
)
}
}
}
fn handle_mov(&mut self, insn: &Instruction) -> bool {
if insn.op_count() != 2 {
return false;
}
if insn.op0_kind() == OpKind::Memory
&& insn.memory_base() == Register::ESP
&& insn.memory_index() == Register::None
{
let val = match insn.op1_kind() {
OpKind::Register => match self.regs.get(&full_reg(insn.op1_register())).cloned() {
Some(v) => v,
None => return false,
},
OpKind::Immediate8
| OpKind::Immediate16
| OpKind::Immediate32
| OpKind::Immediate64
| OpKind::Immediate8to16
| OpKind::Immediate8to32
| OpKind::Immediate8to64
| OpKind::Immediate32to64 => ArgValue::Const(read_signed_immediate(insn)),
_ => return false,
};
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let off = insn.memory_displacement64() as i32;
self.stack_args.insert(off, val);
return true;
}
if insn.op0_kind() != OpKind::Register {
return false;
}
let dst = insn.op0_register();
let value = match insn.op1_kind() {
OpKind::Register => {
let src = insn.op1_register();
if let Some(v) = self.regs.get(&full_reg(src)) {
v.clone()
} else if full_reg(src) == Register::RAX && self.saw_any_call {
ArgValue::PrevCallResult
} else {
return false;
}
}
OpKind::Memory => {
if insn.memory_index() != Register::None {
return false;
}
match insn.memory_base() {
Register::RIP => ArgValue::GlobalLoad {
addr: insn.memory_displacement64(),
},
Register::RBP | Register::EBP => ArgValue::StackLoad {
displacement: signed_displacement(insn),
},
_ => return false,
}
}
OpKind::Immediate8
| OpKind::Immediate16
| OpKind::Immediate32
| OpKind::Immediate64
| OpKind::Immediate8to16
| OpKind::Immediate8to32
| OpKind::Immediate8to64
| OpKind::Immediate32to64 => ArgValue::Const(read_signed_immediate(insn)),
_ => return false,
};
self.regs.insert(full_reg(dst), value);
true
}
fn handle_lea(&mut self, insn: &Instruction) -> bool {
if insn.op_count() != 2 || insn.op0_kind() != OpKind::Register {
return false;
}
if insn.op1_kind() != OpKind::Memory {
return false;
}
if insn.memory_base() != Register::RIP || insn.memory_index() != Register::None {
return false;
}
let dst = insn.op0_register();
self.regs.insert(
full_reg(dst),
ArgValue::Lea {
addr: insn.memory_displacement64(),
},
);
true
}
fn handle_fld(&mut self, insn: &Instruction) -> bool {
if insn.op_count() != 1 || insn.op0_kind() != OpKind::Memory {
return true; }
if insn.memory_index() != Register::None {
self.fpu_top = None;
return true;
}
self.fpu_top = match insn.memory_base() {
Register::RIP | Register::None => Some(ArgValue::GlobalLoad {
addr: insn.memory_displacement64(),
}),
Register::RBP | Register::EBP => Some(ArgValue::StackLoad {
displacement: signed_displacement(insn),
}),
_ => None,
};
true
}
fn handle_fstp(&mut self, insn: &Instruction) -> bool {
if insn.op_count() != 1 || insn.op0_kind() != OpKind::Memory {
self.fpu_top = None;
return true;
}
if insn.memory_index() == Register::None
&& (insn.memory_base() == Register::ESP || insn.memory_base() == Register::RSP)
{
if let Some(val) = self.fpu_top.take() {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let off = insn.memory_displacement64() as i32;
self.stack_args.insert(off, val);
return true;
}
}
self.fpu_top = None;
true
}
fn break_window(&mut self, next_setup_start: usize) {
let prev_rax = self.regs.remove(&Register::RAX);
self.regs.clear();
if let Some(rax) = prev_rax {
self.regs.insert(Register::RAX, rax);
}
self.stack_args.clear();
self.push_chain.clear();
self.fpu_top = None;
self.setup_start = next_setup_start;
}
}
fn full_reg(reg: Register) -> Register {
let full = reg.full_register();
if full == Register::None {
reg
} else {
full
}
}
pub(crate) use crate::signed_memory_displacement as signed_displacement;
#[allow(clippy::cast_possible_wrap)]
fn read_signed_immediate(insn: &Instruction) -> i64 {
match insn.op1_kind() {
OpKind::Immediate8 => i64::from(insn.immediate8() as i8),
OpKind::Immediate16 => i64::from(insn.immediate16() as i16),
OpKind::Immediate32 => i64::from(insn.immediate32() as i32),
OpKind::Immediate64 => insn.immediate64() as i64,
OpKind::Immediate8to16 => i64::from(insn.immediate8to16()),
OpKind::Immediate8to32 => i64::from(insn.immediate8to32()),
OpKind::Immediate8to64 => insn.immediate8to64(),
OpKind::Immediate32to64 => insn.immediate32to64(),
_ => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{decode, Bitness};
#[test]
fn lifts_direct_call_with_one_immediate_arg() {
let bytes = [0xbf, 0x08, 0x00, 0x00, 0x00, 0xe8, 0x00, 0x00, 0x00, 0x00];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let sites = identify_call_sites(&insns);
assert_eq!(sites.len(), 1);
let cs = &sites[0];
assert_eq!(cs.call_target, 0x100a);
assert_eq!(cs.args, vec![ArgValue::Const(8)]);
assert_eq!(cs.setup_start, 0);
assert_eq!(cs.call_idx, 1);
}
#[test]
fn lifts_lea_through_rax_to_rdi() {
let bytes = [
0x48, 0x8d, 0x05, 0x00, 0x10, 0x00, 0x00, 0x48, 0x89, 0xc7, 0xe8, 0x00, 0x00, 0x00, 0x00, ];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let sites = identify_call_sites(&insns);
assert_eq!(sites.len(), 1);
assert_eq!(sites[0].args, vec![ArgValue::Lea { addr: 0x2007 }]);
}
#[test]
fn second_call_reads_prev_result_through_rax() {
let bytes = [
0xe8, 0x00, 0x00, 0x00, 0x00, 0x89, 0xc6, 0xbf, 0x01, 0x00, 0x00, 0x00, 0xe8, 0x00, 0x00, 0x00, 0x00, ];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let sites = identify_call_sites(&insns);
assert_eq!(sites.len(), 2);
assert!(sites[0].args.is_empty());
assert_eq!(
sites[1].args,
vec![ArgValue::Const(1), ArgValue::PrevCallResult]
);
}
#[test]
fn lifts_xmm_arg_via_movsd_load() {
let bytes = [0xf2, 0x0f, 0x10, 0x45, 0xf0, 0xe8, 0x00, 0x00, 0x00, 0x00];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let sites = identify_call_sites(&insns);
assert_eq!(sites.len(), 1);
assert_eq!(
sites[0].args,
vec![ArgValue::StackLoad {
displacement: -0x10
}]
);
}
#[test]
fn lifts_xmm_arg_via_movq_from_rax() {
let bytes = [
0x48, 0x8b, 0x05, 0x00, 0x01, 0x00, 0x00, 0x66, 0x48, 0x0f, 0x6e, 0xc0, 0xe8, 0x00, 0x00, 0x00, 0x00, ];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let sites = identify_call_sites(&insns);
assert_eq!(sites.len(), 1);
assert_eq!(sites[0].args, vec![ArgValue::GlobalLoad { addr: 0x1107 }]);
}
#[test]
fn detect_post_call_spill_recognizes_movq_then_mov() {
let bytes = [0x66, 0x48, 0x0f, 0x7e, 0xc0, 0x48, 0x89, 0x45, 0xf8];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let spill = detect_post_call_spill(&insns, 0).expect("should match");
assert_eq!(spill.displacement, -8);
assert_eq!(spill.insns_consumed, 2);
}
#[test]
fn detect_post_call_spill_recognizes_direct_mov_rax() {
let bytes = [0x48, 0x89, 0x45, 0xf0];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let spill = detect_post_call_spill(&insns, 0).expect("should match");
assert_eq!(spill.displacement, -0x10);
assert_eq!(spill.insns_consumed, 1);
}
#[test]
fn detect_post_call_spill_rejects_unrelated_mov() {
let bytes = [0x48, 0x89, 0xc7];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
assert!(detect_post_call_spill(&insns, 0).is_none());
}
#[test]
fn unmodelled_op_breaks_arg_window() {
let bytes = [
0xbf, 0x01, 0x00, 0x00, 0x00, 0x83, 0x45, 0xfc, 0x01, 0xe8, 0x00, 0x00, 0x00, 0x00,
];
let insns = decode(Bitness::Bits64, &bytes, 0x1000).unwrap();
let sites = identify_call_sites(&insns);
assert_eq!(sites.len(), 1);
assert!(
sites[0].args.is_empty(),
"memory write should have invalidated edi: {:?}",
sites[0].args
);
assert_eq!(sites[0].setup_start, 2); }
#[test]
fn i386_stdcall_push_chain_folds_into_call_args() {
let mut bytes: Vec<u8> = Vec::new();
for disp in [
0x20u8, 0x1c, 0x18, 0x14, 0x10, 0x0c, 0x30, 0x2c, 0x28, 0x24, 0x08, 0x04,
] {
bytes.extend_from_slice(&[0xff, 0x70, disp]);
}
bytes.extend_from_slice(&[0xff, 0x30]); bytes.extend_from_slice(&[0xff, 0x75, 0x08]); bytes.extend_from_slice(&[0xe8, 0x00, 0x00, 0x00, 0x00]); let insns = decode(Bitness::Bits32, &bytes, 0x1000).unwrap();
let sites = identify_call_sites(&insns);
assert_eq!(sites.len(), 1, "exactly one call site");
assert_eq!(
sites[0].args.len(),
14,
"all 14 pushes must surface as args, got {} ({:?})",
sites[0].args.len(),
sites[0].args
);
assert!(
matches!(
sites[0].args.first(),
Some(ArgValue::StackLoad { displacement: 8 })
),
"first arg should be the ebp+8 push (reversed for natural order)"
);
}
}