一、前言
- 本文主要分析当我们调用
[p test1]的过程中,runtime是如何调用的。 - 本文的调试代码地址
- 由于runtime源码无法正常跑在真机上,本文是通过断点
x86代码来类比分析arm64。 - 本文的代码是
objc-750和之前的480有些不一样的地方;
二、缓存查找
先添加如下测试代码

23行添加断点

点击运行程序,程序将断点在23行
2.1、计算方法test1的索引

- 计算方法
test1的缓存索引 ,4294971225 & 3 == 1; - 先打印索引
1的地方有没有占用,可以看到索引为1被init方法占用了,由于是x86架构索引将4294971225➕1然后再& 3== 2,输出为2的位置的_key == 0,所有方法test1索引是2(要是arm64架构的话就是-1了),关于如何缓存的请看这篇
1 |
|
2.2、首次调用test(没有缓存的汇编代码)
在objc-msg-x86_64.s中打下断点

继续调试会到宏CacheLookup代码处,由于此时传的是NORMAL,由于我们的代码最终是跑在真机上的,所以我这里还是分析arm64的汇编吧,x86汇编留给你自己分析吧,读懂arm64的汇编代码,x86汇编不在话下区别就是使用的汇编写法不同(x86是AT&T汇编)
2.2.1、objc-msg-arm64.s汇编代码如下
1 |
|
如果你觉得一头雾水的话我带你一步一步看汇编,首先初步看到这个汇编代码会发现出现很多不知道的寄存器比如p是什么鬼,PTRSHIFT又是什么鬼,原来在objc-750中,苹果使用宏重定义了,其实就是x,可以查看arm64-asm.h头文件可以看到,由于我们是arm64的所以也就是下面的代码
1 | #if __arm64__ |
这里还是不厌其烦的我还是把宏替换成习惯的形式来方便理解,替换后的结果如
1 | .macro CacheLookup |
2.2.2、汇编代码分析
下图表示person类对象的内存所占的内存大小图

2.2.2.1、ldp x10, x11, [x16, #16
_objc_msgSend 调用CacheLookup传的是NORMAL,入参是放在x1 = SEL, x16 = isa中的,最先执行的是ldp x10, x11, [x16, #16],这行命令的意思是从x16往下16个字节长度开始,连续读取8 + 8个字节的长度分别赋值给x10和x11。
- 由于
x16 == isa,那么x16 + 16就是_buckets的地址,赋值给x10; - 由于
x10所占的字节长度是8,那么x11就是接下来的8个字节,也就是读取到了_mask和_occupied,所以x11 == _mask + _occupied,由于是小端存储模式,_mask被存放在低16位,可以通过w11取到。
所以执行完ldp x10, x11, [x16, #16]代码,x10和x11内容如下

2.2.2.2、查找缓存列表
2.2.2.1已经找到了缓存_buckets的地址,下面就开始查找缓存了,_buckets的数据结构以及每个元素的地址可以通过下面的方式计算得出如下

1 | and w12, w1, w11 // x12 = _cmd & mask |
既然已经找到了存放缓存的地址,接下来只要找到调用的方法在缓存中的索引值就可以了,我们需要找的方法是test1,他的SEL为4294971226。
and w12, w1, w11初步计算出方法的索引值,4294971226 & 3 == 2;add x12, x10, x12, LSL #(1+3),步骤1计算出了&的结果,接下来就把指针移动到,序号为2的地方,左移4位正好是16个字节大小,要想跳到哪个序号直接序号左移4位即可,再加上初始的地址x10,就是序号的地址。ldp x17, x9, [x12],分别取出序号2的_imp和_key赋值给寄存器x17和x9。
知道每个元素的地址计算方式了,那么就是挨个判断是否是我们要找的方法了,我们初始值是序号2,接下来进入判断逻辑,流程图如下。

由于我们是第一次调用是没有缓存的,所以即将进入方法__objc_msgSend_uncached,不幸的是这个方法还是汇编代码。
三、方法列表查找 & 动态方法解析
如果缓存中没有我们要找的方法,那么就会进入__objc_msgSend_uncached,汇编代码如下
1 | STATIC_ENTRY __objc_msgSend_uncached |
实际上这个就是调用的宏MethodTableLookup如果宏没有跳转就会调用TailCallFunctionPointer x17了,我们先看MethodTableLookup
1 | .macro MethodTableLookup |
如果懂函数栈帧的话,其实这个段汇编代码很简单,头和尾其实是常规操作,前面是开辟栈空间,保护寄存器的值,尾部恢复寄存器的值,以及恢复栈平衡,关键代码其实是bl __class_lookupMethodAndLoadCache3,这个__class_lookupMethodAndLoadCache3方法是一个我们熟悉的高级语言方法了,其实就是_class_lookupMethodAndLoadCache3
在文件objc-runtime-new.m中
1 | IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) |
lookUpImpOrForward的代码如下(有删减)
1 | IMP lookUpImpOrForward(Class cls, SEL sel, id inst, |
注释都写在后面了,流程图如下:

四、消息转发(_objc_msgForward_impcache)
这个方法是汇编实现的,objc-msg-arm64.s中的汇编代码如下
1 | STATIC_ENTRY __objc_msgForward_impcache |
其实它就是对__objc_msgForward的一个封装而已
1 |
|
__objc_forward_handler是runtime的一个默认实现,代码在objc-runtime.m
1 | __attribute__((noreturn)) void |
需要借助一个demo来继续下面的探索,选择以下Forwarding_demo工程


4.1、查看调用堆栈-【X86架构】
运行以后会闪退,函数堆栈如下
1 | (lldb) bt |
从函数堆栈可以看出调用完performSelector:方法以后,代码就进入了CoreFoundation框架了,然后就到了__forwarding_prep_0___ -> ___forwarding___,CoreFoundation代码是开源的可以去这里下载,我下载的是CF-1153.18 2,遗憾的是虽然CoreFoundation代码是开源的,但是苹果没有给出以上方法的实现。
4.1.1、断点查看方法实现
运行程序,分别增加2个断点
1 | (lldb) breakpoint set -n '__forwarding_prep_0___' |
运行程序,程序首先进入__forwarding_prep_0___
1 | CoreFoundation`__forwarding_prep_0___: |
过掉这个断点程序会进入___forwarding___
1 | CoreFoundation`___forwarding___: |
代码很长,4.1.3会专门分析这个方法
4.1.2、逆向CoreFoundation.framework查看方法实现
除了直接添加断点的方式,还可以通过逆向CoreFoundation.framework来窥探实现,这样更直观,还能知道函数调用关系,CoreFoundation.framework放在/System/Library/Frameworks/CoreFoundation.framework

找到了___forwarding___实现,并且知道是谁调用的,分别是框出来的部分__forwarding_prep_0___和__forwarding_prep_1___,点击进入__forwarding_prep_0___,同样的方式最后定位到了___CFInitialize

通过汇编确实看出___CFInitialize调用了___forwarding___方法,但是开源的代码根本没有这个相关的影子。
4.1.3、分析forwarding实现
我们已经逆向出来了__forwarding__的汇编代码,但是可读性还是太差了,网上有人已经将汇编代码转成熟悉的样子,这个方法就是消息转发的全部了。
1 | void __forwarding__(BOOL isStret, void *frameStackPointer, ...) { |
涉及到的顺序:
forwardingTargetForSelector:;methodSignatureForSelector:;forwardInvocation:;doesNotRecognizeSelector:。
五、objc_msgSend完整流程
