[转载] GCDAsyncSocket 在 iOS15 出现 -[_NSThreadPerformInfo dealloc] 崩溃排查笔记

[转载] GCDAsyncSocket 在 iOS15 出现 -[_NSThreadPerformInfo dealloc] 崩溃排查笔记

转载: 原文地址

本文会通过对 NSThread 的原理进行分析,对 iOS 15 开始出现的 [_NSThreadPerformInfo dealloc] 相关崩溃进行定位,并提供相应的解决方案

一、背景

从 iOS 15.0 Beta5 开始,集成开源库 GCDAsyncSocket 的 APP 开始出现 -[_NSThreadPerformInfo dealloc] 相关的崩溃

Crash on iOS 15.0 Beta5 +[GCDAsyncSocket cfstreamThread] (GCDAsyncSocket.m:7596) · Issue #775 · robbiehanson/CocoaAsyncSocket · GitHub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Thread 32 name: GCDAsyncSocket-CFStream
Thread 32 Crashed:
0 libobjc.A.dylib 0x19b483c50 objc_release + 16
1 Foundation 0x184161344 -[_NSThreadPerformInfo dealloc] + 56
2 Foundation 0x1842d08dc __NSThreadPerform + 160
3 CoreFoundation 0x1829b069c CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION + 28
4 CoreFoundation 0x1829c12f0 __CFRunLoopDoSource0 + 208
5 CoreFoundation 0x1828fabf8 __CFRunLoopDoSources0 + 268
6 CoreFoundation 0x182900404 __CFRunLoopRun + 820
7 CoreFoundation 0x182913fc8 CFRunLoopRunSpecific + 600
8 Foundation 0x184134104 -[NSRunLoop+ 102660 (NSRunLoop) runMode:beforeDate:] + 236
9 MyApp 0x104990290 0x1026b0000 + 36569744
10 Foundation 0x184183950 NSThread__start + 764
11 libsystem_pthread.dylib 0x1f2745a60 _pthread_start + 148
12 libsystem_pthread.dylib 0x1f2744f5c thread_start + 8

因为该堆栈的崩溃发生在 系统库,导致很多 APP 都难以解决此类问题

二、GCDAsyncSocket 简介

GCDAsyncSocket 是一个 TCP 库。它建在 Grand Central Dispatch 之上。

通常情况下,我们可以通过下面的代码创建一个 GCDAsyncSocket 的实例

1
socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];

并通过 connect... 方法建立一个连接

1
2
3
4
5
6
NSError *err = nil;
if (![socket connectToHost:@"deusty.com" onPort:80 error:&err]) // Asynchronous!
{
// If there was an error, it's likely something like "already connected" or "no delegate set"
NSLog(@"I goofed: %@", err);
}

GCDAsyncSocket 建立连接

GCDAsyncSocket 内部会先执行域名解析

1
int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0);

随后,通过 ip 创建 socket 和配置合适的参数

1
2
3
4
5
6
int socketFD = socket(family, SOCK_STREAM, 0);
...
int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]);

...
int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]);

当 socket 创建后,会创建对应 stream

并通过 CFReadStreamSetClient 函数设置 client

最后通过 CFReadStreamScheduleWithRunLoop 注册回调任务的 runloop

当关闭连接时,我们需要通过 CFReadStreamScheduleWithRunLoop 反注册

After scheduling stream with a run loop, its client (set with CFReadStreamSetClient) is notified when various events happen with the stream, such as when it finishes opening, when it has bytes available, and when an error occurs. A stream can be scheduled with multiple run loops and run loop modes. Use CFReadStreamUnscheduleFromRunLoop to later remove stream from the run loop.

GCDAsyncSocket 通过 unscheduleCFStreams: 函数实现反注册

1
2
3
4
5
6
7
8
9
10
11
12
13
+ (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket
{
LogTrace();
NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread");

CFRunLoopRef runLoop = CFRunLoopGetCurrent();

if (asyncSocket->readStream)
CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode);

if (asyncSocket->writeStream)
CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode);
}

