RTTI and Devirtualization in Objective-C
Object Model#
Unlike C++, ObjC classes are runtime objects, not compile-time abstractions. The compiler emits class metadata into Mach-O sections (__objc_data, __objc_const) as struct objc_class:
struct objc_class {
Class isa; // -> metaclass
Class superclass; // -> parent class
void *cache;
uintptr_t bits; // class_rw_t / class_ro_t
};
superclass(offset +8): parent’sobjc_class.isa: points to the metaclass — a hidden class object whose instance methods are this class’s class methods (+alloc, etc). Metaclasses have their own superclass chain.- Symbols:
_OBJC_CLASS_$_Dog,_OBJC_METACLASS_$_Dog.
objc_msgSendSuper2#
[super speak] does not compile to objc_msgSend. Modern ARM64/Apple runtimes use objc_msgSendSuper2, passing a stack-allocated struct:
struct objc_super2 {
id receiver; // self
Class current_class; // class where the call site lives (Dog, not Animal)
};
At runtime, objc_msgSendSuper2 dereferences current_class->superclass and searches upward for the selector. current_class (not the superclass) is passed so the compiler avoids resolving the superclass statically — which means static analysis tools must reconstruct the inheritance edge themselves to devirtualize the call.
RTTI Recovery (librz/arch/objc_rtti.c)#
1. Resolve superclass pointer of a metaclass symbol — read 8 bytes at vaddr+8, match against known symbol vaddrs:
static const RzBinSymbol *get_objc_superclass(RzCore *core,
const RzPVector *symbols, const RzBinSymbol *meta_info) {
ut64 addr;
rz_io_nread_at(core->io, meta_info->vaddr + 8, (ut8 *)&addr, 8);
void **it;
rz_pvector_foreach (symbols, it) {
RzBinSymbol *super_meta = *it;
if (super_meta && super_meta->vaddr == addr)
return super_meta;
}
return NULL;
}
2. Register the base-class edge — strip _OBJC_METACLASS_$_ prefix, call rz_analysis_class_base_set with offset 0 (ObjC single inheritance shares the base pointer):
static void add_objc_superclass(RzAnalysis *analysis, RzPVector *classes,
RzBinSymbol *meta_class, const RzBinSymbol *meta_superclass) {
if (!strstr(meta_superclass->name, "_OBJC_METACLASS_$_")) return;
char *class_name = meta_class->name + strlen("_OBJC_METACLASS_$_");
char *superclass_name = rz_str_dup(meta_superclass->name + strlen("_OBJC_METACLASS_$_"));
RzAnalysisBaseClass base = { .class_name = superclass_name, .offset = 0 };
rz_analysis_class_base_set(analysis, class_name, &base);
rz_analysis_class_base_fini(&base);
}
3. Entry point — iterate all symbols, filter _OBJC_METACLASS_$_*, wire into rz_analysis_rtti_objc():
RZ_API void rz_analysis_rtti_objc(RZ_NULLABLE RzAnalysis *analysis) {
if (!analysis) return;
RzPVector *classes = rz_analysis_class_get_all(analysis, false);
if (!classes) return;
RzBinObject *o = rz_bin_cur_object(analysis->binb.bin);
if (!o) return;
const RzPVector *symbols = rz_bin_object_get_symbols(o);
void **it;
rz_pvector_foreach (symbols, it) {
RzBinSymbol *sym = *it;
if (!sym) continue;
const RzBinSymbol *meta_superclass = NULL;
if (strstr(sym->name, "_OBJC_METACLASS_$_"))
meta_superclass = get_objc_superclass(analysis->core, symbols, sym);
if (meta_superclass)
add_objc_superclass(analysis, classes, sym, meta_superclass);
}
rz_pvector_free(classes);
}
Hooked into librz/arch/rtti.c dispatch:
case RZ_BIN_LANGUAGE_SWIFT:
rz_analysis_rtti_swift(analysis);
break;
+ case RZ_BIN_LANGUAGE_OBJC:
+ rz_analysis_rtti_objc(analysis);
+ // fallthrough
Result#
[0x100000000]> aaa
[0x100000000]> acll
[Animal]
nth addr vt_offset type name
-----------------------------------------
1 0x100003e1c ---------- DEFAULT speak
[Dog: Animal]
nth addr vt_offset type name
-----------------------------------------
1 0x100003e48 ---------- DEFAULT speak
[Dog: Animal] — inheritance is now explicit in Rizin’s class DB.
Why It Matters#
- Devirtualizing
objc_msgSendSuper2: givencurrent_classat a call site, look up its base class and resolve the selector against the parent’s method list instead of leaving the call unresolved. - Override detection: directly see which subclasses override which parent methods.
- Xref precision: a
@selector(speak)call site can be narrowed to candidates within the relevant class hierarchy instead of matching everyspeakimplementation in the binary.