use crate::arch::x86::{
CodeSize, DecodedInsn, FlowControl, Mnemonic, detect_post_call_spill, format_intel,
};
use crate::ast::Stmt;
use super::{Candidate, Pattern, PatternCtx};
pub struct StackArgCall;
impl Pattern for StackArgCall {
fn name(&self) -> &'static str {
"stack_arg_call"
}
#[allow(clippy::too_many_lines)]
fn tentative(
&self,
ctx: &PatternCtx,
insns: &[DecodedInsn],
start: usize,
) -> Option<Candidate> {
let first = insns.get(start)?;
if first.iced.code_size() != CodeSize::Code32 {
return None;
}
let mut args: Vec<String> = Vec::new();
let mut bytes: Vec<u8> = Vec::new();
let mut i = start;
while i < insns.len() {
let ins = &insns[i];
if ins.iced.mnemonic() == Mnemonic::Push {
let sp = ctx.sp_delta_at.get(&ins.iced.ip()).copied();
args.push(render_push_arg(ins, sp));
bytes.extend_from_slice(&ins.original_bytes);
i += 1;
continue;
}
let fc = ins.iced.flow_control();
let is_call = matches!(fc, FlowControl::Call | FlowControl::IndirectCall);
let is_tail_jmp = matches!(
fc,
FlowControl::UnconditionalBranch | FlowControl::IndirectBranch
);
if is_call || is_tail_jmp {
if args.is_empty() {
return None;
}
let tail_prefix: Option<String> = if is_tail_jmp {
let target = ins.iced.near_branch_target();
let in_known_fn = target != 0 && ctx.name_at.contains_key(&target);
let in_local_range =
target != 0 && target >= ctx.fn_addr_start && target < ctx.fn_addr_end;
if in_known_fn {
Some("tail_".to_string())
} else if in_local_range {
Some("goto_".to_string())
} else {
Some("tail_".to_string())
}
} else {
None
};
let direct_call_target = if is_call && !is_tail_jmp {
crate::arch::x86::direct_call_target(&ins.iced).filter(|t| *t != 0)
} else {
None
};
if direct_call_target.is_none() {
bytes.extend_from_slice(&ins.original_bytes);
}
let mut consumed_extra = 0usize;
let mut spill_comment: Option<String> = None;
let mut direct_target = direct_call_target;
if is_call && let Some(spill) = detect_post_call_spill(insns, i + 1) {
if direct_target.is_some() {
bytes.extend_from_slice(&ins.original_bytes);
direct_target = None;
}
for j in 0..spill.insns_consumed {
if let Some(s) = insns.get(i + 1 + j) {
bytes.extend_from_slice(&s.original_bytes);
}
}
consumed_extra = spill.insns_consumed;
let dest = if spill.displacement < 0 {
format!("[rbp-0x{:x}]", spill.displacement.unsigned_abs())
} else {
format!("[rbp+0x{:x}]", spill.displacement)
};
spill_comment = Some(format!("result -> {dest}"));
}
args.reverse();
let raw_name = render_call_target(ins, ctx);
let name = match tail_prefix {
Some(prefix) if !raw_name.starts_with('[') => {
format!("{prefix}{raw_name}")
}
_ => raw_name,
};
let consumed = (i + 1 - start) + consumed_extra;
let mut stmts: Vec<Stmt> = vec![Stmt::Call {
name,
args,
bytes,
direct_target,
}];
if let Some(c) = spill_comment {
stmts.push(Stmt::Comment(c));
}
return Some(Candidate {
pattern: self.name(),
start,
consumed,
priority: 200,
stmts,
});
}
return None;
}
if !args.is_empty() && args.len() >= 2 {
args.reverse();
let last_ip = insns
.last()
.map(|ins| ins.iced.next_ip())
.unwrap_or_default();
let name = if last_ip != 0 {
format!("to_{last_ip:x}")
} else {
"pushed_args".into()
};
return Some(Candidate {
pattern: self.name(),
start,
consumed: i - start,
priority: 200,
stmts: vec![Stmt::Call {
name,
args,
bytes,
direct_target: None,
}],
});
}
None
}
}
fn render_push_arg(insn: &DecodedInsn, sp_delta: Option<i64>) -> String {
let full = format_intel(&insn.iced);
let raw = full
.strip_prefix("push ")
.map_or_else(|| full.clone(), str::to_string);
let trimmed = raw
.trim_start_matches("dword ptr ")
.trim_start_matches("qword ptr ")
.trim_start_matches("word ptr ")
.to_string();
crate::arch::x86::rename_operand_in_ctx(&trimmed, sp_delta)
}
fn render_call_target(insn: &DecodedInsn, ctx: &PatternCtx) -> String {
if let Some(target) = crate::arch::x86::direct_call_target(&insn.iced) {
if let Some(name) = ctx.name_at.get(&target) {
return name.clone();
}
return format!("sub_{target:x}");
}
if let Some(target) = crate::arch::x86::direct_unconditional_branch_target(&insn.iced) {
if let Some(name) = ctx.name_at.get(&target) {
return name.clone();
}
return format!("{target:x}");
}
let full = format_intel(&insn.iced);
let stripped = full
.strip_prefix("call ")
.or_else(|| full.strip_prefix("jmp "))
.unwrap_or(&full);
let operand = stripped
.trim_start_matches("dword ptr ")
.trim_start_matches("qword ptr ")
.trim_start_matches("near ptr ")
.to_string();
if let Some(name) = lookup_indirect_absolute(&operand, ctx) {
return name;
}
operand
}
fn lookup_indirect_absolute(operand: &str, ctx: &PatternCtx) -> Option<String> {
let inner = operand.strip_prefix('[')?.strip_suffix(']')?;
if inner.contains('+') || inner.contains('-') || inner.contains('*') {
return None;
}
let hex = if let Some(s) = inner.strip_suffix('h') {
s
} else {
inner
.strip_prefix("0x")
.or_else(|| inner.strip_prefix("0X"))?
};
let va = u64::from_str_radix(hex, 16).ok()?;
ctx.name_at.get(&va).cloned()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::arch::x86::{Bitness, decode};
use std::collections::HashMap;
fn ctx_empty() -> (HashMap<u64, String>, PatternCtx<'static>) {
let map: HashMap<u64, String> = HashMap::new();
let leaked: &'static HashMap<u64, String> = Box::leak(Box::new(map));
let leaked_sp: &'static HashMap<u64, i64> = Box::leak(Box::new(HashMap::new()));
(
HashMap::new(),
PatternCtx {
fn_addr_start: 0,
fn_addr_end: u64::MAX,
name_at: leaked,
sp_delta_at: leaked_sp,
},
)
}
#[test]
fn lifts_two_push_then_call() {
let bytes = [0x6a, 0x05, 0x6a, 0x07, 0xe8, 0x00, 0x00, 0x00, 0x00];
let insns = decode(Bitness::Bits32, &bytes, 0x1000).unwrap();
let (_, ctx) = ctx_empty();
let cand = StackArgCall.tentative(&ctx, &insns, 0).expect("match");
assert_eq!(cand.consumed, 3);
assert!(matches!(cand.stmts.first(), Some(Stmt::Call { .. })));
if let Some(Stmt::Call { args, .. }) = cand.stmts.first() {
assert_eq!(args, &["7", "5"]);
}
}
#[test]
fn lifts_thirteen_push_then_call() {
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 (_, ctx) = ctx_empty();
let cand = StackArgCall
.tentative(&ctx, &insns, 0)
.expect("14-push then call must lift");
let Stmt::Call { args, .. } = &cand.stmts[0] else {
panic!("expected Call stmt");
};
assert_eq!(args.len(), 14, "should fold every push as an arg");
}
#[test]
fn skips_call_with_no_pushes() {
let bytes = [0xe8, 0x00, 0x00, 0x00, 0x00];
let insns = decode(Bitness::Bits32, &bytes, 0x1000).unwrap();
let (_, ctx) = ctx_empty();
assert!(StackArgCall.tentative(&ctx, &insns, 0).is_none());
}
}