一次_os_object_retain的crash

_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初始化该静态变量即可;

你可能感兴趣的:(一次_os_object_retain的crash)