objc_msgSend()匯編核心探索(x86_64架構(gòu))

寄存器對(duì)應(yīng)須知:
函數(shù)參數(shù)寄存器(%rdi, %rsi, %rdx, %rcx, %r8, %r9)
  64位   32位    16位  8位
  %rax   %eax   %ax   %al
  %rdi   %edi   %di   %dil
  %r8    %r8d   %r8w  %r8b

#define a1  rdi //64位
#define a1d edi     //32位
#define a1b dil     //8位
#define a2  rsi //64位
#define a2d esi     //32位
#define a2b sil     //8位
#define a3  rdx //64位
#define a3d edx     //32位
#define a4  rcx //64位
#define a4d ecx     //32位
#define a5  r8  //64位
#define a5d r8d     //32位
#define a6  r9  //64位
#define a6d r9d     //32位

id objc_msgSend(id self, SEL _cmd,...)匯編實(shí)現(xiàn)

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    NilTest NORMAL      (1)receiver為nil則直接ZeroReturn(帶ret指令);否則繼續(xù)往下走。

    GetIsaFast NORMAL   (2)r10 = self->isa  不為nil則獲取receiver的isa,即所屬類cls的地址

    (3) calls IMP on success      OR    LCacheMiss_objc_msgSend 要么執(zhí)行imp,要么緩存未命中跳至LCacheMiss_objc_msgSend執(zhí)行
    CacheLookup NORMAL, CALL, _objc_msgSend

    (4)返回前清空%rax和%rdx等返回型寄存器:因?yàn)檎瓦\(yùn)算至多使用這兩個(gè)寄存器存放返回值。清空后函數(shù)就返回了。
    //    那為什么要清空呢?因?yàn)開(kāi)objc_msgSend是imp查找、執(zhí)行流,不需要返回值。
    NilTestReturnZero NORMAL    //ZeroReturn(帶ret指令)

    GetIsaSupport NORMAL  (5)這條語(yǔ)句似乎不會(huì)執(zhí)行啊。。。

//cache miss: go search the method lists
LCacheMiss_objc_msgSend:
    // isa still in r10
    jmp __objc_msgSend_uncached  //進(jìn)入慢速查找:Method Lists ---> lookupImpOrForward

    END_ENTRY _objc_msgSend

其匯編實(shí)現(xiàn)等價(jià)于C語(yǔ)言實(shí)現(xiàn):

id objc_msgSend(id self, SEL _cmd,...){
    (1)`NilTest NORMAL`:對(duì)receiver(即self)進(jìn)行非空測(cè)試。
    if (!self) return;
    
    (2)`GetIsaFast NORMAL`:receiver非空,獲取其類cls的地址。
    Class cls = self->getIsa();
    
    (3)`CacheLookup NORMAL, CALL, _objc_msgSend`:遍歷cls的緩存查找_cmd,如命中則返回其Imp;未命中則開(kāi)啟慢查找。
    IMP imp = CacheLookup(cls,_cmd);
    if (!imp) {
        (4)`LCacheMiss_objc_msgSend` ---> `__objc_msgSend_uncached` ---> `MethodTableLookup NORMAL`---> 
           `lookUpImpOrForward(self, _cmd, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);`:緩存未命中,開(kāi)啟慢查找 ---> 方法列表查找
        imp = MethodTableLookup(cls,_cmd);
    }
    jmp *imp;   //匯編語(yǔ)句:執(zhí)行imp。

    return 0;
}

注意:
(0)匯編“函數(shù)”與C/OC函數(shù)的互調(diào):OC代碼調(diào)用匯編“函數(shù)”時(shí)會(huì)給函數(shù)加上一個(gè)_,如objc_msgSend ---> _objc_msgSend_objc_msgSend_uncached ---> __objc_msgSend_uncached,lookupImpOrForward ---> _lookupImpOrForward等;反過(guò)來(lái)會(huì)去掉_,只是作以區(qū)分,知道就好。
(1)其中CacheLookup()為表意函數(shù),即假函數(shù);MethodTableLookup()也為表意函數(shù),但MethodTableLookup同時(shí)也為匯編宏;getIsa()為真函數(shù)。
(2)總的來(lái)說(shuō),objc_msgSend()imp查找、執(zhí)行函數(shù)。它的返回值存放在了%rax/%rdx%xmm0/%xmm1中,只不過(guò)都被設(shè)置為了0值,因?yàn)橐话銢](méi)人需要它的返回值。