GCDAsyncSocket 的实例被释放时,会通过下面的代码将让 类GCDAsyncSocketcfstreamThread 线程执行 + (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket 方法

⚠️ 注意:withObject 参数是 GCDAsyncSocket 的实例
waitUntilDone 参数的值是 YES,表示会阻塞当前线程

1
2
3
4
[[self class] performSelector:@selector(unscheduleCFStreams:)
onThread:cfstreamThread
withObject:self
waitUntilDone:YES];

三、 跨线程执行任务

现在,我们重点看一下系统库是如何实现 performSelector:onThread: 方法的。

通过前面的分析,我们可以注意到,系统库必须完成以下两个任务:

  1. 在另外的线程执行代码
  2. 阻塞当前线程,直到另一个线程执行完毕时恢复执行

本段内容是建立在iOS 12.4.6 (16G183) 系统版本上面进行分析

ReleadeTrack

为了方便对 GCDAsyncSocket 的引用计数进行追踪,我创建了一个子类 ReleadeTrack,读者可以将本文中出现 ReleadeTrack 的地方理解为 GCDAsyncSocket

1
2
3
@interface ReleadeTrack : GCDAsyncSocket

@end

_NSThreadPerformInfo

performSelector:onThread:... 方法执行时,系统库会创建一个私有类 _NSThreadPerformInfo 的实例

_NSThreadPerformInfo 的实例会持有消息相关的信息

⚠️ 注意:_NSThreadPerformInfo 通过 argument 持有了开发者传入的参数 <ReleadeTrack: 0x10160f0a0>, 通过 waiter 持有了 NSCondition 的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(lldb) ivars 0x1015b4c90
<_NSThreadPerformInfo: 0x1015b4c90>:
in _NSThreadPerformInfo:
target (id): <ReleadeTrack: 0x100cabee0>
selector (SEL): unscheduleCFStreams:
argument (id): <ReleadeTrack: 0x10160f0a0>
modes (NSMutableArray*): <__NSArrayM: 0x1015b4b40>
waiter (NSCondition*): <NSCondition: 0x1015b3c70>
signalled (char*): Value not representable, *
in NSObject:
isa (Class): _NSThreadPerformInfo (isa, 0x21a21933b3d1)

(lldb)

并通过调用 -[NSThread _nq:]存到 NSThread 的私有属性 _private

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
(lldb) po $x0
<NSThread: 0x100f1f9a0>{number = 2, name = GCDAsyncSocket-CFStream}

(lldb) po $x2
<_NSThreadPerformInfo: 0x100f058e0>

(lldb) ivars 0x100f1f9a0
<NSThread: 0x100f1f9a0>:
in NSThread:
_private (id): <_NSThreadData: 0x100f318a0>
_bytes (unsigned char[44]): Value not representable, [44C]
in NSObject:
isa (Class): NSThread (isa, 0x41a21933b471)

(lldb) ivars 0x100f318a0
<_NSThreadData: 0x100f318a0>:
in _NSThreadData:
dict (id): <__NSDictionaryM: 0x100f30130>
name (id): @"GCDAsyncSocket-CFStream"
target (id): <ReleadeTrack: 0x100707f58>
selector (SEL): cfstreamThread:
argument (id): nil
seqNum (int): 2
qstate (unsigned char): Value not representable, C
qos (char): 0
cancel (unsigned char): Value not representable, C
status (unsigned char): Value not representable, C
performQ (id): nil
performD (NSMutableDictionary*): nil
attr (struct _opaque_pthread_attr_t): {
__sig (long): 1414022209
__opaque (char[56]): Value not representable, [56c]
}
tid (struct _opaque_pthread_t*): 0x100f31928 -> 0x16fa93000
pri (double): 0.5
defpri (double): 0.5
in NSObject:
isa (Class): _NSThreadData (isa, 0x1a21933b449)

(lldb)

该步执行完毕后,NSThread 的私有属性 _private 会指向 _NSThreadData 的实例,_NSThreadData 的私有属性 performQ 会保存 <_NSThreadPerformInfo: 0x100f058e0>

同时,-[NSThread _nq:] 方法会创建 CFRunloopSource 的实例并注册到 GCDAsyncSocket-CFStream 线程

NSCondition

因为 GCDAsyncSocket 销毁时,会将 waitUntilDone:YES 当做参数传入,所以,NSCondition 的实例会被创建并用于阻塞当前线程

NSCondition 的实例初始化时,会分别初始化pthread_mutex_tpthread_cond_init

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
27
28
29
30
31
Foundation`-[NSCondition init]:
0x1e088accc <+0>: sub sp, sp, #0x30 ; =0x30
0x1e088acd0 <+4>: stp x20, x19, [sp, #0x10]
0x1e088acd4 <+8>: stp x29, x30, [sp, #0x20]
0x1e088acd8 <+12>: add x29, sp, #0x20 ; =0x20
0x1e088acdc <+16>: mov x19, x0
0x1e088ace0 <+20>: bl 0x1df0f6370 ; object_getIndexedIvars
0x1e088ace4 <+24>: mov x20, x0
0x1e088ace8 <+28>: adrp x1, 232117
0x1e088acec <+32>: add x1, x1, #0x868 ; =0x868
-> 0x1e088acf0 <+36>: bl 0x1dfb3d1d0 ; symbol stub for: pthread_mutex_init
0x1e088acf4 <+40>: cbz w0, 0x1e088ad1c ; <+80>
0x1e088acf8 <+44>: adrp x8, 223297
0x1e088acfc <+48>: ldr x8, [x8, #0x220]
0x1e088ad00 <+52>: stp x19, x8, [sp]
0x1e088ad04 <+56>: adrp x8, 180729
0x1e088ad08 <+60>: add x1, x8, #0x417 ; =0x417
0x1e088ad0c <+64>: mov x0, sp
0x1e088ad10 <+68>: bl 0x1df10b720 ; objc_msgSendSuper2
0x1e088ad14 <+72>: mov x19, #0x0
0x1e088ad18 <+76>: b 0x1e088ad2c ; <+96>
0x1e088ad1c <+80>: add x0, x20, #0x40 ; =0x40
0x1e088ad20 <+84>: mov x1, #0x0
0x1e088ad24 <+88>: bl 0x1dfb3d11c ; symbol stub for: pthread_cond_init
0x1e088ad28 <+92>: str xzr, [x20, #0x70]
0x1e088ad2c <+96>: mov x0, x19
0x1e088ad30 <+100>: ldp x29, x30, [sp, #0x20]
0x1e088ad34 <+104>: ldp x20, x19, [sp, #0x10]
0x1e088ad38 <+108>: add sp, sp, #0x30 ; =0x30
0x1e088ad3c <+112>: ret

随后,通过 -[NSCondition lock] 方法间接调用 pthread_mutex_lock 加锁

1
2
3
4
5
6
7
Foundation`-[NSCondition lock]:
-> 0x1e088ae1c <+0>: stp x29, x30, [sp, #-0x10]!
0x1e088ae20 <+4>: mov x29, sp
0x1e088ae24 <+8>: bl 0x1df0f6370 ; object_getIndexedIvars
0x1e088ae28 <+12>: ldp x29, x30, [sp], #0x10
0x1e088ae2c <+16>: b 0x1e0acb720 ; symbol stub for: pthread_mutex_lock

然后,通过 -[NSCondition wait] 方法间接调用 pthread_cond_wait 函数,阻塞当前线程

-[NSCondition wait]方法内部调用 pthread_cond_wait 函数实现阻塞

1
2
3
4
5
6
7
8
9
Foundation`-[NSCondition wait]:
0x1e08f249c <+0>: stp x29, x30, [sp, #-0x10]!
0x1e08f24a0 <+4>: mov x29, sp
0x1e08f24a4 <+8>: bl 0x1df0f6370 ; object_getIndexedIvars
0x1e08f24a8 <+12>: mov x1, x0
0x1e08f24ac <+16>: add x0, x0, #0x40 ; =0x40
0x1e08f24b0 <+20>: ldp x29, x30, [sp], #0x10
-> 0x1e08f24b4 <+24>: b 0x1dfb2ee7c ; pthread_cond_wait

最后,通过 svc 调用阻塞当前线程

GCDAsyncSocket-CFStream 线程

GCDAsyncSocket-CFStream 线程通过 runloop 机制和前面创建的CFRunLoopSource 回调给 __NSThreadPerformPerform 函数

随后,__NSThreadPerformPerform 函数通过performQueueDequeue查找可以被执行的_NSThreadPerformInfo

performQueueDequeue 存在的原因是部分_NSThreadPerformInfo通过下面的方法指定了 mode

1
2
3
4
5
6
7
- (void)performSelector:(SEL)aSelector
onThread:(NSThread *)thr
withObject:(nullable id)arg
waitUntilDone:(BOOL)wait
modes:(nullable NSArray<NSString *> *)array

API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

找到合适的任务后, __NSThreadPerformPerform 函数会通过调用 performSelector:withObject: 完成指定的任务

现在,我们通过在+[GCDAsyncSocket scheduleCFStreams:] 添加断点并打印一下堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(lldb) bt
* thread #4, name = 'GCDAsyncSocket-CFStream', stop reason = breakpoint 16.1
* frame #0: 0x0000000101111828 CocoaAsyncSocket`+[GCDAsyncSocket scheduleCFStreams:](self=ReleadeTrack, _cmd="scheduleCFStreams:", asyncSocket=0x0000000101b0e0b0) at GCDAsyncSocket.m:7700:2
frame #1: 0x00000001e09a2690 Foundation`__NSThreadPerformPerform + 336
frame #2: 0x00000001dfeacf1c CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
frame #3: 0x00000001dfeace9c CoreFoundation`__CFRunLoopDoSource0 + 88
frame #4: 0x00000001dfeac784 CoreFoundation`__CFRunLoopDoSources0 + 176
frame #5: 0x00000001dfea76c0 CoreFoundation`__CFRunLoopRun + 1004
frame #6: 0x00000001dfea6fb4 CoreFoundation`CFRunLoopRunSpecific + 436
frame #7: 0x00000001e087595c Foundation`-[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 300
frame #8: 0x0000000101111780 CocoaAsyncSocket`+[GCDAsyncSocket cfstreamThread:](self=ReleadeTrack, _cmd="cfstreamThread:", unused=0x0000000000000000) at GCDAsyncSocket.m:7689:25
frame #9: 0x00000001e09a24a0 Foundation`__NSThread__start__ + 984
frame #10: 0x00000001dfb392c0 libsystem_pthread.dylib`_pthread_body + 128
frame #11: 0x00000001dfb39220 libsystem_pthread.dylib`_pthread_start + 44
frame #12: 0x00000001dfb3ccdc libsystem_pthread.dylib`thread_start + 4
(lldb)

通过堆栈,可以看到 __NSThreadPerformPerform 调用了 +[GCDAsyncSocket scheduleCFStreams:]

清理 _NSThreadPerformInfo

GCDAsyncSocket-CFStream 线程执行任务结束后,会通过通过releasestr xzr, [x25, x8] 指令销毁 _NSThreadPerformInfo 存储的各种数据

通过反汇编工具,可以反解到以下代码:

1
2
3
r25->target = 0x0;
r25->argument = 0x0;
r25->modes = 0x0;

恢复阻塞的线程

随后,__NSThreadPerformPerform 函数会先调用 -[NSCondition lock]加锁,并通过-[NSCondition signal] 间接调用 pthread_cond_signal 函数的方式恢复另外一个被阻塞的线程


-[NSCondition signal] 方法通过 pthread_cond_signal 函数通知

1
2
3
4
5
6
7
8
Foundation`-[NSCondition signal]:
-> 0x1e08f7dc4 <+0>: stp x29, x30, [sp, #-0x10]!
0x1e08f7dc8 <+4>: mov x29, sp
0x1e08f7dcc <+8>: bl 0x1df0f6370 ; object_getIndexedIvars
0x1e08f7dd0 <+12>: add x0, x0, #0x40 ; =0x40
0x1e08f7dd4 <+16>: ldp x29, x30, [sp], #0x10
0x1e08f7dd8 <+20>: b 0x1dfb3d128 ; symbol stub for: pthread_cond_signal

四、iOS 15.x 新版本的跨线程执行任务

从某个版本开始,苹果对 _NSThreadPerformInfo 相关的设计进行了调整。下面以 iOS 15.2 (19C57) 为例进行分析

_NSThreadPerformInfo

因为 arm64e 架构的原因,_NSThreadPerformInfo 新增了一个 _pac_signature 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(lldb) ivars $x0
<_NSThreadPerformInfo: 0x107824030>:
in _NSThreadPerformInfo:
_target (id): <ReleadeTrack: 0x105114018>
_selector (SEL): scheduleCFStreams:
_argument (id): <ReleadeTrack: 0x1079074d0>
_pac_signature (AQ): Value not representable, AQ
_modes (NSArray*): <__NSSingleObjectArrayI: 0x107823f10>
_waiter (NSCondition*): <NSCondition: 0x107825010>
_state (int): 2
in NSObject:
isa (Class): _NSThreadPerformInfo (isa, 0x1dae17a51)

(lldb)

同时,_NSThreadPerformInfo现在有 3 个实例方法

1
2
3
4
22: regex = '_NSThreadPerformInfo', locations = 3, resolved = 3, hit count = 0
22.1: where = Foundation`-[_NSThreadPerformInfo dealloc], address = 0x0000000182670a1c, resolved, hit count = 0
22.2: where = Foundation`-[_NSThreadPerformInfo signal:], address = 0x00000001827dff68, resolved, hit count = 0
22.3: where = Foundation`-[_NSThreadPerformInfo wait], address = 0x00000001827dffcc, resolved, hit count = 0

-[_NSThreadPerformInfo wait]

当需要阻塞当前线程时,会通过-[_NSThreadPerformInfo wait] 间接调用 -[NSCondition lock]-[NSCondition wait] -[NSCondition unlock] 实现

-[_NSThreadPerformInfo signal:]

当需要恢复被阻塞的线程时,会通过-[_NSThreadPerformInfo signal:] 间接调用-[NSCondition lock]-[NSCondition signal] 实现

-[_NSThreadPerformInfo dealloc]

另外一个重要改变是由-[_NSThreadPerformInfo dealloc] 负责释放各种属性,比如_argument就是由下面的代码触发释放的

_NSThreadPerformInfo 生命周期分析

现在,我们先看看 _NSThreadPerformInfo 的引用计数变化情况

正常情况:

非正常情况:

我们可以注意到,系统库销毁 _NSThreadPerformInfo 的时机存在两种情况:

  1. 触发performSelector:onThread: 的线程销毁
  2. GCDAsyncSocket-CFStream 线程销毁

对于第二种情况,我们结合两个线程的执行顺序梳理后如下:

A 代表触发 performSelector:onThread: 的线程
B 代表 GCDAsyncSocket-CFStream 线程

经过前面的分析,我们可以发现当 A 线程 通过free释放GCDAsyncSocket 实例的内存所有权后,

GCDAsyncSocket-CFStream 线程仍然会通过_NSThreadPerformInfo 持有悬垂指针,并通过 objc_release 减少引用计数

五、objc 内存管理机制

为了更好的理解崩溃堆栈,我们需要简单的回顾一下objc的内存管理机制

示例代码

1
Arc *obj = [Arc new];

ARC 环境下,上面的代码会变成以下的汇编代码:

tip: xor esi, esi 指令是通过异或操作将 esi 寄存器清零

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    0x100003a00 <+0>:  push   rbp
0x100003a01 <+1>: mov rbp, rsp
0x100003a04 <+4>: sub rsp, 0x20
0x100003a08 <+8>: mov qword ptr [rbp - 0x18], rdi
0x100003a0c <+12>: mov qword ptr [rbp - 0x10], rsi
0x100003a10 <+16>: mov rdi, qword ptr [rip + 0x49d1] ; (void *)0x0000000100008408: Arc
0x100003a17 <+23>: call 0x100003d1a ; symbol stub for: objc_opt_new
0x100003a1c <+28>: mov qword ptr [rbp - 0x8], rax
# $rbp-0x8 内存位置存储 obj 的地址
0x100003a20 <+32>: lea rdi, [rbp - 0x8]
# 通过 lea 让 $rdi 寄存器 存储 $rbp-0x8
0x100003a24 <+36>: xor esi, esi
# 通过 xor 指令是通过异或操作将 esi 寄存器清零
0x100003a26 <+38>: call 0x100003d32 ; symbol stub for: objc_storeStrong
# 调用 objc_storeStrong 函数将 实例的地址 和 nil 当做参数传入
-> 0x100003a2b <+43>: add rsp, 0x20
0x100003a2f <+47>: pop rbp
0x100003a30 <+48>: ret

编译器通过添加 objc_storeStrong() 函数将对象进行销毁

objc_storeStrong

objc_storeStrong 的实现逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
void
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}

1
2
rdi = 0x00007ffeefbf8868
rsi = 0x0000000000000000

因为传入的第二个参数是 nil,所以,该步操作后,obj 指向 nil,确保 obj 不会出现悬垂指针。

tip:该设计可以保证 arc 下,可以有效的减少悬垂指针问题

等价于 mrc 代码:

1
2
3
id prev = obj;
obj = nil;
objc_release(prev);

objc_release

1
2
3
4
5
6
7
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (obj->isTaggedPointerOrNil()) return;
return obj->release();
}

