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 位机器中后,虽然逻辑没有任何变化,但这种 NSNumber、NSDate 一类的对象所占用的内存会翻倍。
再来看看效率上的问题,为了存储和访问一个 NSNumber 对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。
2.Tagged Pointer
2.1 实现
为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。由于 NSNumber、NSDate 一类的变量本身的值需要占用的内存大小常常不需要 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_POINTERS和OBJC_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,其余的都是MSB。MSB即数据从高位开始存储分布,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 index,tag index定义了当前对象的类型。
余下的60位的空间的就是payload, payload就是对象实际的值。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_obfuscator和initializeTaggedPointerObfuscator的描述:
/***********************************************************************
* 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解码
- 再通过位移操作,获取
basicTag和extTag - 判断是否有扩展标记,进行结果的返回
3.8 根据 objc_tag_index_t 获取 Class 指针
获取Class 指针的过程,就是根据tag这个下标值,取出objc_tag_classes或objc_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;
}