Tagged Pointer

1,393 阅读4分钟

Tagged Pointer

1.背景

2013年,苹果为iPhone5s 配备了首个采用 64 位架构的 A7 双核处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。

假设我们要存储一个 NSNumber 对象,其值是一个整数。正常情况下,如果这个整数只是一个 NSInteger 的普通变量,那么它所占用的内存是与 CPU 的位数有关,在 32 位 CPU 下占 4 个字节,在 64 位 CPU 下是占 8 个字节的。而指针类型的大小通常也是与 CPU 位数相关,一个指针所占用的内存在 32 位 CPU 下为 4 个字节,在 64 位 CPU 下也是 8 个字节。

所以一个普通的 iOS 程序,如果没有Tagged Pointer对象,从 32 位机器迁移到 64 位机器中后,虽然逻辑没有任何变化,但这种 NSNumberNSDate 一类的对象所占用的内存会翻倍。

再来看看效率上的问题,为了存储和访问一个 NSNumber 对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。

2.Tagged Pointer

2.1 实现

为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。由于 NSNumberNSDate 一类的变量本身的值需要占用的内存大小常常不需要 8 个字节,拿整数来说,4 个字节所能表示的有符号整数就可以达到 20 多亿(注:2^31=2147483648,另外 1 位作为符号位),对于绝大多数情况都是可以处理的。

所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了Tagged Pointer对象之后,64 位 CPU 下 NSNumber 的内存图变成了以下这样:

由于Tagged Pointer对象是将值存在指针里的,所以并没有在堆中开辟空间。

2.2特点

Tagged Pointer特点的介绍:

  • Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
  • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。
  • 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。

3.源码探究

3.1 是否支持Tagged Pointer

定义在objc_config.h中的SUPPORT_TAGGED_POINTERS表示在64位系统中支持Tagged Pointer

#if !__LP64__
#   define SUPPORT_TAGGED_POINTERS 0
#else
#   define SUPPORT_TAGGED_POINTERS 1
#endif

3.2 判断指针变量是否是 Tagged Pointer

objc_object.h文件中,有一个isTaggedPointer函数用来判断一个指针变量是否是Tagged Pointer

inline bool 
objc_object::isTaggedPointer() 
{
    return _objc_isTaggedPointer(this);
}

isTaggedPointer中,调用了objc_internal.h中的_objc_isTaggedPointer。 直接把指针值强制转化为 unsigned long 然后和 _OBJC_TAG_MASK操作,看结果是否还等于_OBJC_TAG_MASK

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

继续查看_OBJC_TAG_MASK宏定义。

#if OBJC_SPLIT_TAGGED_POINTERS  //ARM64 处理器
#   define _OBJC_TAG_MASK (1UL<<63) 
#elif OBJC_MSB_TAGGED_POINTERS  // MSB
#   define _OBJC_TAG_MASK (1UL<<63)  
#else // LSB
#   define _OBJC_TAG_MASK 1UL
#endif

发现_OBJC_TAG_MASK有两种不同的定义,或是最高位为1或是最低位为1.

因此,我们可以说在不同的64位设备上可以通过判断指针值的最高位或最低位是否为1,来判断指针是否是Tagged Pointer

3.3 Tagged Pointer的存储结构

3.3.1 OBJC_SPLIT_TAGGED_POINTERS

在分析Tagged Pointer的存储结构前,先查看两个宏定义,OBJC_SPLIT_TAGGED_POINTERSOBJC_MSB_TAGGED_POINTERS

#if __arm64__
// ARM64 uses a new tagged pointer scheme where normal tags are in
// the low bits, extended tags are in the high bits, and half of the
// extended tag space is reserved for unobfuscated payloads.
#   define OBJC_SPLIT_TAGGED_POINTERS 1
#else
#   define OBJC_SPLIT_TAGGED_POINTERS 0
#endif