objc_release 内部的逻辑是先判断被降低引用计数的对象是否属于 tagged-pointer 或者 nil,如果是可以避免后续的处理

objc_object::release()

随后,开始通过 objc_object::release() 进行再次转发

1
2
3
4
5
6
7
8
9
10
11
/// A pointer to an instance of a class.
typedef struct objc_object *id;

// Equivalent to calling [this release], with shortcuts if there is no override
inline void
objc_object::release()
{
ASSERT(!isTaggedPointer());

rootRelease(true, RRVariant::FastOrMsgSend);
}

objc_object::rootRelease

objc_object::rootRelease 主要进行以下任务:

  1. 通过 getDecodedClass 获取 class
  2. 通过 hasCustomRR 判断该类是否存在自定义的 release 方法,如果存在,则通过objc_msgSend 转发
  3. 执行引用计数减一的操作
  4. 随后判断是否需要执行 dealloc 的流程
  5. 如果需要,会通过开始执行 dealloc 方法
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return false;

bool sideTableLocked = false;

isa_t newisa, oldisa;

oldisa = LoadExclusive(&isa.bits);

if (variant == RRVariant::FastOrMsgSend) {
// These checks are only meaningful for objc_release()
// They are here so that we avoid a re-load of the isa.
if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
ClearExclusive(&isa.bits);
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
swiftRelease.load(memory_order_relaxed)((id)this);
return true;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
return true;
}
}

if (slowpath(!oldisa.nonpointer)) {
// a Class is a Class forever, so we can perform this checkonce
// outside of the CAS loop
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa.bits);
return false;
}
}

retry:
do {
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
return sidetable_release(sideTableLocked, performDealloc);
}
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa.bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
return false;
}

// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

if (slowpath(newisa.isDeallocating()))
goto deallocate;

if (variant == RRVariant::Full) {
if (slowpath(sideTableLocked)) sidetable_unlock();
} else {
ASSERT(!sideTableLocked);
}
return false;

underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate

// abandon newisa to undo the decrement
newisa = oldisa;

if (slowpath(newisa.has_sidetable_rc)) {
if (variant != RRVariant::Full) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}

// Transfer retain count from side table to inline storage.

if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
oldisa = LoadExclusive(&isa.bits);
goto retry;
}

// Try to remove some retain counts from the side table.
auto borrow = sidetable_subExtraRC_nolock(RC_HALF);

bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there

if (borrow.borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
bool didTransitionToDeallocating = false;
newisa.extra_rc = borrow.borrowed - 1; // redo the original decrement too
newisa.has_sidetable_rc = !emptySideTable;

bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);

if (!stored && oldisa.nonpointer) {
// Inline update failed.
// Try it again right now. This prevents livelock on LL/SC
// architectures where the side table access itself may have
// dropped the reservation.
uintptr_t overflow;
newisa.bits =
addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
newisa.has_sidetable_rc = !emptySideTable;
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
if (stored) {
didTransitionToDeallocating = newisa.isDeallocating();
}
}
}

if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
ClearExclusive(&isa.bits);
sidetable_addExtraRC_nolock(borrow.borrowed);
oldisa = LoadExclusive(&isa.bits);
goto retry;
}

// Decrement successful after borrowing from side table.
if (emptySideTable)
sidetable_clearExtraRC_nolock();

if (!didTransitionToDeallocating) {
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
}
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}

deallocate:
// Really deallocate.

ASSERT(newisa.isDeallocating());
ASSERT(isa.isDeallocating());

if (slowpath(sideTableLocked)) sidetable_unlock();

__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}

objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant) 关键代码整理后如下:

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
    if (variant == RRVariant::FastOrMsgSend) {
// These checks are only meaningful for objc_release()
// They are here so that we avoid a re-load of the isa.
if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
ClearExclusive(&isa.bits);
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
swiftRelease.load(memory_order_relaxed)((id)this);
return true;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
return true;
}
}
...
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
...
if (slowpath(newisa.isDeallocating()))
goto deallocate;
...
deallocate:
// Really deallocate.

if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
1
2
3
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}

getDecodedClass

现在,我们重点看一下第 1 步getDecodedClass的实现代码:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
inline Class
isa_t::getDecodedClass(bool authenticated) {
#if SUPPORT_INDEXED_ISA
if (nonpointer) {
return classForIndex(indexcls);
}
return (Class)cls;
#else
return getClass(authenticated);
#endif
}

inline Class
isa_t::getClass(MAYBE_UNUSED_AUTHENTICATED_PARAM bool authenticated) {
#if SUPPORT_INDEXED_ISA
return cls;
#else

uintptr_t clsbits = bits;

# if __has_feature(ptrauth_calls)
# if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
// Most callers aren't security critical, so skip the
// authentication unless they ask for it. Message sending and
// cache filling are protected by the auth code in msgSend.
if (authenticated) {
// Mask off all bits besides the class pointer and signature.
clsbits &= ISA_MASK;
if (clsbits == 0)
return Nil;
clsbits = (uintptr_t)ptrauth_auth_data((void *)clsbits, ISA_SIGNING_KEY, ptrauth_blend_discriminator(this, ISA_SIGNING_DISCRIMINATOR));
} else {
// If not authenticating, strip using the precomputed class mask.
clsbits &= objc_debug_isa_class_mask;
}
# else
// If not authenticating, strip using the precomputed class mask.
clsbits &= objc_debug_isa_class_mask;
# endif

# else
clsbits &= ISA_MASK;
# endif

return (Class)clsbits;
#endif
}


// a better definition is
// (uintptr_t)ptrauth_strip((void *)ISA_MASK, ISA_SIGNING_KEY)
// however we know that PAC uses bits outside of MACH_VM_MAX_ADDRESS
// so approximate the definition here to be constant
template <typename T>
static constexpr T coveringMask(T n) {
for (T mask = 0; mask != ~T{0}; mask = (mask << 1) | 1) {
if ((n & mask) == n) return mask;
}
return ~T{0};
}
const uintptr_t objc_debug_isa_class_mask = ISA_MASK & coveringMask(MACH_VM_MAX_ADDRESS - 1);

# define ISA_MASK 0x0000000ffffffff8ULL


虽然上面的代码看着比较复杂,但是经过编译器处理后,它就变为了以下代码:

1
2
0x1996c9b48 <+8>:   ldr    x8, [x0]
0x1996c9b4c <+12>: and x9, x8, #0xffffffff8

hasCustomRR

接下来,我们再看看第 2 步的实现代码:

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
27
28
29
30
31
// class or superclass has default retain/release/autorelease/retainCount/
// _tryRetain/_}isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR (1UL<<2)

struct objc_object {
private:
isa_t isa;
}

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

bool hasCustomRR() const {
return !bits.getBit(FAST_HAS_DEFAULT_RR);
}
}

struct class_data_bits_t {
friend objc_class;

// Values are the FAST_ flags above.
uintptr_t bits;
private:
bool getBit(uintptr_t bit) const
{
return bits & bit;
}
}

第 2 步的逻辑比较简单,编译处理后的汇编如下:

1
2
->  0x1996c9b50 <+16>:  ldr    x10, [x9, #0x20]
0x1996c9b54 <+20>: tbz w10, #0x2, 0x1996c9bb4 ; <+116>

编译器通过内联优化,最后会变成本次崩溃的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
libobjc.A.dylib`objc_release:
// 判断是否属于nil或者tag
0x1996c9b40 <+0>: cmp x0, #0x1 ; =0x1
0x1996c9b44 <+4>: b.lt 0x1996c9bb0 ; <+112>

// 读取 isa
0x1996c9b48 <+8>: ldr x8, [x0]

// 读取 class
0x1996c9b4c <+12>: and x9, x8, #0xffffffff8

// 读取 class 的 bits
-> 0x1996c9b50 <+16>: ldr x10, [x9, #0x20]

// 判断 class 的 bits 是否存在标志信息
0x1996c9b54 <+20>: tbz w10, #0x2, 0x1996c9bb4 ; <+116>

0x1996c9b58 <+24>: tbz w8, #0x0, 0x1996c9bd4 ; <+148>
0x1996c9b5c <+28>: mov x9, #0x100000000000000
0x1996c9b60 <+32>: lsr x10, x8, #55
0x1996c9b64 <+36>: cbz x10, 0x1996c9bb0 ; <+112>
0x1996c9b68 <+40>: subs x10, x8, x9
0x1996c9b6c <+44>: b.lo 0x1996c9b94 ; <+84>
0x1996c9b70 <+48>: mov x11, x8当

六、崩溃原理

现在,我们对前面的内容进行一下总结:

当出现悬垂指针并且悬垂指针指向的地址被其它代码重新申请后进行赋值操作,并且新值不符合 isTaggedPointer 规定,随后通过isa–> class–>bits 进行内存读取操作时就会触发崩溃。

小结:

经过前面的分析,我们可以得知,iOS 的新系统中存在一个 bug,该 bug 导致即使我们通过将参数waitUntilDone 设置为YES 的方式阻塞当前线程时,仍然存在触发悬垂指针的可能。

1
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

七、解决方案

因为崩溃的原因是调用performSelector:onThread:时,参数会被系统私有类持有导致崩溃,所以,我们可以通过以下方案解决:

  1. 通过单例持有 GCDAsyncSocket,避免调用 -[GCDAsyncSocket dealloc]

  2. 先主动调用-[GCDAsyncSocket disconnect],再释放GCDAsyncSocket的实例

  3. 通过调整withObject:的参数,避免将 GCDAsyncSocket 的实例进行传递

1
2
3
[[self class] performSelector:@selector(unscheduleCFWriteStreams:)
onThread:cfstreamThread
withObject:(__bridge id _Nullable)self->writeStream

完整的修复方案,可以访问链接获取

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道