美文网首页
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