阅读 631
iOS底层原理探索 -- 内存管理 之 Tagged Pointer Format Changes

iOS底层原理探索 -- 内存管理 之 Tagged Pointer Format Changes

写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
复制代码

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend
  9. iOS 底层原理探索 之 Runtime运行时慢速查找流程
  10. iOS 底层原理探索 之 动态方法决议
  11. iOS 底层原理探索 之 消息转发流程
  12. iOS 底层原理探索 之 应用程序加载原理dyld (上)
  13. iOS 底层原理探索 之 应用程序加载原理dyld (下)
  14. iOS 底层原理探索 之 类的加载
  15. iOS 底层原理探索 之 分类的加载
  16. iOS 底层原理探索 之 关联对象
  17. iOS底层原理探索 之 魔法师KVC
  18. iOS底层原理探索 之 KVO原理|8月更文挑战
  19. iOS底层原理探索 之 重写KVO|8月更文挑战
  20. iOS底层原理探索 之 多线程原理|8月更文挑战
  21. iOS底层原理探索 之 GCD函数和队列
  22. iOS底层原理探索 之 GCD原理(上)
  23. iOS底层 - 关于死锁,你了解多少?
  24. iOS底层 - 单例 销毁 可否 ?
  25. iOS底层 - Dispatch Source
  26. iOS底层 - 一个栅栏函 拦住了 数
  27. iOS底层 - 不见不散 的 信号量
  28. iOS底层 GCD - 一进一出 便成 调度组
  29. iOS底层原理探索 - 锁的基本使用
  30. iOS底层 - @synchronized 流程分析
  31. iOS底层 - 锁的原理探索
  32. iOS底层 - 带你实现一个读写锁
  33. iOS底层 - 谈Objective-C block的实现(上)
  34. iOS底层 - 谈Objective-C block的实现(下)
  35. iOS底层 - Block, 全面解析!
  36. iOS底层 - 启动优化(上)
  37. iOS底层 - 启动优化(下)
  38. iOS底层原理探索 -- 内存管理 之 内存五大区

以上内容的总结专栏


细枝末节整理


前言

接着上一篇,我们看下苹果对于Objective——C runtime运行时所作的优化。按照苹果官方的说法,即使我们的app不做任何优化,运行速度也会变快,那么苹果底层做了什么优化来作为支撑的呢? 今天,我们就一探究竟。

内存管理方案

说到内存管理,大家想到的应该都是 ARC MRC 等,这里我们要看的是下面三个:

首先,我们要复习下苹果关于运行时做出的更改

来自苹果WWDC2020关于 Objective-C 运行时做出的更改

Objective-C运行时的进步

深入了解作为每个 Objective-C 和 Swift 类基础的低级位和字节的微观世界。了解内部数据结构、方法列表和标记指针的最新变化如何提供更好的性能和更低的内存使用。我们将演示如何识别和修复依赖于内部细节的代码崩溃,并向您展示如何使您的代码不受运行时更改的影响。

Tagged Pointer Format Changes

什么是Tagged Pointer

image.png

让我们先来看看普通对象指针的结构 通常 当我们看到这些指针时 它们被打印成这些大的十六进制数字 我们经常看到过这些数字; 让我们把它分解成二进制表示法; 我们有 64 位 然而 我们并没有真正地使用到所有这些位; 我们只在一个真正的对象指针中 使用了中间的这些位; 由于对齐要求的存在 低位始终为 0 对象必须总是位于 指针大小倍数的一个地址中; 由于地址空间有限 所以高位始终为 0 我们实际上不会用到 2^64; 这些高位和低位总是 0; 所以 让我们从这些始终为 0 的位中 选择一个位并把它设置为 1这可以让我们立即知道 这不是一个真正的对象指针 然后我们可以给其他所有位 赋予一些其他的意义;

我们称这种指针为 tagged pointer

例如 我们可以在其他位中塞入一个数值

只要我们想教 NSNumber 如何读取这些位 并让运行时适当地处理 tagged pointer 系统的其他部分就可以 把这些东西当做对象指针来处理 并且永远不会知道其中的区别

这样可以节省我们为每一种类似情况 分配一个小数字对象的代价 这是一个重大的改进

顺便说一下 这些值实际上是通过 与进程启动时初始化的随机值相结合 而被混淆的

这一安全措施使得 很难伪造 tagged pointer

在接下来的讨论中 我们将忽略这一点 因为它只是在顶部增加了一层 只是要注意 如果你真的试图在内存中查看这些值 它们会被打乱

Tagged Pointers On Intel

image.png

这就是 Intel 上 tagged pointer 的完整格式

我们把低位设置为 1 表示这是一个 tagged pointer

正如我们所讨论的 对于一个真正的指针 这个位必须始终为 0 所以这让我们可以把它们区分开来

接下来的 3 位是标签号 这表示 tagged pointer 的类型 例如 3 表示它是一个 NSNumber 6 表示它是一个 NSDate

由于我们有 3 个标签位 所以有 8 种可能的标签类型

剩下的位是有效负载 这是特定类型可以随意使用的数据

对于标记的 NSNumber 这是实际的数字

现在标签 7 有一个特殊情况 它表示一个扩展标签 扩展标签使用接下来的 8 位来编码类型 这允许多出 256 个标签类型 但是代价是减少有效负载

这使得我们可以将 tagged pointer 用于更多的类型 只要它们可以将其数据装入更小的空间

这可用于一些东西 如用户界面 colors 或 NSIndexSets

现在 如果这对你来说非常方便 你可能会感到失望 因为只有运行时维护者 即 Apple 可以添加 tagged pointer 类型

但如果你是一个 Swift 程序员 你会感到很高兴 可以创建自己的 tagged pointer 类型 如果你曾经使用过一个具有关联值的枚举 那是一个类似于 tagged pointer 的类

