Objc_MsgSend 消息快速查找之Cache 查找
本文轉載自微信公眾號「網羅開發」,作者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 // don't 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 don't 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。