Q1:objc_msgSend()匯編實(shí)現(xiàn)中的GetIsaSupport NORMAL似乎不會(huì)執(zhí)行啊,沒(méi)看懂。

對(duì)匯編的執(zhí)行流程不甚了解,真誠(chéng)求答。

Q2:對(duì)最后拿到的imp,arm64和x86_64為什么會(huì)有不同的處理方式?

arm64對(duì)最后的imp作了分類處里,而x86_64下則都是直接執(zhí)行imp,難道慢查找返回的imp都是真的sel的imp嗎?會(huì)不會(huì)是占位用的_objc_msgForward_impcache?
待回答。。。

(1)GetIsaFast NORMAL的匯編實(shí)現(xiàn)
.macro GetIsaFast   //快速獲取Isa:receiver不是Tagged Pointer Object
.if $0 != STRET     //對(duì)x86_64而言,LSB最低有效位為T(mén)agged Pointer bit。
    testb   $$1, %a1b  //Z=(%a1b & 1)?1:0,receiver最低位為1?是就是Tagged Pointer,否則就是普通對(duì)象
    PN
    jnz LGetIsaSlow_f       //Z != 0:即為1,說(shuō)明receiver為T(mén)agged Pointer Object
    movq    $$ ISA_MASK, %r10   //Z  = 0:將ISA_MASK:0x00007ffffffffff8ULL賦值給%r10
    andq    (%a1), %r10 //%r10 = %r10 & isa, %r10成了類地址(常識(shí):打印一個(gè)對(duì)象就是打印對(duì)象地址)
.else
    testb   $$1, %a2b
    PN
    jnz LGetIsaSlow_f
    movq    $$ ISA_MASK, %r10
    andq    (%a2), %r10
.endif
LGetIsaDone:
.endmacro

//x86_64 Tagged Pointer Object的存儲(chǔ)和表示:receiver的最低有效位LSB為1。
//(1)basic tagged:receiver的低4位(含LSB)為slot,去_objc_debug_taggedpointer_classes中找isa;
//(2)extended tagged:receiver的第4~11位為slot,去_objc_debug_taggedpointer_ext_classes中找isa;
.macro GetIsaSupport    //慢速獲取Isa:receiver是Tagged Pointer Object
LGetIsaSlow:
.if $0 != STRET
    movl    %a1d, %r11d //傳送receiver的低4字節(jié)至%r11d
.else
    movl    %a2d, %r11d
.endif
    andl    $$0xF, %r11d    //%r11d = %r11d & 0xF   獲取receiver的低4位(basic tagged slot值)
    // basic tagged
    leaq    _objc_debug_taggedpointer_classes(%rip), %r10   //%rip:存放當(dāng)前CPU正在執(zhí)行的指令的地址
    movq    (%r10, %r11, 8), %r10 // read isa from table 從basic tagged表中讀取receiver對(duì)應(yīng)的isa
    leaq    _OBJC_CLASS_$___NSUnrecognizedTaggedPointer(%rip), %r11 //slot為7(15)時(shí)對(duì)應(yīng)的保留類。
    cmp %r10, %r11
    jne LGetIsaDone_b   //cmp結(jié)果不為0:說(shuō)明%r10即為receiver對(duì)應(yīng)的isa!

    // extended tagged    否則,%r10不是一個(gè)真正的類!需要從extended tagged表中讀取receiver對(duì)應(yīng)的isa。
.if $0 != STRET
    movl    %a1d, %r11d //傳送receiver的低4字節(jié)至%r11d
.else
    movl    %a2d, %r11d
.endif
    shrl    $$4, %r11d  //%r11d符號(hào)右移4位(去掉basic tagged的slot)
    andl    $$0xFF, %r11d   //獲取receiver的第4~11(8位)的值(extended tagged slot)
    leaq    _objc_debug_taggedpointer_ext_classes(%rip), %r10
    movq    (%r10, %r11, 8), %r10   // read isa from table 從extended表中讀取receiver對(duì)應(yīng)的isa
    jmp LGetIsaDone_b   //不需要再比較了,因?yàn)闆](méi)有其他情況了!%r10即為receiver對(duì)應(yīng)的isa!
.endmacro

以上匯編等價(jià)于Runtime C內(nèi)聯(lián)函數(shù):

