objc_msgSend()汇编核心探索(arm64架构)

寄存器对应须知:
函数参数寄存器(x0,x1,x2,x3,x4,x5,x6,x7)
p0~p17 --->  x0~x17

id objc_msgSend(id self, SEL _cmd,...)汇编实现

    ENTRY _objc_msgSend    
    UNWIND _objc_msgSend, NoFrame   

//(1)`GetIsaFast `  
    cmp p0, #0          // nil check and tagged pointer check ;ZF = p0-0
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //p0 <= 0(有符号数的比较) ,说明receiver要么为nil,要么就是个Tagged Pointer
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = a raw isa  x0:实例对象的地址,[x0]:取8字节,那不就取到raw isa了嘛!
    GetClassFromIsa_p16 p13     // p16 = isa :p13为a packed isa,需要进行class提取获得实例对象self的所属类。

//(2)`CacheLookup NORMAL, _objc_msgSend`
LGetIsaDone: 
    // calls imp or objc_msgSend_uncached   
    CacheLookup NORMAL, _objc_msgSend

//(3)`LNilOrTagged `
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     //nil check:p0为nil,即receiver为nil,直接返回0结束objc_msgSend()的执行。
    
    // tagged   (系统定义的内部标签类)
    // 这里加载了 _objc_debug_taggedpointer_classes 的地址,即 Tagged Pointer 主表
    // ARM64 需要两条指令来加载一个符号的地址。这是 RISC 架构上的一个标准技术。
    // AMR64 上的指针是 64 位宽的,指令是 32 位宽。所以一个指令无法保存一个完整的指针
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE       // 将页(前半部分)的基址存入 x10
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF   // 将页(后半部分)的基址存入 x10
    ubfx    x11, x0, #60, #4       //从x0的第60位开始连取4位(即最后4位数据,代表具体类的“tag_slot”),存放到x11中。
    ldr x16, [x10, x11, LSL #3]    // x16 = [x10+(x11<<3)],到x10所指向的Tagged Pointer表中查找具体的类!
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone     //如果不想等,说明是系统定义的标签类;否则,就是系统外部定义的标签类。

    // ext tagged  相等: (外部标签类:非系统标签类)
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE     //ext:外部的(标签类们)
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8        //连取8位(52~59)tag_slot
    ldr x16, [x10, x11, LSL #3]     //class = classes[tag_slot]
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

//综合返回值:objc_msgSend函数的综合返回值,可能是整型(用x0和x1),也可能是浮点型(用v0,v1,v2,v3)
LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0      //浮点型寄存器v0的低位,d0为0则整个v0都为0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

    END_ENTRY _objc_msgSend

其汇编实现等价于C语言实现:

id objc_msgSend(id self, SEL _cmd,...){
    (1)`NilTest`:对receiver(即self)进行非空测试。
    if (!self) return;
    
    (2)`LNilOrTagged`:receiver非空,获取其类cls的地址。
    Class cls = self->getIsa();
    
    (3)`CacheLookup NORMAL, _objc_msgSend`:遍历cls的缓存查找_cmd,如命中则返回其imp;未命中则开启慢查找。
    IMP imp = CacheLookup(cls,_cmd);
    if(imp) {
        TailCallCachedImp imp, &imp, _cmd, cls; //汇编语句,arm64缓存查找的汇编显示只走到了这里。
    } else {
        (4) `__objc_msgSend_uncached` ---> `MethodTableLookup`---> 
        `lookUpImpOrForward(self, _cmd, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);`:缓存未命中,开启慢查找 ---> 方法列表查找
        imp = MethodTableLookup(cls,_cmd);
        TailCallFunctionPointer imp;  //汇编语句,arm64慢查找的汇编显示只走到了这里。
    }
    return 0;    
}

注意:这里CacheLookupTailCallCachedImpMethodTableLookup
TailCallFunctionPointer皆为汇编宏。

其中,TailCallCachedImp(CacheHit时)为:

.macro TailCallCachedImp    //“尾部调用缓存IMP”:CacheHit-缓存命中
    // $0 = cached imp, $1 = address of cached imp, $2 = _cmd, $3 = cls
    eor $1, $1, $2  // mix SEL into ptrauth modifier
    eor $1, $1, $3  // mix isa into ptrauth modifier
    brab    $0, $1
.endmacro

TailCallFunctionPointer(慢查找命中时)为:

.macro TailCallFunctionPointer   //“尾部调用函数指针”:CheckMiss-MethodTableHit(imp or forward_imp)
    // $0 = function pointer value  $0可能为真imp,亦可能为forward_imp
    braaz   $0                     
.endmacro
(1)GetIsaFast+LNilOrTagged的汇编实现
//(1)`GetIsaFast `  
    cmp p0, #0          // nil check and tagged pointer check ;ZF = p0-0
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //p0 <= 0(有符号数的比较) ,说明receiver要么为nil,要么就是个Tagged Pointer
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = a raw isa  x0:实例对象的地址,[x0]:取8字节,那不就取到raw isa了嘛!
    GetClassFromIsa_p16 p13     // p16 = isa :p13为a packed isa,需要进行class提取获得实例对象self的所属类。

//(3)`LNilOrTagged `
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     //nil check:p0为nil,即receiver为nil,直接返回0结束objc_msgSend()的执行。
    
    // tagged   (系统定义的内部标签类)
    // 这里加载了 _objc_debug_taggedpointer_classes 的地址,即 Tagged Pointer 主表
    // ARM64 需要两条指令来加载一个符号的地址。这是 RISC 架构上的一个标准技术。
    // AMR64 上的指针是 64 位宽的,指令是 32 位宽。所以一个指令无法保存一个完整的指针
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE       // 将页(前半部分)的基址存入 x10
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF   // 将页(后半部分)的基址存入 x10
    ubfx    x11, x0, #60, #4       //从x0的第60位开始连取4位(即最后4位数据,代表具体类的“tag_slot”),存放到x11中。
    ldr x16, [x10, x11, LSL #3]    // x16 = [x10+(x11<<3)],到x10所指向的Tagged Pointer表中查找具体的类!
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone     //如果不想等,说明是系统定义的标签类;否则,就是系统外部定义的标签类。

    // ext tagged  相等: (外部标签类:非系统标签类)
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE     //ext:外部的(标签类们)
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8        //连取8位(52~59)tag_slot
    ldr x16, [x10, x11, LSL #3]     //class = classes[tag_slot]
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

以上汇编等价于Runtime C内联函数:

inline Class objc_object::getIsa() 
{
    //非TaggedPointer对象,直接获取其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)LGetIsaDone,即CacheLookup NORMAL, _objc_msgSend的汇编实现
// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = _cmd, x16 = cls
.macro CacheHit   //命中则发起调用
.if $0 == NORMAL    //Tail:尾部;尾巴
    TailCallCachedImp x17, x12, x1, x16 //authenticate(证实...是真实的) and call imp
.elseif $0 == GETIMP    //THINK:一个新类,首次调用了一个父类已缓存但是为forward_imp的方法sel
    mov p0, p17
    cbz p0, 9f          // don't ptrauth a nil imp  缓存中为何会存在sel的imp为nil的情况?【待定。。。】
    AuthAndResignAsIMP x0, x12, x1, x16 //authenticate imp and re-sign(签名) as IMP 认证并重新签名IMP
9:  ret             // return IMP       直接结束缓存查找并返imp为nil(p0)
.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.
    AuthAndResignAsIMP x17, x12, x1, x16    // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.else
.abort oops     //$0为非法值
.endif
.endmacro

//p9:从缓存bucket中取出的sel的值。
//进行未命中检查:p9是否为0?(未命中可以,但是sel不能为0!否则就直接宣布Cache中没有进行缓存了!)
//为0就意味着对应的bucket为nil,之前未插入缓存,说明未缓存!那就别再往下找了,缓存中没有!只能去方法列表中查找了!
.macro CheckMiss    //未命中则进入MethodTable中查询!
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached     //cbz指令:CBZ R, label,R为0则跳转至label处执行;否则不跳转。
.elseif $0 == LOOKUP                    //cbnz指令:CBNZ R, label R为非0则跳转至label处执行;否则不跳转。
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

.macro JumpMiss
.if $0 == GETIMP
    b   LGetImpMiss
.elseif $0 == NORMAL
    b   __objc_msgSend_uncached
.elseif $0 == LOOKUP
    b   __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

.macro CacheLookup
LLookupStart$1:

    // p1 = SEL, p16 = cls
    ldr p11, [x16, #CACHE]  // p11 = mask|buckets   mask(16) 0000 buckets(44)
                            //(arm64 && LP64)下,mask占据高16位,buckets占据低48位(高4位为0!)
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets 用#0x0000 0ffffffffff也可以
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask ,x12为_cmd位于buckets中的插入起点下标begin。
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4    //(arm64 && !LP64)下
    and p10, p11, #~0xf         // p10 = buckets
    and p11, p11, #0xf          // p11 = maskShift
    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.        //!arm64下
#endif

    //p10:buckets, p12:begin。
    add p12, p10, p12, LSL #(1+PTRSHIFT)    //PTRSHIFT为3,左移四位是因为每个bucket占16字节
                // p12 = buckets + (begin << (1+PTRSHIFT)):_cmd所应该在缓存中的第一个位置地址!
    //p12:第一个bucket的地址。
    ldp p17, p9, [x12]  // 从x12处连续读区两个8字节,imp--->p17、sel--->p9:arm64与x86_64在这点上是相反的。
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0        // miss if bucket->sel == 0 如果sel为0,直接就未命中(miss)!
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f  //相等则说明当前的selector在Cache中的起始下标为0!
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket 逆序查找!(x12取完后被赋了新值!)
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask  ;p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                // p12 = buckets + (mask << 1+PTRSHIFT) 重置p12为Cache中【最后一个】bucket的地址!
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p12, p12, p11, LSL #(1+PTRSHIFT)
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp           有值相等:命中
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0        // miss if bucket->sel == 0     无值为0:未命中
    cmp p12, p10        // wrap if bucket == buckets    有值不等:下一个
    b.eq    3f  //Cache中必存在sel==0的bucket,所以缓存查找至多一圈(只有capacity为4时才会查一圈)!
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

LLookupEnd$1:       //一般情况下来不到这里!因为Cache中必存在sel==0的bucket,所以缓存查找至多一圈(含最后一个)。
LLookupRecover$1:
3:  // double wrap     第二次遍历到buckets起点的bucket,一般情况下不会出现这种情况!
    JumpMiss $0         //什么情况下才会来到这???强制进来时?【待定。。。】

.endmacro

以上汇编等价于C函数:

IMP CacheLookup(Class cls, SEL _cmd){
    cache_t cache = cls->cache;
    uintptr_t mab = cache._maskAndBuckets;
    mask_t mask = mab >> 48;  
    bucket_t buckets = (mab <<16) >>16;
    int bagin = _cmd & mask;    //如果_cmd被缓存,那么它会先尝试放在buckets中的第bagin个位置。具体可看缓存的核心方法:void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
    
    bucket_t bucket = buckets[bagin];
    while (_cmd != bucket.sel) {
        if (!bucket.sel) {
            return (IMP)0;     //直接返回0。实际上是进行`__objc_msgSend_uncached`,只不过这里单独列到了下一步。
        } else if (bucket == buckets) {    //第一次来到了起点,让bucket指向终点。
            bucket = buckets[mask];
        } else {
            bucket--;      //递减遍历
        }
    }
    return bucket.imp;
}

附加:
__objc_msgSend_uncached:CheckMiss时

//消息未缓存:Cache中之前未进行该sel的插入操作(即之前没调用过该方法或建立了新的缓存空间后没调用过!)
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search

MethodTableLookup   //去方法列表中查找吧!找到的imp放入x17中。
TailCallFunctionPointer x17    //找到imp后触发`TailCallFunctionPointer `

END_ENTRY __objc_msgSend_uncached

MethodTableLookup:开启慢查找、解析、转发流程

.macro MethodTableLookup
    
    // push frame
    SignLR
    stp fp, lr, [sp, #-16]!     //压栈:将帧指针寄存器fp和链接寄存器lr压入栈中,然后sp = sp-16!
    mov fp, sp                  //将fp指向栈底(成了基指针)

    // save parameter registers: x0..x8(8字节), q0..q7(16字节)
    sub sp, sp, #(10*8 + 8*16) //由高到低顺序:【】x8 x7 x6 x5 x4 x3 x2 x1 x0 q7 q6 q5 q4 q3 q2 q1 q0
    stp q0, q1, [sp, #(0*16)]
    stp q2, q3, [sp, #(2*16)]
    stp q4, q5, [sp, #(4*16)]
    stp q6, q7, [sp, #(6*16)]
    stp x0, x1, [sp, #(8*16+0*8)]
    stp x2, x3, [sp, #(8*16+2*8)]
    stp x4, x5, [sp, #(8*16+4*8)]
    stp x6, x7, [sp, #(8*16+6*8)]
    str x8,     [sp, #(8*16+8*8)]
                        //(x0,  x1,  x2,                    x3)
    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    mov x2, x16     //x16:类的地址cls(obj的isa)
    mov x3, #3
    bl  _lookUpImpOrForward //带返回值(可返回继续执行)的跳转:跳转之前,将指令`mov x17, x0`的地址送入lr中。

    // IMP in x0    imp可能为真,也可能为forward_imp(即(IMP)_objc_msgForward_impcache)
    mov x17, x0     //子程序`_lookUpImpOrForward`调用结束并ret,返回值位于x0当中。
    
    // restore registers and return 子程序`MethodTableLookup`调用结束,返回前恢复子程序调用前的寄存器状态!
    ldp q0, q1, [sp, #(0*16)]
    ldp q2, q3, [sp, #(2*16)]
    ldp q4, q5, [sp, #(4*16)]
    ldp q6, q7, [sp, #(6*16)]
    ldp x0, x1, [sp, #(8*16+0*8)]
    ldp x2, x3, [sp, #(8*16+2*8)]
    ldp x4, x5, [sp, #(8*16+4*8)]
    ldp x6, x7, [sp, #(8*16+6*8)]
    ldr x8,     [sp, #(8*16+8*8)]

    mov sp, fp
    ldp fp, lr, [sp], #16   //恢复fp和lr,并重置sp = sp +16;
    AuthenticateLR

.endmacro

总结:arm64x86_64关于objc_msgSend()汇编实现的不同之处主要在于缓存方面:

(1)缓存遍历方式:arm64为递减遍历,遍历至第0个后接着从最后一个往前遍历;x86_64为递增遍历,遍历至最后一个后接着从第0个往后遍历;这实际上与它们缓存时的顺序是一致的。
(2)缓存临界值:arm64可以存储3/4capacity个方法,总有1/4的空间不使用;x86_64可以存储3/4capacity-1个方法,也总有1/4的空间不使用(减1是因为x86_64有一个end Marker)。从而导致同样容量的缓存空间,x86_64上界总比arm64少一个。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容