_os_object_retain一看挂在dispatch里,想当然都会认为难道系统有bug了,但是问题不都是靠瞎猜的,还是有了分析才好下结论。
1.前言
最近新版本刚上线就发现少量特别的新增crash,堆栈如下
0 libdispatch.dylib 0x000000018fc296e0 __os_object_retain$VARIANT$mp + 72
1 mttlite 0x00000001033384ec +[CategorySearchDatas selectedSearchInfo] (CategorySearchDatas.m:0)
//...省略中间无意义堆栈
8 libsystem_pthread.dylib 0x000000018fe6a310 __pthread_body + 128
9 libsystem_pthread.dylib 0x000000018fe6a270 _pthread_start + 48
10 libsystem_pthread.dylib 0x000000018fe6dd08 start_wqthread + 4
0 libdispatch.dylib 0x0000000181dbf900 _os_object_retain + 72
1 mttlite 0x00000001023e84ec +[CategorySearchDatas selectedSearchInfo] (CategorySearchDatas.m:0)
2 mttlite 0x00000001001399d0 -[MttSearchEngineSelectorView selectedEngineView] (MttSearchEngineSelectorView.m:317)
//...省略中间堆栈
40 CoreFoundation 0x0000000182de2dc4 CFRunLoopRunSpecific + 452
41 UIKit 0x00000001890a2fc8 -[UIApplication _run] + 652
42 UIKit 0x000000018909dc9c UIApplicationMain + 196
43 mttlite 0x00000001000a08d0 main (main.mm:35)
44 libdyld.dylib 0x0000000181df159c _dyld_process_info_notify_release + 32
一开始只看到第一个堆栈,总是无法解释为何;直到看到第二个堆栈时;就大概坚定了猜测,这肯定有多线程问题,一看crash附件的代码和调用,还真找到了一处疑似问题代码,如下:
+(dispatch_queue_t) getSearchQueue
{
static dispatch_queue_t searchQueue = nil;
if (!searchQueue) {
searchQueue = dispatch_queue_create("engineInfo.MttSearch.SerialQueue", DISPATCH_QUEUE_SERIAL);
}
return searchQueue;
}
但是光有猜测还是不够的,还是需要验证;
这里我们依旧无法严格复现,那就按照惯例,从汇编代码倒推;
2.分析
原始crash堆栈信息如下:
Thread 13 Crashed:
0 libdispatch.dylib 0x000000018515ea48 __os_object_retain$VARIANT$mp + 72
8 mttlite 0x0000000102f184ec +[CategorySearchDatas selectedSearchInfo] (CategorySearchDatas.m:0)
定位到汇编代码如下:
libdispatch.dylib`_os_object_retain:
-> 0x10a72e0a8 <+0>: mov x8, x0
0x10a72e0ac <+4>: ldr w9, [x8, #0xc]!
0x10a72e0b0 <+8>: orr w10, wzr, #0x7fffffff
0x10a72e0b4 <+12>: cmp w9, w10
0x10a72e0b8 <+16>: b.eq 0x10a72e0d0 ; <+40>
0x10a72e0bc <+20>: ldxr w9, [x8]
0x10a72e0c0 <+24>: add w10, w9, #0x1 ; =0x1
0x10a72e0c4 <+28>: stxr w11, w10, [x8]
0x10a72e0c8 <+32>: cbnz w11, 0x10a72e0bc ; <+20>
0x10a72e0cc <+36>: tbnz w9, #0x1f, 0x10a72e0d4 ; <+44>
0x10a72e0d0 <+40>: ret
0x10a72e0d4 <+44>: stp x20, x21, [sp, #-0x10]!
0x10a72e0d8 <+48>: adrp x20, 49
0x10a72e0dc <+52>: add x20, x20, #0x4b2 ; =0x4b2
0x10a72e0e0 <+56>: adrp x21, 55
0x10a72e0e4 <+60>: add x21, x21, #0xc40 ; =0xc40
0x10a72e0e8 <+64>: str x20, [x21, #0x8]
0x10a72e0ec <+68>: ldp x20, x21, [sp], #0x10
0x10a72e0f0 <+72>: brk #0x1
挂在<+72>,而+72的brk指令就是触发一个中断,其实也就是命中了系统的SIGTRAP策略;
逐行分析汇编代码;关键点在<+36>处,tbnz指令,只要w9的第31位!=0则就一定会走进brk的SIGTRAP中;
接下来开始分析这段代码到底在干什么,关键点是<+20>到<+36>的代码
0x10a72e0ac <+4>: ldr w9, [x8, #0xc]! ;w9=x0->0xC,x8=x8+0xC
0x10a72e0b0 <+8>: orr w10, wzr, #0x7fffffff ;w10=0xefffffff;即int_max
0x10a72e0b4 <+12>: cmp w9, w10 ;比较w9和int_max
0x10a72e0b8 <+16>: b.eq 0x10a72e0d0 ; w9==int_max则直接return不管
0x10a72e0bc <+20>: ldxr w9, [x8] ;w9=x0->0xC,即取x0对应指针结构的成员变量
0x10a72e0c0 <+24>: add w10, w9, #0x1 ; =0x1 ,w10=w9+1=x0->0xC+1
0x10a72e0c4 <+28>: stxr w11, w10, [x8] ;将w10的值写入x8指向的内存即x0->0xC=w10;w11记录写入结果,成功为0
0x10a72e0c8 <+32>: cbnz w11, 0x10a72e0bc ; 如果写入失败,跳到异常处理
0x10a72e0cc <+36>: tbnz w9, #0x1f, 0x10a72e0d4 ; 判定w9的第31位(是符号位)是否为0,为0则为正数>0而继续,否则跳到异常处理;;
大概翻译为如下伪代码
w9=x0->ref_count;
x8=&x0->ref_count; //取地址操作
w10=INT_MAX;
if(w9==w10)
return ;
w9=*x8;//*取指针内容
w10=w9+1;
*x8=w10;//写入指针内容,写入失败则跳到异常处理
if(w9&0x40000000==0)
return ;
//接着进入异常处理逻辑;
这里也就是实现对os_object的retain操作;如果retain失败则说明有问题;
这里很明显是retain失败导致进入了异常处理流程;
那为什么retain失败呢?只可能是对象已经被释放了呗;
为了证实这个猜测,找来dispatch源码一看,的确如此
//https://opensource.apple.com/source/libdispatch/libdispatch-228.18/src/object.c
_os_object_t
_os_object_retain(_os_object_t obj)
{
int xref_cnt = obj->os_obj_xref_cnt;
if (slowpath(xref_cnt == _OS_OBJECT_GLOBAL_REFCNT)) {
return obj; // global object
}
xref_cnt = dispatch_atomic_inc2o(obj, os_obj_xref_cnt);
if (slowpath(xref_cnt <= 0)) {
_OS_OBJECT_CLIENT_CRASH("Resurrection of an object");
}
return obj;
}
//_OS_OBJECT_GLOBAL_REFCNT=INT_MAX;
ps: 为何if (slowpath(xref_cnt <= 0))这行C代码在汇编的解释如此简单,直接变成了位运算判定了?这里没有问题,一时把31位误想成了第30位了。。。这里直接判断符号位就知道是正数即可;
3.结论
由汇编分析再加dispatch源码实锤佐证,得到如下结论:
如果获得的xref_cnt(大概是引用计数)<=0,就进入SIGTRAP逻辑,因为说明对象已经被释放了,再去retain当然有问题了;
这就更加说明传进来的os_object有问题,是个已经被释放的对象了;
至此问题更加明了清晰了,就是由于传入的对象提前被释放,导致执行retain操作而挂了;
解决办法当然简单明了,dispatch_once初始化该静态变量即可;