Objc4-818底层探索(四):isa与类

2021-06-18  本文已影响0人  ShawnAlex

建议先看下Objc4-818底层探索(三):isa

先补充一些之前的知识点:

知识点1:关于掩码

isa 掩码以x86_64环境下为例

#   define ISA_MASK        0x00007ffffffffff8ULL (ULL: unsigned long long无符号长整型 C++语法)
掩码转二进制

0x00007ffffffffff8 转二进制为
0001 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000
其实就是
000...000...中间44个1..000
其他位抹零, 只保留中间44位, 即取到shiftcls类信息

知识点2:关于 __has_feature(ptrauth_calls)

有些时候__has_feature(ptrauth_calls)TARGET_OS_SIMULATOR一起使用, 需要先普及ARM64e概念

// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.

//ARM64模拟器有更大的地址空间,所以使用ARM64e
//即使在为ARM64-not-e构建模拟器时也是如此。

ARM64earm64e架构,用于Apple A12及更高版本A系列处理器 或 新型OS_SIMULATOR 模拟器的设备。详细可参考官方提供的文档, 如下

ARM64e官方文档

先熟悉下几个lldb命令, 方便之后探索

我们还是先看个例子


例子

首先

操作例子

为了探索类是否在底层存多份, 或者系统会创建多个地址我们做下这个操作

        SATest *test = [SATest alloc];
       
        Class cls1 = [SATest class];
        Class cls2 = [SATest alloc].class;
        Class cls3 = object_getClass([SATest alloc]);
        
        NSLog(@"cls1: %p", cls1);
        NSLog(@"cls2: %p", cls2);
        NSLog(@"cls3: %p", cls3);
探索类例子

使用系统方法获取类, 对比下, 系统方法得到的类信息为0x1000081b8为类信息

很显然第二次的不是类, 那么它是什么? 并且第三次为NSObject又是为什么?

MachOView

这里我们要用反编译看一下究竟发生了什么

MachOView加入反编译程序

Symbol TableSymbols中可看到, 实际在底层多了个_OBJC_METACLASS(meta class : 元类)

元类

这里要涉及一个新的知识点元类


元类

元类苹果系统定义的, 其定义和创建都是由编译器完成,在这个过程中,类的归属来自于元类

isa走位图

验证NSObject

之前自定义类的可看到满足isa走位图, 这里再验证下NSObjectisa走向, 看下是否满足

NSObject

可看到NSObjectisa走势为: NSObject根元类根元类自身

NSObject isa 走位验证

验证继承关系

isa走位图没问题满足, 这里再验证下继承关系/父类链是否满足

继承关系例子
自定义类的父类链继承
元类的父类链继承

0x1000080e0元类
0x7fff80815fe0根元类
0x7fff80816008根类(根类是NSObject)

当然我们也可以加个子类打印下继承链关系, 如图


加子类继承关系例子

objct_class

因为所有的类都是继承于objct_class, 那么我们接下来看下objct_class底层的实现

全局搜索objct_class, 在objc-runtime-new.h可以找到struct objc_class : objc_object

objct_class

首先objct_class结构体类型, 继承于objc_object, 同时类结构里面默认一个Class ISA同时包含Class superclass, cache_t cache, class_data_bits_t bits;等等。

objct_class

类结构分析

首先还是先看几个例子

普通指针
类结构分析例子1

定义2个变量a, b = 10, 打印两个变量值以及内存地址

类结构分析例子1
普通指针
普通指针例子

定义2个变量a, b = 10, 打印两个变量值以及内存地址

普通指针例子分析
对象指针
对象指针例子 对象指针例子分析
数组指针
image.png

bits探索

有了上面的概念, 便于我们理解之后的探索objc_class中的类信息

objc-runtime-new.h

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

...
    class_rw_t *data() const {
        return bits.data();
    }

...
}

因为我们之前在Clang看到bits里面存放着类信息, 所以我们先探索下bits。因为我们之前知道, 已知首地址, 可以通过平移方法, 得到我们

接下来我们看下cache, 大小

方法一:

因为cachecache_t类型, 最简单的方法lldb命令 po读一下cache_t

cache

可看到cache_t16字节

方法二:

进入cache_t 底层

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };

...
后面一些函数方法, 而方法不占用内存可以不看
还有一些static属性的内容, 在全局区也可以不看
}

先看下这个源码explicit_atomic

// Version of std::atomic that does not allow implicit conversions
// to/from the wrapped type, and requires an explicit memory order
// be passed to load() and store().
template <typename T>
struct explicit_atomic : public std::atomic<T> {
    explicit explicit_atomic(T initial) noexcept : std::atomic<T>(std::move(initial)) {}
    operator T() const = delete;
    
    T load(std::memory_order order) const noexcept {
        return std::atomic<T>::load(order);
    }
    void store(T desired, std::memory_order order) noexcept {
        std::atomic<T>::store(desired, order);
    }
    
