美文网首页内存管理
深入了解Tagged Pointer

深入了解Tagged Pointer

作者: kikido | 来源:发表于2020-04-02 21:15 被阅读0次

    objc 源码版本:779.1
    当然还是推荐使用这个来学习:可编译的源码

    在 2013 年苹果推出了首个使用 64 位架构的双核处理器的手机 iphone 5s。为了节约内存以及提高执行效率,苹果使用了一种叫做 'Tagged Pointer' 的技术,现在跟着我来了解一下它吧。

    Tagged Pointer

    从 5s 开始,iPhone 均使用 arm64 指令集的处理器。在 64 位系统上,一个指针占 8 个字节,而指针指向的实例变量至少需要 16 个字节,并且还需要执行额外的一些操作,例如:申请内存,销毁内存。为了达到优化的目的,苹果将一些存储数据的类,例如 NSString,NSNumber,当它们需要保存的数据不需要占用那么多的字节时,直接将数据保存在“指针”里面。

    下面让我们用代码来证实Tagged Pointer的存在

    NSNumber *a = [NSNumber numberWithInt:1];
    

    然后打个断点,使用 lldb 的命令调试,x/8xg a,该命令的意思是从a的起始地址开始,打印 8 个 16进制的 8字节长度的值
    输出结果

    image

    说明指针 a 并不是指向 NSNumber 实例的指针。

    或者下面这样更加直观一点

    image

    可以看到 a 并没有 isa 指针,所以它并不是一个 NSNumber 实例指针。


    Tagged Pointer 如何存储数据

    这里你最好打开源码对照着看。

    LSB

    在非 arm64 架构中,将最低位即 LSB 设置为 1,与正常的指针进行区分。
    这样做的原因是,OC 类在创建实例最终调用的是 C 标准库中的 calloc 函数,它所返回的内存地址会是 16 的倍数,参考 Aligned memory management?,虽然里面回答的是 malloc() 函数。。。但我也只能这么解释了。这样的结果就是指针地址低 4 位都是 0,用最低位来表示也合理的

    非 arm64 架构下标记位的设定

    MSB

    在 arm64 架构中,将最高位即 MSB 设置为 1,与正常的指针进行区分。
    这样做的原因的是因为在 arm64 架构中,指针只用了低位的 48 位。原因可以看下这个 为什么64位机指针只用48个位?

    objc-internal.h 372 行以及 384 行,我们 我们可以看到如下的定义来证实:

    #if OBJC_MSB_TAGGED_POINTERS
    #   define _OBJC_TAG_MASK (1UL<<63)
    #   define _OBJC_TAG_INDEX_SHIFT 60
    #else
    #   define _OBJC_TAG_MASK 1UL
    #   define _OBJC_TAG_INDEX_SHIFT 1
    #endif
    

    在 objc-runtime-new.mm 7752 行,我们可以看到tagged pointer类型的注册函数

    void
    _objc_registerTaggedPointerClass(objc_tag_index_t tag, Class cls)
    {
        if (objc_debug_taggedpointer_mask == 0) {
            _objc_fatal("tagged pointers are disabled");
        }
    
        Class *slot = classSlotForTagIndex(tag);
        if (!slot) {
            _objc_fatal("tag index %u is invalid", (unsigned int)tag);
        }
    
        Class oldCls = *slot;
        
        if (cls  &&  oldCls  &&  cls != oldCls) {
            _objc_fatal("tag index %u used for two different classes "
                        "(was %p %s, now %p %s)", tag, 
                        oldCls, oldCls->nameForLogging(), 
                        cls, cls->nameForLogging());
        }
    
        *slot = cls;
    
        // Store a placeholder class in the basic tag slot that is 
        // reserved for the extended tag space, if it isn't set already.
        // Do this lazily when the first extended tag is registered so 
        // that old debuggers characterize bogus pointers correctly more often.
        if (tag < OBJC_TAG_First60BitPayload || tag > OBJC_TAG_Last60BitPayload) {
            Class *extSlot = classSlotForBasicTagIndex(OBJC_TAG_RESERVED_7);
            if (*extSlot == nil) {
                extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
                *extSlot = (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
            }
        }
    }
    

    从上面的函数我们可以了解到,初始化系统时,会生成两个全局的数组变量,一个用来存储系统内置的Tagged Pointer类型,而另一个数组用来存储自定义扩展的Tagged Pointer类型。这两个数组的定义如下(objc-object.h 45行):

    extern "C" { 
        extern Class objc_debug_taggedpointer_classes[_OBJC_TAG_SLOT_COUNT];
        extern Class objc_debug_taggedpointer_ext_classes[_OBJC_TAG_EXT_SLOT_COUNT];
    }
    

    存储内置类型的数组大小为 16,存储扩展类型的数组大小为 256。
    当属于内置类型时,指针的最高2-4位用来存储类型的索引位置,剩余的 60 位用来存储数据(其实只有 56 位,还有 4 位用来保存数据的类型信息)。内置类型有以下几种:

    enum
    {
        // 60-bit payloads
        OBJC_TAG_NSAtom            = 0, 
        OBJC_TAG_1                 = 1, 
        OBJC_TAG_NSString          = 2, 
        OBJC_TAG_NSNumber          = 3, 
        OBJC_TAG_NSIndexPath       = 4, 
        OBJC_TAG_NSManagedObjectID = 5, 
        OBJC_TAG_NSDate            = 6,
    }
    

    而当指针的最高 4 位均为 1 时,则表示这是一个扩展类型。此时,指针的最高 5-12 位用来存储类型的索引信息,而剩余的 52 位用来存储数据。系统定义好的扩展类型有以下几种

    enum
    {
        // ...
        // 52-bit payloads
        OBJC_TAG_Photos_1          = 8,
        OBJC_TAG_Photos_2          = 9,
        OBJC_TAG_Photos_3          = 10,
        OBJC_TAG_Photos_4          = 11,
        OBJC_TAG_XPC_1             = 12,
        OBJC_TAG_XPC_2             = 13,
        OBJC_TAG_XPC_3             = 14,
        OBJC_TAG_XPC_4             = 15,
        OBJC_TAG_NSColor           = 16,
        OBJC_TAG_UIColor           = 17,
        OBJC_TAG_CGColor           = 18,
        OBJC_TAG_NSIndexSet        = 19,
        // ...
    }
    

    让我们用代码来验证下是否正确

    - (void)boo
    {
        NSNumber *a = [NSNumber numberWithInt:1];
        NSNumber *b = [NSNumber numberWithInt:2];
        NSNumber *c = [NSNumber numberWithInt:16];
        
        NSLog(@"pointer a is %lx", a);
        NSLog(@"pointer b is %lx", b);
        NSLog(@"pointer c is %lx", c);
        NSLog(@"pointer d is %lx", d);
    }
    

    输出结果:

    pointer a is ef59c3d36981ed4b
    pointer b is ef59c3d36981ed7b
    pointer c is ef59c3d36981ec5b
    

    等等,不是说 Tagged Pointer 最高 4 位用来保存类型信息,剩下的几位都只用来保存数据嘛,为什么输出结果看起来这么复杂呢?
    原因是从 iOS12 开始,为了系统安全,对Tagged Pointer的值进行混淆。混淆函数如下:

    static inline uintptr_t
    _objc_decodeTaggedPointer(const void * _Nullable ptr)
    {
        return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
    }
    

    objc_debug_taggedpointer_obfuscator是一个extern关键字的常量,既然被 extern 声明,让我们可以用下面的代码来解码,获取真正的值

    extern uintptr_t objc_debug_taggedpointer_obfuscator;
    
    - (void)foo
    {
        NSNumber *a = [NSNumber numberWithInt:1];
        NSNumber *b = [NSNumber numberWithInt:2];
        NSNumber *c = [NSNumber numberWithInt:16];
    
        NSLog(@"pointer a real value is %lx", ((uintptr_t)a ^ objc_debug_taggedpointer_obfuscator));
        NSLog(@"pointer b real value is %lx", ((uintptr_t)b ^ objc_debug_taggedpointer_obfuscator));
        NSLog(@"pointer c real value is %lx", ((uintptr_t)c ^ objc_debug_taggedpointer_obfuscator));
    }
    

    输出结果:

    pointer a real value is b000000000000012
    pointer b real value is b000000000000022
    pointer c real value is b000000000000102
    

    从输出结果可以看出,这几个值都是 0Xb 开头,16进制的 b 用 二级制表示为 1011,最高1用来表示这是一个 Tagged Pointer,而剩余 3 位的10进制数为 3,符合之前的定义OBJC_TAG_NSNumber = 3
    至于为什么结尾都是 0x2,这个后面再解释。
    下面让我们测试下 NSNumber 的 Tagged Pointer 使用多少位来保存数据。从之前的探究我们知道内置类型用 60 位来保存数据,而经过上面的实验我们可以看到还有 4 位用来做别的事了,那么是否剩余的 56 位都用来保存数据了呢?

    - (void)foo
    {
        NSNumber *d = [NSNumber numberWithLongLong:-0x7FFFFFFFFFFFFF];
        NSLog(@"pointer a real value is %lx", ((uintptr_t)d ^ objc_debug_taggedpointer_obfuscator));
    }
    

    输出结果:

    pointer d real value is b800000000000013
    

    结果符合预期。至于为什么最高位 b 后面的数字是 8,是因为高位 5 的位置变成了 1,用来表示这个数是负数(0则表示正数), 而 7 的二进制表示为 ob111,高位 5-8 连起来就是 0b1111,也就是16进制的 8 了。
    还一个值得注意的是低位第一位的数字变成了 3,而不是之前的正整数 2,由此我们可以推测最低位的 4 位是用来表示存储数据类型的数据,例如 int,float,bool 这几个类型生成的 Tagged Pointer 最低4位数字应该是不同的。用下面的代表再来验证下:

        NSNumber *a = [NSNumber numberWithInt:1];
        NSNumber *b = [NSNumber numberWithShort:2];
        NSNumber *c = [NSNumber numberWithFloat:1.];
        NSNumber *d = [NSNumber numberWithLongLong:-0x7FFFFFFFFFFFFF];
    
        NSLog(@"pointer a real value is %lx", ((uintptr_t)a ^ objc_debug_taggedpointer_obfuscator));
        NSLog(@"pointer b real value is %lx", ((uintptr_t)b ^ objc_debug_taggedpointer_obfuscator));
        NSLog(@"pointer c real value is %lx", ((uintptr_t)c ^ objc_debug_taggedpointer_obfuscator));
        NSLog(@"pointer d real value is %lx", ((uintptr_t)d ^ objc_debug_taggedpointer_obfuscator));
    

    输出结果

     pointer a real value is b000000000000012
     pointer b real value is b000000000000021
     pointer c real value is b000000000000014
     pointer d real value is b800000000000013
    

    结果符合预期,说明我们的推测是正确的

    Tagged Pointer 如何使用方法

    经过上面的探究,我们知道了Tagged Pointer只是一个基本数据类型,在栈中分配内存。可我们平时使用时却是拿对象来对待它的,那么,当我们对一个Tagged Pointer对象使用方法时,runtime 是如何处理的呢?

    因为消息用到的函数objc_msgSend使用汇编编写的,我看不大懂。。。但大致的流程是,根据指针高位存储的信息得到相应类的 isa 指针,然后找到方法的 IMP,然后在方法中执行操作。因为不懂得汇编以及逆向我这里就不讲了。

    参考

    NSNumber 与 Tagged Pointer
    深入解构 objc_msgSend 函数的实现

    希望大家看了有所收获吧。

    相关文章

      网友评论

        本文标题:深入了解Tagged Pointer

        本文链接:https://www.haomeiwen.com/subject/smwfphtx.html