OC property多线程读写

crash堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Thread 16 name:  xxx
Thread 16 Crashed:
0 libobjc.A.dylib 0x00000001846c9704 objc_object::release() (in libobjc.A.dylib) + 8
1 _demo_ 0x00000001008e2c38 -[BSAPMMachoManager getMainModuleFrameCount:] (in _demo_) (BSAPMMachoManager.m:127)
2 _demo_ 0x00000001008ed4c0 -[xxx fire] (in _demo_) (xxx.m:60)
3 _demo_ 0x00000001008e4a1c xxx (in _demo_) (xxx.m:52)
4 CoreFoundation 0x0000000185c271d8 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ (in CoreFoundation) + 28
5 CoreFoundation 0x0000000185c26eec __CFRunLoopDoTimer (in CoreFoundation) + 872
6 CoreFoundation 0x0000000185c267a8 __CFRunLoopDoTimers (in CoreFoundation) + 244
7 CoreFoundation 0x0000000185c243a4 __CFRunLoopRun (in CoreFoundation) + 1572
8 CoreFoundation 0x0000000185b522b8 CFRunLoopRunSpecific (in CoreFoundation) + 444
9 CoreFoundation 0x0000000185b9fb44 CFRunLoopRun (in CoreFoundation) + 112
10 _demo_ 0x00000001008e4b10 -[xxx runThreadLoop] (in _demo_) (xxx.m:100)
11 _demo_ 0x00000001008e48e4 -[xxx threadMain] (in _demo_) (xxx.m:44)
12 Foundation 0x000000018678ce68 __NSThread__start__ (in Foundation) + 1024
13 libsystem_pthread.dylib 0x0000000184d0d850 _pthread_body (in libsystem_pthread.dylib) + 240
14 libsystem_pthread.dylib 0x0000000184d0d760 _pthread_body (in libsystem_pthread.dylib) + 0


Binary Images:
0x10005c000 - 0x100fcffff +_demo_ arm64 <71fd4f4612913b2da625f5eb515bcb9d> /var/containers/Bundle/Application/F1846ED9-B732-42A9-B41F-F9C78B064E6A/_demo_.app/_demo_

源码

1
2
3
4
5
6
7
8
- (NSInteger)getMainModuleFrameCount:(NSArray *)frames {
[self getCurrentModuleInfo];
NSInteger count = 0;
if (!_mainModuleInfo) { //此处为 127行
return count;
}
...
}

定位crash变量

符号化显示第127行,难道就是这个_mainModuleInfo变量造成的?

实际上符号化指向的行号是不准确的,一方面oc语言会插入一些retain,release函数导致映射关系不准确,另一方面堆栈显示的是返回地址,并不是进入子函数的位置,因此需要从其他角度进行现场的还原。

直接调试ipa包

原理是通过对ipa包进行重签名就可以结合xcode进行调试了,可以使用IPAPatch或者MonkeyDev这类工具进行。

计算crash发生的地址,设置断点

1
2
3
4
1   _demo_                       0x00000001008e2c38 -[BSAPMMachoManager getMainModuleFrameCount:] (in _demo_) (BSAPMMachoManager.m:127)

Binary Images:
0x10005c000 - 0x100fcffff +_demo_ arm64 <71fd4f4612913b2da625f5eb515bcb9d> /var/containers/Bundle/Application/F1846ED9-B732-42A9-B41F-F9C78B064E6A/_demo_.app/_demo_
  • 计算偏移量

0x00000001008e2c38 - 0x10005c000 = 0x0000000000886c38

  • 在xcode调试控制台 获取ipa包的 载入地址

image list demo

1
2
(lldb) image list _demo_
[ 0] 71FD4F46-1291-3B2D-A625-F5EB515BCB9D 0x00000001000e0000 /Users/winnchen/Library/Developer/Xcode/DerivedData/BSUITestProject-dnxfwcbufkxrwkeokoqpxlkvnphw/Build/Products/Debug-iphoneos/BSUITestProject.app/_demo_
  • 计算实际地址,设置断点

0x00000001000e0000 + 0x0000000000886c38 = 0x0000000100966c38

1
breakpoint set -a 0x0000000100966c38

分析寄存器与变量之间的对应关系

进入断点后,进一步的分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
_demo_`___lldb_unnamed_symbol38887$$_demo_:
0x100966bdc <+0>: sub sp, sp, #0x140 ; =0x140
0x100966be0 <+4>: stp x28, x27, [sp, #0xe0]
0x100966be4 <+8>: stp x26, x25, [sp, #0xf0]
0x100966be8 <+12>: stp x24, x23, [sp, #0x100]
0x100966bec <+16>: stp x22, x21, [sp, #0x110]
0x100966bf0 <+20>: stp x20, x19, [sp, #0x120]
0x100966bf4 <+24>: stp x29, x30, [sp, #0x130]
0x100966bf8 <+28>: add x29, sp, #0x130 ; =0x130
0x100966bfc <+32>: mov x20, x0
0x100966c00 <+36>: adrp x8, 1774
0x100966c04 <+40>: ldr x8, [x8, #0xda8]
0x100966c08 <+44>: ldr x8, [x8]
0x100966c0c <+48>: stur x8, [x29, #-0x58]
0x100966c10 <+52>: mov x0, x2
0x100966c14 <+56>: bl 0x100dad620 ; symbol stub for: objc_retain
0x100966c18 <+60>: mov x19, x0
0x100966c1c <+64>: adrp x8, 2510
0x100966c20 <+68>: ldr x1, [x8, #0x6e8]
0x100966c24 <+72>: mov x0, x20
0x100966c28 <+76>: bl 0x100dad5e4 ; symbol stub for: objc_msgSend
0x100966c2c <+80>: mov x29, x29
0x100966c30 <+84>: bl 0x100dad644 ; symbol stub for: objc_retainAutoreleasedReturnValue
0x100966c34 <+88>: bl 0x100dad608 ; symbol stub for: objc_release
-> 0x100966c38 <+92>: adrp x8, 2536
0x100966c3c <+96>: ldrsw x8, [x8, #0x60]
  • 沿着函数参数进行数据流向分析

0x100966c38是函数返回地址,并不是函数进入的地址,实际上,是在0x100966c34当中执行了objc_release的方法,而objc_release接受了一个野指针所以也就造成了crash。

因此要分析0x100966c34传入的参数,第1个参数对应寄存器x0,第2个参数对应x1,以此类推。此外,x0同时也作为函数的返回值。

0x100966c30执行了objc_retainAutoreleasedReturnValue,也就是x0是来自于这个临时变量的。

大致的伪代码如下:

1
2
3
4
5
6
7
- (NSInteger)getMainModuleFrameCount:(NSArray *)frames {
objc_retain(frames); #对应0x100966c14
x0 = objc_msgSend(self, "getCurrentModuleInfo"); #对应0x100966c28
x0 = objc_retainAutoreleasedReturnValue(x0); #对应0x100966c30
objc_release(x0);
...
}

经过这里的分析,实际上导致crash的变量并不是_mainModuleInfo,而是[self getCurrentModuleInfo]返回的临时变量。

问题分析

定位到crash变量之后进行进一步的分析,为什么这个返回的临时变量会发生crash,首先确认一下是否是多线程操作造成了这种情况。

1
2
3
4
5
6
7
- (NSDictionary *)getCurrentModuleInfo {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
xxx
});
return _moduleInfo;
}

看起来此处逻辑简单,有一个只执行一次的block,然后就是返回一个实例变量。

继续查看代码确认是有另外一个线程对_moduleInfo进行赋值

1
2
3
4
5
- (void)updateModuleInfo {
NSDictionary *moduleInfo = xxx;
_moduleInfo = moduleInfo;
...
}

在多线程直接对实例变量进行读写是会造成crash的,需要进行保护,oc语法上提供了这种属性读写的语法糖,@property (atomic)

编译器会自动生成属性读写多线程安全的代码,通过@property 以及 self.xxx的方式就能避免上面的crash,当然这种自动生成的多线程安全代码也是比较简陋的,具体生成怎样的代码这里就不展开诉述了。