    // Convert a normal pointer to an atomic pointer. This is a
    // somewhat dodgy thing to do, but if the atomic type is lock
    // free and the same size as the non-atomic type, we know the
    // representations are the same, and the compiler generates good
    // code.
    static explicit_atomic<T> *from_pointer(T *ptr) {
        static_assert(sizeof(explicit_atomic<T> *) == sizeof(T *),
                      "Size of atomic must match size of original");
        explicit_atomic<T> *atomic = (explicit_atomic<T> *)ptr;
        ASSERT(atomic->is_lock_free());
        return atomic;
    }
};

可看到explicit_atomic的大小取决于传入的T的大小

我们先看下结构体

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
typedef unsigned int uint32_t;

而后面的explicit_atomic<preopt_cache_t *> _originalPreoptCache;指针类型占8字节

cache8 + 8 = 16字节

已知首地址以及 ISA8字节, superclass8字节, cache16字节, 固bits前面总共8+8+16 = 32字节, 可通过首地址平移32字节获取bits信息。

class

我们检查下是否可以真正读出来, 检测钱先看下class_data_bits_t方便我们下面探索

读取bits
struct objc_class : objc_object {
...
class_rw_t *data() const {
        return bits.data();
    }
}

可看到bit里面有data()函数方法(获取数据方法, class_rw_t有多数据我们之后再讨论)。p $1->data()读取下bits里面数据, 看见返回(class_rw_t *) $2 = 0x00007fff3e24b6e0

class_rw_t

我们接下来看下class_rw_t `, 源码比较多, 我们挑重点的看

struct class_rw_t {
...
    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
        }
    }
};

可看到class_rw_t(结构体)里面提供一些属性properties, 方法列表methods, 协议列表protocols的方法。

那么我们再SATest.h中定义一些成员变量, 属性, 方法打印看一下

@interface SATest : NSObject{
    NSString *SAHobby;
}


@property (nonatomic, strong) NSString *SAName;
@property (nonatomic, assign) int SAAge;

- (void)sayHello;
- (void)sayNB;
+(void)sayGunDan;

@end

属性列表打印

先看属性列表打印情况


属性列表

bits 数据信息在之前的例子我上面已经讲过了, 我们从读bits数据信息$11之后开始

-p *$13.list.ptr读一下指针地址指向内容, 可看到获得属性list信息, count = 2, 也符合我们建的2个属性

方法列表打印

先了解个知识点

struct class_rw_ext_t {
    DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
    class_ro_t_authed_ptr<const class_ro_t> ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    char *demangledName;
    uint32_t version;
};

struct property_t {
    const char *name;
    const char *attributes;
};

struct method_t {
    static const uint32_t smallMethodListFlag = 0x80000000;

    method_t(const method_t &other) = delete;

    // The representation of a "big" method. This is the traditional
    // representation of three pointers storing the selector, types
    // and implementation.
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
...
}

想必与属性列表, 方法列表的相关内容name, types, imp储存在struct big里面(818新改动), 所以获取方法列表里面的信息也要稍微变一下

方法列表 方法列表

当然你要读协议列表的list结构, 那里就p $3.protocols()即可


探索成员变量以及类方法存放位置

探索成员变量

打印过程中我们会发现, 成员变量以及类方法并没有在属性列表, 方法列表里面, 那它究竟在哪里存放的呢? 回过头我们再看struct class_rw_t方法

struct class_rw_t

其实我们发现, 在方法列表上面还有一个ro方法, const class_ro_t *ro(), 看下ro底层

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };

    explicit_atomic<const char *> name;
    // With ptrauth, this is signed if it points to a small list, but
    // may be unsigned if it points to a big list.
    void *baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

  ...
};

可看到const ivar_list_t * ivars;, 有一个ivars属性(ivars: 实例变量), 我们仿照下上面也读一下ro

读取ro

接下来我们仿照属性列表去读取, 发现实例变量储存在ivars_list_t里面, 同时也会发现还有属性的成员变量。这一点之前在Clang时候我们看过, 属性在底层是以成员变量+set/get方法 形式存放的。

探索类方法

所谓的对象/实例方法, 类方法其实是OC上层或者说苹果官方人为加入的概念, 其底层是都是函数, 不区分+, -。但实例方法与类方法还是有必要区分的, 则苹果将实例方法存在里面, 而类方法存在元类里面。一方面避免对象存储太大会发生混乱, 一方面也是为了有个调用区分。

所以类方法要在元类中查找。

类方法查找
(lldb) p $6.get(0).big()
(method_t::big) $7 = {
  name = "sayGunDan"
  types = 0x0000000100003f6e "v16@0:8"
  imp = 0x0000000100003d70 (SAObjcBuild`+[SATest sayGunDan])
}

综上也可看出

上一篇下一篇

猜你喜欢

热点阅读