Skip to main content

macho_cpp/abi/
mod.rs

1//! C++ function body analysis for ABI heuristics.
2//!
3//! Uses `macho-insn` to decode function prologues and infer:
4//! - Whether the function is a stub, thunk, or standard body
5//! - Return channel (GPR, FP/SIMD, aggregate-indirect, void)
6//! - Estimated parameter count from register saves
7//! - `this` adjustment for thunks
8
9use crate::VtableIndex;
10use crate::{
11    ArgumentTypeHint, CppBodyAnalysis, CppBodyKind, CppConfidence, CppEvidence, CppEvidenceKind,
12    CppReturnChannel,
13};
14use macho_core::model::addr::Va;
15use macho_core::model::macho_file::MachoFile;
16use macho_core::model::symbol::{Symbol, SymbolTable};
17use macho_insn::{Arch, BranchTarget, Insn, InsnKind, Operand, RegClass};
18use std::collections::{BTreeMap, BTreeSet};
19
20/// Maximum bytes to read from a function body. Bounded at 16 KiB to cover
21/// essentially any real function while avoiding pathological allocations.
22const MAX_BODY_BYTES: usize = 16384;
23
24/// Maximum instructions to decode. A 16 KiB function is at most ~4000 ARM64
25/// instructions or ~16000 x86_64 instructions; 4000 is a safe ceiling.
26const MAX_INSNS: usize = 4000;
27
28/// How many prologue instructions to scan for register spills.
29const PROLOGUE_WINDOW: usize = 25;
30
31/// How many instructions before RET to examine for return channel evidence.
32const EPILOGUE_WINDOW: usize = 10;
33
34// ABI parameter register limits.
35const ARM64_MAX_GPR_ARGS: u32 = 8; // x0-x7  (AAPCS64)
36const ARM64_MAX_FP_ARGS: u32 = 8; // d0-d7  (AAPCS64)
37const X86_64_MAX_GPR_ARGS: u32 = 6; // rdi,rsi,rdx,rcx,r8,r9 (SysV)
38const X86_64_MAX_FP_ARGS: u32 = 8; // xmm0-xmm7             (SysV)
39
40/// Analyze the body of a C++ symbol for ABI characteristics.
41pub fn analyze_symbol_body(
42    macho: &MachoFile<'_>,
43    symtab: &SymbolTable<'_>,
44    symbol: &Symbol<'_>,
45    vtable_index: Option<&VtableIndex>,
46) -> Option<CppBodyAnalysis> {
47    if !symbol.is_defined() || symbol.value == 0 {
48        return None;
49    }
50
51    let bytes = symbol_bytes(macho, symtab, symbol, MAX_BODY_BYTES)?;
52    let arch_name = macho.header().cpu_type().name().to_string();
53
54    let (
55        kind,
56        return_channel,
57        likely_wrapper,
58        this_adjustment,
59        param_counts,
60        cfg,
61        mut evidence_detail,
62    ) = if arch_name.starts_with("arm64") {
63        let arch = if arch_name == "arm64e" {
64            Arch::Arm64e
65        } else {
66            Arch::Arm64
67        };
68        analyze_arm64(bytes, symbol.value, arch)
69    } else if arch_name == "x86_64" {
70        analyze_x86_64(bytes, symbol.value)
71    } else {
72        (
73            CppBodyKind::Unknown,
74            CppReturnChannel::Unknown,
75            false,
76            None,
77            None,
78            None,
79            "unsupported architecture".to_string(),
80        )
81    };
82
83    let param_count = param_counts.map(|pc| pc.total());
84
85    // Argument type inference: runs on the CFG from the arch analysis.
86    let argument_hints = if let (Some(cfg), Some(counts)) = (&cfg, param_counts) {
87        let is_arm64 = arch_name.starts_with("arm64");
88        infer_argument_types(cfg, counts, is_arm64, macho, symtab, vtable_index)
89    } else {
90        Vec::new()
91    };
92
93    let mut confidence = match kind {
94        CppBodyKind::Thunk | CppBodyKind::Stub => CppConfidence::High,
95        CppBodyKind::Standard => {
96            if return_channel != CppReturnChannel::Unknown {
97                CppConfidence::Medium
98            } else {
99                CppConfidence::Low
100            }
101        }
102        CppBodyKind::Unknown => CppConfidence::Low,
103    };
104    if let Some(cfg) = &cfg {
105        if !cfg.decode_gaps.is_empty() {
106            confidence = CppConfidence::Low;
107            evidence_detail.push_str(&format!(
108                "; lossy decoding skipped {} invalid region(s)",
109                cfg.decode_gaps.len()
110            ));
111        }
112    }
113
114    Some(CppBodyAnalysis {
115        arch: arch_name,
116        kind,
117        return_channel,
118        this_adjustment,
119        likely_wrapper,
120        param_count,
121        argument_hints,
122        evidence: vec![CppEvidence {
123            kind: CppEvidenceKind::BodyAnalysis,
124            confidence,
125            detail: evidence_detail,
126        }],
127    })
128}
129
130fn symbol_bytes<'a>(
131    macho: &'a MachoFile<'_>,
132    symtab: &SymbolTable<'_>,
133    symbol: &Symbol<'_>,
134    max_len: usize,
135) -> Option<&'a [u8]> {
136    let next_va = symtab
137        .defined()
138        .filter(|candidate| candidate.value > symbol.value)
139        .map(|candidate| candidate.value)
140        .min()
141        .unwrap_or(symbol.value + max_len as u64);
142    let len = (next_va - symbol.value).min(max_len as u64) as usize;
143    macho.read_bytes_at_va(Va(symbol.value), len.max(1)).ok()
144}
145
146include!("cfg.rs");
147include!("arm64.rs");
148include!("x86_64.rs");
149include!("arguments.rs");
150include!("tests.rs");