Crash 信息

线上存在一个持续好久的 Crash,因为没有清晰事务栈且量级不算大,让它成为了老赖之一,Crash 栈是这样的:

Thread 55
0  libdispatch.dylib              0x0000000188a8cf8c __os_object_release_internal_n +  80
1  libdispatch.dylib              0x0000000188a96eec __dispatch_lane_invoke +  1152
2  libdispatch.dylib              0x0000000188aa14bc __dispatch_workloop_worker_thread +  764
3  libsystem_pthread.dylib        0x00000001d4bde7a4 __pthread_wqthread +  276
——-
Exception Type: SIGTRAP 
Exception Codes: fault addr: 0x0000000188a8cf8c
Crashed Thread: 55 
Thread 55 crashed with ARM Thread State (64-bit):
    x0:0x0000000281a86580    x1:0x0000000000000002
0x188a8a000 - 0x188acefff  arm64 <ff408738d75b3061ad994a929c0162d2> libdispatch.dylib

因为不能清晰是哪个事务代码引起的,所以先承认 Crash 的目标是哪个类型。

承认方针目标类型

Crash 日志看不出来方针目标类型,只知道是一个 SIGTRAP,应该是 GCD 调用__builtin_trap()触发软中止结束进程 ,尝试从源码下手,顶层函数逻辑是这样的:

DISPATCH_NOINLINE
void _os_object_release_internal_n(_os_object_t obj, uint16_t n) {
	return _os_object_release_internal_n_inline(obj, n);
}
DISPATCH_ALWAYS_INLINE
static inline void _os_object_release_internal_n_inline(_os_object_t obj, int n)
{
	int ref_cnt = _os_object_refcnt_sub(obj, n);
	if (likely(ref_cnt >= 0)) {
		return;
	}
	if (unlikely(ref_cnt < -1)) {
		_OS_OBJECT_CLIENT_CRASH("Over-release of an object");
	}
	// _os_object_refcnt_dispose_barrier() is in _os_object_dispose()
	return _os_object_dispose(obj);
}

_OS_OBJECT_CLIENT_CRASH()便是调用的__builtin_trap(),那承认便是一个os_object_t目标的 Over-Release 问题了。os_object_t界说是这样的:

typedef struct _os_object_s {
	_OS_OBJECT_HEADER(
	const _os_object_vtable_s *os_obj_isa,
	os_obj_ref_cnt,
	os_obj_xref_cnt);
} _os_object_s;
typedef struct _os_object_s *_os_object_t;

这便是 GCD 类的结构体界说,和 NSObject 类似的内存布局,但os_object_t衍生类众多还需清晰是哪一个。

持续看上一个函数_dispatch_lane_invoke,发现它的代码量很大,且因为 GCD 很多的 inline 函数,很难确定是哪里调用了_os_object_release_internal_n。这个时候就要换一种办法,直接反汇编就能快速承认。

运用和 Crash 栈相同系统设备切 release 环境运转,但有点奇怪的是反汇编代码和_dispatch_lane_invoke偏移对不上。那就用 hopper 直接翻开 uuid 对应的 libdispatch.dylib 可执行文件吧,找到偏移处:

记 os_object_release Crash 排查

接下来就要承认bl _os_object_release_internal_nx0寄存器值怎么来的,这个函数一千多行指令分析工作量太大,但这里能够清晰的是这个函数只有这一处调用 _os_object_release_internal_n

那又回到 GCD 源码,估计便是尾部的一个调用了(代码有修改,去除无用代码和 inline 调用):

_dispatch_lane_invoke(…) {
	dispatch_queue_t dq = dqu._dq;
	…
	return _os_object_release_internal_n(dou._os_obj, 2);
}

翻了一下各个 Crash 日志x1寄存器都是 2 能够对得上。同时运转时反汇编指令虽然对不上,但对比找到相同逻辑的汇编代码段,br到这个偏移也能承认x0便是dispatch_queue_t

定位 Crash 场景

既然产生 Over-Release 的目标是 dispatch_queue_t,那估测便是事务代码运用时存在内存办理问题,最蠢的办法便是找到所有的dispatch_queue_create()调用排查各个场景是否有问题。

不过在这之前能够多看一下 Crash 日志,调用栈有dispatch_workloop_worker_thread能够估测当机遇遇是事务block加入了 GCD 队列,现在现已开端调度了。举个比如,如果在dispatch_async(queue, block)时 queue 就现已开释了,那 Crash 栈就会有dispatch_async,说明在调用dispatch_async(queue, block)时 queue 是正常的,在调度过程要结束时 queue 才被其它线程开释,立即走到_dispatch_lane_invoke的尾调用时才触发了 Over-Release。

那其它线程引起 queue 开释的机遇和当时 Crash 机遇应该很近,也便是说其它线程此刻的仓库大概率有开释这个dispatch_queue_t的调用,排查后发现基本上在别的一个线程都有这么一段调用栈:

9  libdispatch.dylib              0x0000000188a8dfc0 -[OS_dispatch_queue _xref_dispose] +  56
10 AnyProject                       0x0000000107c9b724 -[AnySDKClass dealloc] +  164
11 AnyProject                        0x0000000107cbc10c -[AnySDKClass .cxx_destruct] +  76

那大概率问题就出在AnySDKClass,运转时找到其dealloc办法:

…
    0x107ddab88 <+124>: bl     0x109994540               ; symbol stub for: dispatch_sync
    0x107ddab8c <+128>: add    x0, x19, #0x10            ; =0x10 
    0x107ddab90 <+132>: mov    x1, #0x0
    0x107ddab94 <+136>: bl     0x10999589c               ; symbol stub for: objc_storeWeak
    0x107ddab98 <+140>: ldr    x0, [x19, #0x18]
    0x107ddab9c <+144>: str    xzr, [x19, #0x18]
    0x107ddaba0 <+148>: bl     0x1099957e8               ; symbol stub for: objc_release
    0x107ddaba4 <+152>: ldr    x0, [x19, #0x58]
    0x107ddaba8 <+156>: str    xzr, [x19, #0x58]
    0x107ddabac <+160>: bl     0x1099957e8               ; symbol stub for: objc_release

断点到对应偏移0x107ddabac处,找到这个 queue 的类型:

br set -a 0x107ddabac
po $x0
<OS_dispatch_queue_serial: anyName[0x2809e2900] = { xref = 1, ref = 1, sref = 1, target = com.apple.root.default-qos.overcommit[0x12e435100], width = 0x1, state = 0x001ffe2000000000, in-flight = 0}>

那剩余的工作便是找到对应 SDK 源码,分分出这个 serial queue 的内存办理问题了。