objc_msgSend底层探索(下)

2022-01-14 15:11:09 浏览数 (1)

上一篇里面,我从OC层面来探索了objc_msgSend如何进行消息的发送,对普通开发者来说也是比较容易理解的,那很多人都知道,Runtime是由C或者C 以及汇编语言写的一套底层的API。

通常来说我都只会去分析一下C或者C 的源码,因为还是能够勉强看懂的,而很少去了解或者看到过汇编,那现在就到了需要感受一下汇编的时候了,而汇编又是怎么从objc_msgSend引出来的呢?

首先,我需要知道他在哪个底层源码里面,很简单,打一个符号断点,然后直接运行。

断点进来之后,可以很直观的看到,他是在libobjc.A.dylib里面。

然后我打开objc源码,全局搜索objc_msgSend。

竟然有644个!看哪一个呢?我之前已经剧透了,objc_msgSend是用汇编写的,所以我来找汇编文件就可以了,按住command。

点击任意一个收起的小箭头全部收起来,然后看每一个文件的后缀名,汇编文件后缀名是.s,就只有六个。

根据文件名可以知道,主要的区别就在于他们使用在不同的平台,我就看这个arm64的吧,因为arm64是真机运行的真实环境。

展开arm64.s文件之后,也会看到很多的objc_msgSend,如果稍微有点熟悉的话,你就会知道,在汇编里面,经常有这么一个方法,ENTRY。

就是进入的意思,往往就是一个函数的开始,我点进去,最直观的感受就是,他的实现代码量很少,感觉很简单。

这里先插一个问题,为什么objc_msgSend是用汇编写的?而不是用C/C 写呢?我刚刚随便一搜索就搜到了很多的objc_msgSend。

也就是说,源码里面包含了多个版本的objc_msgSend方法,他们是根据返回值的类型和调用者的类型分别处理的,如果说用C或者C 来实现。

那单独一个方法的定义是满足不了多种类型的返回值的需求的,因为C的参数必须很明确,这也就是为什么我在上一篇文章进行直接调用的时候需要进行一个强转的原因,也就是告诉编译器我有两个参数,因为他的参数个数和类型都是不一定的。

并且,这种可变的参数,用汇编来处理是最简单最方便的,效率还贼高,所以你更详细的去了解就会发现,在Runtime里面调用频率比较高的函数,很多都是用汇编写的,还有一点,就是更安全,汇编用寄存器进行存储,贼稳。

然后来看源码,汇编函数进来之后就来到了554行,cmp,他的英文单词叫做compare,就是对比的意思,对比什么呢?

p0,p0是一个地址,那这个地址是谁呢?就是当前的消息的接收者,也就是person,就是说,p0是person的一个地址,然后来对比他和#0,是否有不同呢。

意思就是判断一下当前的消息接收者到底有没有,如果没有消息的接收者,那就不用继续下去了,没有意义了,需要先明确的就是,整个过程我是模拟的person找sayHello,如果不知所云,就请看上一篇文章。

然后继续往下看,555行,是否是一个TAGGED_POINTERS类型,就是标记的指针,他是一个小对象类型,如果是TAGGED_POINTERS类型,就会走556行,那person显然不是一个小对象类型,所以他就会走558行。

b.eq,如果等于了,LRetureZero,返回一个Zero,也就是返回此次的消息为空,那如果这里也不满足呢,就会跑到560行继续执行,那这个LReturnZero是一个什么呢?他就在574行。

然后583行就END_ENTRY,结束了,就相当于一个return返回了,并且返回一个nil,那如果满足的话,继续来看560行,ldr是一个加载指令,它的作用就是把存储器地址为x0的字数据读入寄存器p13,x0是谁呢?就是person。

然后来到561行,GetClassFromIsa_p16,从字面翻译就知道,他是从Isa指针获取class的过程,他的参数什么意思呢,不知道,那就我来搜索一下GetClassFromIsa_p16,在112行,就搜到了一个宏定义.macro,然后给了三个参数。

那他都做了些什么呢?114行就进行了一个判断,支持SUPPORT_INDEXED_ISA吗?显然不满足,这是在32位系统里面的,所以就走126行进行一个判断,needs_auth等于0吗?

