1use serde::{Deserialize, Serialize};
2
3use crate::model::addr::Va;
4use crate::model::macho_file::MachoFile;
5use crate::model::symbol::SymbolTable;
6use crate::{Error, Result};
7use macho_demangle::demangle_symbol;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct VtableIndex {
12 pub vtables: Vec<VtableEntry>,
14 truncated: bool,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct VtableEntry {
20 pub name: Option<String>,
22 pub mangled_name: Option<String>,
24 #[serde(with = "va_serde")]
25 pub va: Va,
27 pub size: u64,
29 pub slots: Vec<VtableSlot>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct VtableSlot {
36 pub offset: u64,
38 #[serde(with = "va_serde")]
39 pub va: Va,
41 pub target: SlotTarget,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(tag = "kind", rename_all = "snake_case")]
47#[non_exhaustive]
49pub enum SlotTarget {
50 Function {
52 name: String,
54 #[serde(with = "va_serde")]
55 va: Va,
57 },
58 PureVirtual,
60 TypeInfo {
62 #[serde(with = "va_serde")]
63 va: Va,
65 },
66 OffsetToTop {
68 value: i64,
70 },
71 Unknown {
73 value: u64,
75 },
76}
77
78mod va_serde {
79 use serde::{Deserialize, Deserializer, Serializer};
80
81 use crate::model::addr::Va;
82
83 pub fn serialize<S>(value: &Va, serializer: S) -> Result<S::Ok, S::Error>
84 where
85 S: Serializer,
86 {
87 serializer.serialize_u64(value.0)
88 }
89
90 pub fn deserialize<'de, D>(deserializer: D) -> Result<Va, D::Error>
91 where
92 D: Deserializer<'de>,
93 {
94 u64::deserialize(deserializer).map(Va)
95 }
96}
97
98#[derive(Debug)]
100enum ResolvedSlotValue {
101 Address(u64),
103 Import { name: String },
105 Raw(u64),
107}
108
109impl VtableIndex {
110 pub fn build(macho: &MachoFile<'_>) -> Result<Self> {
112 Self::build_limited(macho, usize::MAX)
113 }
114
115 pub fn build_from_source<S>(source: &S) -> Result<Self>
121 where
122 S: AsRef<[u8]> + ?Sized,
123 {
124 Self::build_limited_from_source(source, usize::MAX)
125 }
126
127 pub fn build_limited(macho: &MachoFile<'_>, max_vtables: usize) -> Result<Self> {
133 let symtab = macho.ext::<SymbolTable<'_>>()?;
134 let symbols = symtab.symbols();
135
136 let ptr_size: u64 = if macho.is_64bit() { 8 } else { 4 };
137 let image_base = macho.image_base().0;
138
139 let mut va_to_name: std::collections::HashMap<u64, &str> = std::collections::HashMap::new();
141 for sym in symbols {
142 if sym.is_defined() && sym.value != 0 {
143 va_to_name.insert(sym.value, sym.name);
144 }
145 }
146
147 let fixup_map = build_vtable_fixup_map(macho);
151
152 let typeinfo_vas: std::collections::HashSet<u64> = symbols
154 .iter()
155 .filter(|s| s.name.starts_with("__ZTI") || s.name.starts_with("_ZTI"))
156 .filter(|s| s.is_defined() && s.value != 0)
157 .map(|s| s.value)
158 .collect();
159
160 let mut vtable_syms: Vec<_> = symbols
162 .iter()
163 .filter(|s| {
164 s.is_defined()
165 && s.value != 0
166 && (s.name.starts_with("__ZTV") || s.name.starts_with("_ZTV"))
167 })
168 .take(max_vtables.saturating_add(1))
169 .collect();
170 vtable_syms.sort_by_key(|s| s.value);
171 let truncated = vtable_syms.len() > max_vtables;
172 vtable_syms.truncate(max_vtables);
173
174 let mut all_defined_vas: Vec<u64> = symbols
176 .iter()
177 .filter(|s| s.is_defined() && s.value != 0)
178 .map(|s| s.value)
179 .collect();
180 all_defined_vas.sort();
181 all_defined_vas.dedup();
182
183 let mut vtables = Vec::new();
184
185 for vtable_sym in &vtable_syms {
186 let vtable_va = vtable_sym.value;
187
188 let max_size = match all_defined_vas.binary_search(&vtable_va) {
190 Ok(idx) => {
191 if idx + 1 < all_defined_vas.len() {
192 all_defined_vas[idx + 1] - vtable_va
193 } else {
194 256 * ptr_size
196 }
197 }
198 Err(_) => 1024 * ptr_size,
199 };
200
201 let ctx = VtableScanContext {
203 image_base,
204 va_to_name: &va_to_name,
205 typeinfo_vas: &typeinfo_vas,
206 fixup_map: &fixup_map,
207 };
208
209 let slots = match read_vtable_slots(macho, Va(vtable_va), ptr_size, max_size, &ctx) {
210 Ok(s) => s,
211 Err(_) => continue,
212 };
213
214 if slots.is_empty() {
215 continue;
216 }
217
218 let size = slots.last().map(|s| s.offset + ptr_size).unwrap_or(0);
219
220 let demangled = demangle_symbol(vtable_sym.name);
222
223 vtables.push(VtableEntry {
224 name: demangled,
225 mangled_name: Some(vtable_sym.name.to_owned()),
226 va: Va(vtable_va),
227 size,
228 slots,
229 });
230 }
231
232 Ok(Self { vtables, truncated })
233 }
234
235 pub fn build_limited_from_source<S>(source: &S, max_vtables: usize) -> Result<Self>
240 where
241 S: AsRef<[u8]> + ?Sized,
242 {
243 let macho = crate::parse_source(source)?;
244 Self::build_limited(&macho, max_vtables)
245 }
246
247 pub fn find_by_class(&self, class_name: &str) -> Option<&VtableEntry> {
249 self.vtables
250 .iter()
251 .find(|v| v.name.as_ref().is_some_and(|n| n.contains(class_name)))
252 }
253
254 pub fn find_by_va(&self, va: Va) -> Option<&VtableEntry> {
256 self.vtables.iter().find(|v| v.va == va)
257 }
258
259 pub fn slot_at(&self, va: Va) -> Option<(&VtableEntry, &VtableSlot)> {
261 for vtable in &self.vtables {
262 for slot in &vtable.slots {
263 if slot.va == va {
264 return Some((vtable, slot));
265 }
266 }
267 }
268 None
269 }
270
271 pub fn slots_targeting_va(&self, target_va: Va) -> Vec<(&VtableEntry, &VtableSlot)> {
273 let mut results = Vec::new();
274 for vtable in &self.vtables {
275 for slot in &vtable.slots {
276 if let SlotTarget::Function { va, .. } = &slot.target {
277 if *va == target_va {
278 results.push((vtable, slot));
279 }
280 }
281 }
282 }
283 results
284 }
285
286 pub fn vtables(&self) -> &[VtableEntry] {
288 &self.vtables
289 }
290
291 pub fn was_truncated(&self) -> bool {
294 self.truncated
295 }
296
297 pub fn find_slot_by_method(
306 &self,
307 class_name: &str,
308 method_name: &str,
309 ) -> Option<(&VtableEntry, &VtableSlot, usize)> {
310 let entry = self.find_by_class(class_name)?;
311 entry
312 .find_slot_by_name(method_name)
313 .map(|(slot, idx)| (entry, slot, idx))
314 }
315}
316
317impl VtableEntry {
318 pub fn function_slots(&self) -> impl Iterator<Item = (usize, &VtableSlot)> {
320 self.slots
321 .iter()
322 .filter(|s| {
323 matches!(
324 s.target,
325 SlotTarget::Function { .. } | SlotTarget::PureVirtual
326 )
327 })
328 .enumerate()
329 }
330
331 pub fn function_slot_count(&self) -> usize {
333 self.slots
334 .iter()
335 .filter(|s| {
336 matches!(
337 s.target,
338 SlotTarget::Function { .. } | SlotTarget::PureVirtual
339 )
340 })
341 .count()
342 }
343
344 pub fn find_slot_by_name(&self, method_name: &str) -> Option<(&VtableSlot, usize)> {
349 for (func_idx, slot) in self.function_slots() {
350 if let SlotTarget::Function { name, .. } = &slot.target {
351 if extract_method_leaf(name) == method_name || name.contains(method_name) {
353 return Some((slot, func_idx));
354 }
355 }
356 }
357 None
358 }
359
360 pub fn function_slot_at(&self, index: usize) -> Option<&VtableSlot> {
363 self.function_slots()
364 .find(|(i, _)| *i == index)
365 .map(|(_, slot)| slot)
366 }
367}
368
369fn extract_method_leaf(demangled: &str) -> &str {
374 let base = demangled.split('(').next().unwrap_or(demangled);
376 base.rsplit("::").next().unwrap_or(base)
378}
379
380#[derive(Debug, Clone)]
382enum VtableFixup {
383 Rebase(u64),
385 Bind { import_name: String },
387}
388
389fn build_vtable_fixup_map(macho: &MachoFile<'_>) -> std::collections::HashMap<u64, VtableFixup> {
392 use macho_dyld::chained::parse_chained_fixups;
393 use macho_dyld::types::FixupKind;
394
395 let mut map = std::collections::HashMap::new();
396
397 match parse_chained_fixups(macho) {
398 Ok(fixups) => {
399 for fixup in &fixups.fixups {
400 let seg = match macho.segments().get(fixup.segment_index) {
401 Some(s) => s,
402 None => continue,
403 };
404 let file_offset = seg.file_offset().0 + fixup.segment_offset;
405
406 match &fixup.kind {
407 FixupKind::Rebase { target } | FixupKind::AuthRebase { target, .. } => {
408 map.insert(file_offset, VtableFixup::Rebase(*target));
409 }
410 FixupKind::Bind { import_index, .. }
411 | FixupKind::AuthBind { import_index, .. } => {
412 let name = fixups
413 .imports
414 .get(*import_index as usize)
415 .map(|i| i.name.to_string())
416 .unwrap_or_default();
417 map.insert(file_offset, VtableFixup::Bind { import_name: name });
418 }
419 _ => continue,
420 }
421 }
422 }
423 Err(_) => {
424 if let Ok((regular, weak, lazy)) = macho_dyld::bind::parse_bind_entries(macho) {
426 for entry in regular.iter().chain(weak.iter()).chain(lazy.iter()) {
427 if let Some(seg) = macho.segments().get(entry.segment_index) {
428 let file_offset = seg.file_offset().0 + entry.segment_offset;
429 map.insert(
430 file_offset,
431 VtableFixup::Bind {
432 import_name: entry.symbol_name.to_string(),
433 },
434 );
435 }
436 }
437 }
438
439 if let Ok(rebases) = macho_dyld::rebase::parse_rebase_entries(macho) {
440 for entry in &rebases {
441 if let Some(seg) = macho.segments().get(entry.segment_index) {
442 let file_offset = seg.file_offset().0 + entry.segment_offset;
443 map.entry(file_offset).or_insert(VtableFixup::Rebase(0));
444 }
445 }
446 }
447 }
448 }
449
450 map
451}
452
453fn resolve_slot_value(
455 raw_value: u64,
456 file_offset: u64,
457 image_base: u64,
458 fixup_map: &std::collections::HashMap<u64, VtableFixup>,
459 macho: &MachoFile<'_>,
460 endian: crate::format::io::endian::Endian,
461) -> ResolvedSlotValue {
462 if let Some(fixup) = fixup_map.get(&file_offset) {
463 match fixup {
464 VtableFixup::Rebase(target) if *target != 0 => {
465 ResolvedSlotValue::Address(image_base + target)
466 }
467 VtableFixup::Rebase(_) => {
468 let raw =
471 crate::format::io::pod::read_pod::<u64>(macho.bytes(), file_offset as usize)
472 .map(|v| endian.interpret_u64(v))
473 .unwrap_or(raw_value);
474 ResolvedSlotValue::Address(raw)
475 }
476 VtableFixup::Bind { import_name } => ResolvedSlotValue::Import {
477 name: import_name.clone(),
478 },
479 }
480 } else {
481 ResolvedSlotValue::Raw(raw_value)
484 }
485}
486
487struct VtableScanContext<'a> {
488 image_base: u64,
489 va_to_name: &'a std::collections::HashMap<u64, &'a str>,
490 typeinfo_vas: &'a std::collections::HashSet<u64>,
491 fixup_map: &'a std::collections::HashMap<u64, VtableFixup>,
492}
493
494fn read_vtable_slots(
495 macho: &MachoFile<'_>,
496 vtable_va: Va,
497 ptr_size: u64,
498 max_size: u64,
499 ctx: &VtableScanContext<'_>,
500) -> Result<Vec<VtableSlot>> {
501 let endian = macho.endian();
502 let max_slots = max_size / ptr_size;
503 let has_fixups = !ctx.fixup_map.is_empty();
504 let mut slots = Vec::new();
505
506 for i in 0..max_slots {
507 let slot_offset = i * ptr_size;
508 let slot_va = Va(vtable_va.0 + slot_offset);
509
510 let bytes = match macho.read_bytes_at_va(slot_va, ptr_size as usize) {
511 Ok(b) => b,
512 Err(_) => break,
513 };
514
515 let raw_value = if ptr_size == 8 {
516 let arr: [u8; 8] = bytes
517 .try_into()
518 .map_err(|_| Error::format("failed to read 8 bytes for vtable slot"))?;
519 endian.read_u64(arr)
520 } else {
521 let arr: [u8; 4] = bytes
522 .try_into()
523 .map_err(|_| Error::format("failed to read 4 bytes for vtable slot"))?;
524 endian.read_u32(arr) as u64
525 };
526
527 let file_offset = macho
529 .address_map()
530 .va_to_thin_offset(slot_va)
531 .map(|o| o.0)
532 .unwrap_or(0);
533
534 let resolved = resolve_slot_value(
535 raw_value,
536 file_offset,
537 ctx.image_base,
538 ctx.fixup_map,
539 macho,
540 endian,
541 );
542
543 let target = classify_slot(
544 &resolved,
545 raw_value,
546 i,
547 ctx.va_to_name,
548 ctx.typeinfo_vas,
549 has_fixups,
550 );
551
552 if i > 2 {
556 match &resolved {
557 ResolvedSlotValue::Address(0) | ResolvedSlotValue::Raw(0) => break,
558 _ => {}
559 }
560 }
561
562 slots.push(VtableSlot {
563 offset: slot_offset,
564 va: slot_va,
565 target,
566 });
567 }
568
569 Ok(slots)
570}
571
572fn classify_slot(
573 resolved: &ResolvedSlotValue,
574 _raw_value: u64,
575 slot_index: u64,
576 va_to_name: &std::collections::HashMap<u64, &str>,
577 typeinfo_vas: &std::collections::HashSet<u64>,
578 has_fixups: bool,
579) -> SlotTarget {
580 if slot_index == 0 {
583 let value = match resolved {
589 ResolvedSlotValue::Raw(v) => *v as i64,
590 ResolvedSlotValue::Address(v) => *v as i64,
591 ResolvedSlotValue::Import { .. } => 0,
592 };
593 return SlotTarget::OffsetToTop { value };
594 }
595
596 if slot_index == 1 {
598 match resolved {
599 ResolvedSlotValue::Address(va) if typeinfo_vas.contains(va) => {
600 return SlotTarget::TypeInfo { va: Va(*va) };
601 }
602 ResolvedSlotValue::Address(va) if *va != 0 => {
603 return SlotTarget::TypeInfo { va: Va(*va) };
606 }
607 ResolvedSlotValue::Import { .. } => {
608 return SlotTarget::TypeInfo { va: Va(0) };
610 }
611 ResolvedSlotValue::Raw(v) if !has_fixups => {
612 if typeinfo_vas.contains(v) || *v != 0 {
613 return SlotTarget::TypeInfo { va: Va(*v) };
614 }
615 return SlotTarget::TypeInfo { va: Va(0) };
616 }
617 _ => {
618 return SlotTarget::TypeInfo { va: Va(0) };
619 }
620 }
621 }
622
623 let effective_va = match resolved {
625 ResolvedSlotValue::Address(va) => *va,
626 ResolvedSlotValue::Import { name } => {
627 if is_pure_virtual_name(name) {
629 return SlotTarget::PureVirtual;
630 }
631 let demangled = demangle_symbol(name).unwrap_or_else(|| name.clone());
633 return SlotTarget::Function {
634 name: demangled,
635 va: Va(0),
636 };
637 }
638 ResolvedSlotValue::Raw(v) => *v,
639 };
640
641 if let Some(name) = va_to_name.get(&effective_va) {
643 if is_pure_virtual_name(name) {
645 return SlotTarget::PureVirtual;
646 }
647 let demangled = demangle_symbol(name).unwrap_or_else(|| (*name).to_owned());
648 return SlotTarget::Function {
649 name: demangled,
650 va: Va(effective_va),
651 };
652 }
653
654 SlotTarget::Unknown {
656 value: effective_va,
657 }
658}
659
660fn is_pure_virtual_name(name: &str) -> bool {
667 let stripped = name.strip_prefix('_').unwrap_or(name);
668 matches!(stripped, "__cxa_pure_virtual" | "__cxa_deleted_virtual")
669}