iOS 底层原理之objc_msgSend汇编分析(下)

141 阅读4分钟

引言

通过上篇文章我们将源码编译成.cpp文件,发现方法的本质其实就是底层调用了objc_msgSend(receive, sel)进行消息发送,那么objc_msgSend()在底层做了什么?接下来一起分析一下

汇编分析

objc_msgSend() 是在libobjc.A.dylib库中,我们全局搜索objc_msgSend,(然后按住command将搜索结果折叠起来,因为结果太多,不利于我们分析究竟该看哪个文件),这里我们使用的是模拟器,所以选用objc-msg-arm64汇编文件,找到ENTRY _objc_msgSend入口,如下图:

image.png 下面一步步分析汇编代码:

  1. p0就是objc_msgSend(receiver, sel) 中的第一个参数receiver,这里比较了一下方法的调用者是否为nil
cmp p0, #0 // nil check and tagged pointer check p0:receive
  1. x0是对象的内存地址,将x0放到了p13中,调用GetClassFromIsa_p16方法,传递了p13, 1, x0三个参数
ldr p13, [x0]

GetClassFromIsa_p16 p13, 1, x0 // p16 = class

2.1 GetClassFromIsa_p16方法接受了三个参数,分别是对象的内存地址src = p13,是否需要授权 needs_auth = 1,授权地址 auth_address = x0,其中__LP64__是我们常见的情况,又因为需要授权,所以调用了ExtractISA p16, \src, \auth_address方法,传递了对象的内存地址

//  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
... // 暂时不分析这种case

#elif __LP64__ // 这个是我们常见的case

    .if \needs_auth == 0 

        mov p16, \src

    .else // needs_auth 传递过来的值为1, 所以走的是ExtractISA方法

        // 64-bit packed isa

        ExtractISA p16, \src, \auth_address
    .endif

#else
 ...
#endif
.endmacro

2.2 这里的$0 = p16, $1 = src(对象的内存地址), $2 = #ISA_MASK,拿着$1 & #ISA_MASK,也就是对象的内存地址 & isa掩码 = 类的内存地址

.macro ExtractISA

    and  $0, $1, #ISA_MASK

.endmacro

小结:GetClassFromIsa_p16方法就是通过对象的内存地址获取到类地址的过程。

  1. CacheLookup 方法要么调用imp,要么就是没有找到方法,则调用__objc_msgSend_uncached
LGetIsaDone:

// calls imp or objc_msgSend_uncached

CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
  1. CacheLookup方法实现
//  CacheLookup NORMAL|GETIMP|LOOKUP <function> MissLabelDynamic MissLabelConstant

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant


//   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
... // 不常用case,这里不做研究

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    ldr p11, [x16, #CACHE] // p11 = mask|buckets cache_t*
    #if CONFIG_USE_PREOPT_CACHES
    and p10, p11, #0x0000fffffffffffe // p10 = buckets
    tbnz p11, #0, LLookupPreopt\Function
    #endif
    /**
     p1: sel
     p11: cache
     LSR: 右移
     eor: 异或
     mask: 高16位
     bucketmask: 低48位
     p12 = (p1 ^(p1 >> 7))   等价与 value = sel ^(sel >> 7)
     p12 = p12 & (p11 >> 48)  等价与 index = value & (mask)
     */

    eor p12, p1, p1, LSR #7

    and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask

    #else
    ... // 不常用case,这里不做研究
    #endif // CONFIG_USE_PREOPT_CACHES

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
... // 不常用case,这里不做研究
#else

#error Unsupported cache mask storage for ARM64.

#endif


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

// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {

1: ldp p17, p9, [x13], #-BUCKET_SIZE //     {imp, sel} = *bucket--

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;

cmp p13, p10 // } while (bucket >= buckets)

b.hs 1b

LLookupEnd\Function:

逐行分析:

4.1 x16 是类的内存地址,将x16移到了x15

mov x15, x16 // stash the original isa

4.2 获取buckets

x16是内存地址,将x16偏移#CACHE个大小,赋值给p11,全局搜索发现CACHE,发现#define CACHE (2 * __SIZEOF_POINTER__) ,2个指针的大小即16,我们之前有讨论过类地址偏移16位是cache_t,那么p11就是cache_t的内存地址

p11 & #0x0000fffffffffffe = p10cache地址& bucket掩码 = buckets

ldr p11, [x16, #CACHE] // p11 = mask|buckets cache_t
and p10, p11, #0x0000fffffffffffe // p10 = buckets 
tbnz p11, #0, LLookupPreopt\Function

4.3 获取查找bucketsindex

eor 异或,不同为真,同为假。LSR 向右移位。 eor p12, p1, p1, LSR #7p1 << 7 位异或p1最后赋值为p12,即p12 = p1 ^ (p1 << 7),还记得这里p1是谁么,就是objc_msgSend(receover, sel)中的selp12 = sel ^ (sel << 7)

and p12, p12, p11, LSR #48 ,p11cache地址,cache << 48 可以获取到mask值,结合上面得出的p12,可总结为:mask & sel ^ (sel << 7) = p12

eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48

小结:以上两句和inset()函数中cache_hash()计算下标用的是同一种逻辑,如下图:

image.png

  1. buckets 偏移index个大小。

上面已经计算出index,可是它只是一个数值,例如1,无法和p10(buckets)内存地址相加,所以就将数值转换成了内存地址,向左移了4,例如0001 << 4 => 1000,因为bucket中包含了selimp正好16位。则bucket = buckets & (index << 4) = p13

add p13, p10, p12, LSL #(1+PTRSHIFT)
  1. 根据sel寻找对应的imp,找不到则走MissLabelDynamic
1: ldp p17, p9, [x13], #-BUCKET_SIZE //  {imp, sel} = *bucket--

cmp p9, p1 //  sel != _cmd

b.ne 3f //         scan more

2: CacheHit \Mode // hit:    call or return imp

3: cbz p9, \MissLabelDynamic //     if (sel == 0) goto Miss;

cmp p13, p10

b.hs 1b

6.1 第一部分,将x13(bucket)中的selimp分别赋值给p9p17,然后比较sel和传入的_cmd是否相等,相等则跳转到第二部分,不相等则跳转到第三部分。再次来到这里时进行*bucket--移动。

第二部分,CacheHit 命中,返回imp

第三部分,判断sel == 0 ,为0 则走MissLabelDynamic, 不为0,则比较当前的bucket是否等于buckets ,如果不等则跳转到第一部分,进行指针前移。

伪代码实现