磁盘上的 Objective-C ISA 指针与实例化对象时的指针

Objective-C ISA pointer on disk vs when object being instantiated

提问人:Jorayen 提问时间:10/15/2023 最后编辑:Jorayen 更新时间:10/19/2023 访问量:71

问:

Objective-C 运行时 ISA 指针定义如下:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

位字段可以通过此处的定义读取。

当我从磁盘上读到一个男子气概并转到该部分并遵循定义如下的部分时:_objc_classlistobjc_class


struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    ...

定义如下:objc_object

struct objc_object {
private:
    isa_t isa;

public:
    ...

这意味着我应该能够将 的前 8 个字节解释为 的字段,但是当我这样做并尝试解释这些位时,我会得到随机和错误的信息, 另一方面,如果我将前 8 个字节解释为指针,它会将我引导到磁盘上的另一个实例,这通常是类的元类。那么我想知道为什么联合的定义来自 Objective-C 运行时及其字段。当我们实例化某种对象并且从磁盘读取时,它只是指向元类定义的指针时,将其解释为并集是否正确?objc_classbitsisaobjc_classisabitsisabits

编辑:
我从文件中读取结构的方式是使用 python:
objc_class

ISA_MASK = 0x0000000ffffffff8

@dataclass
class Isa():
    bits: ctypes.c_size_t
    _cls: ctypes.c_size_t

    def __init__(self, fp, addr):
        fp.seek(addr)
        self.bits = struct.unpack("<Q", fp.read(8))[0]
        self._cls = self.bits

    def nonpointer(self):
        return self.bits & 1
    
    def has_assoc(self):
        return (self.bits >> 1) & 1
    
    def has_cxx_dtor(self):
        return (self.bits >> 2) & 1
    
    def shiftcls(self):
        return (self.bits >> 3) & 0x7ffffffff
    
    def magic(self):
        return (self.bits >> 36) & 0x3f
    
    def weakly_referenced(self):
        return (self.bits >> 42) & 1
    
    def unused(self):
        return (self.bits >> 43) & 1

    def has_sidetable_rc(self):
        return (self.bits >> 44) & 1

    def extra_rc(self):
        return (self.bits >> 45) & 0x7ffff

    def get_class(self):
        clsbits = self.bits
        clsbits &= ISA_MASK
        return clsbits


@dataclass
class ObjcObject:
    isa: Isa
    _addr: ctypes.c_size_t

    def __init__(self, fp, addr, isa_class, external_block_addr):
        self.isa = None
        self._addr = addr

        fp.seek(addr)

        isa_addr = struct.unpack("<Q", fp.read(8))[0]
        if isa_addr != 0 and isa_addr < external_block_addr:
            self.isa = Isa(fp, isa_addr, external_block_addr)
@dataclass
class ObjcClass(ObjcObject):
    super_class: ObjcClass
    cache: Cache
    class_ro: ClassRo

    def __init__(self, fp, addr, external_block_addr):
        super().__init__(fp, addr, ObjcClass, external_block_addr)
        ...
        ...

例如,我有一个类让我们调用它,在处理地址上的链式修复后,我有它,它的符号和该地址中的定义。A0x0025eed0_OBJC_CLASS_$_Aobjc_class

结构的前 8 个字节是我们通过查看运行时的源代码建立的 ISA。将它作为指针而不是将其视为联合,我得到了符号的另一个结构,即该类的元类。isa_tobjc_class_OBJC_METACLASS_$_A

现在,如果我没有将结构的前 8 个字节视为指向元类的指针,而是尝试将它们解释为联合的位,就像我在提供的代码中一样,例如使用我得到的方法,这是不正确的,因为我可以在结构上清楚地找到这个方法,所以它与我解析的内容不匹配,因此联合似乎不相关到磁盘上类的实际数据。objc_classisa_thas_cxx_dtorFalsemethod_list_tclass_roisa_t

请注意,从 的位中提取数据的方法是查看 isa.h 的源代码,并假设我读取了一个没有 ptr auth 的 ARM64 男子气概,而不是从模拟器读取的。isa_t

iOS系统 Objective-C-运行时 Objective-C-2.0

评论

1赞 Itai Ferber 10/15/2023
你能分享一下你用来读取和解释这些位的代码吗?访问中可能存在导致意外行为的特定内容。
0赞 Jorayen 10/16/2023
@ItaiFerber这不仅仅是我可以在 stackoverflow 上分享的东西,它太大了。但它基本上是按照男子气概的加载命令进行操作,修复链式修复,找到该部分并获取文件中定义的结构,并根据运行时中的定义读取它们。我发现的唯一怪癖是 ISA 字段基本上只是指向当前类的元类的另一个指针,所有这些位字段都没有任何意义,至少当我从磁盘读取时__objc_classlistobjc_classobjc_classobjc_class
0赞 Itai Ferber 10/16/2023
我并不一定是要你分享整个代码,只是一个片段,展示你是如何阅读和解释 isa 的;如果没有一些最小的可重现代码,就很难提供更多帮助......当你说“位字段毫无意义”时,你期望的位与你得到的位是什么?假设字段是并集,则位应准确表示指针的整数值...
0赞 Jorayen 10/18/2023
@ItaiFerber,我提供了有关我如何解析结构以及我所看到的与我期望看到的以及我实际看到的与我提取的其他数据(如类的method_list)相矛盾的其他数据的其他信息。

答:

2赞 Itai Ferber 10/19/2023 #1

在对运行时进行一些挖掘之后,似乎非指针 isa 是仅运行时的概念,并且所有磁盘上的 isa 将始终是常规指针。

对象文件中 Obj-C 类的加载过程:

  1. dyld调用 (/),传入对象标头以从中读取和加载类_objc_map_imagesobjc-internal.hobjc-runtime-new.mm
  2. _objc_map_images在调用之前做一些设置map_images (objc-private.h/objc-runtime-new.mm)
  3. map_images获取运行时锁,然后调用map_images_nolock (objc-private.h/objc-os.mm)
  4. map_images_nolock遍历 mach 标头,搜索 Obj-C 信息并执行一些验证。它将所有包含 Obj-C 类的标头传递给_read_images (objc-private.h/objc-runtime-new.mm)
  5. _read_images是我们真正到达有趣部分的地方。它首先设置对与运行时目标相关的非指针 isas 的支持,并设置一些用于存储类信息的表。在读取并修复选择器后,它开始读取类信息(OBJC_RUNTIME_DISCOVER_CLASSES_START())
    • 对于每个标头,它会循环访问标头中存储的原始数据,接收指向图像中每个类的直接指针classlist
    • 对于以这种方式读取的每个类,它都会调用 (),它解析了错误的类名、Swift 类等——但最终,读取(指向 dyld 类的原始指针)要么被强制转换为(类对象),要么被分配的实例替换readClassobjc-runtime-new.mmclassref_tClassClass

那么,非指针 isas 在哪里发挥作用呢?仅当在运行时设置对象的类时:

  1. 当您通过 either () 创建对象时,或者通过 设置对象的类时,该对象已调用 either 或 () (并且只是调用 to )objc_constructInstanceclass_createInstanceruntime.hobject_setClassobjc_object::initInstanceIsaobjc_object::initIsaobjc-object.hinitInstanceIsainitIsa
  2. objc_object::initIsa有两个实现(一个是 for,另一个是不支持的),但都调用到SUPPORT_NONPOINTER_ISAisa_t::setClass (objc-private.h/objc-object.h)
  3. isa_t::setClass还有两个实现 — 当为 true 时,实现在 ISA 值本身中设置适当的位,根据需要进行设置;当为 false 时,它只是直接设置类SUPPORT_NONPOINTER_ISAshiftclsSUPPORT_NONPOINTER_ISA

(或者反过来,如果你愿意的话:从 / 调用,而 / 本身也只从 // 调用。isa_t::setClassobjc_object::initIsaobjc_object::changeIsaobjc_constructInstanceclass_createInstanceobject_setClass

因此,当您在磁盘上读取这些对象文件时,您只会遇到对象和类的指针 isa;在 ISAS 中实际设置的位仅在运行时完成。如果您希望从这些位中读取详细信息,则需要从周围的 mach-o 数据中自行构建该信息。

评论

0赞 Jorayen 10/19/2023
所以正如我所认为的那样,它只是运行时信息,感谢 Itai 的精彩解释!
1赞 Itai Ferber 10/19/2023
@Jorayen 很高兴能帮上忙!我本来想评论并感谢您为您的问题添加细节,因为它促使我在运行时进行挖掘,看看发生了什么。我觉得我学到了一些有价值的东西!