在分析类的结构时我们知道类中有 cache 这个成员变量,通过名称我们大概能猜到是缓存,但缓存什么呢。
来看一下源码中 cache_t 的结构:
它有5个成员变量,分别为 _bucketsAndMaybeMask,_maybeMask,_flags,_occupied,_originalPreoptCache,虽然有5个,但内存中不是有5个,它有一个 union,union 的特性是互斥,所以其实在下面的探索中 _originalPreoptCache 可以先不管。另外 _flags 是其它一些数据,也可以先不管。
所以在这几个成员变量中,我们只关注 _bucketsAndMaybeMask,_maybeMask,_occupied 。
一、_bucketsAndMaybeMask 和 _maybeMask
在不同的环境的编译下,_bucketsAndMaybeMask 和 _maybeMask 的注释不一样。
-
当
CACHE_MASK_STORAGE为CACHE_MASK_STORAGE_OUTLINED的时候,_bucketsAndMaybeMask是一个buckets_t指针,_maybeMask是buckets掩码。 -
当
CACHE_MASK_STORAGE为CACHE_MASK_STORAGE_HIGH_16的时候,_bucketsAndMaybeMask是低 48 位的buckets_t指针,_maybeMask未使用,掩码存储在前 16 位。
那么,CACHE_MASK_STORAGE 是什么,来看一下它的定义:
#define CACHE_MASK_STORAGE_OUTLINED 1
#define CACHE_MASK_STORAGE_HIGH_16 2
#define CACHE_MASK_STORAGE_LOW_4 3
#define CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS 4
#if defined(__arm64__) && __LP64__
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR // macOS、模拟器
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16 // 真机
#endif
#elif defined(__arm64__) && !__LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif
所以,当 CACHE_MASK_STORAGE 为 CACHE_MASK_STORAGE_OUTLINED 的时候是 macOS 和模拟器,为 CACHE_MASK_STORAGE_HIGH_16 的时候是真机。
二、bucket_t 分析
通过分析 _bucketsAndMaybeMask 和 _maybeMask ,我们知道了 _bucketsAndMaybeMask 是一个指向 bucket_t 的指针,bucket 翻译过来是‘桶’的意思,而苹果的注释是 buckets,个人理解应该就是很多桶的意思,后面的 _t 表示是结构体。
那么 bucket_t 长什么样,我们来看一下源码:
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
// Compute the ptrauth signing modifier from &_imp, newSel, and cls.
uintptr_t modifierForSEL(bucket_t *base, SEL newSel, Class cls) const {
return (uintptr_t)base ^ (uintptr_t)newSel ^ (uintptr_t)cls;
}
// Sign newImp, with &_imp, newSel, and cls as modifiers.
uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
return (uintptr_t)
ptrauth_auth_and_resign(newImp,
ptrauth_key_function_pointer, 0,
ptrauth_key_process_dependent_code,
modifierForSEL(base, newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (uintptr_t)newImp ^ (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (uintptr_t)newImp;
#else
#error Unknown method cache IMP encoding.
#endif
}
public:
static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
#define MAYBE_UNUSED_ISA
#else
#define MAYBE_UNUSED_ISA __attribute__((unused))
#endif
inline IMP rawImp(MAYBE_UNUSED_ISA objc_class *cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
imp ^= (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
#else
#error Unknown method cache IMP encoding.
#endif
return (IMP)imp;
}
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
SEL sel = _sel.load(memory_order_relaxed);
return (IMP)
ptrauth_auth_and_resign((const void *)imp,
ptrauth_key_process_dependent_code,
modifierForSEL(base, sel, cls),
ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
}
inline void scribbleIMP(uintptr_t value) {
_imp.store(value, memory_order_relaxed);
}
template <Atomicity, IMPEncoding>
void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
};
源码很长,但我们发现,bucket_t 中有 _imp 和 _sel 两个成员变量,并且,还有 sel,rawImp,imp函数。所以 bucket_t 存放着方法的 IMP(方法地址) 和 SEL(方法选择器) ,并且我们可以通过 sel 方法和 imp 方法拿到对应的 IMP 和 SEL。
这个时候我们知道 cache_t 是用来缓存方法的,bucket_t 存的是 IMP 和 SEL 相关的,假如我们要存放很多个方法呢?一个 bucket_t 只能存放一个 IMP 和一个 SEL,那怎么能存放更多的方法?
在 cache_t 中有这么一个方法:
struct bucket_t *buckets() const;
这个方法是获取所有的缓存中的 bucket_t,但它是一个结构体指针,不是数组。还记得前面讲到的内存平移的么,接下来我们通过llbd进行内存平移,拿到 SEL 和 IMP 。
三、lldb 获取 cache_t 中缓存的 SEL 和 IMP
先定义一个 SHPerson 对象,并添加方法。
@interface SHPerson : NSObject
- (void)play_basketball;
- (void)play_football;
- (void)play_badminton;
- (void)play_volleyball;
- (void)play_table_tennis;
@end
@implementation SHPerson
- (void)play_basketball {
NSLog(@"%s", __func__);
}
- (void)play_football {
NSLog(@"%s", __func__);
}
- (void)play_badminton {
NSLog(@"%s", __func__);
}
- (void)play_volleyball {
NSLog(@"%s", __func__);
}
- (void)play_table_tennis {
NSLog(@"%s", __func__);
}
@end
打个断点,打印出 cache_t 的结构,再调用 buckets() 拿到 bucket_t*。这里需要注意的是先不要 init,减少不必要的干扰。
我们发现 bucket_t 的 _sel 等于 nil,_imp 等于 0,并且 _maybeMask 和 _occupied 的值都为0,会不会是我们没有调用方法的原因呢,接下来我们调用 play_basketball 方法,再获取 bucket_t*。
首先 bucket_t * 的内存地址由 0x00000001003623d0 变成了 0x000000010175ef30,其次 _sel 和 _imp 都有值了,接下来调用 imp 方法打印出 _imp 的值,看看是否是我们调用后的 play_basketball 方法。
成功的打印出 play_basketball 方法,那么我们再调用 play_football 方法。
我们看到再次打印 bucket_t 的 imp 没有变化,然后想到了内存平移,但 p *($9+1) 打印出的 bucket_t 并不是我们想象的 play_football 方法,而是一个 class 的系统方法,那我们再平移,p *($9+2),打印的才是我们的 play_football 方法。
到这里我们知道,bucket_t* 缓存的方法并不是按顺序进行存储的。
四、验证 _bucketsAndMaybeMask 是否是 bucket_t 指针
还记得第一点里源码分析 _bucketsAndMaybeMask 吗?,_bucketsAndMaybeMask 是 bucket_t 指针,我们重新运行,来验证一下。
_bucketsAndMaybeMask 的地址和 bucket_t* 内存地址一摸一样,由此可见,_bucketsAndMaybeMask 存放的确实是 bucket_t 指针,并且,是 bucket_t* 的首地址,因为我们还可以通过内存平移拿到其它的 bucket_t。
_occupied 和 _maybeMask 都有值了,_maybeMask 是一个掩码,那么 _occupied 是什么?方法是如何插入到 bucket_t* 的?请看下篇文章:cache_t -> insert 方法。
五、关于 _maybeMask 在不同架构的坑点
在探索的过程中,我的 Mac 电脑的芯片是 intel 的,intel 芯片的电脑在探索的时候 _maybeMask 的值是会变化的。如果你用的是 M1 芯片的 Mac 电脑,那么坑就来了,_maybeMask 的值是不会变的,那就意味着我们拿不到 _maybeMask 的值进行下一步的探索。
在 cache_t 结构体中,有一个 mask 方法:
mask_t mask() const;
经过测试,mask 返回的值就是 _maybeMask 的值,所以在用 M1 芯片的电脑探索的时候,拿不到 _maybeMask 的值就调用 mask 方法拿到返回值当作 _maybeMask 使用。