use std::collections::{HashMap, HashSet};
use crate::arch::x86::{
ArgValue, CallSite, DecodedInsn, ExprRenderCtx, OpKind, Register, arg_spill_index,
detect_post_call_spill, direct_call_target, direct_lea_rip_target,
direct_unconditional_branch_target, format_intel, identify_call_sites,
try_lift_epilogue_pattern, try_lift_if_branch_head, try_lift_prologue_pattern,
try_lift_return_pattern, try_lift_return_via_jmp, try_lift_value_block,
};
use crate::ast::{FnDecl, LocalDecl, LocalKind, Signature, Stmt, Type};
use crate::debug::DebugFunction;
use crate::ir::{BasicBlock, Function, Terminator};
use crate::translate::decompile::data_lookup::DataLookup;
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn build_function(
f: &Function<DecodedInsn>,
debug: Option<&DebugFunction>,
name_at: &HashMap<u64, String>,
data: &dyn DataLookup,
) -> FnDecl {
let signature = debug.map(|d| Signature {
params: d.params.clone(),
return_type: d.return_type.clone(),
});
let slot_to_name = collect_slot_to_name(f, signature.as_ref());
let lifts = compute_block_tail_lifts(f, signature.as_ref(), &slot_to_name, name_at);
let groups = identify_if_else_groups(f);
let loops = identify_loop_groups(f);
let sp_delta_at = compute_sp_delta_cfg(f);
let ssa = crate::translate::decompile::ssa::build_ssa(f, &sp_delta_at);
let liveness = crate::translate::decompile::ssa::compute_liveness(f, &sp_delta_at);
let bitness = function_bitness(f);
let mut body = Vec::new();
let func_end = f.addr.0.saturating_add(f.size() as u64);
let codec_bits = match bitness {
crate::arch::x86::Bitness::Bits64 => crate::arch::x86::CodecBits::Bits64,
_ => crate::arch::x86::CodecBits::Bits32,
};
let ctx = EmitCtx {
fn_addr_start: f.addr.0,
fn_addr_end: func_end,
name_at,
sp_delta_at: &sp_delta_at,
signature: signature.as_ref(),
data,
codec_bits,
};
let mut pre_jmp_truncate: HashMap<usize, usize> = HashMap::new();
for lg in loops.iter().flatten() {
if let Some(idx) = lg.pre_jmp_block_idx {
pre_jmp_truncate.insert(idx, 1);
}
}
emit_blocks_in_range(
&mut body,
f,
0..f.blocks.len(),
&loops,
&groups,
&lifts,
&pre_jmp_truncate,
&ctx,
true,
);
forward_propagate_registers(&mut body, &ssa, f.addr.0, bitness);
let bit_width = match bitness {
crate::arch::x86::Bitness::Bits64 => crate::translate::decompile::expr::BitWidth::Bits64,
_ => crate::translate::decompile::expr::BitWidth::Bits32,
};
simplify_body(&mut body, bit_width);
let string_lookup = |va: u64| -> Option<String> { lookup_string_at_va(data, va) };
resolve_strings_in_body(&mut body, &string_lookup);
resolve_indirect_call_names(&mut body, name_at);
annotate_return_values(&mut body);
absorb_save_restore(&mut body);
fold_early_returns(&mut body, f.addr.0, bitness);
fold_dead_register_moves(&mut body, &liveness, f.addr.0);
fold_seh_frame(&mut body);
fold_switch_jump_tables(&mut body, f.addr.0, bitness, data);
recognise_string_idioms(&mut body);
fold_local_if_skip(&mut body, bitness);
fold_gotos_and_labels(&mut body, f.addr.0, bitness);
let locals = discover_locals(f, &sp_delta_at);
let mut attrs = detect_calling_convention_attrs(f, &ssa);
let observed_bits: u32 = if matches!(bitness, crate::arch::x86::Bitness::Bits64) {
64
} else {
32
};
let observed_cf = matches!(
body.first(),
Some(Stmt::Prologue { params: Some(p), .. }) if p.cf_protect
);
let mut attrs_with_profile = attrs.clone();
if observed_bits == 64 {
attrs_with_profile.push(crate::ast::Attribute {
key: "bits".into(),
value: crate::ast::AttrValue::Int(64),
});
}
if observed_cf {
attrs_with_profile.push(crate::ast::Attribute {
key: "cf".into(),
value: crate::ast::AttrValue::Flag,
});
}
let temp_decl = crate::ast::FnDecl {
addr: Some(f.addr.0),
name: f.name.clone(),
attrs: attrs_with_profile.clone(),
signature: signature.clone(),
locals: locals.clone(),
body: body.clone(),
};
let base = profile_inputs_from_fn(&temp_decl);
let mut profile_fp = base.clone();
profile_fp.frame_required = true;
let mut profile_nofp = base.clone();
profile_nofp.frame_required = false;
let try_match = |profile: &crate::arch::x86::ProfileInputs| -> (
bool,
bool,
crate::arch::x86::StructuredPrologue,
crate::arch::x86::StructuredEpilogue,
) {
let dp = crate::arch::x86::default_prologue(profile);
let de = crate::arch::x86::default_epilogue(profile);
let pm = matches!(body.first(), Some(Stmt::Prologue { params: Some(p), .. })
if p.saves == dp.saves
&& p.saves_after == dp.saves_after
&& p.frame == dp.frame
&& p.sub_esp == dp.sub_esp
&& p.cf_protect == dp.cf_protect
&& p.frame_alt == dp.frame_alt_encoding);
let em = matches!(body.last(), Some(Stmt::Epilogue { params: Some(e), .. })
if e.saves == de.saves
&& e.leave == de.leave
&& e.pop_frame == de.pop_frame
&& e.add_esp == de.add_esp
&& e.ret_imm == de.ret_imm);
(pm, em, dp, de)
};
let (pm_fp, em_fp, dp_fp, de_fp) = try_match(&profile_fp);
let (pm_nofp, em_nofp, dp_nofp, de_nofp) = try_match(&profile_nofp);
let fp_score = u32::from(pm_fp) + u32::from(em_fp);
let nofp_score = u32::from(pm_nofp) + u32::from(em_nofp);
let use_nofp = nofp_score > fp_score;
let (pro_matches, epi_matches, default_pro, default_epi) = if use_nofp {
(pm_nofp, em_nofp, dp_nofp, de_nofp)
} else {
(pm_fp, em_fp, dp_fp, de_fp)
};
let mut pro_dropped = false;
let mut epi_dropped = false;
let defaults_apply = true;
if defaults_apply && pro_matches {
body.remove(0);
pro_dropped = true;
} else if let (
true,
Some(Stmt::Prologue {
params: Some(p), ..
}),
) = (defaults_apply, body.first())
{
let saves_is_subset = is_subset(&p.saves, &default_pro.saves)
&& is_subset(&p.saves_after, &default_pro.saves_after)
&& p.frame == default_pro.frame
&& p.sub_esp == default_pro.sub_esp
&& p.cf_protect == default_pro.cf_protect;
if !saves_is_subset && let Some(text) = describe_default_prologue(&default_pro) {
body.insert(0, Stmt::Comment(format!("default: {text}")));
}
}
if defaults_apply && epi_matches {
body.pop();
epi_dropped = true;
} else if let (
true,
Some(Stmt::Epilogue {
params: Some(e), ..
}),
) = (defaults_apply, body.last())
{
let saves_is_subset = is_subset(&e.saves, &default_epi.saves)
&& e.leave == default_epi.leave
&& e.pop_frame == default_epi.pop_frame
&& e.add_esp == default_epi.add_esp
&& e.ret_imm == default_epi.ret_imm;
if !saves_is_subset && let Some(text) = describe_default_epilogue(&default_epi) {
let last = body.len() - 1;
body.insert(last, Stmt::Comment(format!("default: {text}")));
}
}
if pro_dropped {
attrs.push(crate::ast::Attribute {
key: "autogen_pro".into(),
value: crate::ast::AttrValue::Flag,
});
}
if epi_dropped {
attrs.push(crate::ast::Attribute {
key: "autogen_epi".into(),
value: crate::ast::AttrValue::Flag,
});
}
let dropped_any = pro_dropped || epi_dropped;
if dropped_any {
if observed_bits == 64 {
attrs.push(crate::ast::Attribute {
key: "bits".into(),
value: crate::ast::AttrValue::Int(64),
});
}
if observed_cf {
attrs.push(crate::ast::Attribute {
key: "cf".into(),
value: crate::ast::AttrValue::Flag,
});
}
}
if use_nofp && dropped_any {
attrs.push(crate::ast::Attribute {
key: "noframe".into(),
value: crate::ast::AttrValue::Flag,
});
}
let abi_is_thiscall = attrs.iter().any(|a| {
a.key == "abi"
&& matches!(&a.value, crate::ast::AttrValue::String(s) if s == "thiscall" || s == "fastcall")
});
if abi_is_thiscall {
rewrite_ecx_as_this(&mut body);
}
let stack_map = build_stack_local_map(&locals);
if !stack_map.is_empty() {
rewrite_stack_refs(&mut body, &stack_map);
}
let labels = collect_label_addrs(&body);
if !labels.is_empty() {
rewrite_label_refs(&mut body, &labels);
}
rewrite_intel_hex_in_body(&mut body);
let param_regs = build_param_register_map(signature.as_ref(), &attrs, bitness);
if !param_regs.is_empty() {
rewrite_param_register_reads(&mut body, ¶m_regs);
}
FnDecl {
addr: Some(f.addr.0),
name: f.name.clone(),
attrs,
signature,
locals,
body,
}
}
fn detect_calling_convention_attrs(
f: &Function<DecodedInsn>,
ssa: &crate::translate::decompile::ssa::SsaInfo,
) -> Vec<crate::ast::Attribute> {
use crate::arch::x86::{CodeSize, Mnemonic};
let Some(first_insn) = f.blocks.first().and_then(|b| b.insns.first()) else {
return Vec::new();
};
if first_insn.iced.code_size() == CodeSize::Code64 {
return Vec::new();
}
let mut ret_imm: Option<u32> = None;
for block in &f.blocks {
for insn in &block.insns {
if matches!(insn.iced.mnemonic(), Mnemonic::Ret | Mnemonic::Retf) {
let imm = insn.iced.immediate16();
ret_imm = Some(u32::from(imm));
break;
}
}
if ret_imm.is_some() {
break;
}
}
let Some(imm) = ret_imm else {
return Vec::new();
};
if imm == 0 {
return vec![crate::ast::Attribute {
key: "abi".into(),
value: crate::ast::AttrValue::String("cdecl".into()),
}];
}
let ecx_read_early = reg_read_before_write(f, ssa, "ecx");
let edx_read_early = reg_read_before_write(f, ssa, "edx");
let kind = if ecx_read_early && edx_read_early {
"fastcall"
} else if ecx_read_early {
"thiscall"
} else {
"stdcall"
};
vec![crate::ast::Attribute {
key: "abi".into(),
value: crate::ast::AttrValue::String(kind.into()),
}]
}
fn reg_read_before_write(
f: &Function<DecodedInsn>,
ssa: &crate::translate::decompile::ssa::SsaInfo,
reg: &str,
) -> bool {
let Some(entry) = f.blocks.first() else {
return false;
};
let var = crate::translate::decompile::ssa::Var::Reg(reg.to_string());
for insn in &entry.insns {
let ip = insn.iced.ip();
if ssa.use_at.contains_key(&(ip, var.clone())) {
if let Some(def) = ssa.use_at.get(&(ip, var.clone()))
&& matches!(
ssa.defs.get(def.0 as usize).map(|r| &r.site),
Some(crate::translate::decompile::ssa::DefSite::Entry)
)
{
return true;
}
}
if ssa.def_at.contains_key(&(ip, var.clone())) {
return false;
}
}
false
}
fn compute_sp_delta_cfg(f: &Function<DecodedInsn>) -> HashMap<u64, i64> {
use crate::arch::x86::sp_change_for;
let mut out: HashMap<u64, i64> = HashMap::new();
if f.blocks.is_empty() {
return out;
}
let addr_to_idx: HashMap<u64, usize> = f
.blocks
.iter()
.enumerate()
.map(|(i, b)| (b.addr.0, i))
.collect();
let mut block_entry: HashMap<usize, i64> = HashMap::new();
block_entry.insert(0, 0);
let mut queue: Vec<usize> = vec![0];
while let Some(idx) = queue.pop() {
let block = &f.blocks[idx];
let mut delta = block_entry[&idx];
for insn in &block.insns {
out.insert(insn.iced.ip(), delta);
delta = delta.saturating_add(sp_change_for(&insn.iced));
}
let mut successors: Vec<usize> = Vec::new();
match &block.terminator {
Terminator::Fallthrough => {
if idx + 1 < f.blocks.len() {
successors.push(idx + 1);
}
}
Terminator::ConditionalBranch { taken, .. } => {
if idx + 1 < f.blocks.len() {
successors.push(idx + 1);
}
if let Some(&t) = addr_to_idx.get(&taken.0) {
successors.push(t);
}
}
Terminator::UnconditionalBranch { target } => {
if let Some(&t) = addr_to_idx.get(&target.0) {
successors.push(t);
}
}
Terminator::Return | Terminator::IndirectBranch | Terminator::InvalidOrUnreachable => {}
}
for succ in successors {
if let std::collections::hash_map::Entry::Vacant(e) = block_entry.entry(succ) {
e.insert(delta);
queue.push(succ);
}
}
}
for (idx, block) in f.blocks.iter().enumerate() {
if !block_entry.contains_key(&idx) {
for insn in &block.insns {
out.entry(insn.iced.ip()).or_insert(0);
}
}
}
out
}
const PROP_MAX_LEN: usize = 200;
const PROP_MAX_IDENT_REPEATS: usize = 4;
fn any_ident_repeats(text: &str, limit: usize) -> bool {
let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
let mut start = Option::<usize>::None;
let bytes = text.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
let is_ident = b.is_ascii_alphanumeric() || b == b'_';
match (start, is_ident) {
(None, true) => start = Some(i),
(Some(s), false) => {
let ident = &text[s..i];
if !ident.chars().next().is_some_and(|c| c.is_ascii_digit()) {
let n = counts.entry(ident).or_insert(0);
*n += 1;
if *n > limit {
return true;
}
}
start = Option::None;
}
_ => {}
}
}
if let Some(s) = start {
let ident = &text[s..];
if !ident.chars().next().is_some_and(|c| c.is_ascii_digit()) {
let n = counts.entry(ident).or_insert(0);
*n += 1;
if *n > limit {
return true;
}
}
}
false
}
#[derive(Default, Clone)]
struct RegState {
values: std::collections::HashMap<String, String>,
}
impl RegState {
fn write(&mut self, reg: &str, value: String) {
if value.len() > PROP_MAX_LEN || any_ident_repeats(&value, PROP_MAX_IDENT_REPEATS) {
self.values.remove(reg);
return;
}
self.values.insert(reg.to_string(), value);
}
fn invalidate(&mut self, reg: &str) {
self.values.remove(reg);
}
fn invalidate_all(&mut self) {
self.values.clear();
}
fn substitute(&self, text: &str) -> String {
let mut result = text.to_string();
let mut names: Vec<&String> = self.values.keys().collect();
names.sort_by_key(|n| std::cmp::Reverse(n.len()));
for reg in names {
let val = &self.values[reg];
result = replace_register_word(&result, reg, val);
}
result
}
}
fn merge_states(a: &RegState, b: &RegState) -> RegState {
let mut out = RegState::default();
for (reg, val) in &a.values {
if b.values.get(reg) == Some(val) {
out.values.insert(reg.clone(), val.clone());
}
}
out
}
fn replace_register_word(text: &str, reg: &str, value: &str) -> String {
let chars: Vec<char> = text.chars().collect();
let reg_chars: Vec<char> = reg.chars().collect();
let mut out = String::with_capacity(text.len());
let mut i = 0;
let mut depth: i32 = 0;
let mut in_quote = false;
let mut prev_was_escape = false;
while i < chars.len() {
let c = chars[i];
if !in_quote {
if c == '[' {
depth += 1;
} else if c == ']' {
depth -= 1;
}
}
let head_matches =
i + reg_chars.len() <= chars.len() && chars[i..i + reg_chars.len()] == reg_chars[..];
let prefix_ok = i == 0 || !is_ident_char(chars[i - 1]);
let suffix_ok = !head_matches
|| i + reg_chars.len() == chars.len()
|| !is_ident_char(chars[i + reg_chars.len()]);
let matches_here = depth == 0 && !in_quote && head_matches && prefix_ok && suffix_ok;
if matches_here {
let needs_parens = value.contains(' ');
if needs_parens {
out.push('(');
out.push_str(value);
out.push(')');
} else {
out.push_str(value);
}
i += reg_chars.len();
prev_was_escape = false;
continue;
}
prev_was_escape = if c == '"' && !prev_was_escape {
in_quote = !in_quote;
false
} else {
c == '\\' && in_quote && !prev_was_escape
};
out.push(c);
i += 1;
}
out
}
fn is_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
fn forward_propagate_registers(
stmts: &mut [Stmt],
ssa: &crate::translate::decompile::ssa::SsaInfo,
base_ip: u64,
bitness: crate::arch::x86::Bitness,
) {
let writes_at = build_writes_at(ssa);
let mut state = RegState::default();
let mut cursor = base_ip;
forward_propagate_in_seq(stmts, &mut state, &writes_at, &mut cursor, bitness);
}
fn build_writes_at(
ssa: &crate::translate::decompile::ssa::SsaInfo,
) -> HashMap<u64, Vec<crate::translate::decompile::ssa::Var>> {
let mut out: HashMap<u64, Vec<crate::translate::decompile::ssa::Var>> = HashMap::new();
for (ip, var) in ssa.def_at.keys() {
out.entry(*ip).or_default().push(var.clone());
}
out
}
fn forward_propagate_in_seq(
stmts: &mut [Stmt],
state: &mut RegState,
writes_at: &HashMap<u64, Vec<crate::translate::decompile::ssa::Var>>,
cursor: &mut u64,
bitness: crate::arch::x86::Bitness,
) {
for stmt in stmts.iter_mut() {
propagate_one_stmt(stmt, state, writes_at, cursor, bitness);
}
}
#[allow(clippy::too_many_lines)]
fn propagate_one_stmt(
stmt: &mut Stmt,
state: &mut RegState,
writes_at: &HashMap<u64, Vec<crate::translate::decompile::ssa::Var>>,
cursor: &mut u64,
bitness: crate::arch::x86::Bitness,
) {
match stmt {
Stmt::Move { dst, src, bytes } => {
let new_src = state.substitute(src);
if &new_src == dst {
if is_gpr_name(dst) {
state.invalidate(dst);
}
} else {
src.clone_from(&new_src);
if is_gpr_name(dst) {
state.write(dst, new_src);
} else {
*dst = state.substitute(dst);
}
}
*cursor += bytes.len() as u64;
}
Stmt::Call {
name,
args,
bytes,
direct_target,
} => {
for a in args.iter_mut() {
*a = state.substitute(a);
}
*name = state.substitute(name);
for reg in CALLER_SAVED {
state.invalidate(reg);
}
let mut size = bytes.len() as u64;
if direct_target.is_some() {
size += 5;
}
*cursor += size;
}
Stmt::IfBranch {
cond_text,
attrs,
cond_bytes,
pre_body,
then_body,
else_body,
..
} => {
*cond_text = state.substitute(cond_text);
if let Some(hb) = ud_ast_head_bytes_attr(attrs) {
*cursor += hb.len() as u64;
}
forward_propagate_in_seq(pre_body, state, writes_at, cursor, bitness);
*cursor += cond_bytes.len() as u64;
let snapshot = state.clone();
forward_propagate_in_seq(then_body, state, writes_at, cursor, bitness);
let after_then = state.clone();
let after_else = if let Some(eb) = else_body {
*state = snapshot.clone();
forward_propagate_in_seq(eb, state, writes_at, cursor, bitness);
state.clone()
} else {
snapshot
};
*state = merge_states(&after_then, &after_else);
}
Stmt::Loop {
cond_text,
entry_jmp_bytes,
tail_bytes,
body,
..
} => {
*cond_text = state.substitute(cond_text);
if let Some(jmp) = entry_jmp_bytes {
*cursor += jmp.len() as u64;
}
state.invalidate_all();
forward_propagate_in_seq(body, state, writes_at, cursor, bitness);
*cursor += tail_bytes.len() as u64;
state.invalidate_all();
}
Stmt::Asm { text: _, bytes } => {
let stmt_start = *cursor;
let mut clobbered: HashSet<String> = HashSet::new();
let mut has_call = false;
if let Ok(insns) = crate::arch::x86::decode(bitness, bytes, stmt_start) {
for insn in &insns {
let mnemonic = insn.iced.mnemonic();
if matches!(mnemonic, crate::arch::x86::Mnemonic::Call) {
has_call = true;
}
if let Some(vars) = writes_at.get(&insn.iced.ip()) {
for v in vars {
if let crate::translate::decompile::ssa::Var::Reg(name) = v {
clobbered.insert(name.clone());
}
}
}
}
}
if has_call {
for reg in CALLER_SAVED {
state.invalidate(reg);
}
}
for reg in &clobbered {
state.invalidate(reg);
}
*cursor += bytes.len() as u64;
}
Stmt::ReturnExpr { text, bytes } => {
*text = state.substitute(text);
state.invalidate_all();
*cursor += bytes.len() as u64;
}
Stmt::Prologue { bytes, .. }
| Stmt::Epilogue { bytes, .. }
| Stmt::Return { bytes, .. }
| Stmt::ArgSpill { bytes, .. }
| Stmt::LocalSet { bytes, .. }
| Stmt::LocalArith { bytes, .. }
| Stmt::LocalCompound { bytes, .. }
| Stmt::Inc16 { bytes, .. }
| Stmt::RegArith { bytes, .. } => {
state.invalidate_all();
*cursor += bytes.len() as u64;
}
Stmt::Save { reg, bytes } | Stmt::Restore { reg, bytes } => {
if is_gpr_name(reg) {
state.invalidate(reg);
}
*cursor += bytes.len() as u64;
}
Stmt::IfReturn {
cmp_bytes,
target_addr,
wide,
..
}
| Stmt::IfGoto {
cmp_bytes,
target_addr,
wide,
..
} => {
let jcc_ip = cursor.saturating_add(cmp_bytes.len() as u64);
*cursor += cmp_bytes.len() as u64
+ crate::arch::x86::encoded_jcc_size(jcc_ip, *target_addr, *wide) as u64;
}
Stmt::Goto { target_addr, wide } => {
*cursor += crate::arch::x86::encoded_jmp_size(*cursor, *target_addr, *wide) as u64;
}
Stmt::Switch { .. } => {
*cursor += switch_encoded_size(stmt) as u64;
}
Stmt::SehInstall { bytes } | Stmt::SehRestore { bytes } => {
state.invalidate_all();
*cursor += bytes.len() as u64;
}
Stmt::IfBlock {
cond_text,
cond_bytes,
then_body,
then_tail_jmp,
else_body,
} => {
*cond_text = state.substitute(cond_text);
*cursor += cond_bytes.len() as u64;
state.invalidate_all();
forward_propagate_in_seq(then_body, state, writes_at, cursor, bitness);
*cursor += then_tail_jmp.len() as u64;
state.invalidate_all();
forward_propagate_in_seq(else_body, state, writes_at, cursor, bitness);
state.invalidate_all();
}
Stmt::WhileBlock {
cond_text,
entry_bytes,
tail_bytes,
body,
} => {
*cond_text = state.substitute(cond_text);
*cursor += entry_bytes.len() as u64;
state.invalidate_all();
forward_propagate_in_seq(body, state, writes_at, cursor, bitness);
*cursor += tail_bytes.len() as u64;
state.invalidate_all();
}
Stmt::Label { .. } | Stmt::Comment(_) => {}
}
}
fn resolve_strings_in_body(stmts: &mut [Stmt], lookup: &dyn Fn(u64) -> Option<String>) {
for stmt in stmts.iter_mut() {
resolve_strings_in_stmt(stmt, lookup);
}
}
fn resolve_strings_in_stmt(stmt: &mut Stmt, lookup: &dyn Fn(u64) -> Option<String>) {
match stmt {
Stmt::Move { dst, src, .. } => {
*dst = crate::translate::decompile::expr::resolve_strings_in_text(dst, lookup);
*src = crate::translate::decompile::expr::resolve_strings_in_text(src, lookup);
}
Stmt::Call { args, name, .. } => {
*name = crate::translate::decompile::expr::resolve_strings_in_text(name, lookup);
for a in args.iter_mut() {
*a = crate::translate::decompile::expr::resolve_strings_in_text(a, lookup);
}
}
Stmt::IfBranch {
cond_text,
pre_body,
then_body,
else_body,
..
} => {
*cond_text =
crate::translate::decompile::expr::resolve_strings_in_text(cond_text, lookup);
resolve_strings_in_body(pre_body, lookup);
resolve_strings_in_body(then_body, lookup);
if let Some(eb) = else_body {
resolve_strings_in_body(eb, lookup);
}
}
Stmt::Loop {
cond_text, body, ..
} => {
*cond_text =
crate::translate::decompile::expr::resolve_strings_in_text(cond_text, lookup);
resolve_strings_in_body(body, lookup);
}
Stmt::ReturnExpr { text, .. } => {
*text = crate::translate::decompile::expr::resolve_strings_in_text(text, lookup);
}
Stmt::IfReturn {
cond_text,
value_text,
..
} => {
*cond_text =
crate::translate::decompile::expr::resolve_strings_in_text(cond_text, lookup);
*value_text =
crate::translate::decompile::expr::resolve_strings_in_text(value_text, lookup);
}
_ => {}
}
}
fn lookup_string_at_va(data: &dyn DataLookup, va: u64) -> Option<String> {
if va < 0x1000 {
return None;
}
let (section_name, section_data, offset) = data.section_at(va)?;
if section_name == ".text"
|| section_name == ".init"
|| section_name == ".fini"
|| section_name.starts_with(".text.")
{
return None;
}
let slice = section_data.get(offset..)?;
let max_len = slice.len().min(1024);
let nul = slice[..max_len].iter().position(|&b| b == 0)?;
if nul < 4 {
return None;
}
let text_bytes = &slice[..nul];
if !text_bytes
.iter()
.all(|&b| matches!(b, 0x20..=0x7e | b'\n' | b'\r' | b'\t'))
{
return None;
}
std::str::from_utf8(text_bytes).ok().map(str::to_string)
}
fn simplify_body(stmts: &mut [Stmt], width: crate::translate::decompile::expr::BitWidth) {
for stmt in stmts.iter_mut() {
simplify_stmt(stmt, width);
}
}
fn simplify_stmt(stmt: &mut Stmt, width: crate::translate::decompile::expr::BitWidth) {
match stmt {
Stmt::Move { dst, src, .. } => {
*dst = crate::translate::decompile::expr::simplify_text(dst, width);
*src = crate::translate::decompile::expr::simplify_text(src, width);
}
Stmt::Call { args, name, .. } => {
*name = crate::translate::decompile::expr::simplify_text(name, width);
for a in args.iter_mut() {
*a = crate::translate::decompile::expr::simplify_text(a, width);
}
}
Stmt::IfBranch {
cond_text,
pre_body,
then_body,
else_body,
..
} => {
*cond_text = crate::translate::decompile::expr::simplify_text(cond_text, width);
simplify_body(pre_body, width);
simplify_body(then_body, width);
if let Some(eb) = else_body {
simplify_body(eb, width);
}
}
Stmt::Loop {
cond_text, body, ..
} => {
*cond_text = crate::translate::decompile::expr::simplify_text(cond_text, width);
simplify_body(body, width);
}
Stmt::ReturnExpr { text, .. } => {
*text = crate::translate::decompile::expr::simplify_text(text, width);
}
_ => {}
}
}
pub fn profile_inputs_from_fn(f: &crate::ast::FnDecl) -> crate::arch::x86::ProfileInputs {
let abi = f
.attrs
.iter()
.find_map(|a| match (&a.key, &a.value) {
(k, crate::ast::AttrValue::String(s)) if k == "abi" => Some(s.clone()),
_ => None,
})
.unwrap_or_default();
let noframe = f
.attrs
.iter()
.any(|a| a.key == "noframe" && matches!(a.value, crate::ast::AttrValue::Flag));
let cf = f
.attrs
.iter()
.any(|a| a.key == "cf" && matches!(a.value, crate::ast::AttrValue::Flag));
let bits = f
.attrs
.iter()
.find_map(|a| match (&a.key, &a.value) {
(k, crate::ast::AttrValue::Int(n)) if k == "bits" => Some(*n as u32),
_ => None,
})
.unwrap_or(32);
let mut max_neg_off: u32 = 0;
let mut stack_arg_count: u32 = 0;
let mut touched_regs: std::collections::HashSet<&str> = std::collections::HashSet::new();
for local in &f.locals {
match local.kind {
crate::ast::LocalKind::Register => {
touched_regs.insert(local.name.as_str());
}
crate::ast::LocalKind::Stack => {
if let Some(rest) = local.name.strip_prefix("var_") {
if let Ok(n) = u32::from_str_radix(rest, 16)
&& n > max_neg_off
{
max_neg_off = n;
}
} else if let Some(rest) = local.name.strip_prefix("arg_")
&& u32::from_str_radix(rest, 16).is_ok()
{
stack_arg_count += 1;
}
}
}
}
let body_has_call = body_contains_call(&f.body);
let frame_required = !noframe;
let saves_order: &[&str] = if bits == 64 {
&["rbx", "r12", "r13", "r14", "r15"]
} else {
&["ebx", "esi", "edi"]
};
let mut saves_used: Vec<String> = Vec::new();
for r in saves_order {
if reg_views_intersect(&touched_regs, r) {
saves_used.push((*r).to_string());
}
}
crate::arch::x86::ProfileInputs {
saves_used,
frame_required,
sub_esp: max_neg_off,
cf_protect: cf,
stack_arg_count,
abi,
bits,
frame_alt: bits == 64,
body_has_call,
}
}
fn body_contains_call(stmts: &[Stmt]) -> bool {
stmts.iter().any(|s| match s {
Stmt::Call { name, .. } => !name.starts_with("tail_"),
Stmt::IfBranch {
pre_body,
then_body,
else_body,
..
} => {
body_contains_call(pre_body)
|| body_contains_call(then_body)
|| else_body.as_deref().is_some_and(body_contains_call)
}
Stmt::Loop { body, .. } => body_contains_call(body),
Stmt::Asm { text, .. } => text.contains("call "),
_ => false,
})
}
fn reg_views_intersect(touched: &std::collections::HashSet<&str>, reg64: &str) -> bool {
let views: &[&str] = match reg64 {
"rbx" => &["rbx", "ebx", "bx", "bl", "bh"],
"r12" => &["r12", "r12d", "r12w", "r12b"],
"r13" => &["r13", "r13d", "r13w", "r13b"],
"r14" => &["r14", "r14d", "r14w", "r14b"],
"r15" => &["r15", "r15d", "r15w", "r15b"],
"ebx" => &["ebx", "bx", "bl", "bh"],
"esi" => &["esi", "si", "sil"],
"edi" => &["edi", "di", "dil"],
_ => return false,
};
views.iter().any(|v| touched.contains(*v))
}
fn is_subset(small: &[String], large: &[String]) -> bool {
small.iter().all(|s| large.contains(s))
}
fn describe_default_prologue(p: &crate::arch::x86::StructuredPrologue) -> Option<String> {
let mut parts: Vec<String> = Vec::new();
if !p.saves.is_empty() {
parts.push(format!("saves: [{}]", p.saves.join(", ")));
}
if p.frame {
parts.push("frame".into());
}
if p.sub_esp > 0 {
parts.push(format!("sub: {:#x}", p.sub_esp));
}
if !p.saves_after.is_empty() {
parts.push(format!("saves_after: [{}]", p.saves_after.join(", ")));
}
if p.cf_protect {
parts.push("cf_protect".into());
}
if parts.is_empty() {
None
} else {
Some(parts.join(", "))
}
}
fn describe_default_epilogue(e: &crate::arch::x86::StructuredEpilogue) -> Option<String> {
let mut parts: Vec<String> = Vec::new();
if !e.saves.is_empty() {
parts.push(format!("saves: [{}]", e.saves.join(", ")));
}
if e.leave {
parts.push("leave".into());
} else if e.pop_frame {
parts.push("pop_frame".into());
}
if e.add_esp > 0 {
parts.push(format!("add: {:#x}", e.add_esp));
}
if e.ret_imm > 0 {
parts.push(format!("ret: {:#x}", e.ret_imm));
}
if parts.is_empty() {
None
} else {
Some(parts.join(", "))
}
}
fn decode_prologue_params(
bytes: &[u8],
bits: crate::arch::x86::CodecBits,
) -> Option<crate::ast::PrologueParams> {
let p = crate::arch::x86::prologue_roundtrips(bytes, bits)?;
Some(crate::ast::PrologueParams {
saves: p.saves,
saves_after: p.saves_after,
frame: p.frame,
sub_esp: p.sub_esp,
cf_protect: p.cf_protect,
frame_alt: p.frame_alt_encoding,
})
}
fn decode_epilogue_params(
bytes: &[u8],
bits: crate::arch::x86::CodecBits,
) -> Option<crate::ast::EpilogueParams> {
let e = crate::arch::x86::epilogue_roundtrips(bytes, bits)?;
Some(crate::ast::EpilogueParams {
saves: e.saves,
leave: e.leave,
pop_frame: e.pop_frame,
add_esp: e.add_esp,
ret_imm: e.ret_imm,
})
}
fn function_bitness(f: &Function<DecodedInsn>) -> crate::arch::x86::Bitness {
use crate::arch::x86::{Bitness, CodeSize};
let first = f.blocks.first().and_then(|b| b.insns.first());
match first.map(|i| i.iced.code_size()) {
Some(CodeSize::Code16) => Bitness::Bits16,
Some(CodeSize::Code64) => Bitness::Bits64,
_ => Bitness::Bits32,
}
}
fn ud_ast_head_bytes_attr(attrs: &[crate::ast::Attribute]) -> Option<&[u8]> {
for a in attrs {
if a.key == "head_bytes"
&& let crate::ast::AttrValue::ByteList(b) = &a.value
{
return Some(b.as_slice());
}
}
None
}
fn is_gpr_name(s: &str) -> bool {
matches!(
s,
"eax"
| "ebx"
| "ecx"
| "edx"
| "esi"
| "edi"
| "ebp"
| "esp"
| "rax"
| "rbx"
| "rcx"
| "rdx"
| "rsi"
| "rdi"
| "rbp"
| "rsp"
| "r8"
| "r9"
| "r10"
| "r11"
| "r12"
| "r13"
| "r14"
| "r15"
)
}
const CALLER_SAVED: &[&str] = &[
"eax", "ecx", "edx", "rax", "rcx", "rdx", "rsi", "rdi", "r8", "r9", "r10", "r11",
];
fn fold_or_annotate_return(stmts: &mut Vec<Stmt>, i: usize, expr: &str) -> usize {
for k in (0..i).rev() {
match &stmts[k] {
Stmt::Move { dst, src, bytes } if (dst == "eax" || dst == "rax") && src == expr => {
let new_stmt = Stmt::ReturnExpr {
text: src.clone(),
bytes: bytes.clone(),
};
stmts[k] = new_stmt;
return 0;
}
Stmt::Call { .. }
| Stmt::Asm { .. }
| Stmt::Move { .. }
| Stmt::IfBranch { .. }
| Stmt::Loop { .. }
| Stmt::Switch { .. } => break,
_ => {}
}
}
let comment = Stmt::Comment(format!("returns {expr}"));
stmts.insert(i, comment);
1
}
fn annotate_return_values(stmts: &mut Vec<Stmt>) {
let mut state = RegState::default();
annotate_in_seq(stmts, &mut state);
}
fn annotate_in_seq(stmts: &mut Vec<Stmt>, state: &mut RegState) {
let mut i = 0;
while i < stmts.len() {
match &mut stmts[i] {
Stmt::Epilogue { .. } | Stmt::Return { .. } | Stmt::ReturnExpr { .. } => {
let ret = state
.values
.get("eax")
.or_else(|| state.values.get("rax"))
.cloned();
state.invalidate_all();
if let Some(expr) = ret
&& expr != "eax"
&& expr != "rax"
{
let added = fold_or_annotate_return(stmts, i, &expr);
i += added;
}
i += 1;
continue;
}
Stmt::Move { dst, src, .. } => {
if is_gpr_name(dst) {
state.write(dst, src.clone());
} else {
}
}
Stmt::Call { .. } => {
for r in CALLER_SAVED {
state.invalidate(r);
}
}
Stmt::Asm { text, .. } => {
let text_owned = text.clone();
asm_state_effect(&text_owned, state);
}
Stmt::IfBranch {
pre_body,
then_body,
else_body,
..
} => {
annotate_in_seq(pre_body, state);
let snapshot = state.clone();
annotate_in_seq(then_body, state);
if let Some(eb) = else_body {
*state = snapshot;
annotate_in_seq(eb, state);
}
state.invalidate_all();
}
Stmt::Loop { body, .. } | Stmt::WhileBlock { body, .. } => {
state.invalidate_all();
annotate_in_seq(body, state);
state.invalidate_all();
}
Stmt::IfBlock {
then_body,
else_body,
..
} => {
state.invalidate_all();
annotate_in_seq(then_body, state);
state.invalidate_all();
annotate_in_seq(else_body, state);
state.invalidate_all();
}
Stmt::Prologue { .. }
| Stmt::ArgSpill { .. }
| Stmt::LocalSet { .. }
| Stmt::LocalArith { .. }
| Stmt::LocalCompound { .. }
| Stmt::Inc16 { .. }
| Stmt::RegArith { .. } => {
state.invalidate_all();
}
Stmt::Save { reg, .. } | Stmt::Restore { reg, .. } => {
if is_gpr_name(reg) {
state.invalidate(reg);
}
}
Stmt::IfReturn { .. }
| Stmt::Goto { .. }
| Stmt::IfGoto { .. }
| Stmt::Switch { .. }
| Stmt::SehInstall { .. }
| Stmt::SehRestore { .. }
| Stmt::Label { .. }
| Stmt::Comment(_) => {
}
}
i += 1;
}
}
fn resolve_indirect_call_names(stmts: &mut [Stmt], name_at: &HashMap<u64, String>) {
for stmt in stmts.iter_mut() {
match stmt {
Stmt::Call { name, .. } => {
if let Some(resolved) = resolve_absolute_indirect(name, name_at) {
*name = resolved;
}
}
Stmt::IfBranch {
pre_body,
then_body,
else_body,
..
} => {
resolve_indirect_call_names(pre_body, name_at);
resolve_indirect_call_names(then_body, name_at);
if let Some(eb) = else_body {
resolve_indirect_call_names(eb, name_at);
}
}
Stmt::Loop { body, .. } => {
resolve_indirect_call_names(body, name_at);
}
_ => {}
}
}
}
fn resolve_absolute_indirect(text: &str, name_at: &HashMap<u64, String>) -> Option<String> {
let inner = text.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()?;
name_at.get(&va).cloned()
}
fn fold_early_returns(stmts: &mut [Stmt], base_ip: u64, bitness: crate::arch::x86::Bitness) {
let returns_at = collect_return_targets(stmts, base_ip);
let mut cursor = base_ip;
fold_early_returns_in_seq(stmts, &mut cursor, &returns_at, bitness);
}
fn fold_early_returns_in_seq(
stmts: &mut [Stmt],
cursor: &mut u64,
returns_at: &HashMap<u64, String>,
bitness: crate::arch::x86::Bitness,
) {
let mut i = 0;
while i < stmts.len() {
let stmt_start = *cursor;
let advance = stmt_total_bytes(&stmts[i]);
let mut replaced = false;
if let Stmt::Asm { text, bytes } = &stmts[i]
&& let Some(rewrite) =
try_fold_to_if_return(text, bytes, stmt_start, returns_at, bitness)
{
stmts[i] = rewrite;
replaced = true;
}
if !replaced {
match &mut stmts[i] {
Stmt::IfBranch {
attrs,
cond_bytes,
pre_body,
then_body,
else_body,
..
} => {
let mut sub = *cursor;
if let Some(hb) = ud_ast_head_bytes_attr(attrs) {
sub += hb.len() as u64;
}
fold_early_returns_in_seq(pre_body, &mut sub, returns_at, bitness);
sub += cond_bytes.len() as u64;
fold_early_returns_in_seq(then_body, &mut sub, returns_at, bitness);
if let Some(eb) = else_body {
fold_early_returns_in_seq(eb, &mut sub, returns_at, bitness);
}
let _ = sub;
}
Stmt::Loop {
entry_jmp_bytes,
body,
tail_bytes,
..
} => {
let mut sub = *cursor;
if let Some(jmp) = entry_jmp_bytes {
sub += jmp.len() as u64;
}
fold_early_returns_in_seq(body, &mut sub, returns_at, bitness);
sub += tail_bytes.len() as u64;
let _ = sub;
}
_ => {}
}
}
*cursor += advance as u64;
i += 1;
}
}
fn try_fold_to_if_return(
text: &str,
bytes: &[u8],
stmt_start: u64,
returns_at: &HashMap<u64, String>,
bitness: crate::arch::x86::Bitness,
) -> Option<Stmt> {
use crate::arch::x86::{FlowControl, Mnemonic};
if !(text.starts_with("cmp ") || text.starts_with("test ")) {
return None;
}
if !text.contains("; j") {
return None;
}
let insns = crate::arch::x86::decode(bitness, bytes, stmt_start).ok()?;
if insns.len() != 2 {
return None;
}
let cmp = &insns[0];
let jcc = &insns[1];
if !matches!(cmp.iced.mnemonic(), Mnemonic::Cmp | Mnemonic::Test) {
return None;
}
if jcc.iced.flow_control() != FlowControl::ConditionalBranch {
return None;
}
let target = jcc.iced.near_branch_target();
if target == 0 {
return None;
}
let value_text = returns_at.get(&target)?.clone();
let body_form = crate::arch::x86::render_cond_source(&cmp.iced, &jcc.iced);
let cond_text = invert_relational_cond(&body_form);
let jcc_bytes_len = jcc.original_bytes.len();
let cmp_bytes = bytes[..bytes.len() - jcc_bytes_len].to_vec();
let cond_code = crate::arch::x86::jcc_cond_code_from_bytes(&jcc.original_bytes)?;
let wide = jcc_bytes_len > 2;
Some(Stmt::IfReturn {
cond_text,
value_text,
target_addr: target,
cmp_bytes,
cond_code,
wide,
})
}
fn collect_return_targets(stmts: &[Stmt], base_ip: u64) -> HashMap<u64, String> {
let mut out: HashMap<u64, String> = HashMap::new();
let mut cursor = base_ip;
collect_return_targets_in_seq(stmts, &mut cursor, &mut out);
out
}
fn collect_return_targets_in_seq(stmts: &[Stmt], cursor: &mut u64, out: &mut HashMap<u64, String>) {
let mut i = 0;
while i < stmts.len() {
let chunk_start = *cursor;
let mut j = i;
let mut value: Option<String> = None;
let mut steps = 0;
loop {
if j >= stmts.len() {
break;
}
match &stmts[j] {
Stmt::Move { dst, src, .. } if is_gpr_name(dst) => {
if dst == "eax" || dst == "rax" {
value = Some(src.clone());
}
j += 1;
steps += 1;
if steps > 4 {
break;
}
}
Stmt::Comment(text) => {
if let Some(v) = text.strip_prefix("returns ") {
value = Some(v.to_string());
}
j += 1;
}
Stmt::Epilogue { .. } | Stmt::Return { .. } => {
let v = value.unwrap_or_default();
out.insert(chunk_start, v);
break;
}
_ => break,
}
}
let first_bytes = stmt_total_bytes(&stmts[i]);
*cursor += first_bytes as u64;
match &stmts[i] {
Stmt::IfBranch {
attrs,
cond_bytes,
pre_body,
then_body,
else_body,
..
} => {
let mut sub = chunk_start;
if let Some(hb) = ud_ast_head_bytes_attr(attrs) {
sub += hb.len() as u64;
}
collect_return_targets_in_seq(pre_body, &mut sub, out);
sub += cond_bytes.len() as u64;
collect_return_targets_in_seq(then_body, &mut sub, out);
if let Some(eb) = else_body {
collect_return_targets_in_seq(eb, &mut sub, out);
}
}
Stmt::Loop {
entry_jmp_bytes,
body,
..
} => {
let mut sub = chunk_start;
if let Some(jmp) = entry_jmp_bytes {
sub += jmp.len() as u64;
}
collect_return_targets_in_seq(body, &mut sub, out);
}
_ => {}
}
i += 1;
}
}
fn switch_encoded_size(stmt: &Stmt) -> usize {
let Stmt::Switch {
cases, dispatch, ..
} = stmt
else {
return 0;
};
if dispatch != "msvc-jmp-table" {
return 0;
}
let max_value = u32::try_from(cases.len().saturating_sub(1)).unwrap_or(u32::MAX);
let cmp = if max_value <= 0x7f { 3 } else { 6 };
cmp + 6 + 7
}
#[allow(clippy::too_many_lines)]
fn stmt_total_bytes_at(stmt: &Stmt, cursor: u64) -> usize {
#[allow(clippy::match_same_arms)]
match stmt {
Stmt::Asm { bytes, .. }
| Stmt::Return { bytes, .. }
| Stmt::Prologue { bytes, .. }
| Stmt::Epilogue { bytes, .. }
| Stmt::Save { bytes, .. }
| Stmt::Restore { bytes, .. }
| Stmt::ReturnExpr { bytes, .. }
| Stmt::ArgSpill { bytes, .. }
| Stmt::LocalSet { bytes, .. }
| Stmt::LocalArith { bytes, .. }
| Stmt::LocalCompound { bytes, .. }
| Stmt::Move { bytes, .. }
| Stmt::Inc16 { bytes, .. }
| Stmt::SehInstall { bytes }
| Stmt::SehRestore { bytes }
| Stmt::RegArith { bytes, .. } => bytes.len(),
Stmt::Call {
bytes,
direct_target,
..
} => bytes.len() + if direct_target.is_some() { 5 } else { 0 },
Stmt::Goto { target_addr, wide } => {
crate::arch::x86::encoded_jmp_size(cursor, *target_addr, *wide)
}
Stmt::IfGoto {
cmp_bytes,
target_addr,
wide,
..
}
| Stmt::IfReturn {
cmp_bytes,
target_addr,
wide,
..
} => {
let jcc_ip = cursor.saturating_add(cmp_bytes.len() as u64);
cmp_bytes.len() + crate::arch::x86::encoded_jcc_size(jcc_ip, *target_addr, *wide)
}
Stmt::IfBranch {
attrs,
cond_bytes,
pre_body,
then_body,
else_body,
..
} => {
let mut sub = cursor;
if let Some(hb) = ud_ast_head_bytes_attr(attrs) {
sub += hb.len() as u64;
}
let mut n = (sub - cursor) as usize;
let pre_n = stmts_total_bytes_at(pre_body, sub);
n += pre_n;
sub += pre_n as u64;
n += cond_bytes.len();
sub += cond_bytes.len() as u64;
let then_n = stmts_total_bytes_at(then_body, sub);
n += then_n;
sub += then_n as u64;
if let Some(eb) = else_body {
n += stmts_total_bytes_at(eb, sub);
}
n
}
Stmt::Loop {
entry_jmp_bytes,
body,
tail_bytes,
..
} => {
let mut sub = cursor;
let mut n = 0;
if let Some(jmp) = entry_jmp_bytes {
n += jmp.len();
sub += jmp.len() as u64;
}
let body_n = stmts_total_bytes_at(body, sub);
n += body_n;
n += tail_bytes.len();
n
}
Stmt::IfBlock {
cond_bytes,
then_body,
then_tail_jmp,
else_body,
..
} => {
let mut n = cond_bytes.len();
let mut sub = cursor + n as u64;
let then_n = stmts_total_bytes_at(then_body, sub);
n += then_n;
sub += then_n as u64;
n += then_tail_jmp.len();
sub += then_tail_jmp.len() as u64;
n += stmts_total_bytes_at(else_body, sub);
n
}
Stmt::WhileBlock {
entry_bytes,
body,
tail_bytes,
..
} => {
let n_entry = entry_bytes.len();
let body_n = stmts_total_bytes_at(body, cursor + n_entry as u64);
n_entry + body_n + tail_bytes.len()
}
Stmt::Label { .. } | Stmt::Comment(_) => 0,
Stmt::Switch { .. } => switch_encoded_size(stmt),
}
}
fn stmt_total_bytes(stmt: &Stmt) -> usize {
stmt_total_bytes_at(stmt, 0)
}
fn stmts_total_bytes_at(stmts: &[Stmt], mut cursor: u64) -> usize {
let mut n = 0;
for s in stmts {
let sz = stmt_total_bytes_at(s, cursor);
n += sz;
cursor += sz as u64;
}
n
}
pub fn lowered_body_size_at(body: &[Stmt], ip: u64) -> u64 {
stmts_total_bytes_at(body, ip) as u64
}
#[allow(dead_code)]
fn stmts_total_bytes(stmts: &[Stmt]) -> usize {
stmts_total_bytes_at(stmts, 0)
}
fn fold_dead_register_moves(
stmts: &mut Vec<Stmt>,
liveness: &crate::translate::decompile::ssa::Liveness,
base_ip: u64,
) {
let mut cursor = base_ip;
let mut i = 0;
while i < stmts.len() {
let stmt_start = cursor;
let stmt_len = stmt_total_bytes(&stmts[i]);
let mut deleted = false;
if let Stmt::Move { dst, bytes, .. } = &stmts[i]
&& is_gpr_name(dst)
&& !bytes.is_empty()
{
let last_insn_ip = stmt_start;
let var = crate::translate::decompile::ssa::Var::Reg(dst.clone());
let dead_after = liveness
.live_after_insn
.get(&last_insn_ip)
.is_some_and(|live| !live.contains(&var));
let forwarded = false;
if dead_after || forwarded {
let prefix = match &stmts[i] {
Stmt::Move { bytes, .. } => bytes.clone(),
_ => unreachable!(),
};
let next_is_transfer = stmts.get(i + 1).is_some_and(stmt_is_control_transfer);
if !next_is_transfer
&& let Some(next_bytes) = stmts.get_mut(i + 1).and_then(stmt_bytes_field_mut)
{
let mut merged = Vec::with_capacity(prefix.len() + next_bytes.len());
merged.extend_from_slice(&prefix);
merged.extend_from_slice(next_bytes);
*next_bytes = merged;
stmts.remove(i);
deleted = true;
}
}
}
if !deleted {
cursor += stmt_len as u64;
i += 1;
}
}
}
fn stmt_is_control_transfer(stmt: &Stmt) -> bool {
match stmt {
Stmt::Goto { .. }
| Stmt::IfGoto { .. }
| Stmt::IfReturn { .. }
| Stmt::Return { .. }
| Stmt::ReturnExpr { .. }
| Stmt::Epilogue { .. }
| Stmt::Call { .. } => true,
Stmt::Asm { text, .. } => {
text.starts_with("jmp")
|| text.starts_with("call")
|| text.starts_with("ret")
|| (text.starts_with('j')
&& text.len() >= 2
&& text.as_bytes()[1].is_ascii_alphabetic())
}
_ => false,
}
}
fn stmt_bytes_field_mut(stmt: &mut Stmt) -> Option<&mut Vec<u8>> {
match stmt {
Stmt::Asm { bytes, .. }
| Stmt::Return { bytes, .. }
| Stmt::Prologue { bytes, .. }
| Stmt::Epilogue { bytes, .. }
| Stmt::Save { bytes, .. }
| Stmt::Restore { bytes, .. }
| Stmt::ReturnExpr { bytes, .. }
| Stmt::ArgSpill { bytes, .. }
| Stmt::Call { bytes, .. }
| Stmt::LocalSet { bytes, .. }
| Stmt::LocalArith { bytes, .. }
| Stmt::LocalCompound { bytes, .. }
| Stmt::Move { bytes, .. }
| Stmt::Inc16 { bytes, .. }
| Stmt::SehInstall { bytes }
| Stmt::SehRestore { bytes }
| Stmt::RegArith { bytes, .. } => Some(bytes),
Stmt::IfGoto { .. }
| Stmt::IfReturn { .. }
| Stmt::IfBranch { .. }
| Stmt::Loop { .. }
| Stmt::IfBlock { .. }
| Stmt::WhileBlock { .. }
| Stmt::Label { .. }
| Stmt::Comment(_)
| Stmt::Goto { .. }
| Stmt::Switch { .. } => None,
}
}
fn rewrite_ecx_as_this(stmts: &mut [Stmt]) {
for stmt in stmts.iter_mut() {
rewrite_ecx_in_stmt(stmt);
}
}
fn rewrite_ecx_in_stmt(stmt: &mut Stmt) {
fn apply(text: &mut String) {
let new_text = apply_this_rewrite(text);
if new_text != *text {
*text = new_text;
}
}
match stmt {
Stmt::Move { dst, src, .. } => {
apply(dst);
apply(src);
}
Stmt::Call { name, args, .. } => {
apply(name);
for a in args.iter_mut() {
apply(a);
}
}
Stmt::IfBranch {
cond_text,
pre_body,
then_body,
else_body,
..
} => {
apply(cond_text);
rewrite_ecx_as_this(pre_body);
rewrite_ecx_as_this(then_body);
if let Some(eb) = else_body {
rewrite_ecx_as_this(eb);
}
}
Stmt::Loop {
cond_text, body, ..
}
| Stmt::WhileBlock {
cond_text, body, ..
} => {
apply(cond_text);
rewrite_ecx_as_this(body);
}
Stmt::IfBlock {
cond_text,
then_body,
else_body,
..
} => {
apply(cond_text);
rewrite_ecx_as_this(then_body);
rewrite_ecx_as_this(else_body);
}
Stmt::ReturnExpr { text, .. } => apply(text),
Stmt::IfReturn {
cond_text,
value_text,
..
} => {
apply(cond_text);
apply(value_text);
}
Stmt::IfGoto { cond_text, .. } => apply(cond_text),
_ => {}
}
}
fn build_param_register_map(
sig: Option<&Signature>,
attrs: &[crate::ast::Attribute],
bitness: crate::arch::x86::Bitness,
) -> Vec<(String, String)> {
let Some(sig) = sig else {
return Vec::new();
};
let abi = attrs.iter().find_map(|a| match (&a.key, &a.value) {
(k, crate::ast::AttrValue::String(s)) if k == "abi" => Some(s.as_str()),
_ => None,
});
let regs64_sysv = ["rdi", "rsi", "rdx", "rcx", "r8", "r9"];
let regs64_win = ["rcx", "rdx", "r8", "r9"];
let is_ms = matches!(abi, Some("ms" | "win" | "microsoft"));
let convention: &[&str] = match (bitness, is_ms) {
(crate::arch::x86::Bitness::Bits64, true) => ®s64_win,
(crate::arch::x86::Bitness::Bits64, false) => ®s64_sysv,
_ => return Vec::new(),
};
let mut out: Vec<(String, String)> = Vec::new();
let mut int_idx = 0usize;
for p in &sig.params {
if p.name.is_empty() {
int_idx += 1;
continue;
}
if int_idx >= convention.len() {
break;
}
if !param_is_integer_like(&p.ty) {
continue;
}
let reg64 = convention[int_idx];
for view in register_views(reg64) {
out.push(((*view).to_string(), p.name.clone()));
}
int_idx += 1;
}
out
}
fn param_is_integer_like(ty: &crate::ast::Type) -> bool {
matches!(
ty,
crate::ast::Type::I8
| crate::ast::Type::I16
| crate::ast::Type::I32
| crate::ast::Type::I64
| crate::ast::Type::U8
| crate::ast::Type::U16
| crate::ast::Type::U32
| crate::ast::Type::U64
| crate::ast::Type::Bool
| crate::ast::Type::Char
| crate::ast::Type::Pointer(_)
| crate::ast::Type::Unknown
)
}
fn register_views(reg64: &str) -> &'static [&'static str] {
match reg64 {
"rdi" => &["rdi", "edi", "di", "dil"],
"rsi" => &["rsi", "esi", "si", "sil"],
"rdx" => &["rdx", "edx", "dx", "dl"],
"rcx" => &["rcx", "ecx", "cx", "cl"],
"r8" => &["r8", "r8d", "r8w", "r8b"],
"r9" => &["r9", "r9d", "r9w", "r9b"],
_ => &[],
}
}
fn rewrite_param_register_reads(stmts: &mut [Stmt], param_regs: &[(String, String)]) {
let mut live: std::collections::HashMap<String, String> = param_regs.iter().cloned().collect();
rewrite_param_in_seq(stmts, &mut live);
}
fn rewrite_param_in_seq(stmts: &mut [Stmt], live: &mut std::collections::HashMap<String, String>) {
for stmt in stmts.iter_mut() {
match stmt {
Stmt::Move { dst, src, .. } => {
*src = substitute_word_tokens(src, live);
let invalidated = views_invalidated_by(dst);
for v in invalidated {
live.remove(*v);
}
}
Stmt::Call { name, args, .. } => {
*name = substitute_word_tokens(name, live);
for a in args.iter_mut() {
*a = substitute_word_tokens(a, live);
}
live.clear();
}
Stmt::Asm { text, .. } | Stmt::ReturnExpr { text, .. } => {
*text = substitute_word_tokens(text, live);
}
Stmt::IfBranch {
cond_text,
pre_body,
then_body,
else_body,
..
} => {
*cond_text = substitute_word_tokens(cond_text, live);
rewrite_param_in_seq(pre_body, live);
let mut then_live = live.clone();
rewrite_param_in_seq(then_body, &mut then_live);
if let Some(eb) = else_body {
let mut else_live = live.clone();
rewrite_param_in_seq(eb, &mut else_live);
}
live.clear();
}
Stmt::Loop {
cond_text, body, ..
} => {
*cond_text = substitute_word_tokens(cond_text, live);
rewrite_param_in_seq(body, live);
live.clear();
}
Stmt::IfReturn {
cond_text,
value_text,
..
} => {
*cond_text = substitute_word_tokens(cond_text, live);
*value_text = substitute_word_tokens(value_text, live);
}
Stmt::IfGoto { cond_text, .. } => {
*cond_text = substitute_word_tokens(cond_text, live);
}
_ => {}
}
}
}
fn views_invalidated_by(dst: &str) -> &'static [&'static str] {
match dst {
"rdi" | "edi" | "di" | "dil" => &["rdi", "edi", "di", "dil"],
"rsi" | "esi" | "si" | "sil" => &["rsi", "esi", "si", "sil"],
"rdx" | "edx" | "dx" | "dl" => &["rdx", "edx", "dx", "dl"],
"rcx" | "ecx" | "cx" | "cl" => &["rcx", "ecx", "cx", "cl"],
"r8" | "r8d" | "r8w" | "r8b" => &["r8", "r8d", "r8w", "r8b"],
"r9" | "r9d" | "r9w" | "r9b" => &["r9", "r9d", "r9w", "r9b"],
_ => &[],
}
}
fn substitute_word_tokens(text: &str, subs: &std::collections::HashMap<String, String>) -> String {
if subs.is_empty() {
return text.to_string();
}
let mut out = String::with_capacity(text.len());
let mut iter = text.char_indices().peekable();
let mut in_string = false;
let mut escape = false;
while let Some((i, c)) = iter.next() {
if in_string {
out.push(c);
if escape {
escape = false;
} else if c == '\\' {
escape = true;
} else if c == '"' {
in_string = false;
}
continue;
}
if c == '"' {
in_string = true;
out.push('"');
continue;
}
if c.is_ascii_alphabetic() || c == '_' {
let start = i;
let mut end = i + c.len_utf8();
while let Some(&(j, nc)) = iter.peek() {
if nc.is_ascii_alphanumeric() || nc == '_' {
end = j + nc.len_utf8();
iter.next();
} else {
break;
}
}
let word = &text[start..end];
let prev = text[..start].chars().last();
let word_boundary_ok = prev.is_none_or(|p| !p.is_ascii_alphanumeric() && p != '_');
if word_boundary_ok && let Some(replacement) = subs.get(word) {
out.push_str(replacement);
continue;
}
out.push_str(word);
continue;
}
out.push(c);
}
out
}
fn collect_label_addrs(stmts: &[Stmt]) -> std::collections::HashSet<u64> {
use std::collections::HashSet;
let mut out: HashSet<u64> = HashSet::new();
walk_collect_labels(stmts, &mut out);
out
}
fn walk_collect_labels(stmts: &[Stmt], out: &mut std::collections::HashSet<u64>) {
for s in stmts {
match s {
Stmt::Label { addr } => {
out.insert(*addr);
}
Stmt::IfBranch {
pre_body,
then_body,
else_body,
..
} => {
walk_collect_labels(pre_body, out);
walk_collect_labels(then_body, out);
if let Some(eb) = else_body {
walk_collect_labels(eb, out);
}
}
Stmt::Loop { body, .. } => walk_collect_labels(body, out),
_ => {}
}
}
}
fn rewrite_label_refs(stmts: &mut [Stmt], labels: &std::collections::HashSet<u64>) {
for stmt in stmts.iter_mut() {
rewrite_label_refs_in_stmt(stmt, labels);
}
}
fn rewrite_label_refs_in_stmt(stmt: &mut Stmt, labels: &std::collections::HashSet<u64>) {
match stmt {
Stmt::Asm { text, .. } => {
let new_text = rewrite_label_refs_in_text(text, labels);
if new_text != *text {
*text = new_text;
}
}
Stmt::IfBranch {
pre_body,
then_body,
else_body,
..
} => {
rewrite_label_refs(pre_body, labels);
rewrite_label_refs(then_body, labels);
if let Some(eb) = else_body {
rewrite_label_refs(eb, labels);
}
}
Stmt::Loop { body, .. } => rewrite_label_refs(body, labels),
_ => {}
}
}
fn rewrite_intel_hex_in_body(stmts: &mut [Stmt]) {
for stmt in stmts.iter_mut() {
rewrite_intel_hex_in_stmt(stmt);
}
}
fn rewrite_intel_hex_in_stmt(stmt: &mut Stmt) {
fn apply(text: &mut String) {
let new_text = rewrite_intel_hex_in_text(text);
if new_text != *text {
*text = new_text;
}
}
match stmt {
Stmt::Move { dst, src, .. } => {
apply(dst);
apply(src);
}
Stmt::Call { name, args, .. } => {
apply(name);
for a in args.iter_mut() {
apply(a);
}
}
Stmt::Asm { text, .. } | Stmt::ReturnExpr { text, .. } => apply(text),
Stmt::IfBranch {
cond_text,
pre_body,
then_body,
else_body,
..
} => {
apply(cond_text);
rewrite_intel_hex_in_body(pre_body);
rewrite_intel_hex_in_body(then_body);
if let Some(eb) = else_body {
rewrite_intel_hex_in_body(eb);
}
}
Stmt::Loop {
cond_text, body, ..
} => {
apply(cond_text);
rewrite_intel_hex_in_body(body);
}
Stmt::IfReturn {
cond_text,
value_text,
..
} => {
apply(cond_text);
apply(value_text);
}
Stmt::IfGoto { cond_text, .. } => apply(cond_text),
_ => {}
}
}
fn rewrite_intel_hex_in_text(text: &str) -> String {
use std::fmt::Write as _;
let mut out = String::with_capacity(text.len());
let mut iter = text.char_indices().peekable();
let mut in_string = false;
let mut escape = false;
while let Some((i, c)) = iter.next() {
if in_string {
out.push(c);
if escape {
escape = false;
} else if c == '\\' {
escape = true;
} else if c == '"' {
in_string = false;
}
continue;
}
if c == '"' {
in_string = true;
out.push('"');
continue;
}
if c.is_ascii_digit() {
let prev = text[..i].chars().last();
let boundary_ok = prev.is_none_or(|p| !p.is_ascii_alphanumeric() && p != '_');
if boundary_ok {
let start = i;
let mut end = i + c.len_utf8();
while let Some(&(j, nc)) = iter.peek() {
if nc.is_ascii_hexdigit() {
end = j + nc.len_utf8();
iter.next();
} else {
break;
}
}
if let Some(&(_, 'h')) = iter.peek() {
let hex = &text[start..end];
if let Ok(n) = u64::from_str_radix(hex, 16) {
iter.next(); let _ = write!(out, "0x{n:x}");
continue;
}
}
out.push_str(&text[start..end]);
continue;
}
}
out.push(c);
}
out
}
fn rewrite_label_refs_in_text(text: &str, labels: &std::collections::HashSet<u64>) -> String {
use std::fmt::Write as _;
let mut out = String::with_capacity(text.len());
let mut iter = text.char_indices().peekable();
let mut in_string = false;
let mut escape = false;
while let Some((i, c)) = iter.next() {
if in_string {
out.push(c);
if escape {
escape = false;
} else if c == '\\' {
escape = true;
} else if c == '"' {
in_string = false;
}
continue;
}
if c == '"' {
in_string = true;
out.push('"');
continue;
}
if c.is_ascii_hexdigit() {
let prev = text[..i].chars().last();
let boundary_ok = prev.is_none_or(|p| !p.is_ascii_alphanumeric() && p != '_');
if boundary_ok {
let start = i;
let mut end = i + c.len_utf8();
while let Some(&(j, nc)) = iter.peek() {
if nc.is_ascii_hexdigit() {
end = j + nc.len_utf8();
iter.next();
} else {
break;
}
}
if let Some(&(j, 'h')) = iter.peek() {
let hex = &text[start..end];
if let Ok(addr) = u64::from_str_radix(hex, 16)
&& labels.contains(&addr)
{
iter.next(); let _ = j;
let _ = write!(out, "label_{addr:x}");
continue;
}
}
out.push_str(&text[start..end]);
continue;
}
}
out.push(c);
}
out
}
fn build_stack_local_map(locals: &[crate::ast::LocalDecl]) -> Vec<(i64, String)> {
let mut out: Vec<(i64, String)> = Vec::new();
for l in locals {
if !matches!(l.kind, crate::ast::LocalKind::Stack) {
continue;
}
if let Some(rest) = l.name.strip_prefix("var_") {
if let Ok(n) = i64::from_str_radix(rest, 16) {
out.push((-n, l.name.clone()));
}
} else if let Some(rest) = l.name.strip_prefix("arg_")
&& let Ok(n) = i64::from_str_radix(rest, 16)
{
out.push((n, l.name.clone()));
}
}
out
}
fn rewrite_stack_refs(stmts: &mut [Stmt], map: &[(i64, String)]) {
for stmt in stmts.iter_mut() {
rewrite_stack_refs_in_stmt(stmt, map);
}
}
fn rewrite_stack_refs_in_stmt(stmt: &mut Stmt, map: &[(i64, String)]) {
fn apply(text: &mut String, map: &[(i64, String)]) {
let new_text = rewrite_stack_refs_in_text(text, map);
if new_text != *text {
*text = new_text;
}
}
match stmt {
Stmt::Move { dst, src, .. } => {
apply(dst, map);
apply(src, map);
}
Stmt::Call { name, args, .. } => {
apply(name, map);
for a in args.iter_mut() {
apply(a, map);
}
}
Stmt::Asm { text, .. } | Stmt::ReturnExpr { text, .. } => apply(text, map),
Stmt::IfBranch {
cond_text,
pre_body,
then_body,
else_body,
..
} => {
apply(cond_text, map);
rewrite_stack_refs(pre_body, map);
rewrite_stack_refs(then_body, map);
if let Some(eb) = else_body {
rewrite_stack_refs(eb, map);
}
}
Stmt::Loop {
cond_text, body, ..
} => {
apply(cond_text, map);
rewrite_stack_refs(body, map);
}
Stmt::IfReturn {
cond_text,
value_text,
..
} => {
apply(cond_text, map);
apply(value_text, map);
}
Stmt::IfGoto { cond_text, .. } => apply(cond_text, map),
_ => {}
}
}
fn rewrite_stack_refs_in_text(text: &str, map: &[(i64, String)]) -> String {
let mut out = String::with_capacity(text.len());
let mut iter = text.char_indices().peekable();
let mut in_string = false;
let mut escape = false;
while let Some((i, c)) = iter.next() {
if in_string {
out.push(c);
if escape {
escape = false;
} else if c == '\\' {
escape = true;
} else if c == '"' {
in_string = false;
}
continue;
}
if c == '"' {
in_string = true;
out.push('"');
continue;
}
if matches!(c, '[' | 'd' | 'q' | 'w' | 'b' | 'x' | 't')
&& let Some((name, consumed)) = try_match_stack_ref(&text[i..], map)
{
out.push_str(name);
let target = i + consumed;
while iter.peek().is_some_and(|&(j, _)| j < target) {
iter.next();
}
continue;
}
out.push(c);
}
out
}
fn try_match_stack_ref<'a>(text: &str, map: &'a [(i64, String)]) -> Option<(&'a String, usize)> {
let lc_full = text.to_ascii_lowercase();
let bytes = lc_full.as_bytes();
let mut i = 0;
for prefix in &[
"xmmword ptr ",
"qword ptr ",
"dword ptr ",
"word ptr ",
"byte ptr ",
"tbyte ptr ",
] {
if lc_full[i..].starts_with(prefix) {
i += prefix.len();
break;
}
}
if !lc_full[i..].starts_with("[rbp") && !lc_full[i..].starts_with("[ebp") {
return None;
}
i += 4; while i < bytes.len() && bytes[i] == b' ' {
i += 1;
}
if i < bytes.len() && bytes[i] == b']' {
let name = map.iter().find(|(off, _)| *off == 0).map(|(_, n)| n)?;
return Some((name, i + 1));
}
let sign: i64 = match bytes.get(i) {
Some(b'+') => 1,
Some(b'-') => -1,
_ => return None,
};
i += 1;
while i < bytes.len() && bytes[i] == b' ' {
i += 1;
}
let num_start = i;
let value: i64 = if lc_full[i..].starts_with("0x") {
i += 2;
while i < bytes.len() && bytes[i].is_ascii_hexdigit() {
i += 1;
}
if i == num_start + 2 {
return None;
}
i64::from_str_radix(&lc_full[num_start + 2..i], 16).ok()?
} else {
while i < bytes.len() && bytes[i].is_ascii_hexdigit() {
i += 1;
}
if i == num_start {
return None;
}
if bytes.get(i) == Some(&b'h') {
let v = i64::from_str_radix(&lc_full[num_start..i], 16).ok()?;
i += 1; v
} else {
if lc_full[num_start..i].bytes().all(|b| b.is_ascii_digit()) {
lc_full[num_start..i].parse::<i64>().ok()?
} else {
return None;
}
}
};
while i < bytes.len() && bytes[i] == b' ' {
i += 1;
}
if bytes.get(i) != Some(&b']') {
return None;
}
i += 1;
let offset = sign * value;
let name = map.iter().find(|(off, _)| *off == offset).map(|(_, n)| n)?;
Some((name, i))
}
fn apply_this_rewrite(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
if i + 4 <= bytes.len() && &bytes[i..i + 4] == b"[ecx" {
let mut depth = 1;
let mut j = i + 4;
while j < bytes.len() {
match bytes[j] {
b'[' => depth += 1,
b']' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
j += 1;
}
if j < bytes.len() && depth == 0 {
let inner = std::str::from_utf8(&bytes[i + 1..j]).unwrap_or("");
if let Some(field) = parse_ecx_field(inner) {
out.push_str(&field);
i = j + 1;
continue;
}
}
}
if word_at(bytes, i, b"ecx") {
out.push_str("this");
i += 3;
continue;
}
out.push(bytes[i] as char);
i += 1;
}
out
}
fn word_at(bytes: &[u8], i: usize, word: &[u8]) -> bool {
if i + word.len() > bytes.len() {
return false;
}
if &bytes[i..i + word.len()] != word {
return false;
}
let prev_is_ident = i > 0 && is_ident_byte(bytes[i - 1]);
let next_is_ident = i + word.len() < bytes.len() && is_ident_byte(bytes[i + word.len()]);
!prev_is_ident && !next_is_ident
}
fn is_ident_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
fn parse_ecx_field(inner: &str) -> Option<String> {
let s = inner.trim();
if s == "ecx" {
return Some("this->f_0".to_string());
}
let rest = s.strip_prefix("ecx")?;
let rest = rest.trim();
let sign = if let Some(r) = rest.strip_prefix('+') {
("", r)
} else {
("minus_", rest.strip_prefix('-')?)
};
let raw = sign.1.trim();
let parsed = parse_simple_uint(raw)?;
Some(format!("this->f_{}{:x}", sign.0, parsed))
}
fn parse_simple_uint(s: &str) -> Option<u64> {
if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
return u64::from_str_radix(rest, 16).ok();
}
if let Some(rest) = s.strip_suffix('h').or_else(|| s.strip_suffix('H')) {
return u64::from_str_radix(rest, 16).ok();
}
s.parse::<u64>().ok()
}
fn fold_seh_frame(stmts: &mut [Stmt]) {
for stmt in stmts.iter_mut() {
match stmt {
Stmt::Move { dst, src, bytes } if dst == "fs:[0]" => {
let bytes = std::mem::take(bytes);
if src == "esp" {
*stmt = Stmt::SehInstall { bytes };
} else {
*stmt = Stmt::SehRestore { bytes };
}
}
Stmt::IfBranch {
pre_body,
then_body,
else_body,
..
} => {
fold_seh_frame(pre_body);
fold_seh_frame(then_body);
if let Some(eb) = else_body {
fold_seh_frame(eb);
}
}
Stmt::Loop { body, .. } => fold_seh_frame(body),
_ => {}
}
}
}
fn fold_switch_jump_tables(
stmts: &mut Vec<Stmt>,
base_ip: u64,
bitness: crate::arch::x86::Bitness,
data: &dyn DataLookup,
) {
let label_previews = collect_label_previews(stmts, base_ip);
let mut cursor = base_ip;
let mut i = 0;
while i < stmts.len() {
let cmp_start = cursor;
let cmp_bytes_len = stmt_total_bytes(&stmts[i]) as u64;
let mut j = i + 1;
let mut between_bytes: u64 = 0;
while j < stmts.len() {
let len = stmt_total_bytes(&stmts[j]);
if len > 0 {
break;
}
between_bytes += len as u64;
j += 1;
}
let _ = between_bytes; if let Some(jmp_idx) = (j < stmts.len()).then_some(j) {
let jmp_start = cursor + cmp_bytes_len;
if let Some(switch_stmt) = try_switch_pair(
&stmts[i],
&stmts[jmp_idx],
cmp_start,
jmp_start,
bitness,
data,
) {
let dispatch_lines = describe_switch_dispatch(
&switch_stmt,
cmp_start,
jmp_start,
bitness,
&label_previews,
);
stmts[i] = switch_stmt;
for _ in i + 1..=jmp_idx {
stmts.remove(i + 1);
}
let inserted = dispatch_lines.len();
for (off, line) in dispatch_lines.into_iter().enumerate() {
stmts.insert(i + off, Stmt::Comment(line));
}
cursor += stmt_total_bytes(&stmts[i + inserted]) as u64;
i += inserted + 1;
continue;
}
}
cursor += cmp_bytes_len;
i += 1;
}
}
fn collect_label_previews(stmts: &[Stmt], base_ip: u64) -> HashMap<u64, String> {
let mut out: HashMap<u64, String> = HashMap::new();
let mut cursor = base_ip;
for stmt in stmts {
let size = stmt_total_bytes_at(stmt, cursor) as u64;
if size > 0 {
let summary = summarise_stmt_for_preview(stmt);
out.entry(cursor).or_insert(summary);
}
cursor += size;
}
out
}
fn summarise_stmt_for_preview(stmt: &Stmt) -> String {
let raw: String = match stmt {
Stmt::Move { dst, src, .. } => format!("{dst} = {src}"),
Stmt::Call { name, args, .. } => {
if args.is_empty() {
format!("{name}()")
} else if args.len() == 1 {
format!("{name}({})", args[0])
} else {
format!("{name}(…{} args…)", args.len())
}
}
Stmt::Goto { target_addr, .. } => format!("goto label_{target_addr:x}"),
Stmt::IfGoto {
cond_text,
target_addr,
..
} => format!("if ({cond_text}) goto label_{target_addr:x}"),
Stmt::IfBranch { cond_text, .. } => format!("if ({cond_text}) {{ … }}"),
Stmt::Loop { cond_text, .. } => format!("loop ({cond_text}) {{ … }}"),
Stmt::Switch { selector, .. } => format!("switch ({selector}) {{ … }}"),
Stmt::Return { value, .. } => format!("return {value}"),
Stmt::ReturnExpr { text, .. } => format!("return {text}"),
Stmt::Asm { text, .. } => format!("@asm({text:?})"),
Stmt::Epilogue { .. } => "epilogue".into(),
Stmt::ArgSpill { arg_index, .. } => format!("arg_spill({arg_index})"),
_ => "…".into(),
};
truncate_for_preview(&raw, 64)
}
fn truncate_for_preview(s: &str, max: usize) -> String {
let trimmed = s.trim();
if trimmed.chars().count() <= max {
return trimmed.to_string();
}
let cut: String = trimmed.chars().take(max.saturating_sub(1)).collect();
format!("{cut}…")
}
fn describe_switch_dispatch(
switch_stmt: &Stmt,
_cmp_ip: u64,
_jmp_ip: u64,
_bitness: crate::arch::x86::Bitness,
label_previews: &HashMap<u64, String>,
) -> Vec<String> {
let (selector, cases, default_addr, table_va) = match switch_stmt {
Stmt::Switch {
selector,
cases,
default_addr,
table_va,
..
} => (selector, cases, *default_addr, *table_va),
_ => return Vec::new(),
};
let mut out = Vec::new();
let size = switch_encoded_size(switch_stmt);
let max_value = cases.len().saturating_sub(1);
out.push(format!(
"switch dispatch ({size} bytes): cmp {selector},{max_value}; ja 0x{default_addr:x}; jmp dword ptr [{selector}*4+0x{table_va:x}]",
));
let mut grouped: Vec<(u64, Vec<usize>)> = Vec::new();
for (case_idx, target) in cases.iter().enumerate() {
if let Some(slot) = grouped.iter_mut().find(|(t, _)| t == target) {
slot.1.push(case_idx);
} else {
grouped.push((*target, vec![case_idx]));
}
}
grouped.sort_by_key(|(_, c)| *c.first().unwrap_or(&usize::MAX));
for (target, case_idxs) in &grouped {
let head = if case_idxs.len() == 1 {
format!(" case {} → label_{target:x}", case_idxs[0])
} else {
let nums: Vec<String> = case_idxs.iter().map(ToString::to_string).collect();
format!(" cases {} → label_{target:x}", nums.join(", "))
};
let preview = label_previews
.get(target)
.map_or("(label not found in body)", String::as_str);
out.push(format!("{head}: {preview}"));
}
let default_preview = label_previews
.get(&default_addr)
.map_or("(label not found in body)", String::as_str);
out.push(format!(
" default → label_{default_addr:x}: {default_preview}"
));
out
}
fn try_switch_pair(
a: &Stmt,
b: &Stmt,
a_ip: u64,
b_ip: u64,
bitness: crate::arch::x86::Bitness,
data: &dyn DataLookup,
) -> Option<Stmt> {
use crate::arch::x86::{FlowControl, Mnemonic};
let (a_text, a_bytes) = match a {
Stmt::Asm { text, bytes } => (text.as_str(), bytes.as_slice()),
_ => return None,
};
let (b_text, b_bytes) = match b {
Stmt::Asm { text, bytes } => (text.as_str(), bytes.as_slice()),
_ => return None,
};
if !(a_text.starts_with("cmp ") && a_text.contains(';') && a_text.contains("ja ")) {
return None;
}
if !b_text.starts_with("jmp ") {
return None;
}
let a_insns = crate::arch::x86::decode(bitness, a_bytes, a_ip).ok()?;
if a_insns.len() < 2 {
return None;
}
let cmp_insn = a_insns.iter().rev().nth(1)?;
let ja_insn = a_insns.last()?;
if cmp_insn.iced.mnemonic() != Mnemonic::Cmp {
return None;
}
if ja_insn.iced.mnemonic() != Mnemonic::Ja {
return None;
}
let selector_reg =
crate::translate::decompile::ssa::canonical_reg_name(cmp_insn.iced.op0_register())?;
let max_val = u64::from(cmp_insn.iced.immediate32());
let default_addr = ja_insn.iced.near_branch_target();
if default_addr == 0 {
return None;
}
let b_insns = crate::arch::x86::decode(bitness, b_bytes, b_ip).ok()?;
if b_insns.len() != 1 {
return None;
}
let jmp = &b_insns[0];
if jmp.iced.flow_control() != FlowControl::IndirectBranch {
return None;
}
if jmp.iced.memory_index_scale() != 4 {
return None;
}
if jmp.iced.op_count() != 1 || jmp.iced.op_kind(0) != crate::arch::x86::OpKind::Memory {
return None;
}
let table_va = jmp.iced.memory_displacement64();
let case_count = max_val.checked_add(1)?;
if case_count > 4096 {
return None;
}
let (_, sec_data, off) = data.section_at(table_va)?;
let bytes_needed = case_count.checked_mul(4)? as usize;
let slice = sec_data.get(off..off.checked_add(bytes_needed)?)?;
let image_base = data.image_base();
let mut cases: Vec<u64> = Vec::with_capacity(case_count as usize);
for i in 0..case_count as usize {
let raw = &slice[i * 4..i * 4 + 4];
let absolute = u64::from(u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]));
let rva = absolute.saturating_sub(image_base);
cases.push(rva);
}
Some(Stmt::Switch {
selector: selector_reg,
cases,
default_addr,
dispatch: "msvc-jmp-table".into(),
table_va,
})
}
fn recognise_string_idioms(stmts: &mut [Stmt]) {
for stmt in stmts.iter_mut() {
match stmt {
Stmt::Asm { text, bytes } => {
if let Some((name, args)) = string_idiom_lift(text) {
*stmt = Stmt::Call {
name: name.into(),
args: args.into_iter().map(str::to_string).collect(),
bytes: std::mem::take(bytes),
direct_target: None,
};
}
}
Stmt::IfBranch {
pre_body,
then_body,
else_body,
..
} => {
recognise_string_idioms(pre_body);
recognise_string_idioms(then_body);
if let Some(eb) = else_body {
recognise_string_idioms(eb);
}
}
Stmt::Loop { body, .. } => {
recognise_string_idioms(body);
}
_ => {}
}
}
}
fn string_idiom_lift(text: &str) -> Option<(&'static str, Vec<&'static str>)> {
Some(match text {
"rep movsb [edi],[esi]" => ("memcpy", vec!["edi", "esi", "ecx"]),
"rep movsd [edi],[esi]" => ("memcpy_d", vec!["edi", "esi", "ecx"]),
"rep movsw [edi],[esi]" => ("memcpy_w", vec!["edi", "esi", "ecx"]),
"rep movsq [rdi],[rsi]" => ("memcpy_q", vec!["rdi", "rsi", "rcx"]),
"rep stosb [edi]" => ("memset", vec!["edi", "al", "ecx"]),
"rep stosd [edi]" => ("memset_d", vec!["edi", "eax", "ecx"]),
"rep stosw [edi]" => ("memset_w", vec!["edi", "ax", "ecx"]),
"rep stosq [rdi]" => ("memset_q", vec!["rdi", "rax", "rcx"]),
"repe cmpsb [esi],[edi]" => ("memcmp", vec!["esi", "edi", "ecx"]),
"repe cmpsd [esi],[edi]" => ("memcmp_d", vec!["esi", "edi", "ecx"]),
"repne scasb [edi]" => ("strlen_aux", vec!["edi", "al", "ecx"]),
"repne scasd [edi]" => ("scan_d", vec!["edi", "eax", "ecx"]),
_ => return None,
})
}
#[allow(clippy::match_same_arms)]
fn fold_local_if_skip(stmts: &mut Vec<Stmt>, _bitness: crate::arch::x86::Bitness) {
let mut i = 0;
while i < stmts.len() {
let Stmt::IfGoto {
target_addr: skip_to,
..
} = &stmts[i]
else {
i += 1;
continue;
};
let skip_to = *skip_to;
let mut body_end: Option<usize> = None;
for (j, s) in stmts.iter().enumerate().skip(i + 1) {
let stop = match s {
Stmt::Label { addr } if *addr == skip_to => true,
Stmt::Label { .. } => true,
Stmt::Goto { target_addr, .. } => *target_addr != skip_to,
Stmt::IfGoto { target_addr, .. } => *target_addr != skip_to,
Stmt::Switch { .. } | Stmt::IfReturn { .. } => true,
_ => false,
};
if stop {
if matches!(s, Stmt::Label { addr } if *addr == skip_to) {
body_end = Some(j);
}
break;
}
}
let Some(end_idx) = body_end else {
i += 1;
continue;
};
if end_idx == i + 1 {
i += 1;
continue;
}
let (cond_text_inverted, cond_bytes) = match stmts.remove(i) {
Stmt::IfGoto {
cond_text,
target_addr,
cmp_bytes,
cond_code,
wide,
} => {
let jcc = crate::arch::x86::encode_jcc(0, target_addr, cond_code, wide)
.unwrap_or_default();
let mut bytes = cmp_bytes;
bytes.extend_from_slice(&jcc);
(invert_relational_cond(&cond_text), bytes)
}
_ => unreachable!(),
};
let body_count = end_idx - i - 1;
let body: Vec<Stmt> = stmts.drain(i..i + body_count).collect();
stmts.insert(
i,
Stmt::IfBranch {
cond_text: cond_text_inverted,
cond_bytes,
attrs: Vec::new(),
pre_body: Vec::new(),
then_body: body,
else_body: None,
},
);
i += 1;
}
}
fn fold_gotos_and_labels(stmts: &mut Vec<Stmt>, base_ip: u64, bitness: crate::arch::x86::Bitness) {
let func_end = base_ip + stmts_total_bytes_at(stmts, base_ip) as u64;
let mut targets: HashSet<u64> = HashSet::new();
collect_goto_targets(stmts, base_ip, bitness, base_ip, func_end, &mut targets);
let mut cursor = base_ip;
rewrite_gotos_in_seq(stmts, &mut cursor, base_ip, func_end, bitness);
insert_labels_at_targets(stmts, base_ip, &targets);
}
fn collect_goto_targets(
stmts: &[Stmt],
cursor_start: u64,
bitness: crate::arch::x86::Bitness,
fn_start: u64,
fn_end: u64,
out: &mut HashSet<u64>,
) {
let mut cursor = cursor_start;
for stmt in stmts {
match stmt {
Stmt::Asm { bytes, .. } => {
if let Some(target) = jump_target_of(bytes, cursor, bitness)
&& target >= fn_start
&& target < fn_end
{
out.insert(target);
}
cursor += bytes.len() as u64;
}
Stmt::Switch {
cases,
default_addr,
..
} => {
for &c in cases {
if c >= fn_start && c < fn_end {
out.insert(c);
}
}
if *default_addr >= fn_start && *default_addr < fn_end {
out.insert(*default_addr);
}
cursor += switch_encoded_size(stmt) as u64;
}
Stmt::Goto { target_addr, wide } => {
if *target_addr >= fn_start && *target_addr < fn_end {
out.insert(*target_addr);
}
cursor += crate::arch::x86::encoded_jmp_size(cursor, *target_addr, *wide) as u64;
}
Stmt::IfGoto {
target_addr,
cmp_bytes,
wide,
..
} => {
if *target_addr >= fn_start && *target_addr < fn_end {
out.insert(*target_addr);
}
let jcc_ip = cursor + cmp_bytes.len() as u64;
cursor += cmp_bytes.len() as u64
+ crate::arch::x86::encoded_jcc_size(jcc_ip, *target_addr, *wide) as u64;
}
Stmt::Call { name, bytes, .. } => {
if let Some(addr) = parse_addr_prefix_call_name(name)
&& addr >= fn_start
&& addr < fn_end
{
out.insert(addr);
}
cursor += bytes.len() as u64;
}
Stmt::IfBranch {
attrs,
cond_bytes,
pre_body,
then_body,
else_body,
..
} => {
let mut sub = cursor;
if let Some(hb) = ud_ast_head_bytes_attr(attrs) {
sub += hb.len() as u64;
}
collect_goto_targets(pre_body, sub, bitness, fn_start, fn_end, out);
sub += stmts_total_bytes_at(pre_body, sub) as u64 + cond_bytes.len() as u64;
collect_goto_targets(then_body, sub, bitness, fn_start, fn_end, out);
if let Some(eb) = else_body {
sub += stmts_total_bytes_at(then_body, sub) as u64;
collect_goto_targets(eb, sub, bitness, fn_start, fn_end, out);
}
cursor += stmt_total_bytes_at(stmt, cursor) as u64;
}
Stmt::Loop {
entry_jmp_bytes,
body,
..
} => {
let mut sub = cursor;
if let Some(jmp) = entry_jmp_bytes {
sub += jmp.len() as u64;
}
collect_goto_targets(body, sub, bitness, fn_start, fn_end, out);
cursor += stmt_total_bytes_at(stmt, cursor) as u64;
}
_ => {
cursor += stmt_total_bytes_at(stmt, cursor) as u64;
}
}
}
}
fn parse_addr_prefix_call_name(name: &str) -> Option<u64> {
for prefix in ["goto_", "to_", "tail_"] {
if let Some(rest) = name.strip_prefix(prefix)
&& !rest.is_empty()
&& rest.chars().all(|c| c.is_ascii_hexdigit())
{
return u64::from_str_radix(rest, 16).ok();
}
}
None
}
fn jump_target_of(bytes: &[u8], ip: u64, bitness: crate::arch::x86::Bitness) -> Option<u64> {
use crate::arch::x86::FlowControl;
let insns = crate::arch::x86::decode(bitness, bytes, ip).ok()?;
if insns.is_empty() {
return None;
}
let last = insns.last()?;
let flow = last.iced.flow_control();
if !matches!(
flow,
FlowControl::UnconditionalBranch | FlowControl::ConditionalBranch
) {
return None;
}
let target = last.iced.near_branch_target();
if target == 0 {
return None;
}
Some(target)
}
fn rewrite_gotos_in_seq(
stmts: &mut [Stmt],
cursor: &mut u64,
fn_start: u64,
fn_end: u64,
bitness: crate::arch::x86::Bitness,
) {
for stmt in stmts.iter_mut() {
let stmt_start = *cursor;
let advance = stmt_total_bytes_at(stmt, stmt_start) as u64;
let mut replaced = false;
if let Stmt::Asm { text: _, bytes } = stmt
&& let Some(target) = jump_target_of(bytes, stmt_start, bitness)
&& target >= fn_start
&& target < fn_end
{
let bytes_owned = bytes.clone();
let new_stmt = make_goto_stmt(&bytes_owned, stmt_start, target, bitness);
if let Some(s) = new_stmt {
*stmt = s;
replaced = true;
}
}
if !replaced {
match stmt {
Stmt::IfBranch {
attrs,
cond_bytes,
pre_body,
then_body,
else_body,
..
} => {
let mut sub = *cursor;
if let Some(hb) = ud_ast_head_bytes_attr(attrs) {
sub += hb.len() as u64;
}
rewrite_gotos_in_seq(pre_body, &mut sub, fn_start, fn_end, bitness);
sub += cond_bytes.len() as u64;
rewrite_gotos_in_seq(then_body, &mut sub, fn_start, fn_end, bitness);
if let Some(eb) = else_body {
rewrite_gotos_in_seq(eb, &mut sub, fn_start, fn_end, bitness);
}
}
Stmt::Loop {
entry_jmp_bytes,
body,
..
} => {
let mut sub = *cursor;
if let Some(jmp) = entry_jmp_bytes {
sub += jmp.len() as u64;
}
rewrite_gotos_in_seq(body, &mut sub, fn_start, fn_end, bitness);
}
_ => {}
}
}
*cursor += advance;
}
}
fn make_goto_stmt(
bytes: &[u8],
ip: u64,
target_addr: u64,
bitness: crate::arch::x86::Bitness,
) -> Option<Stmt> {
use crate::arch::x86::{FlowControl, Mnemonic};
let insns = crate::arch::x86::decode(bitness, bytes, ip).ok()?;
let last = insns.last()?;
match last.iced.flow_control() {
FlowControl::UnconditionalBranch => {
if insns.len() != 1 {
return None;
}
let wide = bytes.len() > 2;
Some(Stmt::Goto { target_addr, wide })
}
FlowControl::ConditionalBranch => {
let cond_text = if insns.len() == 1 {
jcc_mnemonic_to_text(last.iced.mnemonic())?
} else if insns.len() == 2 {
let cmp = &insns[0];
if !matches!(cmp.iced.mnemonic(), Mnemonic::Cmp | Mnemonic::Test) {
return None;
}
let body_form = crate::arch::x86::render_cond_source(&cmp.iced, &last.iced);
invert_relational_cond(&body_form)
} else {
return None;
};
let jcc_bytes_len = last.original_bytes.len();
let cmp_bytes = bytes[..bytes.len() - jcc_bytes_len].to_vec();
let cond_code = crate::arch::x86::jcc_cond_code_from_bytes(&last.original_bytes)?;
let wide = jcc_bytes_len > 2;
Some(Stmt::IfGoto {
cond_text,
target_addr,
cmp_bytes,
cond_code,
wide,
})
}
_ => None,
}
}
fn jcc_mnemonic_to_text(m: crate::arch::x86::Mnemonic) -> Option<String> {
use crate::arch::x86::Mnemonic;
let s = match m {
Mnemonic::Je => "flags.zero",
Mnemonic::Jne => "!flags.zero",
Mnemonic::Jl => "flags.signed_lt",
Mnemonic::Jle => "flags.signed_le",
Mnemonic::Jg => "flags.signed_gt",
Mnemonic::Jge => "flags.signed_ge",
Mnemonic::Jb => "flags.below",
Mnemonic::Jbe => "flags.below_or_eq",
Mnemonic::Ja => "flags.above",
Mnemonic::Jae => "flags.above_or_eq",
Mnemonic::Js => "flags.sign",
Mnemonic::Jns => "!flags.sign",
Mnemonic::Jp => "flags.parity",
Mnemonic::Jnp => "!flags.parity",
Mnemonic::Jo => "flags.overflow",
Mnemonic::Jno => "!flags.overflow",
_ => return None,
};
Some(s.to_string())
}
fn insert_labels_at_targets(stmts: &mut Vec<Stmt>, base_ip: u64, targets: &HashSet<u64>) {
insert_labels_in_seq(stmts, base_ip, targets);
}
fn insert_labels_in_seq(stmts: &mut Vec<Stmt>, base_ip: u64, targets: &HashSet<u64>) -> u64 {
let mut cursor = base_ip;
let mut i = 0;
let mut labeled_at: HashSet<u64> = HashSet::new();
while i < stmts.len() {
let stmt_start = cursor;
if matches!(&stmts[i], Stmt::Label { addr } if *addr == stmt_start) {
labeled_at.insert(stmt_start);
}
if stmt_start != base_ip
&& targets.contains(&stmt_start)
&& !labeled_at.contains(&stmt_start)
{
stmts.insert(i, Stmt::Label { addr: stmt_start });
labeled_at.insert(stmt_start);
i += 1;
}
let mut sub_handled = false;
match &mut stmts[i] {
Stmt::IfBranch {
attrs,
cond_bytes,
pre_body,
then_body,
else_body,
..
} => {
let mut sub = stmt_start;
if let Some(hb) = ud_ast_head_bytes_attr(attrs) {
sub += hb.len() as u64;
}
let after_pre = insert_labels_in_seq(pre_body, sub, targets);
let after_cond = after_pre + cond_bytes.len() as u64;
let after_then = insert_labels_in_seq(then_body, after_cond, targets);
if let Some(eb) = else_body {
insert_labels_in_seq(eb, after_then, targets);
}
sub_handled = true;
}
Stmt::Loop {
entry_jmp_bytes,
body,
..
} => {
let mut sub = stmt_start;
if let Some(jmp) = entry_jmp_bytes {
sub += jmp.len() as u64;
}
insert_labels_in_seq(body, sub, targets);
sub_handled = true;
}
_ => {}
}
let _ = sub_handled;
cursor += stmt_total_bytes(&stmts[i]) as u64;
i += 1;
}
cursor
}
fn absorb_save_restore(stmts: &mut [Stmt]) {
let mut counter: u32 = 0;
let mut events: Vec<(u32, SaveKind, String)> = Vec::new();
scan_scope(stmts, &mut counter, &mut events);
let mut stack: Vec<(u32, String)> = Vec::new();
let mut transforms: HashMap<u32, SaveKind> = HashMap::new();
for (mark, kind, reg) in events {
match kind {
SaveKind::Push => stack.push((mark, reg)),
SaveKind::Pop => {
if let Some((push_mark, _)) = stack.pop_if(|top| top.1 == reg) {
transforms.insert(push_mark, SaveKind::Push);
transforms.insert(mark, SaveKind::Pop);
}
}
}
}
let mut counter: u32 = 0;
apply_scope(stmts, &mut counter, &transforms);
for stmt in stmts.iter_mut() {
match stmt {
Stmt::IfBranch {
then_body,
else_body,
..
} => {
absorb_save_restore(then_body);
if let Some(eb) = else_body {
absorb_save_restore(eb);
}
}
Stmt::Loop { body, .. } => {
absorb_save_restore(body);
}
_ => {}
}
}
}
#[derive(Copy, Clone)]
enum SaveKind {
Push,
Pop,
}
fn scan_scope(stmts: &[Stmt], counter: &mut u32, out: &mut Vec<(u32, SaveKind, String)>) {
for stmt in stmts {
match stmt {
Stmt::Asm { text, .. } => {
if let Some(reg) = parse_callee_save_push(text) {
out.push((*counter, SaveKind::Push, reg));
*counter += 1;
} else if let Some(reg) = parse_callee_save_pop(text) {
out.push((*counter, SaveKind::Pop, reg));
*counter += 1;
}
}
Stmt::IfBranch { pre_body, .. } => {
scan_scope(pre_body, counter, out);
}
_ => {}
}
}
}
fn apply_scope(stmts: &mut [Stmt], counter: &mut u32, transforms: &HashMap<u32, SaveKind>) {
for stmt in stmts.iter_mut() {
match stmt {
Stmt::Asm { text, bytes } => {
let push_reg = parse_callee_save_push(text);
let pop_reg = parse_callee_save_pop(text);
if push_reg.is_some() || pop_reg.is_some() {
let mark = *counter;
*counter += 1;
if let Some(action) = transforms.get(&mark) {
match action {
SaveKind::Push => {
if let Some(reg) = push_reg {
let bytes = std::mem::take(bytes);
*stmt = Stmt::Save { reg, bytes };
}
}
SaveKind::Pop => {
if let Some(reg) = pop_reg {
let bytes = std::mem::take(bytes);
*stmt = Stmt::Restore { reg, bytes };
}
}
}
}
}
}
Stmt::IfBranch { pre_body, .. } => {
apply_scope(pre_body, counter, transforms);
}
_ => {}
}
}
}
fn parse_callee_save_push(text: &str) -> Option<String> {
let stripped = text.strip_prefix("push ")?;
if stripped.contains(',') || stripped.contains(';') || stripped.contains('[') {
return None;
}
let reg = stripped.trim();
if is_callee_saved_reg(reg) {
Some(reg.to_string())
} else {
None
}
}
fn parse_callee_save_pop(text: &str) -> Option<String> {
let stripped = text.strip_prefix("pop ")?;
if stripped.contains(',') || stripped.contains(';') || stripped.contains('[') {
return None;
}
let reg = stripped.trim();
if is_callee_saved_reg(reg) {
Some(reg.to_string())
} else {
None
}
}
fn is_callee_saved_reg(reg: &str) -> bool {
matches!(
reg,
"ebx"
| "esi"
| "edi"
| "ebp"
| "rbx"
| "rsi"
| "rdi"
| "rbp"
| "r12"
| "r13"
| "r14"
| "r15"
)
}
fn asm_state_effect(text: &str, state: &mut RegState) {
let head = text.split_whitespace().next().unwrap_or("");
let is_branch = matches!(
head,
"jmp"
| "ret"
| "je"
| "jne"
| "jl"
| "jle"
| "jg"
| "jge"
| "jb"
| "jbe"
| "ja"
| "jae"
| "js"
| "jns"
| "jo"
| "jno"
| "jp"
| "jnp"
| "jc"
| "jnc"
| "jecxz"
| "jcxz"
| "jrcxz"
) || text.contains("; j")
|| text.contains("; ret");
if is_branch {
state.invalidate_all();
return;
}
if head == "call" {
for reg in CALLER_SAVED {
state.invalidate(reg);
}
return;
}
let is_pure_read = matches!(
head,
"push" | "cmp" | "test" | "nop" | "cdq" | "cwd" | "rep" | "repe" | "repne" | "leave"
);
if is_pure_read {
return;
}
let modifies_single_dst = matches!(
head,
"pop"
| "inc"
| "dec"
| "neg"
| "not"
| "shl"
| "shr"
| "sar"
| "rol"
| "ror"
| "sal"
| "rcl"
| "rcr"
);
if modifies_single_dst && let Some(rest) = text.split_once(' ').map(|(_, r)| r) {
let first = rest.split(',').next().unwrap_or("").trim();
if is_gpr_name(first) {
state.invalidate(first);
return;
}
}
state.invalidate_all();
}
#[allow(clippy::too_many_lines)]
fn discover_locals(f: &Function<DecodedInsn>, sp_table: &HashMap<u64, i64>) -> Vec<LocalDecl> {
use std::collections::BTreeMap;
let mut stack: BTreeMap<i64, (u32, bool)> = BTreeMap::new();
let mut stack_pointer_slots: HashSet<i64> = HashSet::new();
let mut regs: BTreeMap<String, u32> = BTreeMap::new();
let mut reg_signed: HashSet<String> = HashSet::new();
let mut reg_order: Vec<String> = Vec::new();
for block in &f.blocks {
for insn in &block.insns {
let ip = insn.iced.ip();
for op_idx in 0..insn.iced.op_count() {
match insn.iced.op_kind(op_idx) {
OpKind::Memory
if insn.iced.memory_index() == Register::None
&& matches!(
insn.iced.memory_base(),
Register::EBP | Register::ESP | Register::RBP | Register::RSP
) =>
{
let base = insn.iced.memory_base();
let disp = crate::arch::x86::signed_memory_displacement(&insn.iced);
let stable = match base {
Register::EBP | Register::RBP => disp,
Register::ESP | Register::RSP => {
let delta = sp_table.get(&ip).copied().unwrap_or(0);
delta + disp + 4
}
_ => continue,
};
if stable == 0 || stable == 4 {
continue;
}
let size = memory_size_bytes(insn.iced.memory_size());
let signed = is_signed_memory_size(insn.iced.memory_size());
if size > 0 {
stack
.entry(stable)
.and_modify(|(s, sg)| {
*s = (*s).max(size);
*sg = *sg || signed;
})
.or_insert((size, signed));
}
}
OpKind::Register => {
let reg = insn.iced.op_register(op_idx);
if let Some(name) = canonical_register_name(reg) {
let size = register_size_bytes(reg);
if size > 0 {
regs.entry(name.clone())
.and_modify(|s| *s = (*s).max(size))
.or_insert_with(|| {
reg_order.push(name.clone());
size
});
}
if uses_register_as_signed(&insn.iced, reg) {
reg_signed.insert(name);
}
}
}
_ => {}
}
}
for op_idx in 0..insn.iced.op_count() {
if insn.iced.op_kind(op_idx) == OpKind::Memory {
let base = insn.iced.memory_base();
if !matches!(
base,
Register::EBP
| Register::ESP
| Register::RBP
| Register::RSP
| Register::None
) {
let _ = stack_pointer_slots.insert(0); }
}
}
}
}
let _ = stack_pointer_slots;
let mut out: Vec<LocalDecl> = Vec::with_capacity(stack.len() + regs.len());
for (disp, (size, signed)) in &stack {
let name = if *disp >= 0 {
#[allow(clippy::cast_sign_loss)]
let d = *disp as u64;
format!("arg_{d:x}")
} else {
#[allow(clippy::cast_sign_loss)]
let d = (-disp) as u64;
format!("var_{d:x}")
};
out.push(LocalDecl {
name,
ty: bytes_to_type_signed(*size, *signed),
kind: LocalKind::Stack,
});
}
for name in ®_order {
if matches!(name.as_str(), "ebp" | "esp" | "rbp" | "rsp") {
continue;
}
let size = regs[name];
let signed = reg_signed.contains(name);
out.push(LocalDecl {
name: name.clone(),
ty: bytes_to_type_signed(size, signed),
kind: LocalKind::Register,
});
}
out
}
fn is_signed_memory_size(size: crate::arch::x86::MemorySize) -> bool {
use crate::arch::x86::MemorySize;
matches!(
size,
MemorySize::Int8 | MemorySize::Int16 | MemorySize::Int32 | MemorySize::Int64
)
}
fn uses_register_as_signed(insn: &crate::arch::x86::Instruction, reg: Register) -> bool {
use crate::arch::x86::Mnemonic;
let signed_op = matches!(
insn.mnemonic(),
Mnemonic::Idiv
| Mnemonic::Imul
| Mnemonic::Sar
| Mnemonic::Cdq
| Mnemonic::Cwde
| Mnemonic::Cbw
| Mnemonic::Movsx
| Mnemonic::Movsxd
| Mnemonic::Cdqe
);
if !signed_op {
return false;
}
let touched = (0..insn.op_count()).any(|i| {
insn.op_kind(i) == crate::arch::x86::OpKind::Register && insn.op_register(i) == reg
});
if !touched && matches!(insn.mnemonic(), Mnemonic::Idiv | Mnemonic::Imul) {
return matches!(
reg,
Register::EAX
| Register::EDX
| Register::RAX
| Register::RDX
| Register::AX
| Register::DX
);
}
touched
}
fn bytes_to_type_signed(bytes: u32, signed: bool) -> Type {
match (bytes, signed) {
(1, true) => Type::I8,
(2, true) => Type::I16,
(4, true) => Type::I32,
(8, true) => Type::I64,
_ => bytes_to_type(bytes),
}
}
fn memory_size_bytes(size: crate::arch::x86::MemorySize) -> u32 {
use crate::arch::x86::MemorySize;
match size {
MemorySize::UInt8 | MemorySize::Int8 => 1,
MemorySize::UInt16 | MemorySize::Int16 => 2,
MemorySize::UInt32 | MemorySize::Int32 | MemorySize::Float32 => 4,
MemorySize::UInt64 | MemorySize::Int64 | MemorySize::Float64 => 8,
_ => 0,
}
}
fn canonical_register_name(reg: Register) -> Option<String> {
use Register::{
EAX, EBP, EBX, ECX, EDI, EDX, ESI, ESP, R8, R8D, R9, R9D, R10, R10D, R11, R11D, R12, R12D,
R13, R13D, R14, R14D, R15, R15D, RAX, RBP, RBX, RCX, RDI, RDX, RSI, RSP,
};
let full = match reg {
Register::AL | Register::AH | Register::AX | EAX | RAX => "eax",
Register::CL | Register::CH | Register::CX | ECX | RCX => "ecx",
Register::DL | Register::DH | Register::DX | EDX | RDX => "edx",
Register::BL | Register::BH | Register::BX | EBX | RBX => "ebx",
Register::SPL | Register::SP | ESP | RSP => "esp",
Register::BPL | Register::BP | EBP | RBP => "ebp",
Register::SIL | Register::SI | ESI | RSI => "esi",
Register::DIL | Register::DI | EDI | RDI => "edi",
Register::R8L | Register::R8W | R8D | R8 => "r8",
Register::R9L | Register::R9W | R9D | R9 => "r9",
Register::R10L | Register::R10W | R10D | R10 => "r10",
Register::R11L | Register::R11W | R11D | R11 => "r11",
Register::R12L | Register::R12W | R12D | R12 => "r12",
Register::R13L | Register::R13W | R13D | R13 => "r13",
Register::R14L | Register::R14W | R14D | R14 => "r14",
Register::R15L | Register::R15W | R15D | R15 => "r15",
_ => return None,
};
Some(full.to_string())
}
fn register_size_bytes(reg: Register) -> u32 {
let info = reg.info();
info.size().try_into().unwrap_or(0)
}
fn bytes_to_type(bytes: u32) -> Type {
match bytes {
1 => Type::U8,
2 => Type::U16,
4 => Type::U32,
8 => Type::U64,
_ => Type::Unknown,
}
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
fn emit_blocks_in_range(
out: &mut Vec<Stmt>,
f: &Function<DecodedInsn>,
range: std::ops::Range<usize>,
loops: &[Option<LoopGroup>],
groups: &[Option<IfElseGroup>],
lifts: &[Option<BlockTailLift>],
pre_jmp_truncate: &HashMap<usize, usize>,
ctx: &EmitCtx<'_>,
is_top_level: bool,
) {
let range_start = range.start;
let range_end = range.end;
let mut i = range_start;
while i < range_end {
let is_first_of_range = i == range_start;
if let Some(lg) = loops.get(i).and_then(Option::as_ref) {
if lg.tail_idx < range_end {
let mut body_stmts = Vec::new();
emit_blocks_in_range(
&mut body_stmts,
f,
i..lg.tail_idx,
loops,
groups,
lifts,
pre_jmp_truncate,
ctx,
false,
);
let tail_block = &f.blocks[lg.tail_idx];
let head = try_lift_if_branch_head(&tail_block.insns);
let head_consumed = head
.as_ref()
.map_or(0, |h| h.insns_consumed.min(tail_block.insns.len()));
let pre_test_count = tail_block.insns.len() - head_consumed;
let tail_bytes: Vec<u8> = tail_block.insns[pre_test_count..]
.iter()
.flat_map(|insn| insn.original_bytes.iter().copied())
.collect();
if pre_test_count > 0 {
let mut tail_view = tail_block.clone();
tail_view.insns.truncate(pre_test_count);
tail_view.terminator = crate::ir::Terminator::Fallthrough;
emit_block_stmts(
&mut body_stmts,
&tail_view,
BlockEmitConfig {
is_first: false,
emit_block_comment: false,
truncate_trailing: 0,
emit_terminator_comment: false,
},
None,
ctx,
);
}
let entry_jmp_bytes = lg.pre_jmp_block_idx.and_then(|pre_idx| {
f.blocks[pre_idx]
.insns
.last()
.map(|insn| insn.original_bytes.clone())
});
out.push(Stmt::Loop {
cond_text: lg.cond_text.clone(),
entry_jmp_bytes,
tail_bytes,
body: body_stmts,
});
i = lg.tail_idx + 1;
continue;
}
}
if let Some(group) = groups.get(i).and_then(Option::as_ref) {
if group.end_idx() <= range_end {
let block = &f.blocks[i];
let block_len = block.insns.len();
let separated = group.head_consumed == 1 && group.pre_body_count > 0;
let head_trim = if separated {
group.head_consumed + group.pre_body_count + 1
} else {
group.head_consumed
};
emit_block_stmts(
out,
block,
BlockEmitConfig {
is_first: is_first_of_range && is_top_level,
emit_block_comment: !is_first_of_range || is_top_level && i > 0,
truncate_trailing: head_trim,
emit_terminator_comment: false,
},
lifts[i].as_ref(),
ctx,
);
let mut pre_body_stmts = group.pre_body.clone();
if separated {
let intervening_start = block_len - group.head_consumed - group.pre_body_count;
let intervening_end = block_len - group.head_consumed;
let mut intervening_view = block.clone();
intervening_view.insns =
block.insns[intervening_start..intervening_end].to_vec();
intervening_view.terminator = crate::ir::Terminator::Fallthrough;
emit_block_stmts(
&mut pre_body_stmts,
&intervening_view,
BlockEmitConfig {
is_first: false,
emit_block_comment: false,
truncate_trailing: 0,
emit_terminator_comment: false,
},
None,
ctx,
);
}
let mut then_body = Vec::new();
emit_blocks_in_range(
&mut then_body,
f,
group.then_range.clone(),
loops,
groups,
lifts,
pre_jmp_truncate,
ctx,
false,
);
let else_body = group.else_range.clone().map(|er| {
let mut v = Vec::new();
emit_blocks_in_range(
&mut v,
f,
er,
loops,
groups,
lifts,
pre_jmp_truncate,
ctx,
false,
);
v
});
out.push(Stmt::IfBranch {
cond_text: group.cond_text.clone(),
cond_bytes: group.cond_bytes.clone(),
attrs: group.attrs.clone(),
pre_body: pre_body_stmts,
then_body,
else_body,
});
i = group.end_idx();
continue;
}
}
let is_last_of_range = i + 1 == range_end;
let truncate_trailing = pre_jmp_truncate.get(&i).copied().unwrap_or(0);
let emit_term_default = truncate_trailing == 0;
let emit_block_comment = if is_top_level {
i > 0
} else {
!is_first_of_range
};
let emit_term = if is_top_level {
emit_term_default
} else {
!is_last_of_range && emit_term_default
};
emit_block_stmts(
out,
&f.blocks[i],
BlockEmitConfig {
is_first: is_first_of_range && is_top_level,
emit_block_comment,
truncate_trailing,
emit_terminator_comment: emit_term,
},
lifts[i].as_ref(),
ctx,
);
i += 1;
}
}
#[derive(Clone, Copy)]
struct BlockEmitConfig {
is_first: bool,
emit_block_comment: bool,
truncate_trailing: usize,
emit_terminator_comment: bool,
}
struct EmitCtx<'a> {
fn_addr_start: u64,
fn_addr_end: u64,
name_at: &'a HashMap<u64, String>,
sp_delta_at: &'a HashMap<u64, i64>,
signature: Option<&'a Signature>,
data: &'a dyn DataLookup,
codec_bits: crate::arch::x86::CodecBits,
}
#[allow(clippy::too_many_lines)]
fn emit_block_stmts(
out: &mut Vec<Stmt>,
block: &BasicBlock<DecodedInsn>,
cfg: BlockEmitConfig,
lift: Option<&BlockTailLift>,
ctx: &EmitCtx<'_>,
) {
let _ = cfg.emit_block_comment;
let prologue_consumed = if cfg.is_first {
if let Some(lifted) = try_lift_prologue_pattern(&block.insns) {
let bytes: Vec<u8> = block.insns[..lifted.insns_consumed]
.iter()
.flat_map(|insn| insn.original_bytes.iter().copied())
.collect();
let params = decode_prologue_params(&bytes, ctx.codec_bits);
out.push(Stmt::Prologue {
kind: lifted.kind.to_string(),
params,
bytes,
});
lifted.insns_consumed
} else {
0
}
} else {
0
};
let tail_consumed = match lift {
Some(
BlockTailLift::Return { insns_consumed, .. }
| BlockTailLift::Epilogue { insns_consumed, .. }
| BlockTailLift::ReturnExpr { insns_consumed, .. },
) => *insns_consumed,
None => 0,
};
let asm_count = block
.insns
.len()
.saturating_sub(tail_consumed + cfg.truncate_trailing);
let call_sites = identify_call_sites(&block.insns);
let mut call_at: HashMap<usize, &CallSite> = HashMap::new();
let mut consumed_by_call: HashSet<usize> = HashSet::new();
let mut call_end_idx: HashMap<usize, usize> = HashMap::new();
let mut post_call_spill: HashMap<usize, i64> = HashMap::new();
for (site_idx, site) in call_sites.iter().enumerate() {
if site.call_idx >= asm_count {
continue;
}
let setup_start = site.setup_start.max(prologue_consumed);
if setup_start > site.call_idx {
continue;
}
call_at.insert(site.call_idx, site);
for i in setup_start..site.call_idx {
consumed_by_call.insert(i);
}
let mut end_idx = site.call_idx;
if let Some(spill) = detect_post_call_spill(&block.insns, site.call_idx + 1) {
let spill_end = site.call_idx + spill.insns_consumed;
let next_setup_start = call_sites
.get(site_idx + 1)
.map_or(usize::MAX, |s| s.setup_start);
if spill_end < next_setup_start && spill_end < asm_count {
for i in (site.call_idx + 1)..=spill_end {
consumed_by_call.insert(i);
}
post_call_spill.insert(site.call_idx, spill.displacement);
end_idx = spill_end;
}
}
call_end_idx.insert(site.call_idx, end_idx);
}
let pattern_ctx = crate::translate::decompile::patterns::PatternCtx {
fn_addr_start: ctx.fn_addr_start,
fn_addr_end: ctx.fn_addr_end,
name_at: ctx.name_at,
sp_delta_at: ctx.sp_delta_at,
};
let pattern_matches = crate::translate::decompile::patterns::apply_patterns(
&pattern_ctx,
&block.insns[..asm_count],
);
let mut global_idx = prologue_consumed;
while global_idx < asm_count {
let insn = &block.insns[global_idx];
if consumed_by_call.contains(&global_idx) {
global_idx += 1;
continue;
}
if let Some(m) = pattern_matches.get(&global_idx) {
let overlaps_call_setup =
(global_idx + 1..global_idx + m.consumed).any(|k| consumed_by_call.contains(&k));
if !overlaps_call_setup {
for stmt in &m.stmts {
out.push(stmt.clone());
}
global_idx += m.consumed;
continue;
}
}
if let Some(site) = call_at.get(&global_idx) {
let setup_start = site.setup_start.max(prologue_consumed);
let end_idx = *call_end_idx.get(&site.call_idx).unwrap_or(&site.call_idx);
let spill_disp = post_call_spill.get(&site.call_idx).copied();
let direct_target = if end_idx == site.call_idx && site.call_target != 0 {
Some(site.call_target)
} else {
None
};
let mut bytes = Vec::new();
let bytes_end_inclusive = if direct_target.is_some() {
if site.call_idx == 0 {
None
} else {
Some(site.call_idx - 1)
}
} else {
Some(end_idx)
};
if let Some(end) = bytes_end_inclusive {
for j in setup_start..=end {
bytes.extend_from_slice(&block.insns[j].original_bytes);
}
}
let name = ctx
.name_at
.get(&site.call_target)
.cloned()
.unwrap_or_else(|| format!("sub_{:x}", site.call_target));
let args = site
.args
.iter()
.map(|a| render_arg_value(a, ctx))
.collect::<Vec<_>>();
out.push(Stmt::Call {
name,
args,
bytes,
direct_target,
});
if let Some(disp) = spill_disp {
let dest = if disp < 0 {
format!("[rbp-0x{:x}]", disp.unsigned_abs())
} else {
format!("[rbp+0x{disp:x}]")
};
out.push(Stmt::Comment(format!("result -> {dest}")));
}
global_idx += 1;
continue;
}
if let Some(arg_index) = arg_spill_lift_index(insn, ctx.signature) {
out.push(Stmt::ArgSpill {
arg_index,
bytes: insn.original_bytes.clone(),
});
global_idx += 1;
continue;
}
if let Some(consumed) = try_lift_local_compound(
block,
global_idx,
asm_count,
&consumed_by_call,
&call_at,
out,
) {
global_idx += consumed;
continue;
}
if let Some((slot, value)) = crate::arch::x86::match_local_set_immediate(&insn.iced) {
out.push(Stmt::LocalSet {
slot,
value,
bytes: insn.original_bytes.clone(),
});
global_idx += 1;
continue;
}
if let Some((slot, op, value)) = crate::arch::x86::match_local_arith_immediate(&insn.iced) {
out.push(Stmt::LocalArith {
slot,
op: op.to_string(),
value,
bytes: insn.original_bytes.clone(),
});
global_idx += 1;
continue;
}
out.push(Stmt::asm(
format_intel(&insn.iced),
insn.original_bytes.clone(),
));
if let Some(annotation) =
call_annotation(insn, ctx.fn_addr_start, ctx.fn_addr_end, ctx.name_at)
{
out.push(Stmt::Comment(annotation));
}
if let Some(annotation) = lea_target_annotation(insn, ctx.data, ctx.name_at) {
out.push(Stmt::Comment(annotation));
}
global_idx += 1;
}
if cfg.truncate_trailing > 0 {
return;
}
if let Some(lift) = lift {
let lifted_bytes: Vec<u8> = block.insns[asm_count..]
.iter()
.flat_map(|insn| insn.original_bytes.iter().copied())
.collect();
match lift {
BlockTailLift::Return { value, .. } => {
out.push(Stmt::Return {
value: *value,
bytes: lifted_bytes,
});
}
BlockTailLift::Epilogue { kind, .. } => {
let params = decode_epilogue_params(&lifted_bytes, ctx.codec_bits);
out.push(Stmt::Epilogue {
kind: (*kind).to_string(),
params,
bytes: lifted_bytes,
});
}
BlockTailLift::ReturnExpr { text, .. } => {
out.push(Stmt::ReturnExpr {
text: text.clone(),
bytes: lifted_bytes,
});
}
}
return;
}
let _ = cfg.emit_terminator_comment;
}
enum BlockTailLift {
Return { insns_consumed: usize, value: u64 },
Epilogue {
insns_consumed: usize,
kind: &'static str,
},
ReturnExpr { insns_consumed: usize, text: String },
}
fn compute_block_tail_lifts(
f: &Function<DecodedInsn>,
signature: Option<&Signature>,
slot_to_name: &HashMap<i64, String>,
name_at: &HashMap<u64, String>,
) -> Vec<Option<BlockTailLift>> {
let mut out: Vec<Option<BlockTailLift>> = (0..f.blocks.len()).map(|_| None).collect();
let Some(last_idx) = f.blocks.len().checked_sub(1) else {
return out;
};
let epilogue_addr = f.blocks[last_idx].addr.0;
let return_lift_allowed = match signature {
Some(s) => return_type_is_integer_like(&s.return_type),
None => true,
};
let tail_is_epilogue = try_lift_epilogue_pattern(&f.blocks[last_idx].insns).is_some();
for (i, block) in f.blocks.iter().enumerate() {
if i == last_idx {
if return_lift_allowed && let Some(lifted) = try_lift_return_pattern(&block.insns) {
out[i] = Some(BlockTailLift::Return {
insns_consumed: lifted.insns_consumed,
value: lifted.value,
});
continue;
}
if return_lift_allowed && let Some(epi) = try_lift_epilogue_pattern(&block.insns) {
let split = block.insns.len() - epi.insns_consumed;
if split > 0 {
let leading = &block.insns[..split];
if let Some(value) = try_lift_value_block(leading, name_at) {
let render_ctx = ExprRenderCtx {
slot_to_name,
name_at,
};
out[i] = Some(BlockTailLift::ReturnExpr {
insns_consumed: block.insns.len(),
text: value.expr.render(&render_ctx),
});
continue;
}
}
}
if let Some(lifted) = try_lift_epilogue_pattern(&block.insns) {
out[i] = Some(BlockTailLift::Epilogue {
insns_consumed: lifted.insns_consumed,
kind: lifted.kind,
});
}
continue;
}
if return_lift_allowed
&& let Some(lifted) = try_lift_return_via_jmp(&block.insns, epilogue_addr)
{
out[i] = Some(BlockTailLift::Return {
insns_consumed: lifted.insns_consumed,
value: lifted.value,
});
continue;
}
if matches!(block.terminator, Terminator::Return)
&& let Some(lifted) = try_lift_epilogue_pattern(&block.insns)
{
out[i] = Some(BlockTailLift::Epilogue {
insns_consumed: lifted.insns_consumed,
kind: lifted.kind,
});
continue;
}
if return_lift_allowed
&& tail_is_epilogue
&& let Terminator::Fallthrough = block.terminator
&& i + 1 == last_idx
&& let Some(lifted) = try_lift_value_block(&block.insns, name_at)
{
let render_ctx = ExprRenderCtx {
slot_to_name,
name_at,
};
out[i] = Some(BlockTailLift::ReturnExpr {
insns_consumed: lifted.insns_consumed,
text: lifted.expr.render(&render_ctx),
});
}
}
out
}
struct LoopGroup {
tail_idx: usize,
cond_text: String,
pre_jmp_block_idx: Option<usize>,
}
fn identify_loop_groups(f: &Function<DecodedInsn>) -> Vec<Option<LoopGroup>> {
let mut out: Vec<Option<LoopGroup>> = (0..f.blocks.len()).map(|_| None).collect();
if f.blocks.len() < 2 {
return out;
}
let addr_to_idx: HashMap<u64, usize> = f
.blocks
.iter()
.enumerate()
.map(|(i, b)| (b.addr.0, i))
.collect();
for (tail_idx, tail) in f.blocks.iter().enumerate() {
let Terminator::ConditionalBranch { taken, .. } = tail.terminator else {
continue;
};
let Some(&body_first_idx) = addr_to_idx.get(&taken.0) else {
continue;
};
if body_first_idx >= tail_idx {
continue;
}
let body_span_clean = (body_first_idx..tail_idx)
.all(|j| matches!(f.blocks[j].terminator, Terminator::Fallthrough));
if !body_span_clean {
continue;
}
let Some(head) = try_lift_if_branch_head(&tail.insns) else {
continue;
};
let pre_jmp_block_idx = body_first_idx.checked_sub(1).and_then(|pre_idx| {
let pre = &f.blocks[pre_idx];
match pre.terminator {
Terminator::UnconditionalBranch { target } if target == tail.addr => pre
.insns
.last()
.and_then(|insn| direct_unconditional_branch_target(&insn.iced))
.filter(|&target| target == tail.addr.0)
.map(|_| pre_idx),
_ => None,
}
});
out[body_first_idx] = Some(LoopGroup {
tail_idx,
cond_text: head.cond_text,
pre_jmp_block_idx,
});
}
out
}
struct IfElseGroup {
head_consumed: usize,
pre_body_count: usize,
#[allow(dead_code)]
absorbed_blocks: usize,
cond_text: String,
cond_bytes: Vec<u8>,
attrs: Vec<crate::ast::Attribute>,
pre_body: Vec<Stmt>,
then_range: std::ops::Range<usize>,
else_range: Option<std::ops::Range<usize>>,
}
impl IfElseGroup {
fn end_idx(&self) -> usize {
let then_end = self.then_range.end;
match &self.else_range {
Some(r) => r.end.max(then_end),
None => then_end,
}
}
}
fn identify_if_else_groups(f: &Function<DecodedInsn>) -> Vec<Option<IfElseGroup>> {
let mut groups: Vec<Option<IfElseGroup>> = (0..f.blocks.len()).map(|_| None).collect();
if f.blocks.len() < 2 {
return groups;
}
let addr_to_idx: HashMap<u64, usize> = f
.blocks
.iter()
.enumerate()
.map(|(i, b)| (b.addr.0, i))
.collect();
for (a_idx, slot) in groups.iter_mut().enumerate() {
if let Some(group) = try_detect_if_else_at(f, a_idx, &addr_to_idx) {
*slot = Some(group);
}
}
groups
}
fn try_detect_if_else_at(
f: &Function<DecodedInsn>,
a_idx: usize,
addr_to_idx: &HashMap<u64, usize>,
) -> Option<IfElseGroup> {
let a = &f.blocks[a_idx];
let Terminator::ConditionalBranch { taken, fallthrough } = a.terminator else {
return None;
};
let head = try_lift_if_branch_head(&a.insns)?;
if head.jcc_target != taken.0 {
return None;
}
let f_idx = a_idx + 1;
if f.blocks.get(f_idx).map(|b| b.addr.0) != Some(fallthrough.0) {
return None;
}
let &t_idx = addr_to_idx.get(&taken.0)?;
if t_idx <= f_idx {
return None;
}
if !is_clean_fallthrough_run(f, f_idx..t_idx - 1) {
return None;
}
let then_last = &f.blocks[t_idx - 1];
let then_exit_join = match then_last.terminator {
Terminator::Fallthrough
| Terminator::Return
| Terminator::IndirectBranch
| Terminator::InvalidOrUnreachable => {
let (attrs, pre_body, pre_body_count) = build_if_head_extras(&head, a);
return Some(IfElseGroup {
head_consumed: head.insns_consumed,
pre_body_count,
absorbed_blocks: 0,
cond_text: head.cond_text,
cond_bytes: head.cond_bytes,
attrs,
pre_body,
then_range: f_idx..t_idx,
else_range: None,
});
}
Terminator::UnconditionalBranch { target } => Some(target.0),
Terminator::ConditionalBranch {
taken: inner_taken, ..
} => {
if let Some(group) =
try_detect_compound(f, a, &head, f_idx, t_idx, then_last, inner_taken.0, taken.0)
{
return Some(group);
}
return None;
}
};
let join_idx = match then_exit_join {
Some(j) => *addr_to_idx.get(&j)?,
None => f.blocks.len(),
};
if join_idx <= t_idx {
return None;
}
if !is_clean_fallthrough_run(f, t_idx..join_idx - 1) {
return None;
}
let else_last = &f.blocks[join_idx - 1];
let else_meets_join = match (then_exit_join, &else_last.terminator) {
(None, _) if join_idx == f.blocks.len() => true,
(Some(j), Terminator::Fallthrough) => f.blocks.get(join_idx).is_some_and(|b| b.addr.0 == j),
(Some(j), Terminator::UnconditionalBranch { target }) => target.0 == j,
(
Some(_),
Terminator::Return | Terminator::IndirectBranch | Terminator::InvalidOrUnreachable,
) => true,
_ => false,
};
if !else_meets_join {
return None;
}
let (attrs, pre_body, pre_body_count) = build_if_head_extras(&head, a);
Some(IfElseGroup {
head_consumed: head.insns_consumed,
pre_body_count,
absorbed_blocks: 0,
cond_text: head.cond_text,
cond_bytes: head.cond_bytes,
attrs,
pre_body,
then_range: f_idx..t_idx,
else_range: Some(t_idx..join_idx),
})
}
fn invert_relational_cond(text: &str) -> String {
const PAIRS: &[(&str, &str)] = &[
(" <=u ", " >u "),
(" >=u ", " <u "),
(" <u ", " >=u "),
(" >u ", " <=u "),
(" <= ", " > "),
(" >= ", " < "),
(" == ", " != "),
(" != ", " == "),
(" < ", " >= "),
(" > ", " <= "),
];
for (op, inverse) in PAIRS {
if let Some(idx) = text.find(op) {
let lhs = &text[..idx];
let rhs = &text[idx + op.len()..];
return format!("{lhs}{inverse}{rhs}");
}
}
format!("!({text})")
}
#[allow(clippy::too_many_arguments)]
fn try_detect_compound(
f: &Function<DecodedInsn>,
_a: &BasicBlock<DecodedInsn>,
head: &crate::arch::x86::LiftedIfBranchHead,
f_idx: usize,
t_idx: usize,
inner: &BasicBlock<DecodedInsn>,
inner_taken: u64,
outer_taken: u64,
) -> Option<IfElseGroup> {
if head.insns_consumed != 2 {
return None;
}
if inner.insns.len() != 2 {
return None;
}
let inner_head = crate::arch::x86::try_lift_if_branch_head(&inner.insns)?;
if inner_head.insns_consumed != 2 {
return None;
}
let inner_fall = match inner.terminator {
Terminator::ConditionalBranch { fallthrough, .. } => fallthrough.0,
_ => return None,
};
let addr_to_idx: HashMap<u64, usize> = f
.blocks
.iter()
.enumerate()
.map(|(i, b)| (b.addr.0, i))
.collect();
let (cond_text, then_range, else_range) = if inner_taken == outer_taken {
let post_idx = *addr_to_idx.get(&inner_fall)?;
if t_idx <= post_idx {
return None;
}
if !is_clean_fallthrough_run(f, post_idx..t_idx - 1) {
return None;
}
let cond = format!("{} && {}", head.cond_text, inner_head.cond_text);
(cond, post_idx..t_idx, Some(t_idx..f.blocks.len()))
} else if inner_fall == outer_taken {
let e_idx = *addr_to_idx.get(&inner_taken)?;
if e_idx <= t_idx {
return None;
}
let a_taken = invert_relational_cond(&head.cond_text);
let cond = format!("{} || {}", a_taken, inner_head.cond_text);
(cond, t_idx..e_idx, Some(e_idx..f.blocks.len()))
} else {
return None;
};
let mut cond_bytes = Vec::with_capacity(head.cond_bytes.len() + inner_head.cond_bytes.len());
cond_bytes.extend_from_slice(&head.cond_bytes);
cond_bytes.extend_from_slice(&inner_head.cond_bytes);
let _ = f_idx;
Some(IfElseGroup {
head_consumed: head.insns_consumed,
pre_body_count: 0,
absorbed_blocks: 1,
cond_text,
cond_bytes,
attrs: Vec::new(),
pre_body: Vec::new(),
then_range,
else_range,
})
}
fn build_if_head_extras(
head: &crate::arch::x86::LiftedIfBranchHead,
block: &BasicBlock<DecodedInsn>,
) -> (Vec<crate::ast::Attribute>, Vec<Stmt>, usize) {
if head.insns_consumed >= 2 {
return (Vec::new(), Vec::new(), 0);
}
let jcc_idx = block.insns.len().saturating_sub(1);
let mut cmp_idx: Option<usize> = None;
for i in (0..jcc_idx).rev() {
let ins = &block.insns[i];
let m = ins.iced.mnemonic();
if matches!(
m,
crate::arch::x86::Mnemonic::Cmp | crate::arch::x86::Mnemonic::Test
) {
cmp_idx = Some(i);
break;
}
if ins.iced.rflags_modified() != 0 {
return (Vec::new(), Vec::new(), 0);
}
}
let Some(cmp_idx) = cmp_idx else {
return (Vec::new(), Vec::new(), 0);
};
let head_bytes = block.insns[cmp_idx].original_bytes.clone();
let attrs = vec![crate::ast::Attribute {
key: "head_bytes".into(),
value: crate::ast::AttrValue::ByteList(head_bytes),
}];
let pre_body_count = jcc_idx - cmp_idx - 1;
(attrs, Vec::new(), pre_body_count)
}
fn is_clean_fallthrough_run(f: &Function<DecodedInsn>, range: std::ops::Range<usize>) -> bool {
range
.into_iter()
.all(|i| matches!(f.blocks[i].terminator, Terminator::Fallthrough))
}
fn collect_slot_to_name(
f: &Function<DecodedInsn>,
signature: Option<&Signature>,
) -> HashMap<i64, String> {
let mut out = HashMap::new();
let Some(sig) = signature else {
return out;
};
let Some(entry) = f.blocks.first() else {
return out;
};
for insn in &entry.insns {
let Some(idx) = arg_spill_index(&insn.iced) else {
continue;
};
let Some(param) = sig.params.get(idx as usize) else {
continue;
};
if param.name.is_empty() {
continue;
}
let disp = crate::arch::x86::signed_memory_displacement(&insn.iced);
out.insert(disp, param.name.clone());
}
out
}
fn return_type_is_integer_like(t: &Type) -> bool {
matches!(
t,
Type::I8
| Type::I16
| Type::I32
| Type::I64
| Type::U8
| Type::U16
| Type::U32
| Type::U64
| Type::Bool
| Type::Char
)
}
fn call_annotation(
insn: &DecodedInsn,
fn_start: u64,
fn_end: u64,
name_at: &HashMap<u64, String>,
) -> Option<String> {
if let Some(target) = direct_call_target(&insn.iced)
&& let Some(name) = name_at.get(&target)
{
return Some(format!("-> {name}"));
}
if let Some(target) = direct_unconditional_branch_target(&insn.iced) {
let outside_function = target < fn_start || target >= fn_end;
if outside_function && let Some(name) = name_at.get(&target) {
return Some(format!("tail-call -> {name}"));
}
}
None
}
fn render_arg_value(value: &ArgValue, ctx: &EmitCtx<'_>) -> String {
match value {
ArgValue::Const(n) if *n > 0 => {
#[allow(clippy::cast_sign_loss)]
let addr = *n as u64;
if let Some(name) = ctx.name_at.get(&addr) {
return format!("&{name}");
}
if let Some((section_name, data, off)) = ctx.data.section_at(addr)
&& is_string_data_section(section_name)
&& let Some(s) = read_cstring_at(data, off)
{
return shorten_for_display(s);
}
n.to_string()
}
ArgValue::Const(n) => n.to_string(),
ArgValue::Lea { addr } => {
if let Some(name) = ctx.name_at.get(addr) {
return format!("&{name}");
}
if let Some((section_name, data, off)) = ctx.data.section_at(*addr)
&& is_string_data_section(section_name)
&& let Some(s) = read_cstring_at(data, off)
{
return shorten_for_display(s);
}
format!("&0x{addr:x}")
}
ArgValue::GlobalLoad { addr } => {
if let Some(name) = ctx.name_at.get(addr) {
return format!("*{name}");
}
format!("*0x{addr:x}")
}
ArgValue::StackLoad { displacement } => {
if *displacement < 0 {
format!("[rbp-0x{:x}]", displacement.unsigned_abs())
} else {
format!("[rbp+0x{displacement:x}]")
}
}
ArgValue::PrevCallResult => "result".into(),
ArgValue::Reg(name) => name.clone(),
ArgValue::Raw(text) => text.clone(),
}
}
fn lea_target_annotation(
insn: &DecodedInsn,
data: &dyn DataLookup,
name_at: &HashMap<u64, String>,
) -> Option<String> {
let addr = direct_lea_rip_target(&insn.iced)?;
if let Some(name) = name_at.get(&addr) {
return Some(format!("= &{name}"));
}
let (section_name, section_data, sec_offset) = data.section_at(addr)?;
if is_string_data_section(section_name)
&& let Some(text) = read_cstring_at(section_data, sec_offset)
{
return Some(format!(
"= {section_name} @ 0x{addr:x} ({:?})",
shorten_for_display(text)
));
}
if section_name.is_empty() {
return Some(format!("= 0x{addr:x}"));
}
Some(format!("= {section_name} @ 0x{addr:x}"))
}
fn is_string_data_section(name: &str) -> bool {
matches!(
name,
".rodata"
| ".rodata.str1.1"
| ".rodata.str1.8"
| ".data.rel.ro"
| ".data.rel.ro.local"
| ".rdata"
| ".rdata$zzz"
)
}
fn read_cstring_at(data: &[u8], offset: usize) -> Option<&str> {
let tail = data.get(offset..)?;
let nul = tail.iter().position(|&b| b == 0)?;
if nul == 0 {
return None;
}
std::str::from_utf8(&tail[..nul]).ok()
}
fn shorten_for_display(s: &str) -> String {
const MAX_CHARS: usize = 60;
if s.chars().count() <= MAX_CHARS {
return s.to_string();
}
let truncated: String = s.chars().take(MAX_CHARS).collect();
format!("{truncated}…")
}
fn arg_spill_lift_index(insn: &DecodedInsn, signature: Option<&Signature>) -> Option<u32> {
let idx = arg_spill_index(&insn.iced)?;
let sig = signature?;
let param = sig.params.get(idx as usize)?;
if param.name.is_empty() {
return None;
}
Some(idx)
}
fn try_lift_local_compound(
block: &BasicBlock<DecodedInsn>,
start: usize,
asm_count: usize,
consumed_by_call: &HashSet<usize>,
call_at: &HashMap<usize, &CallSite>,
out: &mut Vec<Stmt>,
) -> Option<usize> {
let max_window = (asm_count - start).min(3);
if max_window < 2 {
return None;
}
for k in start..start + max_window {
if consumed_by_call.contains(&k) || call_at.contains_key(&k) {
return None;
}
}
let window: Vec<_> = block.insns[start..start + max_window]
.iter()
.map(|i| i.iced)
.collect();
let (consumed, dst, op, src) = crate::arch::x86::match_local_compound(&window)?;
let mut bytes = Vec::new();
for j in start..start + consumed {
bytes.extend_from_slice(&block.insns[j].original_bytes);
}
out.push(Stmt::LocalCompound {
dst,
op: op.to_string(),
src,
bytes,
});
Some(consumed)
}