ARM64使用了一种新的标记指针方案,就是分开标记,扩展标记位于高位,普通标记位于低位。并且扩展标记的所占空间的一半要保留出来,留给未混淆的负载。

3.3.2 OBJC_MSB_TAGGED_POINTERS

#if (TARGET_OS_OSX || TARGET_OS_MACCATALYST) && __x86_64__
    // 64-bit Mac - tag bit is LSB
    // 在 64 位 Mac 下 - 最低有效位(LSB)作为 tagged pointer 的标记位
#   define OBJC_MSB_TAGGED_POINTERS 0
#else
    // Everything else - tag bit is MSB
    // 其他情况下,采用最高有效位(MSB)作为 tagged pointer 的标记位
#   define OBJC_MSB_TAGGED_POINTERS 1
#endif

采用64位x86架构的mac系统,采用的是LSB,其余的都是MSBMSB即数据从高位开始存储分布,LSB则相反。

3.3.3 存储结构

在 objc-runtime-new.mm 有一段 Tagged pointer objects 的注释如下:

/***********************************************************************
* Tagged pointer objects.
*
* Tagged pointer objects store the class and the object value in the 
* object pointer; the "pointer" does not actually point to anything.
* 
* Tagged pointer objects currently use this representation:
* (LSB)
*  1 bit   set if tagged, clear if ordinary object pointer
*  3 bits  tag index
* 60 bits  payload
* (MSB)
* The tag index defines the object's class. 
* The payload format is defined by the object's class.
*
* If the tag index is 0b111, the tagged pointer object uses an 
* "extended" representation, allowing more classes but with smaller payloads:
* (LSB)
*  1 bit   set if tagged, clear if ordinary object pointer
*  3 bits  0b111
*  8 bits  extended tag index
* 52 bits  payload
* (MSB)
*
* Some architectures reverse the MSB and LSB in these representations.
*
* This representation is subject to change. Representation-agnostic SPI is:
* objc-internal.h for class implementers.
* objc-gdb.h for debuggers.
**********************************************************************/

LSB下,最低位存储是否是tagged pointer的标志位。 然后3位来存储tag indextag index定义了当前对象的类型。 余下的60位的空间的就是payloadpayload就是对象实际的值。payload的格式由它的类型决定。 MSB的存储结构刚好与LSB相反。

LSB下,当tag index的值为0b111即7时,tag index的值不在指代对象的类型,而是表示要额外占用8位的空间来存储tag index。这时tag index可以表示的类更多了,但是用来存储数据的空间缺变小了,减少了8位,变为最多占用52位。

objc_internal.h中定义了一些在操作tagged pointer时,会用到的位移值的宏定义,有助于我们了解tagged pointer存储结构。

#if OBJC_SPLIT_TAGGED_POINTERS //扩展标记在高位 普通标记在低位
#   define _OBJC_TAG_MASK (1UL<<63) //标志位在最高位
#   define _OBJC_TAG_INDEX_SHIFT 0  //获取tag index时需要移动的位数 tag index在低位 占3位 和mask(111)做与运算
#   define _OBJC_TAG_SLOT_SHIFT 0   //从taggeed pointer对象中获取Class时用到
#   define _OBJC_TAG_PAYLOAD_LSHIFT 1 //负载左边的位数 左边只有一个最高位标志位
#   define _OBJC_TAG_PAYLOAD_RSHIFT 4 //负载右边的位数 右边是tag 先左移后右移 就能取出实际的负载值
#   define _OBJC_TAG_EXT_MASK (_OBJC_TAG_MASK | 0x7UL)  //64位和7(111) 做与运算
#   define _OBJC_TAG_NO_OBFUSCATION_MASK ((1UL<<62) | _OBJC_TAG_EXT_MASK)
#   define _OBJC_TAG_CONSTANT_POINTER_MASK \
        ~(_OBJC_TAG_EXT_MASK | ((uintptr_t)_OBJC_TAG_EXT_SLOT_MASK << _OBJC_TAG_EXT_SLOT_SHIFT))
