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’s objc_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: given current_class at 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 every speak implementation in the binary.