Swift 运行时将枚举判别器存储在 关联值有效负载的备用位中

而且 Swift 对值类型的使用 实际上使得 tagged pointer 变得没那么重要了 因为值不再需要完全是指针大小

例如 Swift UUID 类型 可以是两个字并保持内联 而不是分配一个单独的对象 因为它不适合在一个指针里面

这就是 intel 上的 tagged pointer 让我们来看看 ARM

Tagged Pointers On ARM64

image.png

在 arm64 上 这些是反过来的

最高位设置为 1 而不是最低位 用来表示一个 tagged pointer

然后在接下来的 3 个位中出现标签号

而有效负载使用剩余的位

为什么我们在 ARM 上 使用顶部位来表示 tagged pointer 而不是像在 intel 上那样 使用底部位来表示呢?

嗯 这实际上是对 objc_msgSend 的一个小优化

我们希望 msgSend 中最常见的路径 可以尽可能地快 而最常见的路径是一个普通的指针

我们有两种不太常见的情况

tagged pointer 和 nil

事实证明 当我们使用最高位时 我们可以通过一次比较对这两个进行检查 相比于分开检查 nil 和 tagged pointer 这就为 msgSend 中的 常见情况节省了一个条件分支

和 Intel 中一样 对于标签 7 我们有一个特殊情况 接下来的 8 位被用作扩展标签 然后剩下的位用于有效负载 或者说 这其实是 iOS 13 使用的旧格式


image.png

在2020年的版本中 我们做了一些改动 我们将标签位保持在最高位 因为 msgSend 的优化还是非常有用的

标签号现在移到了最下面的 3 个位

如果正在使用扩展标签 那么它会占据标签位后的高 8 位

为什么我们要这样做呢? 好吧 让我们再来看看正常指针

image.png

我们现有的工具 比如动态链接 会忽略指针的前 8 位 这是由于 一个名为 Top Byte Ignore 的 ARM 特性

而我们会把扩展标签 放在 Top Byte Ignore 位

对于一个对齐指针 底部 3 个位总是 0

但我们可以改变这一点 只需要通过在指针上添加一个小数字

我们将添加 7 以将低位设置为 1 请记住 7 表示这是一个扩展标签

这意味着我们实际上可将上面的这个指针 放入一个扩展标签指针有效负载中

这个结果是 一个 tagged pointer 以及 其有效负载中包含一个正常指针

为什么这很有用呢? 好的 它开启了 tagged pointer 的能力 引用二进制文件中的常量数据的能力 例如字符串或其他数据结构 否则它们将不得不占用 dirty memory

当然 现在这些变化意味着 今年晚些时候 iOS 14 发布时 直接访问这些位的代码将会失效

在过去 像这样的位检查是可以进行的

但在未来的 OS上 它会给你错误的答案 然后 你的 app 会开始 莫名其妙地破坏用户数据

所以 不要使用那些依赖于 我们所谈到的任何东西的代码 相反 你大概可以猜到我要说什么 也就是要使用 API

像 isKindOfClass 这样的类型检查 它们在旧的 tagged pointer 格式上工作 在新的 tagged pointer 格式上 它们也将继续工作 所有的 NSString 或 NSNumber 方法 都能继续工作 这些 tagged pointer 中的所有信息 都可以通过标准的 API 来检索


按照苹果的说法, Tagget Pointer 有3倍的空间效率和106倍的创建销毁性能的提升。


代码验证

模拟器环境

NSTaggedPointerString

image.png

其地址 0xe162ff2824e12ccf 看起来有一点那么多不科学;上一篇内存五大区中,如果是在栈区通常以 0x7 开头,如果在堆区 通常以 0x6 开头,如果在全局区通常以 0x1开头, 然而这个 NSTaggedPointerString 地址以 0xa 开头。

下面,我们来到源码环境探索下其内容:

_objc_decodeTaggedPointer


/// 这里就是 Ben 同学说的 这些值实际上是通过 与进程启动时初始化的随机值相结合 而被混淆的
// 这一安全措施使得 很难伪造 tagged pointer
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;
}

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;
}

----

/***********************************************************************
* initializeTaggedPointerObfuscator
* Initialize objc_debug_taggedpointer_obfuscator with randomness.
*
* 带标记的指针混淆器旨在使其更加困难 
* 对于攻击者来说,要构造一个带有标记的指针的特定对象, 
* 出现缓冲区溢出或其他写控制时 内存。
* 设置时,模糊器是带有标记指针的xord 或检索有效载荷值。
* 它们首先充满了随机性 使用。
**********************************************************************/
static void
initializeTaggedPointerObfuscator(void)
{
    if (!DisableTaggedPointerObfuscation) {
        // 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;

#if OBJC_SPLIT_TAGGED_POINTERS
        // The obfuscator doesn't apply to any of the extended tag mask or the no-obfuscation bit.
        objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);

        // Shuffle the first seven entries of the tag permutator.
        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;
    }
}

复制代码

_objc_makeTaggedPointer

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);
    }
}
复制代码

在read_images 的时候, 会初始化这个随机数;

我们可以尝试将这个混淆的值拿出来,来操作一下,就有机会得到真正的指针。

我们来看下一个例子:

taggedpoint.001.jpeg

这里的类型是如何对应的呢?

在如下定义中:

image.png

真机环境

OpenEmu-master.zip.001.jpeg

这里实际打印的内容和WWDC中Ben同学说的还是不一样的,说明苹果又有了小的改动(我测试的真机系统版本未 14.2 );

看完string,我们再看看Number:

未命名.001.jpeg

补充

最后,我们是在调试环境中 加入 了环境变量来关闭混淆做的打印测试哦。

文章分类
iOS
文章标签