上一篇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
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这两个指令作用如下图标红处:
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
根据传进来的参数,1是src也就是p13也就是isa。所以ExtractISA的操作就是把isa和mask做一个与操作,赋值到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的代码,发现其中有好几个函数,LLookupStart,LLookupEnd,LLookupRecover,LLookupPreopt,我们暂时先研究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,最后一位是不要了嘛???
(后面理解了这个部分会继续更新)
这个过程看了五六遍,还参考了同学们的各种博客,查阅了好多汇编命令,希望有问题多多交流讨论,再接再厉!咔咔就是补作业!!
网友评论