#   define _OBJC_TAG_EXT_INDEX_SHIFT 55 //获取扩展标记位时 左移动位数 55 然后与扩展标记下标掩码0xff做与运算
#   define _OBJC_TAG_EXT_SLOT_SHIFT 55 //从taggeed pointer对象中获取Class时用到
#   define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 9 //有扩展时负载左边的位数
#   define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12 //有扩展时负载右边的位数
#elif OBJC_MSB_TAGGED_POINTERS  //高位优先排序下的宏定义 MSB
#   define _OBJC_TAG_MASK (1UL<<63)  //标志位在最高位
#   define _OBJC_TAG_INDEX_SHIFT 60  //获取tag index时需要移动的位数  右移60位后 取得高位4位的值 再和mask(111)做与运算  tag占三位
#   define _OBJC_TAG_SLOT_SHIFT 60  //从taggeed pointer对象中获取Class时用到
#   define _OBJC_TAG_PAYLOAD_LSHIFT 4 //负载左边的位数
#   define _OBJC_TAG_PAYLOAD_RSHIFT 4 //负载右边的位数 先左移后右移 就能取出实际的负载值
#   define _OBJC_TAG_EXT_MASK (0xfUL<<60)  //扩展标记掩码 60-64位 1111
#   define _OBJC_TAG_EXT_INDEX_SHIFT 52  //获取扩展标记位时 左移动位数 52 然后与扩展标记下标掩码0xff做与运算
#   define _OBJC_TAG_EXT_SLOT_SHIFT 52 //从taggeed pointer对象中获取Class时用到
#   define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 12 //有扩展时负载左边的位数
#   define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12 //有扩展时负载右边的位数
#else //低位优先排序下的宏定义 LSB
#   define _OBJC_TAG_MASK 1UL  //判断是否是taggeed pointer对象的掩码 取出第一位的值
#   define _OBJC_TAG_INDEX_SHIFT 1  //获取tag index时需要移动的位数  右移1位后和mask(111)做与运算  tag占三位
#   define _OBJC_TAG_SLOT_SHIFT 0 //从taggeed pointer对象中获取Class时用到
#   define _OBJC_TAG_PAYLOAD_LSHIFT 0 //负载左边的位数
#   define _OBJC_TAG_PAYLOAD_RSHIFT 4 //负载右边的位数 先左移后右移 就能取出实际的负载值
#   define _OBJC_TAG_EXT_MASK 0xfUL   //扩展标记掩码  1111
#   define _OBJC_TAG_EXT_INDEX_SHIFT 4  //获取扩展标记位时 右移动位数 4 然后与扩展标记下标掩码0xff做与运算
#   define _OBJC_TAG_EXT_SLOT_SHIFT 4 //从taggeed pointer对象中获取Class时用到
#   define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 0 //有扩展时负载左边的位数
#   define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12 //有扩展时负载右边的位数 8 + 3 + 1
#endif

借助上面的宏定义,我们可以继续画出MSB下的结构图。 无扩展标记时 有扩展标记时

ARM64下,无扩展标记时 ARM64下,有扩展标记时

3.4 编码/解码

3.4.1 混淆值

extern uintptr_t objc_debug_taggedpointer_obfuscator;

objc_debug_taggedpointer_obfuscator是一个常量。

objc_runtime_new.mm中,有一段关于objc_debug_taggedpointer_obfuscatorinitializeTaggedPointerObfuscator的描述:

/***********************************************************************
* initializeTaggedPointerObfuscator
* Initialize objc_debug_taggedpointer_obfuscator with randomness.
*
* The tagged pointer obfuscator is intended to make it more difficult
* for an attacker to construct a particular object as a tagged pointer,
* in the presence of a buffer overflow or other write control over some
* memory.
 
 The obfuscator is XORed with the tagged pointers when setting
* or retrieving payload values. They are filled with randomness on first
* use.
 **********************************************************************/