561行传过来的是1并不是0,所以走else语句来到了130行,然后这一句代码做的事情就是把isa存放到了p16,不信的话我来全局搜一下。

搜到了两个宏定义,我就看第二个简单的,他们是一样的,第一个只是多了一些条件判断,那197行的代码是什么意思呢?

首先是一个and符号,就相当于当前传过来的值,也就是$1,与上ISA_MASK,然后存到了$0里面,也就是存到了p16里面,而且,这里的$1就是person,也就是把当前的class存在了p16里面。

再回到561行,注释也写的很清楚,class就存到了p16里面,所以p16=class,我就可以得到一个结论了,到这里为止,所有的操作,用OC的话来说,就是通过person,获取到了LGPerson。

然后562行获取Isa完成之后,就来到了564行,CacheLookup,字面意思是查找Cache,为什么要查找Cache呢?只能说明sayHello在Cache里面,我才会去找,而之所以前面要获取到LGPerson,也就是class,就是因为Cache在class里面,我真是个天才。

其实objc_msgSend,就是通过第二个参数SEL,去从第一个参数的class里面的Cache中找到IMP的一个过程,我来搜一下CacheLookup,在326行就有一个.macro。

就非常清晰了,他后面有四个参数,需要关注的是前面两个参数,第一个传过来的是NORMAL,第二个Function就是_objc_msgSend,然后就来到了349行,隐藏原始的isa,然后来到352行执行一个判断语句。

如果我要运行在真机上,就是arm64结构,所以走的是357行,然后又有很多if判断,感觉很麻烦,不要心急,先来看358行,把x16的地址,进行平移CACHE的大小,那这个CACHE等于多少呢。

不知道,我又来进行搜索,有79个,一个个看就太傻了,我直接在前面,加一个空格,然后在最前面加一个e,就是define的意思,这是一个小技巧,或者还可以加一个f,表示if,看是否有相关的一些定义的地方,在83行就有一个define,就很明显了,2乘以SIZEOF_POINTER,SIZEOF_POINTER是指针的大小,所以,CACHE就等于16。

然后回到358行,这里的x16就是class,class平移16得到什么呢?其实就等于cache_t对象,接下来又把cache_t存到了p11,也就是说p11等于cache_t。接下来359行又是一个if,那这个if满不满足呢?我来全局搜索,就看第一个。

很简单,支持arm64,并且是IOS的OS,并且不是SIMULATOR,也不是MACOS,所以是满足的,就来到了360行,这个if不用多说,他不满足,应该走else,也就是364行。

然后拿到p11,与上了一个掩码#0x0000fffffffffffe,为什么这么来操作呢?我来全局搜一下cache_t,前面加一个空格,找到这个结构体。

368行的条件满足的话,下面就有maskShift=48,maskZeroBits=4,然后bucketsMask等于一个表达式,这个表达式里面就存在maskShift和maskZeroBits。

也就意味着,mask和bucket是有存储位数的一些相关的关系的,那373行的48位是什么意思呢,我先返回汇编文件364行拷贝一下掩码0x0000fffffffffffe(不带井号),打开计算器直接粘贴进来(如果不是科学计算器,按command 3)。

16进制来看,1号位置到47号位置都是1,就相当于47个1,低位的48位,就取47个1,和最后一个0,为什么末尾是个0呢?

因为在384行bucketsMask的表达式最后面减去了1,nice,就意味着,汇编文件364行这里的p11与上mask,就得到了buckets,并且存在了p10里面。

然后就来到了365行,这句话的意思是,p11与#0做比较,如果p11不存在,就直接走后面的Function,如果存在的话,就走LookupPreopt,下面的代码我都不用看了,因为p11显然是存在的,我就去搜索LLookupPreopt,在443行。

if判断不成立,直接来到了450行,这里后面也有一些连续的处理,但是这个过程看起来很麻烦,不想往下分析了,浪费时间,我就直接跳过。

因为这个分析的过程中,如果事无巨细去看每一句代码,会变得越来越复杂,我不建议这么来做,就简单一点,直接来到384行。

这里获取到了p10和p12进行按位左移,得到了buckets,p10等于什么呢?就在370行,首先p11就等于cache,然后按位与,得到p10,p10就等于buckets,但是我要去获取sel,他在buckets里面的哪一个地方呢?

当前的sel和imp,都是存在buckets里面的,就意味着,我需要要去找到对应的bucket。

怎么找呢,他会有一个下标index,那要去获取这个index的话,就涉及到了哈希函数,先来回顾一下,全局搜insert(sel。

875行进行取值,就是拿到sel和mask,去找到当前的哈希地址,我直接点进去,311行,这个sel按位与上了7。

除此之外,313行拿到sel与上了mask,就是取出哈希地址,我现在有了一个sel,所以还缺少一个mask,有了mask,就能够得到index,所以要怎么取mask呢?

来看汇编文件的371行,p11按位右移48位,得到的就是mask的值,然后mask就放到了p12里面,但是在放在p12里面之前,有一个and符号,什么意思呢,就是让p1于上mask,为什么要与呢?

注释写的很清楚,p1就是_cmd,然后得到了index存到了p12,并且p10存的是buckets,然后来看384行,p12进行按位左移1 PTRSHIFT,然后注释里面,p13就等于一个表达式,这一步操作是干什么呢?首先要知道这个PTRSHIFT是什么,同样的来全局搜索一下。

有一个宏定义,就等于3,所以就是左移4位,那为什么cmd和mask要左移4位呢?首先,这里的buckets是一个地址,所以他是进行内存平移,他只能移动1个单位、2个单位、3个单位,而不能够直接移动一个地址,所以这里其实就是把一个16进制的地址类型。

转换成一个int类型,然后就能够得到buckets对应的那个地址,然后下面388行就有3个步骤,第一步,首先获取到一个BUCKET_SIZE,就是一个长度单位,然后取bucket的长度单位,就是取bucket--,然后存到了p9里面,然后ldp就是取当前偏移之后的地址,存到p17里面、和p9里面来。

就是说,偏移之后往这两个地方同时存,我已经知道bucket里面有imp和sel,所以,p17和p9,里面存的就是imp和sel,然后下一步拿到p9和p1比较,p1就是我要查找的那个sel,也就是sayHello。

如果一样,就来到了392行第二步,CacheHit,这就是非常著名的缓存命中,如果p1和p9不相等就会走390行的3f,也就是对第三步进行循环,然后cbz p9,判断当前的p9是否存在。

虽然我找到了p9,但是我不知道他存不存在,如果p9为空的话,就走MissLabelDynamic,如果不为空的话,就继续走395行去匹配,p10就是首地址,p13不确定在哪里。

如果p13大于等于首地址,就意味着可以进行内存平移,然后就到1b,然后进行bucket减减,也就是再次的哈希减减,减减之后继续判断,直到找到缓存为止,找到就会去执行CacheHit,所以这里其实就是一个死循环,在不断的查找,找到之后做了什么呢?搜索一下CacheHit,在306行。

CacheHit携带了一个Mode过来,Mode就等于之前传过来的NORMAL,所以307行判断是否等于NORMAL是成立的,然后来到308行执行TailCallCachedImp,305行的注释也很清晰,x17等于cached IMP。

就是我找到的那个p17,p10就是首地址,sel就是要查找的x1,x16就是isa,然后我来全局搜索TailCallCachedImp,有两个宏,看下面简单的吧。

eor是按位异或,就是拿到$0,异或$3,为什么他们两个要异或呢?$0是x17,$3是isa,也就是imp的编码,存的时候需要编码,所以在取的时候也要编码,才能得到真正的imp。

接下来175行就直接br,跳转返回到imp并且对imp进行调用,也就是说,从缓存里面找到之后就直接调用,这就是objc_msgSend函数在底层通过 sel 去找 imp 的一个过程,到这里就全部疏通了。

这个分析的过程是非常枯燥和乏味的,但是一旦从中找到了思路和学习方式,成就感也会油然而生,对以后的应用层开发也会有不小的帮助,我就写到这里吧,如若内容有任何错误恳请指出,一起学习,共同成长。

- END -

0 人点赞