「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」。
在上一篇类de数据结构分析(总)中基本已经将OC的类进行了全面分析,本篇内容则是顺着思路继续对类中方法的调用作分析、记录。(本篇总字约2396字)
本篇重点
- 部分常见的
arm64
汇编指令objc_msgSend
快速查找流程分析
准备工作
- objc4-818源码
- 本篇对汇编逻辑的静态分析以arm64架构分支为主。
- 分析过程中的宏定义详细含义可查阅objc源码常见宏定义。
一、简单的汇编命令
开始分析之前,先来补充一些关于arm64
的汇编命令,以避免稍后在分析objc_msgSend快速查找流程时的十脸懵。
1.1 数据操作
-
mov:将某一寄存器的值复制到另一寄存器**(只能用于寄存器与寄存器或者寄存器与常量之间传值,不能用于内存地址)**
mov x1, x0 //就是将x0的值复制到x1 中
-
add:将某一寄存器的值和另一寄存器的 **值相加 **后的结果保存在另一寄存器中
add x0, x1, x2 //将x1、x2的值相加后保存到x0中
-
sub:将某一寄存器的值和另一寄存器的 **值相减 **并将结果保存在另一寄存器中:
sub x0, x1, x2 //将x1、x2的值相减后保存到x0中
-
and:将某一寄存器的值和另一寄存器的 **值按位与 **并将结果保存到另一寄存器中
and x0, x0, #0x1 // 将寄存器x0 的值与常量 1 按位与后保存到寄存器x0 中
-
orr:将某一寄存器的值和另一寄存器的 **值按位或 **并将结果保存到另一寄存器中
orr x0, x0, #0x1 // 将寄存器x0 的值与常量 1 按位或后保存到寄存器x0 中
-
eor(xor):将某一寄存器的值和另一寄存器的 **值按位异或 **并将结果保存到另一寄存器中
orr x0, x0, #0x1 // 将寄存器x0 的值与常量 1 **按位异或 **后保存到寄存器x0 中
- lsr:逻辑右移,>>
- lsl:逻辑左移, <<
lsr x0, #48 //x0>>48,配合 add、mov、eor等指令使用。
1.2 加载 - 单次
-
str : 将寄存器中的值写入内存中
str x0, [x0, x8] //将寄存器x0的值写到栈内存 x0+x8 处
-
ldr:将内存中的值读取到寄存器中
ldr x0, [x0, x8] //将寄存器x0 + x8 后的值作为地址,将地址的值存入x0
1.3 比较
-
cmp:比较指令,比较两个寄存器的值是否相等。
cmp x0, x1 //比较x0, x1,配合跳转指令执行
-
cmmp:多重比较指令,比较两个寄存器的值是否相等。
ccmp x13, x12, #0x0, ne //x13、 x12同时与#0x0比较,ne为条件域。x13、x12 != 0
-
cbz:和 0 比较,结果为ture就转移
-
cbnz:和 非0 比较,结果为ture就转移
cmp x0, address //比较x0 ?= 0,==0跳转address
-
tbz:测试位和 0比较,结果为ture就转移
-
tbnz:测试位和 非0 比较,结果为ture就转移
tbz p1, #0x6, /Function //即判断 p1第6位为0,则跳转执行/Function
1.4 跳转
-
b:最简单的跳转指令,遇到一个b指令,ARM处理器会立即跳转到给定的目标地址,从那里继续执行。
b address //跳转address
-
bl:同样是跳转指令,在跳转之前,会将下一条指令保存到LR寄存器中。因此,可以通过将lr的内容重新加载到pc中,返回到跳转指令之后的指令执行。
-
blr:与bl指令相似,但是跳转地址是从特定的寄存器中取得。
-
ret:返回,按return理解。
1.5 条件域
- b.le:<=
- b.ge:>=
- b.lt:<
- b.gt:>
- b.eq:=
- b.ne:!=
- b.hi:无符号 >
- b.hs:无符号 >=
- b.lo:无符号 <
- b.ls:无符号 <=
知识点补充:
跳转指令b.le 1b
、b.eg 2f
、b.gt 4f
后面的b和f是什么意思?
- 其中的 b: backward, f: forward。
- 以Objc源码中汇编为例:在一份汇编源码内可能有多个局部标签1、标签2、标签3,在跳转标签是说明查找的方向,
以上就是为分析源码而补充全部汇编指令,都是比较常见的指令,更多arm64汇编指令介绍,可查阅文档armDeveloper -- A64 General Instructions
二、快速查找流程
因为objc_msgSend
的快速查找是使用汇编语言写的,所以一开篇就为顺利分析汇编而进行了相关指令知识的补充,并且在后面的分析过程中如果遇到上述记录的指令,还会进行标注说明。那么进入正题,开始对OC的消息机制-objc_msgSend的快速查找流程进行试探、分析。
通过类de数据结构分析(总)已知:OC层 [xx 方法] 的调用最终都是转换成底层 runtime的api进行执行,比如alloc
、init
等这些方法的调用都是通过_objc_msgSend( id self, SEL op, ...)
发送消息而执行的,那么就来看看_objc_msgSend
的消息是如何发送的?
2.1 查找msgSend源码
汇编文件后缀是(.s),所以全局搜索objc_msgSend
后将所有文件折起来,就可以找到对应架构的源码文件,或者使用底部Filter来过滤(.s)文件。
找到文件后展开,找到 ENTRY _objc_msgSend
开始分析:
整个ENTRY段代码如图所示仅这么多,根据cmp p0, #0
这句判断,逻辑上分了3种情况,:
- LNilOrTagged:tagged_pointer,优化isa->class->CacheLookup。
- LReturnZero :reviver == nil
- GetClassFromIsa_p16:raw_isa,原始isa->class->CacheLookup。
不过归结来讲,1、3两条逻辑都是通过instance.isa找到Class,最终执行CacheLokkup(红框3),开始在Class.cache中查找。
instance -> isa ->class -> cache -> lookup
2.2 GetClassFromIsap16
然后来分析一下其中的GetClassFromIsap16
,看看如何通过isa 找到cls并存储到p16中的:
注意一点:
__LP64__
是数据模型,并不是MacOS的CPU架构。除了LP64
之外,还有LLP64
、ILP64
、SILP64
、ILP32
等。更多模型信息可到objc源码常见宏定义中查看。
2.3 CacheLookup分析
分析完objc_msgSend的ENTRY
,接下来分析CacheLookup
实现,看看找到cls后是如何进行imp查找的,同时这部分也是快速查找流程的核心逻辑。
寻找imp
的代码相比于在ENTRY
时isa->cls
的是多了不少,分析起来的难度也加了不少,不过好在逐行的详细逻辑分析、指令含义已经在图中标记完成了,对于图中整体逻辑进行一个简化汇总 (注意其中重点的逻辑部分):
class -> lookup cache_t —> (buckets —> mask —> index) —> do…while(buckets [0-index] ) —> do…while((buckets [index-max])
总的来说:CacheLoopup
通过两遍查找遍历完成了整个buckets
进行sel
的查找比较,找到则是CacheHit
,否则就是__objc_msgSend_uncached
(在下一篇中展开分析)。
2.4 CacheHit分析
既然找到sel
,想必Hit
中的逻辑就简单了,应该是:同存储{imp,sel}
时候一样,根据不同的cpu架构进行编码/解码然后返回真实IMP即可:
2.5 浏览真机汇编
至此,objc_msgSend
快速查找逻辑的静态分析已经完成了,接着找一段简单的代码通过真机码跑起来,看看动态调试下汇编代码的逻辑是否同静态分析整合后的逻辑路径相同,验证的同时还可以在一次巩固对快速查找流程的理解。
UIPerson* p = [[UIPerson alloc]init];
[p superclass];
- 执行到断点
[p superclass]
- Xcode菜单 -> Debug -> Debug Workfolw -> Always Show Disassemly
- hold ctrl + setp into
MacOS版本太低,iOS太高、Xcode又升不上去,死活进不了
DEBUG
,图片以后再补充
三、总结
以上就是本篇对于objc_msgSend
快速查找的汇编源码进行的全面分析,除了对于源码逻辑的文字分析外,大部分源码的逻辑解释分析都附带到了截图中,图片同样需要认真看。
至此算是已经命中IMP
了,倘若还是没找到呢?那就进入下一篇底层探索 -- OC消息机制(二)lookUpImp慢速查找,跳入__objc_msgSend_uncached
后继续慢速查的分析。
篇中分析、记录的内容如有帮助,欢迎点赞👍、收藏✨、评论✍️。如果错误,欢迎指正🙆🏻♂️。