Tagged Pointer 详解

Tagged Pointer 详解
Photo by Marius Niveri / Unsplash

Tagged Pointer是苹果在64bit设备引入的一种技术,是用于优化NSNumber、NSDate、NSString等小对象的存储。

查看objc4源码可以找到只要64位系统才支持

#if __LP64__  //文件objc-internal.h line 283
#define OBJC_HAVE_TAGGED_POINTERS 1
#endif

同时也可以找到,Tagged Pointer 所支持的类型,一个objc_tag_index_t类型的枚举,如下

    // 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,

    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, 

    // 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,
    OBJC_TAG_NSMethodSignature = 20,
    OBJC_TAG_UTTypeRecord      = 21,

ARM64位的设备,tags在最低位,扩展的tags在最高位。64位的Mac,tags存储在LSB(Least Significant Bit 最低位)。其它情况比如模拟器和32位的真机,tag存储在MSB(Most Significant Bit 最高位)。

#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

#if (TARGET_OS_OSX || TARGET_OS_MACCATALYST) && __x86_64__
    // 64-bit Mac - tag bit is LSB
#   define OBJC_MSB_TAGGED_POINTERS 0
#else
    // Everything else - tag bit is MSB
#   define OBJC_MSB_TAGGED_POINTERS 1
#endif

这里主要写下ARM64位下的情况,其他的如果感兴趣可以查看源码。写之前,先新建个iOS工程,然后写入以下代码,并在最后以后打上断点,然后运行

NSString *str1 = @"a";
NSString *str2 = [NSString stringWithFormat:@"a"];
NSLog(@"%@ %p %@",str1, str1, str1.class);
NSLog(@"%@ %p %@",str2, str2, str2.class);
NSLog(@"END");

然后会看到如下输出结果,并且会进入调试状态

str2的Class 是NSTaggedPointerString,确实是TaggedPoint,但是内存地址很大,并看不出来数据是怎么存储的。

然后打开Xcode的内存查看窗口,路径Debug->Debug Workflow->View Memory如下图

并在打开窗口的最下方Address里面搜索两个内存地址,str2的地址确实是搜索不到的,证明这个指针并没有指向任何内存。

这个地址是怎么生成的呢?这时候需要从源码中寻找答案了。在源码中搜索TaggedPoint,找到一个_objc_makeTaggedPointer的方法

static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
    if (tag <= OBJC_TAG_Last60BitPayload) {
        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 {
        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);
    }
}

可以看到,这里区分了60位playload和52位payload。点objc_tag_index_t进去看下

    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 

这里先分析OBJC_TAG_Last60BitPayload的情况,可以查看到小于等于6包含的类型如下

    // 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,

点击方法内的宏定义进去,可以找到位移操作所对应的值,这里只截取ARM64所对应的

#if OBJC_SPLIT_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#   define _OBJC_TAG_INDEX_SHIFT 0
#   define _OBJC_TAG_SLOT_SHIFT 0
#   define _OBJC_TAG_PAYLOAD_LSHIFT 1
#   define _OBJC_TAG_PAYLOAD_RSHIFT 4
#   define _OBJC_TAG_EXT_MASK (_OBJC_TAG_MASK | 0x7UL)
#   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
#   define _OBJC_TAG_EXT_SLOT_SHIFT 55
#   define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 9
#   define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12

通过分析代码可得,指针的最高1位是_OBJC_TAG_MASK,因为60位payload情况下,tag的最大值是6,二进制位110,可得最低3位是tag,然后返回的值还要通过_objc_encodeTaggedPointer处理

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
    if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
        return (void *)ptr;
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);
    value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
    value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;
#endif
    return (void *)value;
}

可以看到实际的值还需要异或一个objc_debug_taggedpointer_obfuscator,搜索可的这是一个随机值,并且是在iOS12开始引入的。

// tagged pointers are obfuscated by XORing with a random value
// decoded_obj = (obj ^ obfuscator)
OBJC_EXPORT uintptr_t objc_debug_taggedpointer_obfuscator
    OBJC_AVAILABLE(10.14, 12.0, 12.0, 5.0, 3.0);

继续搜索找到initializeTaggedPointerObfuscator(void)方法,可以看到objc_debug_taggedpointer_obfuscator确实是个随机值,并且至少2018后的dyld版本才生效

