4

Obj-C 中的 DisguisedPtr

 3 years ago
source link: https://kingcos.me/posts/2021/disguised_ptr_in_objc/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Obj-C 中的 DisguisedPtr

2021.06.01 by kingcos

Preface · 序
DisguisedPtr<T> 是 Apple 开源代码中的一个结构体类型,其使用于诸多 Obj-C 运行时组件中,比如 weak@synchronized 等。

Release Notes ↕

正如 Apple 官方对于 DisguisedPtr<T> 的注释:「acts like pointer type T*(行为类似于指针类型 T*)」,即其本身的作用等同于指针引用对象,而不同之处则在于其将引用对象的内存地址隐藏了。我们可以在 Apple 开源的 objc4-818.2 中找到其具体实现(如下)。

DisguisedPtr<T> 的本质是 C++ 的模版类,其中只含有一个 uintptr_t 类型的成员变量 valueuintptr_t 是无符号整型,64 位下的大小为 8 字节,等同于指针的大小,可以用来存储指针。因此 DisguisedPtr 类型的对象大小其实也是 8 字节。当我们将指针构造为该类型的对象时,将通过 disguise 静态函数首先将指针存储的内存地址本身强制转换为 uintptr_t 无符号整型的十进制数据,并取负达到隐藏的目的。当然,其也提供了 undisguise 静态函数将隐藏的数据转换回内存地址,以及一些操作符便于外界直接使用。

// objc-private.h

// DisguisedPtr<T> acts like pointer type T*, except the
// stored value is disguised to hide it from tools like `leaks`.
// nil is disguised as itself so zero-filled memory works as expected,
// which means 0x80..00 is also disguised as itself but we don't care.
// Note that weak_entry_t knows about this encoding.
//
// DisguisedPtr<T> 的行为类似于指针类型 T* 一样,唯一的不同在于存储的值是伪装的,以避免被 leaks 等工具发现。
// nil 被伪装成了自己,所以零填充的内存可以像预期的那样工作,这意味着 0x80...00 也被伪装成了自己,但我们并不在意。
// 注意,weak_entry_t 也使用了该编码。

template <typename T>
class DisguisedPtr {
    // _uintptr_t.h:
    // typedef unsigned long uintptr_t;
    // uintptr_t 表示能够存储指针地址的无符号整数类型(不同操作系统可能存在不同)
    uintptr_t value;

    static uintptr_t disguise(T* ptr) {
        // 将指针指向的地址转换为 unsigned long,并取负
        return -(uintptr_t)ptr;
    }

    static T* undisguise(uintptr_t val) {
        // 转换回指针指向的地址
        return (T*)-val;
    }

 public:
    // 无参构造方法
    DisguisedPtr() { }
    
    // 指针构造(value = disguise(ptr))
    DisguisedPtr(T* ptr)
        : value(disguise(ptr)) { }
    
    // 使用 DisguisedPtr 类型构造(value = ptr.value)
    DisguisedPtr(const DisguisedPtr<T>& ptr)
        : value(ptr.value) { }

    // 实现 = 运算符,右操作数类型为 T*
    DisguisedPtr<T>& operator = (T* rhs) {
        value = disguise(rhs);
        return *this;
    }
    
    // 实现 = 运算符,右操作数类型为 DisguisedPtr<T>
    DisguisedPtr<T>& operator = (const DisguisedPtr<T>& rhs) {
        value = rhs.value;
        return *this;
    }

    operator T* () const {
        // Foo *someFoo = disguisedFooPtr;
        return undisguise(value);
    }
    T* operator -> () const {
        // disguisedFooPtr->bar
        return undisguise(value);
    }
    T& operator * () const {
        // *disguisedFooPtr
        return *undisguise(value);
    }
    T& operator [] (size_t i) const {
        // &disguisedFooPtr[0]
        return undisguise(value)[i];
    }

    // pointer arithmetic operators omitted
    // because we don't currently use them anywhere
    // 省略了指针运算符,因为我们目前没有在任何地方使用它们
};

// fixme type id is weird and not identical to objc_object*
static inline bool operator == (DisguisedPtr<objc_object> lhs, id rhs) {
    return lhs == (objc_object *)rhs;
}
static inline bool operator != (DisguisedPtr<objc_object> lhs, id rhs) {
    return lhs != (objc_object *)rhs;
}

在平时的开发中,我们其实不会接触到这一「画蛇添足」的类型,但在 Apple 的许多官方组件中,却常常能见到其身影,大多数时候我们只要认为其等同于指针引用对象即可。下面是其 API 的部分用例,需要注意的是,正如上文提到的 DisguisedPtr<T> 属于 C++ 模版类,因此需要将引入其头文件的实现文件后缀改为 .mm

// main.mm

#import <Foundation/Foundation.h>
#import "objc-private.h"

class Foo {
public:
    int bar;
    double baz;
};

int main(int argc, const char * argv[]) {
    // 创建对象
    Foo foo;
    foo.bar = 1;
    foo.baz = 3.14;
    
    // 指针 fooPtr 指向 foo(即 fooPtr 存储了 foo 的内存地址)
    Foo *fooPtr = &foo;
    
    // 构造 DisguisedPtr
    DisguisedPtr<Foo> disguisedFooPtr = DisguisedPtr<Foo>(fooPtr);
    
    Foo *someFoo = disguisedFooPtr; // => operator T* ()
    NSLog(@"%d", someFoo->bar);     // 1
    
    disguisedFooPtr->bar = 10;            // => T* operator -> ()
    NSLog(@"%d", disguisedFooPtr->bar);   // 10
    
    NSLog(@"%d", (*disguisedFooPtr).bar); // 10 => T& operator * ()
    
    NSLog(@"%p", &disguisedFooPtr[0]);    // 0x16fdff320 => T& operator * ()
    NSLog(@"%p", &disguisedFooPtr[1]);    // 0x16fdff330 => T& operator * ()
    
    return 0; // BREAKPOINT 🔴
}

// OUTPUT:
// 1
// 10
// 10
// 0x16fdff320
// 0x16fdff330

// LLDB:
// (lldb) memory read 0x16fdff320
// 0x16fdff320: 0a 00 00 00 00 00 00 00 1f 85 eb 51 b8 1e 09 40  ...........Q...@
// 0x16fdff330: 88 f3 df 6f 01 00 00 00 01 00 00 00 00 00 00 00  ...o............
// (lldb) memory read 0x16fdff330
// 0x16fdff330: 88 f3 df 6f 01 00 00 00 01 00 00 00 00 00 00 00  ...o............
// 0x16fdff340: 60 f3 df 6f 01 00 00 00 50 d4 26 89 01 00 00 00  `..o....P.&.....

那么为什么 Apple 要如此「画蛇添足」地实现这一类型呢?根据官方注释:「to hide it from tools like leaks(以避免被 leaks 等工具发现)」,由于可参考的信息太少,网络上大部分关于这一点的介绍也仅此而已,本文也仅只是「猜想」。

leaks 是 macOS 自带的一款内存泄漏检测工具,我们可以在命令行 man leaks 以及 Reference 中的参考链接了解更多关于 leaks 的细节。DisguisedPtr<T> 避免了被 leaks 工具的检测,我们猜想这也意味着在使用 leaks 确认问题时,避免了被运行时底层的信息干扰。

Reference


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK