最近再次遇到多线程读写导致的crash 问题,写了一个测试demo,记录分析过程。
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];
});
}
上面是暴力重现多线程读写的崩溃,在debug环境下,开启zombie ,窗口会输出:
message sent to deallocated instance 0x170200c50
上面用了10000次碰撞才触发崩溃,日常debug 环境下很难出现。但是到了线上环境,用户量一大,问题就出现了。然后,我们只能通过崩溃日志查找崩溃。
下面截取有用的崩溃日志部分:
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 访问了非法的地址(地址还没有从系统映射到当前进程的内存空间), 一般是野指针导致, 而野指针一般由于多线程操作对象导致.
SIGABRT 一般是Exception或者其他的代码主动退出的问题.
SIGTRAP 代码里面触发了调试指令, 该指令可能由编译器提供的trap方法触发, 如'__builtin_trap()'
SIGBUS 一般由于地址对齐问题导致, 单纯的OC代码挺难触发的, 主要是系统库方法或者其他c实现的方法导致
SIGILL 表示执行了非法的cpu指令, 但是一般是由于死循环导致
通过崩溃日志,定位到崩溃的点在:
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 ,每个frame 都有一个编号,它是当前frame 在整个调用栈的索引。看到frame 3 是demo代码调用的地方,当前pc 地址** 0x0000000184f48894** 对应frame 0 调用地址,而其他的frame 都是历史记录,不会保存当前frame所有寄存器的值,只存了lr 寄存器的内容(FYI: lr 是方法调用完之后,要返回的地址)。
从frame 2 就可以知道,对象被over release 了。实际情况一般是:丢失重要的堆栈信息。下面纯粹是在只有frame 3 的堆栈下,怎么定位问题。
frame 3 ,只有一个 setObject:也就是: self.object = [TestObject new]; 咋一看,不怎么可能崩溃。下面来分析一下:
可以看到堆栈地址是: 0x00000001000e03f4,程序加载到内存的地址在0x1000d8000 - 0x1000e3fff 之间。
通过计算 0x00000001000e03f4 - 0x1000d8000 = 0x83F4。
0x83F4 为相对偏移,这时候使用hopper 看看在0x83F4 究竟是什么。
frame 3 的lr 寄存器保存了调用方法的下一个指令地址,那么可以确定崩溃发生在: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
// 保存方法调用的现场
00000001000083c0 adrp x8, #0x10000d000
00000001000083c4 add x8, x8, #0x538 ; _OBJC_IVAR_$_ViewController._object
// 动态定位获取ViewController._object的描述地址, 放入x8
00000001000083c8 stur x0, [x29, #-0x8]
00000001000083cc str x1, [sp, #0x10]
00000001000083d0 str x2, [sp, #0x8]
// 把参数self/selector/传进来的TestObject对象, 存到栈里
00000001000083d4 ldr x0, [sp, #0x8]
00000001000083d8 ldur x1, [x29, #-0x8]
00000001000083dc ldrsw x8, x8
00000001000083e0 add x8, x1, x8
// 从x8里把_object的在ViewController对象的偏移量取出来, 并与x1相加, 也就是`self指针+偏移量`, 结果存在x8 里面
00000001000083e4 str x0, sp
// 把传进来的对象存入栈
00000001000083e8 mov x0, x8
// 把`self指针+偏移量`指针放入x0
00000001000083ec ldr x1, sp
// 把传进来的对象从栈里取出来放到x1
00000001000083f0 bl imp___stubs__objc_storeStrong
// 把x1里传进来的对象赋值给x0, 然后强引用一次
00000001000083f4 ldp x29, x30, [sp, #0x20]
00000001000083f8 add sp, sp, #0x30
// 恢复最前面保存的现场
00000001000083fc ret
// 返回
上面其实就是一段setter 的代码,崩溃发生在imp___stubs__objc_storeStrong,通过查看苹果开源代码: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 并不是原子性操作,当线程A可能执行到*location = obj 时,另外一个线程B执行 prev = *location; 。那么当线程A继续执行到objc_release(prev); 线程B 继续执行 ,跑到objc_release(prev), 此刻,prev已经被释放过了。Crash ~~~