内容摘要:本文转载自微信公众号「网罗开发」,作者HotPotCat 。转载本文请联系网罗开发公众号。上一篇文章分析了 objc_msgSend 的汇编实现,这边文章继续分析 objc_msgSend 中缓存的查

本文转载自微信公众号「网罗开发」,消息快作者HotPotCat 。速查转载本文请联系网罗开发公众号。找之找
上一篇文章分析了 objc_msgSend 的消息快汇编实现,这边文章继续分析 objc_msgSend 中缓存的速查查找逻辑以及汇编代码是如何进入 c/c++ 代码的。
1. CacheLookup 查找缓存
1.1 CacheLookup源码分析
//NORMAL,找之找 _objc_msgSend, __objc_msgSend_uncached .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant // requirements: // //缓存不存在返回NULL,x0设置为0 // GETIMP: // The cache-miss is just returning NULL (setting x0 to 0) // 参数说明 // NORMAL and LOOKUP: // - x0 contains the receiver // - x1 contains the selector // - x16 contains the isa // - other registers are set as per calling conventions // //调用过来的消息快p16存储的是cls,将cls存储在x15. mov x15,速查 x16 // stash the original isa //_objc_msgSend LLookupStart\Function: // p1 = SEL, p16 = isa //arm64 64 OSX/SIMULATOR #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS //isa->cache,首地址也就是找之找_bucketsAndMaybeMask ldr p10, [x16, #CACHE] // p10 = mask|buckets //lsr逻辑右移 p11 = _bucketsAndMaybeMask >> 48 也就是 mask lsr p11, p10, #48 // p11 = mask //p10 = _bucketsAndMaybeMask & 0xffffffffffff = buckets(保留后48位) and p10, p10, #0xffffffffffff // p10 = buckets //x12 = cmd & mask w1为第二个参数cmd(self,cmd...),消息快w11也就是速查p11 也就是执行cache_hash。这里没有>>7位的找之找操作 and w12, w1, w11 // x12 = _cmd & mask //arm64 64 真机这里p11计算后是_bucketsAndMaybeMask #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 ldr p11, [x16, #CACHE] // p11 = mask|buckets //arm64 + iOS + !模拟器 + 非mac应用 #if CONFIG_USE_PREOPT_CACHES //iphone 12以后指针验证 #if __has_feature(ptrauth_calls) //tbnz 测试位不为0则跳转。与tbz对应。消息快
p11 第0位不为0则跳转 LLookupPreopt\Function。速查 tbnz p11,找之找 #0, LLookupPreopt\Function //p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets and p10, p11, #0x0000ffffffffffff // p10 = buckets #else //p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe = buckets and p10, p11, #0x0000fffffffffffe // p10 = buckets //p11 第0位不为0则跳转 LLookupPreopt\Function。 tbnz p11, #0, LLookupPreopt\Function #endif //eor 逻辑异或(^) 格式为:EOR{ S}{ cond} Rd, Rn, Operand2 //p12 = selector ^ (selector >> 7) select 右移7位&自己给到p12 eor p12, p1, p1, LSR #7 //p12 = p12 & (_bucketsAndMaybeMask >> 48) = index & mask值 = buckets中的下标 and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask #else //p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets and p10, p11, #0x0000ffffffffffff // p10 = buckets //p12 = selector & (_bucketsAndMaybeMask >>48) = sel & mask = buckets中的下标 and p12, p1, p11, LSR #48 // x12 = _cmd & mask #endif // CONFIG_USE_PREOPT_CACHES //arm64 32 #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 //后4位为mask前置0的个数的case ldr p11, [x16, #CACHE] // p11 = mask|buckets and p10, p11, #~0xf // p10 = buckets 相当于后4位置为0,取前32位 and p11, p11, #0xf // p11 = maskShift 取的
香港云服务器是后4位,为mask前置位的0的个数 mov p12, #0xffff lsr p11, p12, p11 // p11 = mask = 0xffff >> p11 and p12, p1, p11 // x12 = _cmd & mask #else #error Unsupported cache mask storage for ARM64. #endif //通过上面的计算 p10 = buckets,p11 = mask(arm64真机是_bucketsAndMaybeMask), p12 = index // p13(bucket_t) = buckets + 下标 << 4 PTRSHIFT arm64 为3. <<4 位为16字节 buckets + 下标 *16 = buckets + index *16 也就是直接平移到了第几个元素的地址。 add p13, p10, p12, LSL #(1+PTRSHIFT) // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) //这里就直接遍历查找了,因为arm64下cache_next相当于遍历(这里只扫描了前面) // do { //p17 = imp, p9 = sel 1: ldp p17, p9, [x13], #-BUCKET_SIZE // { imp, sel} = *bucket-- //sel - _cmd != 0 则跳转 3:,也就意味着没有找到就跳转到__objc_msgSend_uncached cmp p9, p1 // if (sel != _cmd) { b.ne 3f // scan more // } else { //找到则调用或者返回imp,Mode为 NORMAL 2: CacheHit \Mode // hit: call or return imp 命中 // } //__objc_msgSend_uncached //缓存中找不到方法就走__objc_msgSend_uncached逻辑了。 //cbz 为0跳转 sel == nil 跳转 \MissLabelDynamic 3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss; 有空位没有找到说明没有缓存 //bucket_t - buckets 由于是递减操作 cmp p13, p10 // } while (bucket >= buckets) //⚠️ 这里一直是往前找,后面的元素在后面还有一次循环。 //无符号大于等于 则跳转1:f b 分别代表front与back b.hs 1b //没有命中cache 查找 p13 = mask对应的元素,也就是倒数第二个 #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS //p13 = buckets + (mask << 4) 平移找到对应mask的bucket_t。UXTW 将w11扩展为64位后左移4 add p13, p10, w11, UXTW #(1+PTRSHIFT) // p13 = buckets + (mask << 1+PTRSHIFT) #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //p13 = buckets + (mask >> 44) 这里右移44位,少移动4位就不用再左移了。因为maskZeroBits的存在 就找到了mask对应元素的地址 add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)) // p13 = buckets + (mask << 1+PTRSHIFT) // see comment about maskZeroBits #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 //p13 = buckets + (mask << 4) 找到对应mask的bucket_t。 add p13, p10, p11, LSL #(1+PTRSHIFT) // p13 = buckets + (mask << 1+PTRSHIFT) #else #error Unsupported cache mask storage for ARM64. #endif //p12 = buckets + (p12<<4) index对应的bucket_t add p12, p10, p12, LSL #(1+PTRSHIFT) // p12 = first probed bucket //之前已经往前查找过了,
云服务器提供商这里从后往index查找 // do { //p17 = imp p9 = sel 4: ldp p17, p9, [x13], #-BUCKET_SIZE // { imp, sel} = *bucket-- //sel - _cmd cmp p9, p1 // if (sel == _cmd) //sel == _cmd跳转CacheHit b.eq 2b // goto hit //sel != nil cmp p9, #0 // } while (sel != 0 && // ccmp p13, p12, #0, ne // bucket > first_probed) //有值跳转4: b.hi 4b LLookupEnd\Function: LLookupRecover\Function: //仍然没有找到缓存,缓存彻底不存在 __objc_msgSend_uncached() b \MissLabelDynamic
核心逻辑:
根据不同架构找到 buckets 中 sel 对应的 index,p10 = buckets,p11 = mask / _bucketsAndMaybeMask(arm64_64 是 _bucketsAndMaybeMask),p12 = index。
arm64_64 的情况下如果 _bucketsAndMaybeMask 第 0 位为 1 则执行 LLookupPreopt\Function。
p13 = buckets + index << 4 找到 cls 对应的 buckets 地址,地址平移找到对应 bucket_t do-while 循环扫描 buckets[index] 的前半部分(后半部分逻辑不在这里)。 如果存在 sel 为空,则说明是没有缓存的,就直接 `__objc_msgSend_uncached()``。 命中直接 CacheHit \Mode,这里 Mode 为 NORMAL。 平移获得 p13 = buckets[mask] 对应的元素,也就是最后一个元素(arm64 下最后一个不存自身地址,也就相当于 buckets[count - 1])。 p13 = buckets + mask << 4 找到 mask 对应的 buckets 地址,地址平移找到对应 bucket_t do-while 循环扫描 buckets[mask] 的前面元素,直到 index(不包含 index)。 命中 CacheHit \Mode 如果存在 sel 为空,则说明是
高防服务器没有缓存的,就直接结束循环。 最终仍然没有找到则执行 __objc_msgSend_uncached() CACHE 是 cache_t 相对 isa 的偏移。#define CACHE (2 * SIZEOF_POINTER) maskZeroBits 始终是 4 位 0,p13 = buckets + (_bucketsAndMaybeMask >> 44)右移 44 位后就不用再 <<4 找到对应 bucket_t 的地址了。这是因为 maskZeroBits 在 arm64_64 下存在的原因。 f b 分别代表 front 与 back,往下往上的意思。
1.2 CacheLookup 伪代码实现
//NORMAL, _objc_msgSend, __objc_msgSend_uncached void CacheLookup(Mode,Function,MissLabelDynamic,MissLabelConstant) { //1. 根据架构不同集算sel在buckets中的index if (arm64_64 && OSX/SIMULATOR) { p10 = isa->cache //_bucketsAndMaybeMask p11 = _bucketsAndMaybeMask >> 48//mask p10 = _bucketsAndMaybeMask & 0xffffffffffff//buckets x12 = sel & mask //index 也就是执行cache_hash } else if (arm64_64) { //真机 //这个分支下没有计算mask p11 = isa->cache //_bucketsAndMaybeMask if (arm64 + iOS + !模拟器 + 非mac应用) { if (开启指针验证 ) { if (_bucketsAndMaybeMask 第0位 != 0) { goto LLookupPreopt\Function } else { p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff//buckets } } else { p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe //buckets if (_bucketsAndMaybeMask 第0位 != 0) { goto LLookupPreopt\Function } } //计算index p12 = selector ^ (selector >> 7) p12 = p12 & (_bucketsAndMaybeMask & 48) = p12 & mask//index } else { p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff //buckets p12 = selector & (_bucketsAndMaybeMask >>48) //index } } else if (arm64_32) { p11 = _bucketsAndMaybeMask p10 = _bucketsAndMaybeMask &(~0xf)//buckets 相当于后4位置为0,取前32位 p11 = _bucketsAndMaybeMask & 0xf //mask前置位0的个数 p11 = 0xffff >> p11 //获取到mask的值 x12 = selector & mask //index } else { #error Unsupported cache mask storage for ARM64. } //通过上面的计算 p10 = buckets,p11 = mask/_bucketsAndMaybeMask, p12 = index p13 = buckets + index << 4 //找到cls对应的buckets地址。地址平移找到对应bucket_t。 //2.找缓存(这里只扫描了前面) do { p13 = *bucket-- //赋值后指向前一个bucket p17 = bucket.imp p9 = bucket.sel if (p9 != selector) { if (p9 == 0) { //说明没有缓存 __objc_msgSend_uncached() } } else { //缓存命中,走命中逻辑 call or return imp CacheHit \Mode } } while(bucket >= buckets) //buckets是首地址,bucket是index对应的buckct往前移动 //查找完后还没有缓存? //查找 p13 = mask对应的元素,也就是最后一个元素 if (arm64_64 && OSX/SIMULATOR) { p13 = buckets + (mask << 4) } else if (arm64_64) { //真机 p13 = buckets + (_bucketsAndMaybeMask >> 44)//这里右移44位,少移动4位就不用再左移了。这里就找到了对应index的bucket_t。 } else if (arm64_32) { p13 = buckets + (mask << 4) } else { #error Unsupported cache mask storage for ARM64. } //index的bucket_t 从mask对应的buckets开始再往前找 p12 = buckets + (index<<4) do { p17 = imp; p9 = sel; *p13--; if (p9 == selector) { //命中 CacheHit \Mode } } while (p9 != nil && bucket > p12)//从后往前 p9位nil则证明没有存,也就不存在缓存了。 //仍然没有找到缓存,缓存彻底不存在。 __objc_msgSend_uncached() }
2. LLookupPreopt\Function
在 arm64_64 真机的情况下,如果 _bucketsAndMaybeMask 的第 0 位为 1 则会执行 LLookupPreopt\Function 的逻辑。简单看了下汇编发现与 cache_t 中的 _originalPreoptCache 有关。
2.1 LLookupPreopt\Function 源码分析
LLookupPreopt\Function: #if __has_feature(ptrauth_calls) //p10 = _bucketsAndMaybeMask & 0x007ffffffffffffe = buckets and p10, p11, #0x007ffffffffffffe // p10 = x //buckets x16为cls 验证 autdb x10, x16 // auth as early as possible #endif // x12 = (_cmd - first_shared_cache_sel) //(_cmd >> 12 + PAGE) << 12 + PAGEOFF 第一个sel adrp x9, _MagicSelRef@PAGE ldr p9, [x9, _MagicSelRef@PAGEOFF] //差值index sub p12, p1, p9 // w9 = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask) #if __has_feature(ptrauth_calls) // bits 63..60 of x11 are the number of bits in hash_mask // bits 59..55 of x11 is hash_shift // 取到 hash_shift... lsr x17, x11, #55 // w17 = (hash_shift, ...) //w9 = index >> hash_shift lsr w9, w12, w17 // >>= shift //x17 = _bucketsAndMaybeMask >>60 //mask_bits lsr x17, x11, #60 // w17 = mask_bits mov x11, #0x7fff //x11 = 0x7fff >> mask_bits //mask lsr x11, x11, x17 // p11 = mask (0x7fff >> mask_bits) //x9 = x9 & mask and x9, x9, x11 // &= mask #else // bits 63..53 of x11 is hash_mask // bits 52..48 of x11 is hash_shift lsr x17, x11, #48 // w17 = (hash_shift, hash_mask) lsr w9, w12, w17 // >>= shift and x9, x9, x11, LSR #53 // &= mask #endif //x17 = el_offs | (imp_offs << 32) ldr x17, [x10, x9, LSL #3] // x17 == sel_offs | (imp_offs << 32) // cmp x12 x17 是否找到sel cmp x12, w17, uxtw .if \Mode == GETIMP b.ne \MissLabelConstant // cache miss //imp = isa - (sel_offs >> 32) sub x0, x16, x17, LSR #32 // imp = isa - imp_offs //注册imp SignAsImp x0 ret .else b.ne 5f // cache miss //imp(x17) = (isa - sel_offs>> 32) sub x17, x16, x17, LSR #32 // imp = isa - imp_offs .if \Mode == NORMAL //跳转imp br x17 .elseif \Mode == LOOKUP //x16 = isa | 3 //这里为或的意思 orr x16, x16, #3 // for instrumentation, note that we hit a constant cache //注册imp SignAsImp x17 ret .else .abort unhandled mode \Mode .endif //x9 = buckets-1 5: ldursw x9, [x10, #-8] // offset -8 is the fallback offset //计算回调isa x16 = x16 + x9 add x16, x16, x9 // compute the fallback isa //使用新isa重新查找缓存 b LLookupStart\Function // lookup again with a new isa .endif 找到 imp 就跳转/返回。 没有找到返回下一个 isa 重新 CacheLookup。 这块进入的查找共享缓存, 与 cache_t 的 _originalPreoptCache 有关。maskZeroBits 这 4 位就是用来判断是否有 _originalPreoptCache 的。
@TODO 真机调试的时候进不到这块流程,这块分析的还不是很透彻,后面再补充。
3. CacheHit
在查找缓存命中后会执行 CacheHit。
3.1 CacheHit源码分析
#define NORMAL 0 #define GETIMP 1 #define LOOKUP 2 // CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa .macro CacheHit //这里传入的为NORMAL .if $0 == NORMAL //调用imp TailCallCachedImp(imp,buckets,sel,isa) TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp .elseif $0 == GETIMP //返回imp mov p0, p17 //imp == nil跳转9: cbz p0, 9f // dont ptrauth a nil imp //有imp执行AuthAndResignAsIMP(imp,buckets,sel,isa)最后给到x0返回。 AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP 9: ret // return IMP .elseif $0 == LOOKUP // No nil check for ptrauth: the caller would crash anyway when they // jump to a nil IMP. We dont care if that jump also fails ptrauth. //找imp(imp,buckets,sel,isa) AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP //isa与x15比较 cmp x16, x15 //cinc如果相等 就将x16+1,否则就设成0. cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class) ret // return imp via x17 .else .abort oops .endif .endmacro 这里其实走的是 NORMAL 逻辑,NORMAL 的 case 直接验证并且跳转 imp。 TailCallCachedImp 内部执行的是 imp^cls,对 imp 进行了解码。 GETIMP 返回 imp。 LOOKUP 查找注册 imp 并返回。
3.1 CacheHit 伪代码实现
//x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa void CacheHit(Mode) { if (Mode == NORMAL) { //imp = imp^cls 解码 TailCallCachedImp x17, x10, x1, x16 // 解码跳转imp } else if (Mode == GETIMP) { p0 = IMP if (p0 == nil) { return } else { AuthAndResignAsIMP(imp,buckets,sel,isa)//resign cached imp as IMP } } else if (Mode == LOOKUP) { AuthAndResignAsIMP(x17, buckets, sel, isa)//resign cached imp as IMP if (isa == x15) { x16 += 1 } else { x16 = 0 } } else { .abort oops//报错 } }
4. __objc_msgSend_uncached
在缓存没有命中的情况下会走到 __objc_msgSend_uncached() 的逻辑:
STATIC_ENTRY __objc_msgSend_uncached UNWIND __objc_msgSend_uncached, FrameWithNoSaves // THIS IS NOT A CALLABLE C FUNCTION // Out-of-band p15 is the class to search //查找imp MethodTableLookup //跳转imp TailCallFunctionPointer x17 END_ENTRY __objc_msgSend_uncached MethodTableLookup 查找 imp TailCallFunctionPointer 跳转 imp .macro MethodTableLookup SAVE_REGS MSGSEND // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER) // receiver and selector already in x0 and x1 //x2 = cls mov x2, x16 //x3 = LOOKUP_INITIALIZE|LOOKUP_RESOLVER //是否初始化,imp没有实现尝试resolver //_lookUpImpOrForward(receiver,selector,cls,LOOKUP_INITIALIZE | LOOKUP_RESOLVER) mov x3, #3 bl _lookUpImpOrForward // IMP in x0 mov x17, x0 RESTORE_REGS MSGSEND .endmacro 调用 _lookUpImpOrForward 查找 imp。这里就调用到了 c/c++ 的代码了: IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
最终会调用 _lookUpImpOrForward 进入 c/c++ 环境逻辑。
对于架构的一些理解:
LP64 //64位 x86_64 // interl 64位 i386 // intel 32位 arm // arm指令 32 位 arm64 //arm64指令 arm64 && LP64 //arm64 64位 arm64 && !LP64 //arm64 32 位
当然也可以通过真机跟踪汇编代码读取寄存器进行,与源码分析的是一致的,走其中的一个分支。
5. objc_msgSend流程图

总结
判断 receiver 是否存在。 通过 isa 获取 cls。 cls 内存平移 0x10 获取 cache 也就是 _bucketsAndMaybeMask。 通过 buckets & bucketsMask 获取 buckets地址。 通过 bucketsMask >> maskShift 获取 mask。 通过 sel & mask 获取第一次查找的 index。 buckets + index << 4 找到 index 对应的地址。 do-while 循环判断找缓存,这次从 [index~0] 查找 imp。 取到 buckets[mask] 继续 do-while 循环,从 [mask~index) 查找 imp。两次查找过程中如果有 sel 为空则会结束查找。走 __objc_msgSend_uncached 的逻辑。 找到 imp 就解码跳转 imp。