inline Class objc_object::getIsa() 
{
    //非TaggedPointer對(duì)象,直接獲取其isa
    if (fastpath(!isTaggedPointer())) return ISA();

    extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
    uintptr_t slot, ptr = (uintptr_t)this;
    Class cls;

    slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    // basic tagged
    cls = objc_tag_classes[slot];    
    if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
        slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        // extended tagged
        cls = objc_tag_ext_classes[slot];  
    }
    return cls;
}
(2)CacheLookup NORMAL, CALL, _objc_msgSend
.macro CacheHit

    // r11 = found bucket!
    
.if $1 == GETIMP    //一級(jí)
    movq    cached_imp(%r11), %rax  // return imp
    cmpq    $$0, %rax
    jz  9f          // don't xor a nil imp
    xorq    %r10, %rax      // xor the isa with the imp
9:  ret

.else

.if $1 == CALL      //一級(jí):objc_msgSend Normal,call  %r10為類地址,%r11為_(kāi)cmd在buckets中的地址。
    movq    cached_imp(%r11), %r11  // load imp  拿到了imp!
    xorq    %r10, %r11          // xor imp and isa  應(yīng)該是反混淆
.if $0 != STRET //二級(jí)
    // ne already set for forwarding by `xor`
.else
    cmp %r11, %r11      // set eq for stret forwarding
.endif
    jmp *%r11           // call imp  ?。?!【最終直接發(fā)起調(diào)用】!??!

.elseif $1 == LOOKUP    //一級(jí)
    movq    cached_imp(%r11), %r11
    xorq    %r10, %r11      // return imp ^ isa
    ret
    
.else
.abort oops
.endif

.endif      //這個(gè)endif貌似有點(diǎn)多余啊,它結(jié)束的是誰(shuí)???

.endmacro


.macro  CacheLookup //實(shí)際上整個(gè)緩存至多遍歷一圈!因?yàn)閟el的值為0時(shí)便會(huì)直接跳至LCacheMiss_objc_msgSend!
                    //而這樣的情況總是會(huì)出現(xiàn)!因?yàn)閷?duì)x86_64而言,緩存達(dá)3/4便會(huì)擴(kuò)容!詳情cache_t::insert()函數(shù)。

LLookupStart$2:

.if $0 != STRET
    movq    %a2, %r11       // r11 = _cmd   即sel(一個(gè)地址值)
.else
    movq    %a3, %r11       // r11 = _cmd
.endif               //小字節(jié)序:isa -> super -> cache -> (mask/_flags/_occupied)
    andl    24(%r10), %r11d  //r11 = r11 & class->cache.mask 獲取cache中的mask并執(zhí)行位&得到begin
    shlq    $$4, %r11       // r11 = offset = r11<<4 每個(gè)bucket占16字節(jié)
    addq    16(%r10), %r11      // r11 = class->cache.buckets + offset  sel的begin的地址。
            // 匯編代碼下,是沒(méi)有數(shù)據(jù)類型的概念的,指針只是一個(gè)數(shù)字,加1就是加1,而不是加“一個(gè)單位”!
.if $0 != STRET
    cmpq    cached_sel(%r11), %a2   // if (bucket->sel != _cmd) cached_sel為0,cached_imp為8。
.else
    cmpq    cached_sel(%r11), %a3   // if (bucket->sel != _cmd)
.endif
    jne     1f          // scan more _cmd的地址與緩存中起始位置處的sel不相等!需繼續(xù)掃描!
    CacheHit $0, $1         // call or return imp   //相等則為緩存命中

1:
    // loop     當(dāng)前bucket是否為cache的最后一個(gè)bucket:
    cmpq    $$1, cached_sel(%r11) //x86_64 Cache中的最后一個(gè)bucket的sel被置為1,imp指向第一個(gè)bucket
    jbe 3f  //YES(be:小于或等于)! if (bucket->sel <= 1) wrap(包裹) or miss(未擊中;錯(cuò)過(guò))

    addq    $$16, %r11      //NO, bucket++
2:  
.if $0 != STRET
    cmpq    cached_sel(%r11), %a2   // if (bucket->sel != _cmd)
.else
    cmpq    cached_sel(%r11), %a3   // if (bucket->sel != _cmd)
.endif
    jne     1b          //     scan more    往上跳
    CacheHit $0, $1         // call or return imp

    