static void
initializeTaggedPointerObfuscator(void)
{
    if (!DisableTaggedPointerObfuscation && dyld_program_sdk_at_least(dyld_fall_2018_os_versions)) {
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
#if OBJC_SPLIT_TAGGED_POINTERS
        objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);
        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 {
        objc_debug_taggedpointer_obfuscator = 0;
    }
}

再搜索DisableTaggedPointerObfuscation,发现这是个环境变量,是可以设置关闭混淆的

OPTION( DisableTaggedPointerObfuscation, OBJC_DISABLE_TAG_OBFUSCATION,    "disable obfuscation of tagged pointers")

为了方便分析,回到工程,把OBJC_DISABLE_TAG_OBFUSCATION设置为YES

运行会看到如下结果

str2已经不再是混淆的状态,然后在调试窗口输入 p/t str2,查看下指针的2进制结果如下

可以看出,最高位为1,最低3位,十进制为2,OBJC_TAG_NSString=2,可以证明对函数的分析是正确的。

查ASCII 表可得,a的二进制为01100001,分析可得如下

TAG和值中间4位表示什么意思呢?查资料得,这4位是Type Index,表示数据的类型。然后写以下代码帮助分析,并在最后一行打上断点

    NSNumber *number1 = [NSNumber numberWithChar:2];
    NSNumber *number2 = [NSNumber numberWithShort:2];
    NSNumber *number3 = [NSNumber numberWithInt:2];

    NSLog(@"%@ %p %@",number1, number1, number1.class);
    NSLog(@"%@ %p %@",number2, number2, number2.class);
    NSLog(@"%@ %p %@",number3, number3, number3.class);
    NSLog(@"END");

运行后,在调试窗口,分别通过p/t输出3个指针的二进制,可得如下

可以看出,不同类型值确实是不一样的,char为0、short为1,int为2。然后看到这个显示的是__NSCFNumber类型,查看CF源码,在CFNumber里面找到如下定义

static const struct {
    uint16_t canonicalType:5;	// canonical fixed-width type
    uint16_t floatBit:1;	// is float
    uint16_t storageBit:1;	// storage size (0: (float ? 4 : 8), 1: (float ? 8 : 16) bits)
    uint16_t lgByteSize:3;	// base-2 log byte size of public type
    uint16_t unused:6;
} __CFNumberTypeTable[] = {
    //省略
    /* kCFNumberCharType */	{kCFNumberSInt8Type, 0, 0, 0, 0},
    /* kCFNumberShortType */	{kCFNumberSInt16Type, 0, 0, 1, 0},
    /* kCFNumberIntType */	{kCFNumberSInt32Type, 0, 0, 2, 0},
    //省略
};

发现Type Index的值可以和这个结构体里面的lgByteSize对应上。

综上,可以知道Tagged Pointer在OBJC_TAG_Last60BitPayload情况下,最高1位是标志位,因为define _OBJC_TAG_MASK (1UL<<63),是1的话,就表示这个值为Tagged Pointer。

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

低3位表示tag,区分Tagged Pointer的类型,低4-7位区分数据类型。所有剩下的56位可以表示数据。最高位预留符号位,那么剩下55位可表示理论上最大数字范围就是-2^55~2^55-1这么大,十进制就是-36028797018963968~36028797018963967。然后写以下代码测试下

    NSNumber *number1 = [NSNumber numberWithLong:36028797018963967];
    NSNumber *number2 = [NSNumber numberWithLong:36028797018963968];
    NSNumber *number3 = [NSNumber numberWithLong:-36028797018963968];
    NSNumber *number4 = [NSNumber numberWithLong:-36028797018963967];

    NSLog(@"%@ %p %@",number1, number1, number1.class);
    NSLog(@"%@ %p %@",number2, number2, number2.class);
    NSLog(@"%@ %p %@",number3, number3, number3.class);
    NSLog(@"%@ %p %@",number4, number4, number4.class);

可以看到输出结果

可见最大值确实是2^55-1,但是最小值却是-2^55+1,为什么呢?再写以下代码测试,断点打在最后一行。

    NSNumber *number1 = [NSNumber numberWithLong:-36028797018963968];
    NSNumber *number2 = [NSNumber numberWithLong:-36028797018963967];
    NSNumber *number3 = [NSNumber numberWithLong:-36028797018963966];

    NSLog(@"%@ %p %@",number1, number1, number1.class);
    NSLog(@"%@ %p %@",number2, number2, number2.class);
    NSLog(@"%@ %p %@",number3, number3, number3.class);
    NSLog(@"END");

运行代码,在调试窗口分别p/t输出number2和number3的值

可以看出,最小值+1的时候,也就是值为-36028797018963966的时候,数据位的产生了进位。如果最小值-1的话,存储数值的所有位都为0了,还需要多一位来显示数据才可以。所以最小值为-2^55 + 1。

所以Tagged Pointer可表示的数字范围是-2^55+1 ~ 2^55-1,超出这个范围的数字,NSNumber会转换为普通的Objective-C对象分配在堆上。

Read more

Category和关联对象

Category和关联对象

Category也就是分类,可以在没有类定义的情况下为现有类添加方法、属性、协议,众所周知是不能增加成员变量的,但是是可以通过关联对象给类增加的。 struct category_t { const char *name; classref_t cls; WrappedPtr instanceMethods; WrappedPtr classMethods; struct protocol_list_t *protocols; struct property_list_t *instanceProperties; // Fields below this point are not always present on disk. struct property_list_t *_classProperties; method_list_t *methodsF

By LEMON
内存管理-AutoreleasePool

内存管理-AutoreleasePool

AutoreleasePool(自动释放池)是Objective-C中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。 @autoreleasepool { ... }可以通过 clang -rewrite-objc 命令转成C++代码如下 /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; } 然后可以从objc源码中找到__AtAutoreleasePool的定义 struct __AtAutoreleasePool { __AtAutoreleasePool() { atautoreleasepoolobj = objc_autoreleasePoolPush(); } ~__AtAutoreleasePool() { objc_autoreleasePoolPop(atautoreleasepoolobj); } void * atautoreleasepoolobj; }; 这个结构体先

By LEMON
如何为macOS应用开发插件

如何为macOS应用开发插件

本文章只用于学习和交流,转载请注明出处。 玩过越狱手机的应该都了解,iOS的越狱插件是可以为系统或者应用添加一些特色的功能的。越狱插件的开发,已经有完整的工具链,并且插件的加载运行Substrate框架已经为我们做好。 更改应用的功能实现有两种方法,一种是通过反编译工具更改汇编代码后生成新的可执行文件。另一种是写一个动态库,然后注入到可执行文件。第一种适合简单的逻辑修改,毕竟用汇编实现功能是需要很大的工作量的。 第二种要怎么注入呢?了解Mach-O 二进制文件的应该知道,Mach-O 二进制文件Load Commands中的 LC_LOAD_DYLIB 标头告诉 macOS在执行期间要加载哪些动态库 (dylib)。所以我们只需要在二进制文件中添加一条LC_LOAD_DYLIB就可以。而insert_dylib工具已经为我们实现了添加的功能。所以现在唯一需要考虑的就是插件如何开发。 废话不多说,现在就以mac版的迅雷为例开发一款属于我们自己的插件。首先下载最新版本的迅雷(版本5.0.2)。然后用Xcode新建一个macOS下的Framework,如下 新建完目录结构如下,如果

By LEMON
深入分析objc_msgSend尾调用优化

深入分析objc_msgSend尾调用优化

无意间看到了这篇文章 iOS objc_msgSend尾调用优化详解,讲的是在Release模式下,当某函数的最后一项操作是调用另外一个函数,编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的“栈帧”(frame stack),以实现栈帧复用。 栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。所以在每一次函数调用之前,需要保存下执行的环境,以确保正常返回到调用位置。下面是- (void)viewDidLoad方法的汇编代码 可以看出在调用[super viewDidLoad]之前,先申请栈空间,然后保存x29和x30寄存器的值,调用结束后恢复寄存器的值,然后平栈。x29寄存器也就是fp寄存器,保存栈底的地址。x30寄存器也就是lr寄存器,保存返回地址。那栈帧复用又是怎么实现的呢?本着知其然知其所以然的精神,深入研究下。用下文章中的例子 - (NSInteger)func1:(NSInteger)num{ if (num >= 2000000) { return num; } return

By LEMON
京ICP备15024336号-4