tagged pointer混淆值 旨在使攻击者在存在缓冲区溢出或对某些内存的其他写入控制时,更难将特定对象构造为标记指针。简言之就是为了安全。 混淆值与tagged pointer进行异或操作。

initializeTaggedPointerObfuscator用来初始化混淆值。

static void
initializeTaggedPointerObfuscator(void)
{
    if (!DisableTaggedPointerObfuscation
//        && dyld_program_sdk_at_least(dyld_fall_2018_os_versions)
        ) {
        // Pull random data into the variable, then shift away all non-payload bits.
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        //先生成一个随机数
        
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
        //生成的随机数与 ~_OBJC_TAG_MASK 做 与 运算
        //这样是随机的最高位或最低位置为0
        
#if OBJC_SPLIT_TAGGED_POINTERS
        // The obfuscator doesn't apply to any of the extended tag mask or the no-obfuscation bit.
        //ARM64下, obfuscator 并不适用于extended tag mask 和 no-obfuscation bit
        //将相应的位置为了0
        objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);

        // Shuffle the first seven entries of the tag permutator.
         // 对objc_debug_tag60_permutations数组里面的值 也进行了随机的交换
        //也就是把tag index 的位置调换了,更深一层次的类型混淆
        int max = 7;
        for (int i = max - 1; i >= 0; i--) {
            int target = arc4random_uniform(i + 1);
            swap(objc_debug_tag60_permutations[i],
                 objc_debug_tag60_permutations[target]);
        }
#endif
    } else {
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        objc_debug_taggedpointer_obfuscator = 0;
    }
}

3.4.2 编码

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
        //先将 objc_debug_taggedpointer_obfuscator 与 指针 进行 异或运算 得到新值value
    uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
#if OBJC_SPLIT_TAGGED_POINTERS //ARM64下
    //如果不需要混淆 则返回原指针
    if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
        return (void *)ptr;
    
    //取出基础的tag index 7以内的
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    //取出混淆后的tag index
    uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);
    //将value的存储tag index的位置 抹零
    value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
    //将混淆后的tag index 存到value
    value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;
#endif
    return (void *)value;
}

3.4.3 解码

解码 Tagged Pointer,就是与混淆器 objc_debug_taggedpointer_obfuscator 进行异或操作

static inline uintptr_t
_objc_decodeTaggedPointer_noPermute(const void * _Nullable ptr)
{
    uintptr_t value = (uintptr_t)ptr;
#if OBJC_SPLIT_TAGGED_POINTERS
    if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
        return value;
#endif
    
    return value ^ objc_debug_taggedpointer_obfuscator;
    
}

在这里将混淆的tag index置回原值

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;

    value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
    value |= _objc_obfuscatedTagToBasicTag(basicTag) << _OBJC_TAG_INDEX_SHIFT;
#endif
    return value;
}

3.5 创建 TaggedPointer

