在 Objective-C 語言中笨觅,實(shí)例對象執(zhí)行方法蓉媳,而執(zhí)行方法的過程也可以稱為給實(shí)例對象發(fā)送消息。發(fā)送消息的過程執(zhí)行在編譯階段會轉(zhuǎn)化成對 objc_msgSend
函數(shù)的調(diào)用。本文將分析 objc_msgSend
匯編部分主要部分(fast path)牵触。
文章中用到的匯編指令可以參考我個(gè)人的匯編學(xué)習(xí)筆記
Objective-C 實(shí)例對象執(zhí)行方法步驟
objc_msgSend
前2個(gè)傳入?yún)?shù)有對象實(shí)例 receiver
和方法名 selector
,執(zhí)行過程可以簡單概括為:
- 獲取
receiver
對應(yīng)的類 Class - 在 Class 緩存列表中根據(jù)選擇子
selector
查找 IMP - 若緩存中沒有找到咐低,則在方法列表中繼續(xù)查找
- 若方法列表沒有揽思,則從父類查找,重復(fù)以上步驟
- 若最終沒有找到见擦,則進(jìn)行消息轉(zhuǎn)發(fā)操作
objc_msgSend 匯編源碼內(nèi)部邏輯
ENTRY _objc_msgSend // _objc_msgSend 入口
MESSENGER_START
cmp x0, #0 // nil check and tagged pointer check, cmp 指令執(zhí)行完钉汗,設(shè)置Z-flag(零標(biāo)志)
b.le LNilOrTagged // 如果 x0 的值==0,CPSR寄存器的 Z 標(biāo)識==1鲤屡,跳轉(zhuǎn)標(biāo)簽判斷是否 self 是否為 nil 或者是 tagged pointer 類型
// 跳轉(zhuǎn)之前 lr 寄存器會保存 pc 寄存器當(dāng)前內(nèi)容
ldr x13, [x0] // x13 = isa损痰,把 self 指針賦值到 x13,self 是 objc_object 結(jié)構(gòu)體酒来,結(jié)構(gòu)體第一個(gè)屬性是 isa卢未,所以這里 x13 指向了 isa
and x9, x13, #ISA_MASK // x9 = class,與運(yùn)算來移除掉這些多余的信息堰汉,將一個(gè)真實(shí)指向類的指針保存在 x9 里
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
LNilOrTagged: // 執(zhí)行到這里說明 self 的值等于 0辽社。小于零則代表為 Tagged Pointer 情況,等于說明為 nil
b.eq LReturnZero // nil check衡奥,判斷 self 是否為 nil
// tagged
// 這里加載了 _objc_debug_taggedpointer_classes 的地址爹袁,即 Tagged Pointer 主表
// ARM64 需要兩條指令來加載一個(gè)符號的地址。這是 RISC 樣架構(gòu)上的一個(gè)標(biāo)準(zhǔn)技術(shù)矮固。
// AMR64 上的指針是 64 位寬的失息,指令是 32 位寬。所以一個(gè)指令無法保存一個(gè)完整的指針
adrp x10, _objc_debug_taggedpointer_classes@PAGE // 將頁(前半部分)的基址存在 x10
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF // 將頁(后半部分)的基址存在 x10
ubfx x11, x0, #60, #4 // 它從 self 中的第 60 位開始档址,提取 4 位盹兢,保存到 x11 中。
ldr x9, [x10, x11, LSL #3] // x9 = x10 + (x11<<3)守伸,這里通過 x11 里的索引到 x10 所指向的 Tagged Pointer 表中查找具體的類
b LGetIsaDone
LReturnZero:
// x0 is already zero
// 因?yàn)榻鼇碇耙呀?jīng)通過 `cmp x0, #0` 判斷绎秒,所以 x0 寄存器的值是0
// 整型的返回值保存在 x0 和 x1 中
// 浮點(diǎn)型的返回值會被保存在 v0 到 v3 這幾個(gè)向量寄存器中,
// d0 到 d3這幾個(gè)寄存器是相關(guān)v寄存器的后半部分尼摹,向他們存值的時(shí)候會將對應(yīng) v 寄存器的前半部分置 0
mov x1, #0 // 1见芹、首先先把 x1 清空剂娄,x0 這里是 self,已經(jīng)是0玄呛,所以不需要清空阅懦,
movi d0, #0 // 2、清空 v 寄存器
movi d1, #0
movi d2, #0
movi d3, #0
MESSENGER_END_NIL
ret
END_ENTRY _objc_msgSend
_objc_msgSend
函數(shù)可以分解2個(gè)主線
- receiver 為 nil 或者屬于 tagged pointer 類型
- receiver 不為空徘铝,正常查找 IMP
receiver 不為空
首先先分析 receiver 不為空的情況
1. ldr x13, [x0]
2. and x9, x13, #ISA_MASK
3. LGetIsaDone:
CacheLookup NORMAL
x0
當(dāng)前存儲的是 self 指針耳胎,ldr 指令 self 指針?biāo)赶虻膬?nèi)存位置讀取數(shù)據(jù)并保存到x13
寄存器中,這時(shí)候x13
存儲了 isaisa 和 ISA_MASK 做與運(yùn)算惕它,移除掉這些多余的信息得到 Class 并存儲到
x9
開始從 Class 緩存中查找 IMP
CacheLookup 是一個(gè)宏
.macro CacheLookup
// x1 = SEL, x9 = isa
// x9 保存著 objc_class 指針
...
.endmacro
進(jìn)入宏之前怕午,x1
保存了 SEL (ARM寄存器 x0-x7 寄存器是用來傳遞參數(shù)的,objc_msgSend 函數(shù)的前2個(gè)參數(shù)分別是 self 和 _cmd)淹魄,還有之前處理得到的 isa 保存在 x9郁惜。
ldp x10, x11, [x9, #CACHE] // x10 = buckets, x11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)
-
ldp x10, x11, [x9, #CACHE]
:CACHE 是一個(gè)常數(shù)(0x10),以 objc_class 地址為基準(zhǔn)揭北,然后讀 16 字節(jié)的數(shù)據(jù)(可以參考 objc-runtime-new.h objc_class 結(jié)構(gòu)體)扳炬,x10 = buckets,x11 = occupied|mask (高32位:occupied搔体,低32位:mask);mask 代表哈希表的位數(shù)半醉,它的值總是2 - 1的冪疚俱,或者用二進(jìn)制表示就是000000001111111,末尾有一個(gè)可變的1
and w12, w1, w11
:進(jìn)行 AND 運(yùn)算缩多,得到選擇子的查詢索引add x12, x10, x12, LSL #4
:x12
左移4位也就是乘以16呆奕,這是因?yàn)槊總€(gè)哈希表的 bucket 是 16 字節(jié),計(jì)算得出要搜索位置的 第一個(gè) bucket 的地址并保存在x12
中
ldp x16, x17, [x12] // {x16, x17} = *bucket
1: cmp x16, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->cls == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x16, x17, [x12, #-16]!
b 1b // loop
3: // wrap: x12 = first bucket, w11 = mask
add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)
ldp x16, x17, [x12]
: 從 bucket 指針指向的內(nèi)存地址讀取數(shù)據(jù)衬吆,x16
存儲要查找 bucket 中的 key(選擇子)梁钾,x17
存儲了 IMP-
cmp x16, x1
: 判斷第一個(gè) bucket 中的 sel 跟參數(shù) _cmd 是否相同- 相同: 跳轉(zhuǎn)到 CacheHit 繼續(xù)執(zhí)行,改標(biāo)簽中會執(zhí)行指令
br x17
逊抡,也就是執(zhí)行 IMP - 不相同: 跳轉(zhuǎn)到 CheckMiss 繼續(xù)執(zhí)行
cbz x16, __objc_msgSend_uncached_impcache
姆泻,cbz 指令比較寄存器值是否等于0,如果是0則跳轉(zhuǎn)冒嫡;這里x16
中記錄了從 bucket 加載到的選擇子拇勃。首先先將其與 0 進(jìn)行比較,如果等于 0 則會跳轉(zhuǎn)至 C 函數(shù)__objc_msgSend_uncached_impcache
進(jìn)行更復(fù)雜的查找
- 相同: 跳轉(zhuǎn)到 CacheHit 繼續(xù)執(zhí)行,改標(biāo)簽中會執(zhí)行指令
cmp x12, x10
: 判斷當(dāng)前的 bucket 指針是不是和數(shù)組 buckets 指針相同孝凌,相同則說明在列表頭-
判斷當(dāng)前 bucket 的位置:
-
如果 bucket == buckets方咆,則把指針指向 buckets 列表尾
b.eq 3f add x12, x12, w11, UXTW #4 ldp x16, x17, [x12]
cmp 指令執(zhí)行之后如果,如果 x12 - x10 == 0蟀架,csrp 寄存器 Z 標(biāo)志位置位1瓣赂,反之為0榆骚。
b.eq 當(dāng) Z 標(biāo)志位為 1,跳轉(zhuǎn)到 3f煌集,執(zhí)行
add x12, x12, w11, UXTW #4
x12 存儲了 buckets 指針寨躁,指向了第一個(gè) bucket,w11 是存儲表的掩碼牙勘,描述了表的大小职恳,相加之后當(dāng)前指針指向最后一個(gè) bucket
-
如果 bucket != buckets,
ldp x16, x17, [x12, #-16]! b 1b // loop
x12-16 獲取新的 bucket 地址并重新寫入到 x12 中 (!符號代表寄存器回寫)方面,指向前一個(gè) bucket放钦,
x16
存儲要查找 bucket 中的 key(選擇子 ,x17
存儲了 IMP恭金,然后重復(fù)之前的步驟
-
接著上面的4.1步驟操禀,bucket 指針指向 buckets 列表尾
1: cmp x16, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->cls == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x16, x17, [x12, #-16]! // {x16, x17} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
- 判斷當(dāng)前 bucket 的選擇子和傳入?yún)?shù) _cmd 是否相同,相同則跳轉(zhuǎn)到 CacheHit 執(zhí)行對應(yīng)的 IMP横腿,不相同則往下走
- 執(zhí)行 CheckMiss 宏判斷 bucket 的選擇子是否為空颓屑,若未空跳轉(zhuǎn)執(zhí)行
__objc_msgSend_uncached_impcache
C 函數(shù) -
cmp x12, x10
,檢查是否在 buckets 表頭循環(huán)搜索完 或者 是hash碰撞耿焊,如果是則跳轉(zhuǎn)到 JumpMiss揪惦,最終會執(zhí)行__objc_msgSend_uncached_impcache
函數(shù)執(zhí)行,進(jìn)行更復(fù)雜的查找 - 若步驟3不成立罗侯,則 bucket 指針前移器腋,重復(fù)1-3的步驟
recever 不為空的情況下, objc_msgSend
全部過程分析到此完畢
receiver 等于 nil
LNilOrTagged: // 執(zhí)行到這里說明 self 的值小于等于 0钩杰。小于零則代表為 Tagged Pointer 情況纫塌,等于說明為 nil
b.eq LReturnZero // nil check,判斷 self 是否為 nil
objc_msgSend
開始會將利用 cmp
指令將 receiver 和 0 做比較讲弄,若結(jié)果是小于等于0則會跳轉(zhuǎn)到 LNilOrTagged 執(zhí)行措左。
若 receiver == 0,則跳轉(zhuǎn)到 LReturnZero
LReturnZero:
// x0 is already zero
// 因?yàn)榻鼇碇耙呀?jīng)通過 `cmp x0, #0` 判斷避除,所以 x0 寄存器的值是0
// 整型的返回值保存在 x0 和 x1 中
// 浮點(diǎn)型的返回值會被保存在 v0 到 v3 這幾個(gè)向量寄存器中怎披,
// d0 到 d3這幾個(gè)寄存器是相關(guān)v寄存器的后半部分,向他們存值的時(shí)候會將對應(yīng) v 寄存器的前半部分置 0
mov x1, #0 // 1驹饺、首先先把 x1 清空钳枕,x0 這里是 self,已經(jīng)是0赏壹,所以不需要清空鱼炒,
movi d0, #0 // 2、清空 v 寄存器
movi d1, #0
movi d2, #0
movi d3, #0
MESSENGER_END_NIL
ret
這里先后把整形寄存器和向量寄存器都置為0蝌借,這樣做的好處是:
objc_msgSend
不知道調(diào)用者希望獲得什么類型的返回值昔瞧,是一個(gè)整型指蚁?兩個(gè)组橄?還是浮點(diǎn)類型或是其他類型钳踊?把所有返回值的寄存器都覆蓋為0尚卫,后面調(diào)用者不管是想得到整型還是浮點(diǎn)型褥符,都是0值。
那么如果調(diào)用者需要的返回值類型不是屬于整型/浮點(diǎn)型嵌赠,比如是寄存器不夠存儲的畅买,更大結(jié)構(gòu)的返回值需要調(diào)用者在內(nèi)存中分配合適的內(nèi)存空間并把內(nèi)存地址傳入 x8
同仆,函數(shù)通過寫入這塊內(nèi)存來返回值混巧。
objc_msgSend
執(zhí)行過程中并不知道 x8
內(nèi)存枪向,所以在 LReturnZero 中并沒有清除內(nèi)存。解決辦法是編譯器生成代碼會 objc_msgSend
執(zhí)行前用0填滿這塊內(nèi)存咧党。
Tagged pointer 處理
Tagged Pointer
通過在其最后一個(gè) bit 位設(shè)置一個(gè)特殊標(biāo)記秘蛔,用于將數(shù)據(jù)直接保存在指針本身中。具體細(xì)節(jié)可以參考深入理解 Tagged Pointer
// tagged
// 這里加載了 _objc_debug_taggedpointer_classes 的地址傍衡,即 Tagged Pointer 主表
// ARM64 需要兩條指令來加載一個(gè)符號的地址深员。這是 RISC 樣架構(gòu)上的一個(gè)標(biāo)準(zhǔn)技術(shù)。
// AMR64 上的指針是 64 位寬的蛙埂,指令是 32 位寬倦畅。所以一個(gè)指令無法保存一個(gè)完整的指針
adrp x10, _objc_debug_taggedpointer_classes@PAGE // 將頁(前半部分)的基址存在 x10
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF // 將頁(后半部分)的基址存在 x10
ubfx x11, x0, #60, #4 // 它從 self 中的第 60 位開始,提取 4 位箱残,保存到 x11 中滔迈。
ldr x9, [x10, x11, LSL #3] // x9 = x10 + (x11<<3)
b LGetIsaDone
- 通過
adrp
指令計(jì)算 _objc_debug_taggedpointer_classes 表(存儲可用的 Tagged Pointer 的類)的數(shù)據(jù)地址到當(dāng)前pc寄存器值相對偏移。 - AMR64 上的指針是 64 位寬的被辑,指令是 32 位寬。所以一個(gè)指令無法保存一個(gè)完整的指針敬惦,于是還要通過
add
指令把后半部分讀取存儲到x10
中 -
ubfx
指令讀取x11
中最后4位數(shù)據(jù)盼理,也就是 Tagged pointer 表所以標(biāo)志 -
x9 = x10 + (x11<<3)
,這里通過 x11 里的索引到 x10 所指向的 Tagged pointer 表中查找具體的 Tagged pointer 類 - 獲取到 isa 之后進(jìn)行 CacheLookup 步驟
自此 objc_msgSend
過程已經(jīng)全部分析完俄删,整個(gè)流程可以用下圖表示:
[圖片上傳失敗...(image-e392dd-1563026243771)]
為什么要用匯編實(shí)現(xiàn)
objc_msgSend
函數(shù)實(shí)現(xiàn)并不是用 Objective-C宏怔、C 或者 C++ 實(shí)現(xiàn)的,而是利用匯編語言開發(fā)畴椰。
那為什么會采用匯編語言實(shí)現(xiàn)呢臊诊?首先看一個(gè)例子:
NSUInteger n = [array count];
id obj = [array objectAtIndex:1];
我們可以理解上面2行代碼編譯時(shí)期會轉(zhuǎn)化為:
NSUInteger n = objc_msgSend(array, @selector(count));
id obj = objc_msgSend(array, @selector(objectAtIndex:), 1);
假設(shè) objc_msgSend
是 C 或者 C++ 實(shí)現(xiàn)的,這里不可能編譯成功斜脂,因?yàn)榉祷刂狄膊荒芡瑫r(shí)是 NSUInteger
和 id
抓艳;這里可以使用類型強(qiáng)制轉(zhuǎn)化來解決:
NSUInteger n = (NSUInteger (*)(id, SEL))objc_msgSend(array, @selector(count));
id obj = (id (*)(id, SEL, NSUInteger))objc_msgSend(array, @selector(objectAtIndex:), 6);
從例子上可以看的出,objc_msgSend
有2個(gè)特點(diǎn):
- 可以調(diào)用任意參數(shù)類型帚戳、數(shù)量的任意函數(shù)
- 支持不同類型的返回值
對于特點(diǎn)1玷或,調(diào)用 objc_msgSend 的之前儡首,棧幀(stack frame)的狀態(tài)、數(shù)據(jù)偏友,和各個(gè)寄存器的組合形式蔬胯、數(shù)據(jù),跟調(diào)用具體的函數(shù)指針(IMP)時(shí)所需的狀態(tài)位他、數(shù)據(jù)氛濒,是完全一致的。
基于這個(gè)前提鹅髓,遍歷并找到 IMP 之后舞竿,只要所有的對棧、寄存器的操作回復(fù)到調(diào)用 objc_msgSend 之前的狀態(tài)迈勋,通過 jump/call 指令執(zhí)行函數(shù)即可炬灭。
在 ARM 上,IMP 函數(shù)執(zhí)行完靡菇, r0
寄存器會保存其返回值重归,能滿足其返回不同類型返回值的需求
參考文章: