美文网首页
iOS-底层原理-消息流程objc_msgSend分析之汇编查询

iOS-底层原理-消息流程objc_msgSend分析之汇编查询

作者: 如意神王 | 来源:发表于2022-05-28 17:07 被阅读0次

    本文的主要目的是理解objc_msgSend的方法查找流程,首先查找的是cache缓存而且用汇编实现的,因此称之为快速查找,对应的methoList查询,称之为慢速查找。

    之前的流程分析了cache insert buckets的流程,以及LLDB调试获取buckets的过程,那么objc_msgSend查找cache流程与我们手动LLDB查找非常类似而且原理是一样一样的

    1.Runtime介绍

    runtime称为运行时,它区别于编译时

    运行时代码跑起来,被装载到内存中的过程,如果此时出错,则程序会崩溃,是一个动态阶段

    编译时源代码翻译成机器能识别的代码的过程,主要是对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态的阶段

    runtime的使用有以下三种方式,其三种实现方法与编译层和底层的关系如图所示

    通过OC代码,例如 [person sayNB]

    通过NSObject方法,例如isKindOfClass

    通过Runtime API,例如class_getInstanceSize

    Runtime 三种方式及底层的关系.png

    2.方法的本质

    使用clang编译main.cpp文件,通过查看main函数中方法调用的实现,如下所示

    CJLPerson * person = [CJLPerson alloc];
    [person sayHello];
    
    CJLPerson * person = ((CJLPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CJLPerson"), sel_registerName("alloc"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
    

    通过上述代码可以看出,方法的本质就是objc_msgSend消息发送

    1.objc_msgSend汇编查询 arm64架构下源码入口

    objc_msgSend_arm64.jpeg

    2.objc4-818.2 objc_msgSend 查询cache流程图

    _objc_msgSend汇编流程图.jpg

    3.objc_msgSend 汇编查询cache源码

    1.ENTRY _objc_msgSend,入口,获取isa和class

        ENTRY _objc_msgSend
        UNWIND _objc_msgSend, NoFrame
    
        cmp p0, #0          // nil check and tagged pointer check // 判断receiver是否为空
    #if SUPPORT_TAGGED_POINTERS // 支持小对象
        b.le    LNilOrTagged        //  (MSB tagged pointer looks negative) // 支持小对象调整
    #else
        b.eq    LReturnZero
    #endif
        ldr p13, [x0]       // p13 = isa //x0寄存器获取isa 
        GetClassFromIsa_p16 p13, 1, x0  // p16 = class // 根据isa获取class
    LGetIsaDone: // 获取isa后面的操作流程
        // calls imp or objc_msgSend_uncached
        CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached // 跳转到CacheLookup代码段
    //CacheLookup调用完毕,如果没有CacheHit,则执行这个代码段 __objc_msgSend_uncached 
    #if SUPPORT_TAGGED_POINTERS
    LNilOrTagged:
        b.eq    LReturnZero     // nil check 
        GetTaggedClass 
        b   LGetIsaDone
    // SUPPORT_TAGGED_POINTERS
    #endif
    

    主要有以下几步

    第一,判断objc_msgSend方法的第一个参数receiver是否为空

    第二,是否支撑小对象,如果支持tagged pointer,跳转至LNilOrTagged ---> GetTaggedClass ---> LGetIsaDone
    如果小对象为空,则直接返回空,即LReturnZero

    第三,获取isa,p13 = isa, 获取class,GetClassFromIsa_p16,通过 isa & ISA_MASK 获取shiftcls位域的类信息,即class,p16 = class

    第四,执行标签LGetIsaDone: ---> CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached,进入CacheLookup代码段

    2 .macro CacheLookup 代码段

    .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
        //
        // Restart protocol:
        //
        //   As soon as we're past the LLookupStart\Function label we may have
        //   loaded an invalid cache pointer or mask.
        //
        //   When task_restartable_ranges_synchronize() is called,
        //   (or when a signal hits us) before we're past LLookupEnd\Function,
        //   then our PC will be reset to LLookupRecover\Function which forcefully
        //   jumps to the cache-miss codepath which have the following
        //   requirements:
        //
        //   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
        //
    
        mov x15, x16            // stash the original isa
    LLookupStart\Function:
        // p1 = SEL, p16 = isa
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
        ldr p10, [x16, #CACHE]              // p10 = mask|buckets
        lsr p11, p10, #48           // p11 = mask
        and p10, p10, #0xffffffffffff   // p10 = buckets
        and w12, w1, w11            // x12 = _cmd & mask
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 // 64位真机
        ldr p11, [x16, #CACHE]          // p11 = mask|buckets // x16 平移16字节到cache,16 = isa 8 + superclass 8
    #if CONFIG_USE_PREOPT_CACHES         // p11 = _bucketsAndMaybeMask,即cache的第一个8字节
    #if __has_feature(ptrauth_calls)// #define CLASS  __SIZEOF_POINTER__  #define CACHE (2 * __SIZEOF_POINTER__) --> 2 * 8 = 16
        tbnz    p11, #0, LLookupPreopt\Function
        and p10, p11, #0x0000ffffffffffff   // p10 = buckets  
    #else                                                              
        and p10, p11, #0x0000fffffffffffe   // p10 = buckets
        tbnz    p11, #0, LLookupPreopt\Function
    #endif
        eor p12, p1, p1, LSR #7
        and p12, p12, p11, LSR #48      // x12 = (_cmd ^ (_cmd >> 7)) & mask
    #else // _bucketsAndMaybeMask = mask(高位16) + buckets指针(低48位) p10 = buckets
        and p10, p11, #0x0000ffffffffffff   // p10 = buckets // _bucketsAndMaybeMask & 0x0000ffffffffffff(后48位为1) 
        and p12, p1, p11, LSR #48       // x12 = _cmd & mask // p1 = _cmd,_bucketsAndMaybeMask逻辑右移48位获取到mask
    #endif // CONFIG_USE_PREOPT_CACHES // p12 = _cmd & mask = 初始哈希下标(begin)
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
        ldr p11, [x16, #CACHE]              // p11 = mask|buckets
        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.
    #endif
    // #define PTRSHIFT 3
        add p13, p10, p12, LSL #(1+PTRSHIFT) // p12 逻辑左移4位即扩大16倍,指针平移到对应的bucket位置上
                            // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) // p13 指向哈希下标对应的bucket
    // insert bucket的时候,do-while写入,哈希和二次哈希,读取的时候也是do-while读取cache
                            // do { // p17 = imp, p9 = sel,bucket中imp和sel分别赋给p17和p9
    1:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket-- //赋值完成后p13 -= BUCKET_SIZE,指向前一个bucket
        cmp p9, p1              //     if (sel != _cmd) { //获取的sel和_cmd,如果不相等,调整到3f,
        b.ne    3f              //         scan more
                            //     } else {
    2:  CacheHit \Mode              // hit:    call or return imp  //获取的sel和_cmd,如果相等,缓存命中,call or return imp
                            //     }
    3:  cbz p9, \MissLabelDynamic       //     if (sel == 0) goto Miss; // 如果取出的sel位nil,则goto Miss
        cmp p13, p10            // } while (bucket >= buckets) //如果bucket >= buckets,即没有到最前面
        b.hs    1b // 则,继续比较前一个bucket,如果到最前面继续执行后续代码
    
        // wrap-around:
        //   p10 = first bucket
        //   p11 = mask (and maybe other bits on LP64)
        //   p12 = _cmd & mask
        //
        // A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
        // So stop when we circle back to the first probed bucket
        // rather than when hitting the first bucket again.
        //
        // Note that we might probe the initial bucket twice
        // when the first probed slot is the last entry.
    
    
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
        add p13, p10, w11, UXTW #(1+PTRSHIFT)
                            // p13 = buckets + (mask << 1+PTRSHIFT)
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)) //p11 = _bucketsAndMaybeMask,逻辑右移44位,相当于mask逻辑左移4位
                            // p13 = buckets + (mask << 1+PTRSHIFT) //指向最后的bucket
                            // see comment about maskZeroBits //正常是p11右移48位获取到mask
    // 再左移4位获取到mask指向的bucket,相当于p11右移了44位
    // bucket >= buckets,再次从最后到最前面进行一次do-while循环查找
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
        add p13, p10, p11, LSL #(1+PTRSHIFT)
                            // p13 = buckets + (mask << 1+PTRSHIFT)
    #else
    #error Unsupported cache mask storage for ARM64.
    #endif
        add p12, p10, p12, LSL #(1+PTRSHIFT)
                            // p12 = first probed bucket
    
                            // do {
    4:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket-- // 这里重复1:标签,从mask--->0 查找,从后到前查找
        cmp p9, p1              //     if (sel == _cmd) // 汇编里没有do-while,p13指向最后再重复一次1:标签的逻辑
        b.eq    2b              //         goto hit
        cmp p9, #0              // } while (sel != 0 &&
        ccmp    p13, p12, #0, ne        //     bucket > first_probed)
        b.hi    4b
    
    LLookupEnd\Function:
    LLookupRecover\Function:
        b   \MissLabelDynamic
    
    

    主要分为以下几步

    p1 = SEL , p16 = isa

    第一,获取到指向cache_bucketsAndMaybeMask

    通过p16 = class = isa ,首地址平移16字节(因为在objc_class中,首地址距离cache正好16字节,即isa首地址 占8字节,superClass8字节),获取cahce,p11指向cache中第一个8字节_bucketsAndMaybeMask_bucketsAndMaybeMask中高16位存mask,低48位存buckets_bucketsAndMaybeMask = mask(高位16) + buckets指针(低48位),即p11 = _bucketsAndMaybeMask

    第二,从_bucketsAndMaybeMask中分别取出buckets和mask,并由mask根据哈希算法计算出哈希下标

    p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets
    _bucketsAndMaybeMask >> 48 = mask
    p12 = _cmd & mask = 哈希下标,记作 begin

    将objc_msgSend的参数p1(即第二个参数_cmd)& msak,通过哈希算法,得到需要查找存储sel-imp的bucket下标begin,即p12 = begin = _cmd & mask,为什么通过这种方式呢?因为在存储sel-imp时,也是通过同样哈希算法计算哈希下标进行存储

    第三,根据所得的哈希下标beginbuckets首地址,取出哈希下标对应的bucket

    add p13, p10, p12, LSL #(1+PTRSHIFT)
    #define PTRSHIFT 3

    p12 = begin 逻辑左移4位,即扩大16倍,一个bucket占用16个字节,即sizeof(bucket_t) = 16sel占用8字节,imp占用8字节,p12左移4位就是按照结构体bucket_t步长在移动指针,和alloc16字节对齐算法原理一样
    newX = (x + 15) >> 4,16以下清零,缩小16倍
    newX << 4扩大16倍恢复

    p10 = buckets,首地址,first bucket
    p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    p13 = buckets + begin * 16 ---> 指向begin哈希下标对应的bucket

    根据计算的哈希下标begin 乘以 单个bucket占用的内存大小,得到buckets首地址距离begin下标指向的bucket实际内存中的偏移量
    通过首地址 + 实际偏移量,获取哈希下标begin对应的bucket

    第四,进入do-while循环步骤如下

    第一步,取出selimp
    ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
    首先取出p13指向的当前bucket里面的impselp17 = imp,p9 = sel,赋值完成后p13 -= BUCKET_SIZE,指向前一个bucket

    第二步,p9和_cmd是否相等
    cmp p9, p1
    p9 == p1,缓存命中执行CacheHit
    不相等,执行下面的逻辑

    第三步,p9 == nil ? p9是否为nil
    cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss
    如果p9 == nil,则指向goto Miss,默认没找到,这里忽略了哈希冲突后二次哈希可能导致begin下标和真实写入的index之间存在差异, 而且初始化或扩容后,里面的bucket都是空的sel和imp 都是``nil,直接简单粗暴,p9即指向的sel为nil```,则认为丢失,也是为了更快

    第四步,p9 != nil,判断p13是否 已经执行到最前面了
    cmp p13, p10 // } while (bucket >= buckets)
    如果bucket >= buckets,则跳转到第一步,while循环开始,while (bucket < buckets) while循环结束,依然没有找到,则跳转到最后的bucket,即mask下标所指向的bucket从后到前再次查找一遍

    第五步,begin --> 0,依然没有找到,跳转到最后,mask指向的bucket
    add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
    // p13 = buckets + (mask << 1+PTRSHIFT)

    //p11 = _bucketsAndMaybeMask,逻辑右移44位,相当于mask逻辑左移4位
    // p13 = buckets + (mask << 1+PTRSHIFT)//指向最后的bucket
    正常是p11右移48位获取到mask,再左移4位,相当于_bucketsAndMaybeMask右移44位
    此时p13,指向最后的bucket,while循环,跳转到第一步

    以上流程总结
    第一次do-while循环,从begin ---> 0 查找一遍,如果没命中,p9不为nil,开始第二次do-while循环
    第二次do-while循环,从mask ---> 0再次查找一遍,
    依然如此,则执行__objc_msgSend_uncached ---> MethodTableLookup ---> _lookUpImpOrForward开始查找方法列表

    CacheHit

    // CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
    .macro CacheHit
    .if $0 == NORMAL
        TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp //调用imp
    .elseif $0 == GETIMP
        mov p0, p17
        cbz p0, 9f          // don't ptrauth a nil imp
        AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
    9:  ret             // return IMP //返回imp
    .elseif $0 == LOOKUP // 执行__objc_msgSend_uncached,开始方法列表查找
        // 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, x10, x1, x16    // authenticate imp and re-sign as IMP
        cmp x16, x15
        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
    
    

    __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
        
    MethodTableLookup //核心代码段 
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgSend_uncached
    

    MethodTableLookup

    .macro MethodTableLookup
        
    SAVE_REGS MSGSEND
    
    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    mov x2, x16
    mov x3, #3
    bl  _lookUpImpOrForward //bl跳转,cache里找不到,跳转到方法列表里查找
    
    // IMP in x0
    mov x17, x0
    
    RESTORE_REGS MSGSEND
    
    .endmacro
    

    4.总结

    isa ---> class ---> cache ---> _bucketsAndMaybeMask ---> mask 和 buckets ---> (buckets + mask << 4) == current bucket
    1.current bucket ---> imp和sel ---> current bucket -= BUCKET_SIZE,指向前一个bucket
    2.sel == _cmd,缓存命中,CacheHit ---> hit: call or return imp,cache查找流程结束
    3.sel != _cmd,sel == nil,goto Miss,cache查找流程结束,执行6
    4.sel != nil 且bucket >= buckets,即没到最前面,则执行 begin ---> 0执行的bucket,do-while循环检查
    5.bucket < buckets,则bucket指向最后,buckets + (mask << 4),bucket >= buckets,do-while循环检查,执行1
    6.两遍依然没找到imp,则执行__objc_msgSend_uncached ---> MethodTableLookup ---> _lookUpImpOrForward开始查找方法列表
    7. 汇编里没有do-while,p13指向最后再重复一次1:标签的逻辑

    相关文章

      网友评论

          本文标题:iOS-底层原理-消息流程objc_msgSend分析之汇编查询

          本文链接:https://www.haomeiwen.com/subject/dwqiprtx.html