最近再次遇到多線程讀寫(xiě)導(dǎo)致的crash 問(wèn)題,寫(xiě)了一個(gè)測(cè)試demo厉碟,記錄分析過(guò)程喊巍。
for (int i = 0; i < 10000; i++)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.object = [TestObject new];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.object = [TestObject new];
});
}
上面是暴力重現(xiàn)多線程讀寫(xiě)的崩潰,在debug環(huán)境下墨榄,開(kāi)啟zombie ,窗口會(huì)輸出:
message sent to deallocated instance 0x170200c50
上面用了10000次碰撞才觸發(fā)崩潰玄糟,日常debug 環(huán)境下很難出現(xiàn)。但是到了線上環(huán)境袄秩,用戶量一大阵翎,問(wèn)題就出現(xiàn)了逢并。然后,我們只能通過(guò)崩潰日志查找崩潰郭卫。
下面截取有用的崩潰日志部分:
Incident Identifier: A22F5FFF-F98D-4F3B-95C3-45790E61F049
CrashReporter Key: 33c3939d695bcfab6c9a16efca18399fae8a83c3
Hardware Model: iPhone6,2
Process: Crash_mulThread [716]
Path: /private/var/containers/Bundle/Application/7CCB0B27-4B51-4D77-B571-A49153C8E8B7/Crash_mulThread.app/Crash_mulThread
Identifier: vedon.Crash-mulThread
Version: 1 (1.0)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: vedon.Crash-mulThread [1266]
Date/Time: 2017-05-05 23:58:50.9184 +0800
Launch Time: 2017-05-05 23:58:50.6346 +0800
OS Version: iPhone OS 10.2.1 (14D27)
Report Version: 104
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x00000003a42abec8
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAL, Code 0xb
Terminating Process: exc handler [0]
Triggered by Thread: 4
Filtered syslog:
None found
Thread 4 name: Dispatch queue: com.apple.root.default-qos
Thread 4 Crashed:
0 libobjc.A.dylib 0x0000000184f48894 objc_class::demangledName(bool) + 28
1 libobjc.A.dylib 0x0000000184f5bc04 objc_object::overrelease_error() + 24
2 libobjc.A.dylib 0x0000000184f5bc04 objc_object::overrelease_error() + 24
3 Crash_mulThread 0x00000001000e03f4 -[ViewController setObject:] (ViewController.m:13)
4 Crash_mulThread 0x00000001000e0148 __29-[ViewController viewDidLoad]_block_invoke (ViewController.m:29)
5 libdispatch.dylib 0x00000001853921fc _dispatch_call_block_and_release + 24
6 libdispatch.dylib 0x00000001853921bc _dispatch_client_callout + 16
7 libdispatch.dylib 0x00000001853a0a4c _dispatch_queue_override_invoke + 732
8 libdispatch.dylib 0x00000001853a234c _dispatch_root_queue_drain + 572
9 libdispatch.dylib 0x00000001853a20ac _dispatch_worker_thread3 + 124
10 libsystem_pthread.dylib 0x000000018559b2a0 _pthread_wqthread + 1288
11 libsystem_pthread.dylib 0x000000018559ad8c start_wqthread + 4
Thread 4 crashed with ARM Thread State (64-bit):
x0: 0x00000003a42abea8 x1: 0x0000000000000000 x2: 0x000000017401a1f0 x3: 0x000000017401a200
x4: 0x00000001700f0e00 x5: 0x0000000000000000 x6: 0x0000000000000000 x7: 0x0000000000000000
x8: 0xbaddf653a42abead x9: 0x000009a1000e5665 x10: 0xffffe9a1000e5665 x11: 0x000000330000007f
x12: 0x000000010101e110 x13: 0x000005a1000e554d x14: 0x00000001a597a340 x15: 0x0000000000397c01
x16: 0x0000000184f580f4 x17: 0x00000001000e03b4 x18: 0x0000000000000000 x19: 0x0000000170019be0
x20: 0x00000003a42abea8 x21: 0x0000000000000000 x22: 0x0000000000000000 x23: 0x00000001aa54d200
x24: 0x000000016e1bf0e0 x25: 0x00000001abc326c0 x26: 0x0000000000000014 x27: 0x0000000000000004
x28: 0xffffffffffffffff fp: 0x000000016e1bed10 lr: 0x0000000184f5bc04
sp: 0x000000016e1becd0 pc: 0x0000000184f48894 cpsr: 0x80000000
Binary Images:
0x1000d8000 - 0x1000e3fff Crash_mulThread arm64 <e2e3d2adf95930e19b6da09621898c31> /var/containers/Bundle/Application/7CCB0B27-4B51-4D77-B571-A49153C8E8B7/Crash_mulThread.app/Crash_mulThread
0x1001dc000 - 0x10020bfff dyld arm64 <f54ed85a94253887886a8028e20ed8ba> /usr/lib/dyld
0x184ebc000 - 0x184ebdfff libSystem.B.dylib arm64 <1b4d75209f4a37969a9575de48d48668> /usr/lib/libSystem.B.dylib
0x184ebe000 - 0x184f13fff libc++.1.dylib arm64 <b2db8b1d09283b7bafe1b2933adc5dfd> /usr/lib/libc++.1.dylib
0x184f14000 - 0x184f34fff libc++abi.dylib arm64 <e3419bbaface31b5970c6c8d430be26d> /usr/lib/libc++abi.dylib
0x184f38000 - 0x185311fff libobjc.A.dylib arm64 <538f809dcd7c35ceb59d99802248f045> /usr/lib/libobjc.A.dylib
FYI
SIGSEGV 訪問(wèn)了非法的地址(地址還沒(méi)有從系統(tǒng)映射到當(dāng)前進(jìn)程的內(nèi)存空間), 一般是野指針導(dǎo)致, 而野指針一般由于多線程操作對(duì)象導(dǎo)致.
SIGABRT 一般是Exception或者其他的代碼主動(dòng)退出的問(wèn)題.
SIGTRAP 代碼里面觸發(fā)了調(diào)試指令, 該指令可能由編譯器提供的trap方法觸發(fā), 如'__builtin_trap()'
SIGBUS 一般由于地址對(duì)齊問(wèn)題導(dǎo)致, 單純的OC代碼挺難觸發(fā)的, 主要是系統(tǒng)庫(kù)方法或者其他c實(shí)現(xiàn)的方法導(dǎo)致
SIGILL 表示執(zhí)行了非法的cpu指令, 但是一般是由于死循環(huán)導(dǎo)致
通過(guò)崩潰日志砍聊,定位到崩潰的點(diǎn)在:
0 libobjc.A.dylib 0x0000000184f48894 objc_class::demangledName(bool) + 28
1 libobjc.A.dylib 0x0000000184f5bc04 objc_object::overrelease_error() + 24
2 libobjc.A.dylib 0x0000000184f5bc04 objc_object::overrelease_error() + 24
3 Crash_mulThread 0x00000001000e03f4 -[ViewController setObject:] (ViewController.m:13)
每條崩潰堆棧的記錄稱為frame ,每個(gè)frame 都有一個(gè)編號(hào),它是當(dāng)前frame 在整個(gè)調(diào)用棧的索引贰军〔r颍看到frame 3 是demo代碼調(diào)用的地方,當(dāng)前pc 地址** 0x0000000184f48894** 對(duì)應(yīng)frame 0 調(diào)用地址词疼,而其他的frame 都是歷史記錄俯树,不會(huì)保存當(dāng)前frame所有寄存器的值,只存了lr 寄存器的內(nèi)容(FYI: lr 是方法調(diào)用完之后贰盗,要返回的地址)许饿。
從frame 2 就可以知道,對(duì)象被over release 了舵盈。實(shí)際情況一般是:丟失重要的堆棧信息陋率。下面純粹是在只有frame 3 的堆棧下,怎么定位問(wèn)題秽晚。
frame 3 ,只有一個(gè) setObject:也就是: self.object = [TestObject new]; 咋一看瓦糟,不怎么可能崩潰。下面來(lái)分析一下:
可以看到堆棧地址是: 0x00000001000e03f4赴蝇,程序加載到內(nèi)存的地址在0x1000d8000 - 0x1000e3fff 之間菩浙。
通過(guò)計(jì)算 0x00000001000e03f4 - 0x1000d8000 = 0x83F4。
0x83F4 為相對(duì)偏移扯再,這時(shí)候使用hopper 看看在0x83F4 究竟是什么芍耘。
frame 3 的lr 寄存器保存了調(diào)用方法的下一個(gè)指令地址,那么可以確定崩潰發(fā)生在:imp___stubs__objc_storeStrong熄阻,下面分析一下這段匯編做了什么斋竞。
00000001000083b4 sub sp, sp, #0x30 ; Objective C Implementation defined at 0x10000c478 (instance method), DATA XREF=0x10000c478
00000001000083b8 stp x29, x30, [sp, #0x20]
00000001000083bc add x29, sp, #0x20
// 保存方法調(diào)用的現(xiàn)場(chǎng)
00000001000083c0 adrp x8, #0x10000d000
00000001000083c4 add x8, x8, #0x538 ; _OBJC_IVAR_$_ViewController._object
// 動(dòng)態(tài)定位獲取ViewController._object的描述地址, 放入x8
00000001000083c8 stur x0, [x29, #-0x8]
00000001000083cc str x1, [sp, #0x10]
00000001000083d0 str x2, [sp, #0x8]
// 把參數(shù)self/selector/傳進(jìn)來(lái)的TestObject對(duì)象, 存到棧里
00000001000083d4 ldr x0, [sp, #0x8]
00000001000083d8 ldur x1, [x29, #-0x8]
00000001000083dc ldrsw x8, x8
00000001000083e0 add x8, x1, x8
// 從x8里把_object的在ViewController對(duì)象的偏移量取出來(lái), 并與x1相加, 也就是`self指針+偏移量`, 結(jié)果存在x8 里面
00000001000083e4 str x0, sp
// 把傳進(jìn)來(lái)的對(duì)象存入棧
00000001000083e8 mov x0, x8
// 把`self指針+偏移量`指針?lè)湃離0
00000001000083ec ldr x1, sp
// 把傳進(jìn)來(lái)的對(duì)象從棧里取出來(lái)放到x1
00000001000083f0 bl imp___stubs__objc_storeStrong
// 把x1里傳進(jìn)來(lái)的對(duì)象賦值給x0, 然后強(qiáng)引用一次
00000001000083f4 ldp x29, x30, [sp, #0x20]
00000001000083f8 add sp, sp, #0x30
// 恢復(fù)最前面保存的現(xiàn)場(chǎng)
00000001000083fc ret
// 返回
上面其實(shí)就是一段setter 的代碼,崩潰發(fā)生在imp___stubs__objc_storeStrong秃殉,通過(guò)查看蘋果開(kāi)源代碼:objc_storeStrong
void
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
objc_storeStrong 并不是原子性操作坝初,當(dāng)線程A可能執(zhí)行到*location = obj 時(shí),另外一個(gè)線程B執(zhí)行 prev = *location; 钾军。那么當(dāng)線程A繼續(xù)執(zhí)行到objc_release(prev); 線程B 繼續(xù)執(zhí)行 鳄袍,跑到objc_release(prev), 此刻吏恭,prev已經(jīng)被釋放過(guò)了拗小。Crash ~~~