static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
    // PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts.
    // They are reversed here for payload insertion.

    // ASSERT(_objc_taggedPointersEnabled());
    if (tag <= OBJC_TAG_Last60BitPayload) {
        // ASSERT(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value);
        uintptr_t result =
            (_OBJC_TAG_MASK | 
             ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) | 
             ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    } else {
        // ASSERT(tag >= OBJC_TAG_First52BitPayload);
        // ASSERT(tag <= OBJC_TAG_Last52BitPayload);
        // ASSERT(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
        uintptr_t result =
            (_OBJC_TAG_EXT_MASK |
             ((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
             ((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    }
}

创建TaggedPointer时,分两种情况:

  • 有扩展标记
  • 无扩展标记

当无扩展标记时,根据前面的是结构图,我们可以知道,tagged point主要分为三部分:tag mask、tag index、payload。我们先分别将它们的值通过位移计算 移动到正确的位置,然后再进行运算将它们组合到一起就行了。

有扩展标记时,过程也类似,_OBJC_TAG_EXT_MASK此掩码值包含了标记位和7的值。

3.6 获取TaggedPointer的value

static inline uintptr_t
_objc_getTaggedPointerValue(const void * _Nullable ptr) 
{
    // ASSERT(_objc_isTaggedPointer(ptr));
    uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    if (basicTag == _OBJC_TAG_INDEX_MASK) {
        return (value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
    } else {
        return (value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
    }
    
    //函数实现很简单,首先 Tagged Pointer 解码,与 objc_debug_taggedpointer_obfuscator 进行异或操作,然后根据不同平台的宏定义进行移位操作。
}

static inline intptr_t
_objc_getTaggedPointerSignedValue(const void * _Nullable ptr) 
{
    // ASSERT(_objc_isTaggedPointer(ptr));
    uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    if (basicTag == _OBJC_TAG_INDEX_MASK) {
        return ((intptr_t)value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
    } else {
        return ((intptr_t)value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
    }
}
  • 先将tagged pointer解码
  • 再判断是否有扩展标记
  • 通过相应的位移操作,得到value

3.7 获取TaggedPointer的tag

static inline objc_tag_index_t 
_objc_getTaggedPointerTag(const void * _Nullable ptr) 
{
    // ASSERT(_objc_isTaggedPointer(ptr));
    uintptr_t value = _objc_decodeTaggedPointer(ptr);
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    uintptr_t extTag =   (value >> _OBJC_TAG_EXT_INDEX_SHIFT) & _OBJC_TAG_EXT_INDEX_MASK;
    if (basicTag == _OBJC_TAG_INDEX_MASK) {
        return (objc_tag_index_t)(extTag + OBJC_TAG_First52BitPayload);
    } else {
        return (objc_tag_index_t)basicTag;
    }
}
  • 先将tagged pointer解码
  • 再通过位移操作,获取basicTagextTag
  • 判断是否有扩展标记,进行结果的返回

3.8 根据 objc_tag_index_t 获取 Class 指针

获取Class 指针的过程,就是根据tag这个下标值,取出objc_tag_classesobjc_tag_ext_classes类数组中的Class

static Class *
classSlotForBasicTagIndex(objc_tag_index_t tag)
{
#if OBJC_SPLIT_TAGGED_POINTERS
    uintptr_t obfuscatedTag = _objc_basicTagToObfuscatedTag(tag);
    return &objc_tag_classes[obfuscatedTag];
#else
    uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator
                                >> _OBJC_TAG_INDEX_SHIFT)
                               & _OBJC_TAG_INDEX_MASK);
    uintptr_t obfuscatedTag = tag ^ tagObfuscator;

    // Array index in objc_tag_classes includes the tagged bit itself
    // objc_tag_classes 中的数组索引包括标记位本身
    
#   if SUPPORT_MSB_TAGGED_POINTERS
    return &objc_tag_classes[0x8 | obfuscatedTag];
#   else
    return &objc_tag_classes[(obfuscatedTag << 1) | 1];
#   endif
#endif
}
static Class *  
classSlotForTagIndex(objc_tag_index_t tag)
{
    if (tag >= OBJC_TAG_First60BitPayload && tag <= OBJC_TAG_Last60BitPayload) {
        return classSlotForBasicTagIndex(tag);
    }

    if (tag >= OBJC_TAG_First52BitPayload && tag <= OBJC_TAG_Last52BitPayload) {
        int index = tag - OBJC_TAG_First52BitPayload;
#if OBJC_SPLIT_TAGGED_POINTERS
        if (tag >= OBJC_TAG_FirstUnobfuscatedSplitTag)
            return &objc_tag_ext_classes[index];
#endif
        uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator
                                    >> _OBJC_TAG_EXT_INDEX_SHIFT)
                                   & _OBJC_TAG_EXT_INDEX_MASK);
        return &objc_tag_ext_classes[index ^ tagObfuscator];
    }

    return nil;
}