[TOC]
一、前言
本文主要分析当我们调用
[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了),关于如何缓存的请看这篇
#if __arm__ || __x86_64__ || __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
2.2、首次调用test
(没有缓存的汇编代码)
在objc-msg-x86_64.s
中打下断点
继续调试会到宏CacheLookup
代码处,由于此时传的是NORMAL
,由于我们的代码最终是跑在真机上的,所以我这里还是分析arm64
的汇编吧,x86
汇编留给你自己分析吧,读懂arm64
的汇编代码,x86
汇编不在话下区别就是使用的汇编写法不同(x86是AT&T汇编)
2.2.1、objc-msg-arm64.s
汇编代码如下
.macro CacheLookup
// p1 = SEL, p16 = isa
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
如果你觉得一头雾水的话我带你一步一步看汇编,首先初步看到这个汇编代码会发现出现很多不知道的寄存器比如p
是什么鬼,PTRSHIFT
又是什么鬼,原来在objc-750
中,苹果使用宏重定义了,其实就是x
,可以查看arm64-asm.h
头文件可以看到,由于我们是arm64
的所以也就是下面的代码
#if __arm64__
#if __LP64__
// true arm64
#define SUPPORT_TAGGED_POINTERS 1
#define PTR .quad
#define PTRSIZE 8
#define PTRSHIFT 3 // 1<<PTRSHIFT == PTRSIZE
// "p" registers are pointer-sized
#define UXTP UXTX
#define p0 x0
#define p1 x1
#define p2 x2
#define p3 x3
#define p4 x4
#define p5 x5
#define p6 x6
#define p7 x7
#define p8 x8
#define p9 x9
#define p10 x10
#define p11 x11
#define p12 x12
#define p13 x13
#define p14 x14
#define p15 x15
#define p16 x16
#define p17 x17
// true arm64
#else
这里还是不厌其烦的我还是把宏替换成习惯的形式来方便理解,替换后的结果如
.macro CacheLookup
// x1 = SEL, x16 = isa
ldp x10, x11, [x16, #16] // x10 = buckets, x11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #(1+3)
// x12 = buckets + ((_cmd & mask) << (1+3))
ldp x17, x9, [x12] // {imp, sel} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x17, x9, [x12, #-16]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: x12 = first bucket, w11 = mask
add x12, x12, w11, UXTW #(1+3)
// x12 = buckets + (mask << 1+3)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp x17, x9, [x12] // {imp, sel} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x17, x9, [x12, #-16]! // {imp, sel} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
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
的数据结构以及每个元素的地址可以通过下面的方式计算得出如下
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #(1+3) // x12 = buckets + ((_cmd & mask) << (1+3))
ldp x17, x9, [x12] // {imp, sel} = *bucket
既然已经找到了存放缓存的地址,接下来只要找到调用的方法在缓存中的索引值就可以了,我们需要找的方法是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
,汇编代码如下
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
实际上这个就是调用的宏MethodTableLookup
如果宏没有跳转就会调用TailCallFunctionPointer x17
了,我们先看MethodTableLookup
.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3
// IMP in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
如果懂函数栈帧的话,其实这个段汇编代码很简单,头和尾其实是常规操作,前面是开辟栈空间,保护寄存器的值,尾部恢复寄存器的值,以及恢复栈平衡,关键代码其实是bl __class_lookupMethodAndLoadCache3
,这个__class_lookupMethodAndLoadCache3
方法是一个我们熟悉的高级语言方法了,其实就是_class_lookupMethodAndLoadCache3
在文件objc-runtime-new.m
中
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
lookUpImpOrForward
的代码如下(有删减)
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
retry:
// 1.查找缓存
imp = cache_getImp(cls, sel);
if (imp) goto done;
// 2.查找自己的方法列表并存入缓存
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// 3.递归查找父类的缓存或者方法列表
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// 3.1 查找父类的缓存
imp = cache_getImp(curClass, sel);
// 如果父类缓存中有 !!!存入的是自己的缓存列表,并不是存到父类的缓存列表!!!
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// 不在缓存的话存入自己的缓存
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// 3.2 父类缓存中没有 , 就查找父类的方法列表
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
// 如果父类方法列表中有 !!!存入的是自己的缓存列表,并不是存到父类的缓存列表!!!
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// 以上情况都没有 就会进入动态方法解析
if (resolver && !triedResolver) {
// 也就是我们熟悉的 【+resolveClassMethod or +resolveInstanceMethod.】
_class_resolveMethod(cls, sel, inst);
triedResolver = YES;
goto retry;
}
// 以上还没有找到 就会进入消息转发阶段
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
注释都写在后面了,流程图如下:
四、消息转发(_objc_msgForward_impcache)
这个方法是汇编实现的,objc-msg-arm64.s
中的汇编代码如下
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
其实它就是对__objc_msgForward
的一个封装而已
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
__objc_forward_handler
是runtime
的一个默认实现,代码在objc-runtime.m
__attribute__((noreturn)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
需要借助一个demo
来继续下面的探索,选择以下Forwarding_demo
工程
4.1、查看调用堆栈-【X86架构】
运行以后会闪退,函数堆栈如下
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000107ca2705 libobjc.A.dylib`objc_exception_throw
frame #1: 0x0000000108bfdf44 CoreFoundation`-[NSObject(NSObject) doesNotRecognizeSelector:] + 132
frame #2: 0x0000000108be3ed6 CoreFoundation`___forwarding___ + 1446
frame #3: 0x0000000108be5da8 CoreFoundation`__forwarding_prep_0___ + 120
* frame #4: 0x000000010738572c Forwarding_demo`-[ViewController viewDidLoad]
【...】
从函数堆栈可以看出调用完performSelector:
方法以后,代码就进入了CoreFoundation
框架了,然后就到了__forwarding_prep_0___
-> ___forwarding___
,CoreFoundation
代码是开源的可以去这里下载,我下载的是CF-1153.18 2
,遗憾的是虽然CoreFoundation
代码是开源的,但是苹果没有给出以上方法的实现。
4.1.1、断点查看方法实现
运行程序,分别增加2个断点
(lldb) breakpoint set -n '__forwarding_prep_0___'
Breakpoint 3: where = CoreFoundation`__forwarding_prep_0___, address = 0x00000001079efd30
(lldb) breakpoint set -n '___forwarding___'
Breakpoint 4: where = CoreFoundation`___forwarding___, address = 0x00000001079ed930
(lldb)
运行程序,程序首先进入__forwarding_prep_0___
CoreFoundation`__forwarding_prep_0___:
-> 0x1079efd30 <+0>: pushq %rbp
[...]
0x1079efda3 <+115>: callq 0x1079ed930 ; ___forwarding___
[...]
过掉这个断点程序会进入___forwarding___
CoreFoundation`___forwarding___:
-> 0x1079ed930 <+0>: pushq %rbp
0x1079ed931 <+1>: movq %rsp, %rbp
0x1079ed934 <+4>: pushq %r15
0x1079ed936 <+6>: pushq %r14
0x1079ed938 <+8>: pushq %r13
0x1079ed93a <+10>: pushq %r12
0x1079ed93c <+12>: pushq %rbx
0x1079ed93d <+13>: subq $0x28, %rsp
0x1079ed941 <+17>: movq 0x26c798(%rip), %rax ; (void *)0x000000010907b070: __stack_chk_guard
0x1079ed948 <+24>: movq (%rax), %rax
0x1079ed94b <+27>: movq %rax, -0x30(%rbp)
[...]
代码很长,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__
的汇编代码,但是可读性还是太差了,网上有人已经将汇编代码转成熟悉的样子,这个方法就是消息转发的全部了。
void __forwarding__(BOOL isStret, void *frameStackPointer, ...) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 4);
Class receiverClass = object_getClass(receiver);
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget) {
return objc_msgSend(forwardingTarget, sel, ...);
}
}
const char *className = class_getName(object_getClass(receiver));
const char *zombiePrefix = "_NSZombie_";
size_t prefixLen = strlen(zombiePrefix);
if (strncmp(className, zombiePrefix, prefixLen) == 0) {
CFLog(kCFLogLevelError,
@"-[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
sel_getName(sel),
receiver);
<breakpoint-interrupt>
}
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature) {
BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
if (signatureIsStret != isStret) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.",
sel_getName(sel),
signatureIsStret ? "" : not,
isStret ? "" : not);
}
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature
frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
receiver,
className);
return 0;
}
}
}
const char *selName = sel_getName(sel);
SEL *registeredSel = sel_getUid(selName);
if (sel != registeredSel) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
sel,
selName,
registeredSel);
} else if (class_respondsToSelector(receiverClass, @selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
receiver,
className);
}
// The point of no return.
kill(getpid(), 9);
}
涉及到的顺序:
-
forwardingTargetForSelector:
; -
methodSignatureForSelector:
; -
forwardInvocation:
; -
doesNotRecognizeSelector:
。