1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
/// Build a mapping from stub section VAs to imported symbol names.
///
/// When code calls an external function like `strlen`, the call target is a
/// stub entry in `__stubs`. This function reads the indirect symbol table to
/// map each stub's VA to the imported function name.
fn build_stub_map(macho: &MachoFile<'_>) -> BTreeMap<u64, String> {
use macho_core::model::load_command::LoadCommand;
use macho_core::model::section::SectionType;
let mut map = BTreeMap::new();
let symtab = match macho.ext::<SymbolTable<'_>>() {
Ok(st) => st,
Err(_) => return map,
};
let dysymtab = match macho.find_load_command(|lc| matches!(lc, LoadCommand::Dysymtab(_))) {
Some(lc) => match lc.kind().as_dysymtab() {
Some(d) => d.clone(),
None => return map,
},
None => return map,
};
if dysymtab.nindirectsyms == 0 {
return map;
}
let indirect_off = dysymtab.indirectsymoff as usize;
let n_indirect = dysymtab.nindirectsyms as usize;
let endian = macho.endian();
let indirect_data = match macho.read_bytes_at(
macho_core::model::addr::ThinFileOffset(indirect_off as u64),
n_indirect * 4,
) {
Ok(data) => data,
Err(_) => return map,
};
for sect in macho.all_sections() {
if !matches!(
sect.section_type(),
SectionType::SymbolStubs
| SectionType::NonLazySymbolPointers
| SectionType::LazySymbolPointers
) {
continue;
}
let indirect_start = sect.reserved1() as usize;
let entry_size = match sect.section_type() {
SectionType::SymbolStubs => {
if sect.reserved2() == 0 {
continue;
}
sect.reserved2() as u64
}
_ => {
if macho.is_64bit() {
8u64
} else {
4u64
}
}
};
let Some(n_entries) = sect
.size()
.checked_div(entry_size)
.and_then(|count| usize::try_from(count).ok())
else {
continue;
};
for i in 0..n_entries {
let isym_idx = indirect_start + i;
if isym_idx >= n_indirect {
break;
}
let table_offset = isym_idx * 4;
if table_offset + 4 > indirect_data.len() {
break;
}
let raw_index = endian.interpret_u32(u32::from_ne_bytes([
indirect_data[table_offset],
indirect_data[table_offset + 1],
indirect_data[table_offset + 2],
indirect_data[table_offset + 3],
]));
if raw_index & 0xC000_0000 != 0 {
continue;
}
let stub_va = sect.addr().0 + i as u64 * entry_size;
if let Some(sym) = symtab.get(raw_index as usize) {
map.insert(stub_va, sym.name.to_string());
}
}
}
map
}
/// Per-argument usage data collected from the function body.
#[derive(Default)]
struct ArgUsage {
/// True if this argument register was used as a memory base (dereferenced).
is_pointer: bool,
/// Displacements at which the pointer was dereferenced.
deref_offsets: Vec<i64>,
/// True if a known string function receives this argument.
passed_to_string_fn: bool,
/// True if a known ObjC runtime function receives this as arg0.
passed_to_objc_fn: bool,
}
/// Infer argument types from instruction-level usage patterns.
fn infer_argument_types(
cfg: &FunctionCfg,
counts: ParamCounts,
is_arm64: bool,
macho: &MachoFile<'_>,
symtab: &SymbolTable<'_>,
_vtable_index: Option<&VtableIndex>,
) -> Vec<ArgumentTypeHint> {
// Determine which GPR numbers are argument registers (in ABI order).
let gpr_arg_nums: Vec<u8> = if is_arm64 {
(0..8u8).collect() // x0-x7
} else {
vec![X86_RDI, X86_RSI, X86_RDX, X86_RCX, X86_R8, X86_R9]
};
let gpr_count = counts.gpr.min(gpr_arg_nums.len() as u32) as usize;
let fp_count = counts.fp as usize;
// Track which arg registers are "live" (still hold the original argument).
// Once a register is overwritten (appears as Reg destination in a non-store),
// subsequent uses as a memory base don't indicate the *argument* is a pointer.
let active_gpr_args: BTreeSet<u8> = gpr_arg_nums[..gpr_count].iter().copied().collect();
let mut gpr_usage: BTreeMap<u8, ArgUsage> = BTreeMap::new();
let mut overwritten: BTreeSet<u8> = BTreeSet::new();
// Build VA → name maps for call-target resolution.
// sym_by_va: defined symbols (local functions).
// stub_by_va: PLT stubs → imported function names (strlen, objc_msgSend, etc.).
let sym_by_va: BTreeMap<u64, &str> = symtab
.symbols()
.iter()
.filter(|s| s.value != 0)
.map(|s| (s.value, s.name))
.collect();
let stub_by_va = build_stub_map(macho);
// ── Single pass over all reachable instructions ──
//
// For each instruction we:
// 1. Check if an arg register is used as a Mem base (pointer detection),
// but only if the register hasn't been overwritten yet.
// 2. Check if this is a Call to a known string/ObjC function, and if so,
// record which arg register is in the callee's first-arg position.
// 3. Track register overwrites: if an arg register appears as the first
// Reg operand in a non-store, non-push instruction, mark it overwritten.
for block in &cfg.blocks {
if !block.reachable {
continue;
}
for i in block.start..block.end {
let insn = &cfg.insns[i];
let ops = insn.operands();
// -- Pointer detection --
for op in ops {
if let Operand::Mem { base, disp } = op {
if base.class == RegClass::Gpr
&& active_gpr_args.contains(&base.num)
&& !overwritten.contains(&base.num)
{
let usage = gpr_usage.entry(base.num).or_default();
usage.is_pointer = true;
usage.deref_offsets.push(*disp);
}
}
}
// -- Call-target correlation --
if let InsnKind::Call(bi) = &insn.kind {
if let BranchTarget::Direct(offset) = bi.target {
let target_va = cfg.insn_va(insn).wrapping_add_signed(offset);
// Look up the target in both the defined-symbol map and the
// stub map. Stubs cover dynamically-linked calls (strlen, etc.).
let name = sym_by_va
.get(&target_va)
.copied()
.or_else(|| stub_by_va.get(&target_va).map(|s| s.as_str()));
if let Some(name) = name {
let clean = name.trim_start_matches('_').trim_start_matches('$');
let is_string_fn = STRING_FUNCTIONS.contains(&clean);
let is_objc_fn = OBJC_FUNCTIONS.contains(&clean);
if is_string_fn || is_objc_fn {
// The callee receives args in the same ABI registers.
// The first GPR arg register that is still live and is
// a pointer gets the string/ObjC tag.
// This is a simplification: we assume the first live
// pointer-arg register is the one being passed.
for ®_num in &gpr_arg_nums[..gpr_count] {
if overwritten.contains(®_num) {
continue;
}
let usage = gpr_usage.entry(reg_num).or_default();
if is_string_fn {
usage.passed_to_string_fn = true;
}
if is_objc_fn {
usage.passed_to_objc_fn = true;
}
break; // tag only the first live arg
}
}
}
}
}
// -- Register overwrite tracking --
// Delegated to the decoder: `insn.writes_op0_reg` is ground truth
// from iced (x86_64) / bad64-format decode (ARM64). It catches both
// the destination-first conventions (`MOV rdi, …`, `LDR x0, …`) and
// in-place arithmetic (`ADD rdi, rax`) that the previous
// operand-shape heuristic missed, while still skipping `CMP`/`TEST`
// and their ARM64 `CMP`/`CMN`/`TST` equivalents (which are
// comparison flag-setters, not register writes to op0).
if insn.writes_op0_reg {
if let Some(r) = insn.op0_write_target() {
if r.class == RegClass::Gpr && active_gpr_args.contains(&r.num) {
overwritten.insert(r.num);
}
}
}
}
}
// ── Classify each argument ──
let mut hints = Vec::with_capacity(gpr_count + fp_count);
// GPR arguments (in ABI position order).
// Call-correlation tags (ObjcObject, CString) take priority — the argument
// is being passed to a known function, which is strong evidence of its type
// even if the caller never locally dereferences it.
for ®_num in &gpr_arg_nums[..gpr_count] {
if let Some(usage) = gpr_usage.get(®_num) {
if usage.passed_to_objc_fn {
hints.push(ArgumentTypeHint::ObjcObject);
} else if usage.passed_to_string_fn {
hints.push(ArgumentTypeHint::CString);
} else if usage.is_pointer {
if usage.deref_offsets.contains(&0) && usage.deref_offsets.len() > 1 {
hints.push(ArgumentTypeHint::StructPointer);
} else {
hints.push(ArgumentTypeHint::Pointer);
}
} else {
hints.push(ArgumentTypeHint::Scalar);
}
} else {
hints.push(ArgumentTypeHint::Scalar);
}
}
// FP arguments.
for _ in 0..fp_count {
hints.push(ArgumentTypeHint::FloatingPoint);
}
hints
}