[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
中打下斷點
繼續(xù)調試會到宏CacheLookup
代碼處,由于此時傳的是NORMAL
,由于我們的代碼最終是跑在真機上的懦傍,所以我這里還是分析arm64
的匯編吧雹舀,x86
匯編留給你自己分析吧,讀懂arm64
的匯編代碼粗俱,x86
匯編不在話下區(qū)別就是使用的匯編寫法不同(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
如果你覺得一頭霧水的話我?guī)阋徊揭徊娇磪R編,首先初步看到這個匯編代碼會發(fā)現(xiàn)出現(xiàn)很多不知道的寄存器比如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
怜姿,入?yún)⑹欠旁?code>x1 = SEL, x16 = isa中的慎冤,最先執(zhí)行的是ldp x10, x11, [x16, #16]
,這行命令的意思是從x16
往下16
個字節(jié)長度開始沧卢,連續(xù)讀取8
+ 8
個字節(jié)的長度分別賦值給x10
和x11
蚁堤。
- 由于
x16 == isa
,那么x16 + 16
就是_buckets
的地址但狭,賦值給x10
披诗; - 由于
x10
所占的字節(jié)長度是8
,那么x11
就是接下來的8
個字節(jié)立磁,也就是讀取到了_mask
和_occupied
呈队,所以x11 == _mask + _occupied
,由于是小端存儲模式,_mask
被存放在低16
位唱歧,可以通過w11
取到宪摧。
所以執(zhí)行完ldp x10, x11, [x16, #16]
代碼粒竖,x10
和x11
內容如下
2.2.2.2、查找緩存列表
2.2.2.1
已經(jīng)找到了緩存_buckets
的地址几于,下面就開始查找緩存了,_buckets
的數(shù)據(jù)結構以及每個元素的地址可以通過下面的方式計算得出如下
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
既然已經(jīng)找到了存放緩存的地址蕊苗,接下來只要找到調用的方法在緩存中的索引值就可以了,我們需要找的方法是test1
孩革,他的SEL
為4294971226
。
-
and w12, w1, w11
初步計算出方法的索引值得运,4294971226 & 3 == 2
膝蜈; -
add x12, x10, x12, LSL #(1+3)
,步驟1計算出了&
的結果熔掺,接下來就把指針移動到饱搏,序號為2
的地方,左移4
位正好是16
個字節(jié)大小置逻,要想跳到哪個序號直接序號左移4
位即可推沸,再加上初始的地址x10
,就是序號的地址券坞。 -
ldp x17, x9, [x12]
鬓催,分別取出序號2
的_imp
和_key
賦值給寄存器x17
和x9
。
知道每個元素的地址計算方式了恨锚,那么就是挨個判斷是否是我們要找的方法了宇驾,我們初始值是序號2
,接下來進入判斷邏輯,流程圖如下猴伶。
由于我們是第一次調用是沒有緩存的课舍,所以即將進入方法__objc_msgSend_uncached
,不幸的是這個方法還是匯編代碼他挎。
三筝尾、方法列表查找 & 動態(tài)方法解析
如果緩存中沒有我們要找的方法,那么就會進入__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
如果懂函數(shù)棧幀的話,其實這個段匯編代碼很簡單呢撞,頭和尾其實是常規(guī)操作贸街,前面是開辟棧空間狸相,保護寄存器的值薛匪,尾部恢復寄存器的值,以及恢復棧平衡脓鹃,關鍵代碼其實是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);
// 如果父類緩存中有 !!娇跟!存入的是自己的緩存列表岩齿,并不是存到父類的緩存列表!0盹沈!
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) {
// 如果父類方法列表中有 3砸ァF蚍狻!存入的是自己的緩存列表岗憋,并不是存到父類的緩存列表K嗤怼!仔戈!
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// 以上情況都沒有 就會進入動態(tài)方法解析
if (resolver && !triedResolver) {
// 也就是我們熟悉的 【+resolveClassMethod or +resolveInstanceMethod.】
_class_resolveMethod(cls, sel, inst);
triedResolver = YES;
goto retry;
}
// 以上還沒有找到 就會進入消息轉發(fā)階段
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
注釋都寫在后面了关串,流程圖如下:
四、消息轉發(fā)(_objc_msgForward_impcache)
這個方法是匯編實現(xiàn)的监徘,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
的一個默認實現(xiàn)晋修,代碼在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
來繼續(xù)下面的探索,選擇以下Forwarding_demo
工程
4.1、查看調用堆棧-【X86架構】
運行以后會閃退凰盔,函數(shù)堆棧如下
(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]
【...】
從函數(shù)堆椃沈荆可以看出調用完performSelector:
方法以后,代碼就進入了CoreFoundation
框架了廊蜒,然后就到了__forwarding_prep_0___
-> ___forwarding___
趴拧,CoreFoundation
代碼是開源的可以去這里下載,我下載的是CF-1153.18 2
,遺憾的是雖然CoreFoundation
代碼是開源的,但是蘋果沒有給出以上方法的實現(xiàn)山叮。
4.1.1著榴、斷點查看方法實現(xiàn)
運行程序,分別增加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查看方法實現(xiàn)
除了直接添加斷點的方式锐借,還可以通過逆向CoreFoundation.framework
來窺探實現(xiàn)问麸,這樣更直觀,還能知道函數(shù)調用關系钞翔,CoreFoundation.framework
放在/System/Library/Frameworks/CoreFoundation.framework
找到了___forwarding___
實現(xiàn)严卖,并且知道是誰調用的,分別是框出來的部分__forwarding_prep_0___
和__forwarding_prep_1___
布轿,點擊進入__forwarding_prep_0___
哮笆,同樣的方式最后定位到了___CFInitialize
通過匯編確實看出___CFInitialize
調用了___forwarding___
方法来颤,但是開源的代碼根本沒有這個相關的影子。
4.1.3稠肘、分析forwarding實現(xiàn)
我們已經(jīng)逆向出來了__forwarding__
的匯編代碼福铅,但是可讀性還是太差了,網(wǎng)上有人已經(jīng)將匯編代碼轉成熟悉的樣子项阴,這個方法就是消息轉發(fā)的全部了滑黔。
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:
略荡。