os_object_release Crash 排查记录分析
波儿菜 人气:0Crash 信息
线上存在一个持续很久的 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 可执行文件吧,找到偏移处:
接下来就要确认bl _os_object_release_internal_n
时x0
寄存器值怎么来的,这个函数一千多行指令分析工作量太大,但这里可以明确的是这个函数只有这一处调用 _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 的内存管理问题了。
加载全部内容