1.前言
最近線上突然多了一些crash假勿,類型是SEGV_ACCER,一看就認(rèn)為是對象野指針了,基本都是多線程讀寫導(dǎo)致的;
但是仔細(xì)再一看crash堆棧,不是平常的objc_xxxx,而是 cache_getImp 這就有點(diǎn)怪了绑蔫,和平常的objc_msgSend或objc_retain等掛的不太一樣运沦;
我們實(shí)際的業(yè)務(wù)代碼掛在如下
[delegate performSelector:didReceiveDataSelector withObject:self withObject:data];
實(shí)際的crash堆棧是
Thread 0 Crashed:
0 libobjc.A.dylib 0x00000001837bcd04 _cache_getImp + 4
1 libobjc.A.dylib 0x00000001837b1900 _lookUpImpOrNil + 12
2 libobjc.A.dylib 0x00000001837a7578 class_respondsToSelector + 32
3 CoreFoundation 0x00000001845f41d8 ____forwarding___ + 372
4 CoreFoundation 0x00000001844da41c _CF_forwarding_prep_0 + 80
5 mttlite 0x0000000102cd94f4 -[QBASIHTTPRequest passOnReceivedData:] (QBASIHTTPRequest.m:2111)
6 Foundation 0x000000018503a0ec ___NSThreadPerformPerform + 340
7 CoreFoundation 0x0000000184597404 ___CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
+ 24
8 CoreFoundation 0x0000000184596c2c ___CFRunLoopDoSources0 + 276
2.分析
這個(gè)問題又是不好復(fù)現(xiàn)的問題,那只能沿著Crash堆棧反向去推了配深;
順手就找來objc源碼對比著看携添;
掛的代碼在_cache_getImp 這里實(shí)際上是一段匯編代碼(蘋果為了優(yōu)化效率該函數(shù)時(shí)直接匯編實(shí)現(xiàn)的),
oc對應(yīng)源碼如下
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
Class curClass;
IMP imp = nil;
Method meth;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);//這里是匯編實(shí)現(xiàn)篓叶,crash發(fā)生在這里
if (imp) return imp;
}
//...省略之后代碼
}
我們看到cache_getImp傳了2個(gè)參數(shù)cls和sel烈掠;嘗試一步步step into最終定位到了crash堆棧的匯編代碼如下:
這里x0就是上面C代碼的cls參數(shù)羞秤,x1就是上面C代碼的sel;
libobjc.A.dylib`cache_getImp:
-> 0x182a70d00 <+0>: and x16, x0, #0xffffffff8
0x182a70d04 <+4>: ldp x10, x11, [x16, #0x10]
0x182a70d08 <+8>: and w12, w1, w11
0x182a70d0c <+12>: add x12, x10, x12, lsl #4
0x182a70d10 <+16>: ldp x9, x17, [x12]
0x182a70d14 <+20>: cmp x9, x1
0x182a70d18 <+24>: b.ne 0x182a70d24 ; <+36>
0x182a70d1c <+28>: mov x0, x17
0x182a70d20 <+32>: ret
0x182a70d24 <+36>: cbz x9, 0x182a70d68 ; <+104>
0x182a70d28 <+40>: cmp x12, x10
0x182a70d2c <+44>: b.eq 0x182a70d38 ; <+56>
0x182a70d30 <+48>: ldp x9, x17, [x12, #-0x10]!
0x182a70d34 <+52>: b 0x182a70d14 ; <+20>
0x182a70d38 <+56>: add x12, x12, w11, uxtw #4
0x182a70d3c <+60>: ldp x9, x17, [x12]
0x182a70d40 <+64>: cmp x9, x1
0x182a70d44 <+68>: b.ne 0x182a70d50 ; <+80>
0x182a70d48 <+72>: mov x0, x17
0x182a70d4c <+76>: ret
0x182a70d50 <+80>: cbz x9, 0x182a70d68 ; <+104>
0x182a70d54 <+84>: cmp x12, x10
0x182a70d58 <+88>: b.eq 0x182a70d64 ; <+100>
0x182a70d5c <+92>: ldp x9, x17, [x12, #-0x10]!
0x182a70d60 <+96>: b 0x182a70d40 ; <+64>
0x182a70d64 <+100>: b 0x182a70d68 ; <+104>
0x182a70d68 <+104>: mov x0, #0x0
0x182a70d6c <+108>: ret
0x182a70d70 <+112>: nop
0x182a70d74 <+116>: nop
0x182a70d78 <+120>: nop
0x182a70d7c <+124>: nop
寄存器信息如下
(lldb) re read -a
General Purpose Registers:
x0 = 0x0000000106c13998 (void *)0x000001a106c139c1
x1 = 0x000000018e24619b "forwardingTargetForSelector:"
x2 = 0x0000000000000000
x3 = 0x0000000000000000
x4 = 0x0000000000000001
x5 = 0x0000000000000001
x6 = 0x0000000000000000
x7 = 0x0000000000000000
x8 = 0x0000000000000000
x9 = 0x0000000000000002
x10 = 0x000000012fed70f0
x11 = 0x000000130000001f
x12 = 0x000000012fed7150
x13 = 0x000001a106c1399d
x14 = 0x0000000000000000
x15 = 0x0005210000052100
x16 = 0x0000000106c13998 (void *)0x000001a106c139c1
x17 = 0x000000018378e3c0 CoreFoundation`_CF_forwarding_prep_0
x18 = 0x0000000000000000
x19 = 0x0000000000000000
x20 = 0x000000018e24619b "forwardingTargetForSelector:"
x21 = 0x0000000106c13998 (void *)0x000001a106c139c1
x22 = 0x0000000000000001
x23 = 0x0000000105f3cae0 "QBWeakProxy"
x24 = 0x0000000000000000
x25 = 0x000000018e24619b "forwardingTargetForSelector:"
x26 = 0x0000000000000000
x27 = 0x00000001b440f000 Foundation`NSUnarchiver.map
x28 = 0x000000010a878090
fp = 0x000000016dd1a580
lr = 0x0000000182a65974 libobjc.A.dylib`lookUpImpOrForward + 64
sp = 0x000000016dd1a530
pc = 0x0000000182a70d00 libobjc.A.dylib`cache_getImp
掛在了<+4>偏移左敌,即第2條指令這里瘾蛋,這里是在干什么;
第一步指令and x16, x0, #0xffffffff8這里應(yīng)該是為了內(nèi)存對齊矫限,
接著ldp x10, x11, [x16, #0x10] 這里是關(guān)鍵哺哼;這里是從x16對應(yīng)指針+16偏移處進(jìn)行間接尋址,取出對應(yīng)內(nèi)存地址開始的16字節(jié)內(nèi)容叼风,依次賦值給x10,x11取董;實(shí)際的作用是從Class的cache_t中取出緩存的方法列表;
關(guān)于為什么是+16无宿,參照如下結(jié)構(gòu)就能明白了
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
//```省略
}
因?yàn)閤0指向了一個(gè)Class對象茵汰, 所以其內(nèi)存地址偏移16字節(jié)的地方自然就是objc_class的第3個(gè)成員變量即cache_t cache;
好的孽鸡,crash正好就發(fā)生在這里蹂午,讀取這個(gè)內(nèi)存的時(shí)候出錯(cuò)了,而且這個(gè)地址是一個(gè)非法地址梭灿,因?yàn)榈刂房臻g不在主進(jìn)程的地址空間范圍內(nèi)画侣,只能懷疑是傳入的cls有問題
cls有問題,那就說明delegate的isa有問題堡妒,但是一般而言isa是不可能有問題的,否則crash的堆棧就不是在這里了配乱,那么只有一種可能delegate有問題,導(dǎo)致delegate+8地址偏移出錯(cuò)皮迟,取出來的isa地址越界搬泥,所以導(dǎo)致了這個(gè)crash;
3.結(jié)論
由上分析伏尼,初步認(rèn)為這個(gè)crash本質(zhì)還是因?yàn)閭魅氲膶ο笠爸羔樍朔揲荩瑢?dǎo)致獲取其isa時(shí)出現(xiàn)了問題,接著再操作讀取這個(gè)問題isa時(shí)自然就會(huì)野指針了爆阶;所以解決辦法還是對傳入對象做多線程讀寫互斥加鎖燥透;
至于為什么這里不是平常的objc_msgSend問題呢?
這里有個(gè)特別的地方在于之前我們?yōu)榱私鉀QASIHTTPRequest的assign delegate的問題辨图,引用了NSProxy去管理weak delegate班套;所以最終delegate實(shí)際上是一個(gè)把消息轉(zhuǎn)發(fā)給weak delegate的NSProxy實(shí)例,而不是一個(gè)NSObject實(shí)例