一、前言
- 本文主要分析当我们调用
[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:
。