3:  // wrap or miss  wrap指的是Cache中的最后一個(gè)bucket“包裹”著第一個(gè)bucket!miss就是未占用的緩存bucket。
    jb  LCacheMiss$2    // if (bucket->sel < 1) cache miss :未占用的緩存bucket={0,0}
                // LCacheMiss$2:LCacheMiss_objc_msgSend

    // wrap  第一次遍歷到了Cache中的最后一個(gè)bucket,尚未遍歷一圈,還有可能緩存命中!請(qǐng)繼續(xù)遍歷cache!
    movq    cached_imp(%r11), %r11  //bucket->imp is really first bucket
                    //bucket->sel == 1:說(shuō)明是最后一個(gè)bucket,其imp指向第一個(gè)bucket
    jmp     2f  //往下跳!

1:
    // loop     為了代碼的通用性,每次都要判斷當(dāng)前bucket是否為最后一個(gè)bucket。
    cmpq    $$1, cached_sel(%r11)   //第二次比較:其主要目的看是否小于1,而不是等于1(因?yàn)椴粫?huì)再次等于1了)
    jbe 3f          // if (bucket->sel <= 1) wrap or miss

    addq    $$16, %r11      // >1:普通的bucket。 bucket++
2:  
.if $0 != STRET
    cmpq    cached_sel(%r11), %a2   // if (bucket->sel != _cmd)
.else
    cmpq    cached_sel(%r11), %a3   // if (bucket->sel != _cmd)
.endif
    jne     1b          //     scan more
    CacheHit $0, $1         // call or return imp

3:
    // double wrap or miss      第二次 wrap or miss 已遍歷一圈了,就不用再遍歷了!
    jmp LCacheMiss$2        //對(duì)于x86_64而言,buckets中至少有一個(gè)bucket={0,0},所以來(lái)到這時(shí),
                    //只意味著bucket={0,0},未命中!
LLookupEnd$2:
.endmacro

以上匯編等價(jià)于C函數(shù):

IMP CacheLookup(Class cls, SEL _cmd){
    cache_t cache = cls->cache;
    bucket_t buckets = cache._buckets;
    int bagin = _cmd & cache.mask;    //如果_cmd被緩存,那么它會(huì)先嘗試放在buckets中的第bagin個(gè)位置。具體可看緩存的核心方法:void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
    
    bucket_t bucket = buckets[bagin];
    while (_cmd != bucket.sel) {
        switch ((uintptr_t)bucket.sel) {
            case 0:     //當(dāng)前bucket為空,直接宣布未命中!
                return nil;       //實(shí)際上為`LCacheMiss_objc_msgSend`,只不過(guò)這里做了分步處理,將其獨(dú)立出來(lái)了。
            case 1:     //當(dāng)前bucket為緩存中的最后一個(gè)bucket,即Mark end bucket;
                bucket = bucket.imp;    //最后一個(gè)bucket的imp為buckets,即緩存的開(kāi)始處。
                break;
            default:
                bucket++;
                break;
        }
    }
    return bucket.imp;
}

注意:
(1)LCacheMiss$2中的$2為匯編調(diào)用中的第3個(gè)參數(shù),前兩個(gè)為$0、$1;這里的$2為匯編調(diào)用者_objc_msgSend,所以當(dāng)緩存命中失敗時(shí)會(huì)調(diào)用LCacheMiss_objc_msgSend,類似字符串拼接,哈哈哈。
(2)對(duì)非arm而言,如x86_64arm64等:cache->buckets中的最后一個(gè)bucketEnd marker(尾標(biāo)記),其bucket = {(SEL)(uintptr_t)1, (IMP)buckets},意味著buckets遍歷到頭了,同時(shí)也可快速繼續(xù)從頭遍歷;End marker可參看Runtime的buckets::allocateBuckets()函數(shù)。
(3)switchcase 0:case 1:的情況至多出現(xiàn)一次!也即cache至多能遍歷一圈(而且還是capacity為4的情況),因?yàn)榫彺嬷锌偸谴嬖?code>{0,0}和{1,buckets}這兩個(gè)bucket,這是由緩存的設(shè)計(jì)機(jī)制所決定的。詳情請(qǐng)看cache_t::insert()函數(shù)(x86_64:緩存達(dá)到3/4capacity -1時(shí),再有方法進(jìn)行緩存時(shí)便會(huì)Double擴(kuò)容;arm64:緩存達(dá)到3/4*capacity時(shí)再有方法進(jìn)行緩存時(shí)便會(huì)Double擴(kuò)容)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容