美文网首页
8.iOS底层学习之objc_msgSend

8.iOS底层学习之objc_msgSend

作者: 牛牛大王奥利给 | 来源:发表于2021-08-24 21:24 被阅读0次

    上一篇Runtime 运行时&方法的本质文章初步了解了objc_msgSend的定义,这篇文章会介绍一下具体的过程

    objc_msgSend的流程

    通过全局搜索可了解到objc_msgSend有好多处,我们拿arm64架构下的objc_msgSend为例子进行分析,在该文件中可以找到ENTRY _objc_msgSend相关,解释是进入到objc_msgSend的流程,我们来看一下这段代码,从ENTRY到END_ENTRY,嗯,是汇编实现的😓。


    image.png

    流程解读:

    • ENTRY _objc_msgSend
      ENTRY伪指令用于指定汇编程序的入口点。在一个完整的汇编程序中至少要有一个 ENTRY (也可以有多个,当有多个 ENTRY 时,程序的真正入口点由链接器指定),但在一个源文件里最多只能有一个 ENTRY (可以没有)。

    • UNWIND _objc_msgSend, NoFrame
      \color{red}{UNWIND:}unwind 形式的栈回溯,可以拿到栈顶指针和相应的内存以及相应的寄存器。(这里我理解的也有点模糊查了好多资料,待进一步考证。)我理解出来大概的意思就是处理一些中断然后栈保留,然后栈回溯这样一些操作,当高优先级的程序代码指令过来时,cpu要中断当前低优先级的程序并进行栈保留操作,高优先级程序执行完毕后,进行栈回溯,来继续之前没有完成的工作。

    • cmp p0, #0 // nil check and tagged pointer check
      这是一个判空操作。判断消息接受者是不是存在。

    • 跳转 LNilOrTagged或者LReturnZero

    #if SUPPORT_TAGGED_POINTERS
       b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    #else
       b.eq    LReturnZero
    #endif
    

    接下来的流程首先是一个SUPPORT_TAGGED_POINTERS,是否支持TaggedPointers类型,如果支持那么会走:b.le LNilOrTagged,不支持会走:b.eq LReturnZero。
    b.le,b.eq这两个指令作用如下图标红处:

    image.png
    b.le:小于等于(less than or equal to),执行标号,否则不跳转;
    b.eq:等于(equal to),执行标号,否则不跳转。
    所以完整的流程是这样:
    如果支持TaggedPointers类型,并且cmp p0和0比较的结果小于等于0的时候,跳转到LNilOrTagged。否则继续执行:ldr p13, [x0]。
    如果不支持aggedPointers类型,并且并且cmp p0和0比较的结果等于0的时候,跳转到LReturnZero。否则继续执行:ldr p13, [x0]。
    • ldr p13, [x0] // p13 = isa
      把x0(寄存器)中的内容读出来赋值给p13。

    LDR指令:LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用于从存储器中读取32位的字数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。

    • GetClassFromIsa_p16 p13, 1, x0
      GetClassFromIsa_p16的宏定义如下,所以src参数接收的是p13,needs_auth对应1,auth_address对应x0。
    .macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
    
    #if SUPPORT_INDEXED_ISA
        // Indexed isa
        mov p16, \src           // optimistically set dst = src
        tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
        // isa in p16 is indexed
        adrp    x10, _objc_indexed_classes@PAGE
        add x10, x10, _objc_indexed_classes@PAGEOFF
        ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
        ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
    1:
    #elif __LP64__
    .if \needs_auth == 0 // _cache_getImp takes an authed class already
        mov p16, \src
    .else
        // 64-bit packed isa
        ExtractISA p16, \src, \auth_address
    .endif
    #else
        // 32-bit raw isa
        mov p16, \src
    
    #endif
    .endmacro
    

    然后我们进入到GetClassFromIsa_p16中,有预编译SUPPORT_INDEXED_ISA,他的定义如下:

    #if __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
    #   define SUPPORT_INDEXED_ISA 1
    #else
    #   define SUPPORT_INDEXED_ISA 0
    #endif
    

    然后在ARM.cpp找到关于ARM_ARCH_7K的赋值代码为:

      // Unfortunately, __ARM_ARCH_7K__ is now more of an ABI descriptor. The CPU
      // happens to be Cortex-A7 though, so it should still get __ARM_ARCH_7A__.
    if (getTriple().isWatchABI())
    Builder.defineMacro("__ARM_ARCH_7K__", "2");
    

    所以这看起来是watchOS下处理的,当前的运行环境下SUPPORT_INDEXED_ISA值为0,然后传进来的needs_auth是1,所以接下来走:ExtractISA p16, \src, \auth_address

    • ExtractISA
    .macro ExtractISA
        and    $0, $1, #ISA_MASK
    .endmacro
    

    根据传进来的参数,0是p16,1是src也就是p13也就是isa。所以ExtractISA的操作就是把isa和mask做一个与操作,赋值到0,也就是p16,所以p16 =0=isa&MASK= class。

    然后我们退回到主流程GetClassFromIsa_p16 这个时候已经取到了class存在p16里,接着往下进行。

    • CacheLookup
    LGetIsaDone:
        // calls imp or objc_msgSend_uncached
        CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
    

    我们来到了CacheLookup,查看关于CacheLookup的代码,发现其中有好几个函数,LLookupStartLLookupEndLLookupRecoverLLookupPreopt,我们暂时先研究CacheLookup的LLookupStart的部分如下,我们只看真机架构的部分所以省略掉一部分源码,带注释:

    LLookupStart\Function:
        // p1 = SEL, p16 = isa
          // p11 = mask|buckets  [x16,#CACHE]把x16往后移CACHE个大小 CACHE经过查找为16大小 根据objc_class的结构也就是说Class往下平移16拿到cache_t (前两个分别是isa 和supclass),平移十六是cache。
            ldr p11, [x16, #CACHE]  
    //CONFIG_USE_PREOPT_CACHES = 1
    // p10 = buckets p11和#0x0000fffffffffffe进行与操作结果放到p10里,因为p11是cache,cache与上0x0000fffffffffffe这个值说明正好取出了cache的低四十八位,根据cache的结构来看cache在真机的情况下低四十八位放的是_bucketsAndMaybeMask   ,(为啥舍掉了一位我也没太搞懂,先往下看)
            and p10, p11, #0x0000fffffffffffe   
     //例:TBNZ X1,#3 label  若X1[3]!=0,则跳转到label,所以p11的第0位不为零则跳转到LLookupPreopt
        tbnz    p11, #0, LLookupPreopt\Function
    
        //这两个步骤是根据buckets插入时的反推 拿到hash的index
            eor p12, p1, p1, LSR #7
        and p12, p12, p11, LSR #48
    

    所以总结下这个查找的过程:
    1.先通过内存平移拿到cache的地址,也就是_bucketsAndMaybeMask = class+0x10 = p11;
    2.然后通过按位与操作得到_bucketsAndMaybeMask的低48位,拿到buckets的首地址。
    3.通过首地址进行运算拿到插入bucket时候的下标index,放到了p12里。

    • 遍历buckets
    //找到下标对应的buckets地址 ,PTRSHIFT= 3 ,p12左移4位再和p10相加,结果放到p13
    add p13, p10, p12, LSL #(1+PTRSHIFT)
                            // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    
    //开始循环查找
                            // do {
    //p17 =imp,p9 =sel ,x13 的index对应的buckets取最后一组{imp, sel}分别赋值给p17 和p9
    1:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
    
    //拿传入的p1,就是要查找的sel和p9进行比较,b.ne =  not equal,p1和p9不相等跳转到3,否则顺序执行2:CacheHit
        cmp p9, p1              //     if (sel != _cmd) {
        b.ne    3f              //         scan more
                            //     } else {
    2:  CacheHit \Mode              // hit:    call or return imp
                            //     }
    
    3:  cbz p9, \MissLabelDynamic       //     if (sel == 0) goto Miss; 判断p9有没有值,没有值走MissLabelDynamic。
        cmp p13, p10            // } while (bucket >= buckets) 判断p13有没有减到p10的地址,也就是首地址,没有的话跳转到1,继续减一然后判断,查找到头了还没查到会走3之后的流程。
        b.hs    1b
    

    所以大致流程就是一个从index拿到一个相应的bucket,然后这个bucket的最后开始往前遍历,判断sel是不是相等,如果相等那么直接执行缓存命中,不相等先去看看这个bucket存的sel是不是丢了,丢了去走MissLabelDynamic,没丢接着循环这个流程,直到这个bucket一直遍历到首地址,然后去走别的流程。

    • CacheHit
    .macro CacheHit
    .if $0 == NORMAL
        TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
    
    .macro TailCallCachedImp
        // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
        eor $0, $0, $3
        br  $0
    .endmacro
    

    缓存命中的过程就调用了方法TailCallCachedImp,这就是个解码imp的过程,然后返回相应的imp。
    至此分析先告一段落。


    遗留问题:
    没理解取buckets的时候为什么与的是0x0000fffffffffffe,最后一位是不要了嘛???
    (后面理解了这个部分会继续更新)


    这个过程看了五六遍,还参考了同学们的各种博客,查阅了好多汇编命令,希望有问题多多交流讨论,再接再厉!咔咔就是补作业!!

    相关文章

      网友评论

          本文标题:8.iOS底层学习之objc_msgSend

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