卢卡斯的幸福 發表於 2023-1-29 11:43:43

99% iOS开发都不知道的KVO崩溃分析详解

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>背景</li><li>分析堆栈</li><ul class="second_class_ul"><li>__os_unfair_lock_corruption_abort</li><li>__os_unfair_lock_lock_slow</li><li>__NSSetBoolValueAndNotify</li><li>os_unfair_recursive_lock_lock_with_options</li><li>object_getIndexedIvars</li></ul><li>debug 调试</li><ul class="second_class_ul"><li>object_getIndexedIvars</li><li>objc_allocateClassPair</li><li>_NSKVONotifyingCreateInfoWithOriginalClass</li></ul><li>结论</li><ul class="second_class_ul"></ul></ul></div><p class="maodian"></p><h2>背景</h2>
<p>crash 监控发现有大量的新增崩溃,堆栈如下</p>
<div class="jb51code"><pre class="brush:cpp;">        libsystem_platform.dylib        __os_unfair_lock_corruption_abort()
        libsystem_platform.dylib        __os_unfair_lock_lock_slow()
        Foundation        __NSSetBoolValueAndNotify()
</pre></div>
<p class="maodian"></p><h2>分析堆栈</h2>
<p class="maodian"></p><h3>__os_unfair_lock_corruption_abort</h3>
<p>log 翻译:lock 已损坏</p>
<div class="jb51code"><pre class="brush:cpp;">_os_unfair_lock_corruption_abort(os_ulock_value_t current)
{
        __LIBPLATFORM_CLIENT_CRASH__(current, "os_unfair_lock is corrupt");
}
</pre></div>
<p class="maodian"></p><h3>__os_unfair_lock_lock_slow</h3>
<p>在这个方法里面 __ulock_wait 返回 EOWNERDEAD 调用 corruption abort 方法。</p>
<div class="jb51code"><pre class="brush:cpp;">int ret = __ulock_wait(UL_UNFAIR_LOCK | ULF_NO_ERRNO | options,
                                l, current, 0);
if (unlikely(ret &amp;lt; 0)) {
switch (-ret) {
    case EINTR:
    case EFAULT:
      continue;
    case EOWNERDEAD:
      _os_unfair_lock_corruption_abort(current);
      break;
    default:
      __LIBPLATFORM_INTERNAL_CRASH__(-ret, "ulock_wait failure");
}
}
</pre></div>
<p>EOWNERDEAD 的定义</p>
<p>#define EOWNERDEAD &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;105 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/* Previous owner died */</p>
<p>到这里猜测是 lock 的 owner 已经野指针了,继续向下看。</p>
<p class="maodian"></p><h3>__NSSetBoolValueAndNotify</h3>
<p>google 下这个方法是在 KVO 里面修改属性的时候调用,伪代码:</p>
<div class="jb51code"><pre class="brush:cpp;">int __NSSetBoolValueAndNotify(int arg0, int arg1, int arg2) {
    r31 = r31 - 0x90;
    var_30 = r24;
    stack[-56] = r23;
    var_20 = r22;
    stack[-40] = r21;
    var_10 = r20;
    stack[-24] = r19;
    saved_fp = r29;
    stack[-8] = r30;
    r20 = arg2;
    r21 = arg1;
    r19 = arg0;
    r0 = object_getClass(arg0);
    r0 = object_getIndexedIvars(r0); // 理清这个崩溃的关键方法,这里和汇编代码不一致,汇编代码的入参是 r0 + 0x20
    r23 = r0;
    os_unfair_recursive_lock_lock_with_options();
    CFDictionaryGetValue(*(r23 + 0x18), r21);
    r22 = _objc_msgSend$copyWithZone:();
    os_unfair_recursive_lock_unlock();
    if (*(int8_t *)(r23 + 0x28) != 0x0) {
            _objc_msgSend$willChangeValueForKey:();
            (class_getMethodImplementation(*r23, r21))(r19, r21, r20);
            _objc_msgSend$didChangeValueForKey:();
    }
    else {
            _objc_msgSend$_changeValueForKey:key:key:usingBlock:();
    }
    var_38 = **qword_9590e8;
    r0 = objc_release_x22();
    if (**qword_9590e8 != var_38) {
            r0 = __stack_chk_fail();
    }
    return r0;
}
</pre></div>
<p class="maodian"></p><h3>os_unfair_recursive_lock_lock_with_options</h3>
<p>崩溃调用栈中间还有这一层的内联调用 os_unfair_recursive_lock_lock_with_options。这里的 lock owner 有个比较赋值的操作,如果 oul_value 等于 OS_LOCK_NO_OWNER 则赋值 self 然后 return。崩溃时这里继续向下执行了,那这里的 oul_value 的取值只能是 lock-&gt;oul_value。到这里猜测崩溃的原因是 lock-&gt;oul_value 野指针了。</p>
<div class="jb51code"><pre class="brush:cpp;">void
os_unfair_recursive_lock_lock_with_options(os_unfair_recursive_lock_t lock,
                os_unfair_lock_options_t options)
{
        os_lock_owner_t cur, self = _os_lock_owner_get_self();
        _os_unfair_lock_t l = (_os_unfair_lock_t)&amp;lock-&gt;ourl_lock;
        if (likely(os_atomic_cmpxchgv2o(l, oul_value,
                        OS_LOCK_NO_OWNER, self, &amp;cur, acquire))) {
                return;
        }
        if (OS_ULOCK_OWNER(cur) == self) {
                lock-&gt;ourl_count++;
                return;
        }
        return _os_unfair_lock_lock_slow(l, self, options);
}
OS_ALWAYS_INLINE OS_CONST
static inline os_lock_owner_t
_os_lock_owner_get_self(void)
{
        os_lock_owner_t self;
        self = (os_lock_owner_t)_os_tsd_get_direct(__TSD_MACH_THREAD_SELF);
        return self;
}
</pre></div>
<p class="maodian"></p><p class="maodian"></p><h3>object_getIndexedIvars</h3>
<p>__NSSetBoolValueAndNotify 里面的获取 lock 的方法,这个函数非常关键。</p>
<div class="jb51code"><pre class="brush:cpp;">/**
* Returns a pointer to any extra bytes allocated with an instance given object.
*
* @param obj An Objective-C object.
*
* @return A pointer to any extra bytes allocated with \e obj. If \e obj was
*   not allocated with any extra bytes, then dereferencing the returned pointer is undefined.
*
* @note This function returns a pointer to any extra bytes allocated with the instance
*(as specified by \c class_createInstance with extraBytes&gt;0). This memory follows the
*object's ordinary ivars, but may not be adjacent to the last ivar.
* @note The returned pointer is guaranteed to be pointer-size aligned, even if the area following
*the object's last ivar is less aligned than that. Alignment greater than pointer-size is never
*guaranteed, even if the area following the object's last ivar is more aligned than that.
* @note In a garbage-collected environment, the memory is scanned conservatively.
/**
* Returns a pointer immediately after the instance variables declared in an
* object.This is a pointer to the storage specified with the extraBytes
* parameter given when allocating an object.
*/
void *object_getIndexedIvars(id obj)
{
    uint8_t *base = (uint8_t *)obj;
    if (_objc_isTaggedPointerOrNil(obj)) return nil;
    if (!obj-&gt;isClass()) return base + obj-&gt;ISA()-&gt;alignedInstanceSize();
    Class cls = (Class)obj;
    if (!cls-&gt;isAnySwift()) return base + sizeof(objc_class);
    swift_class_t *swcls = (swift_class_t *)cls;
    return base - swcls-&gt;classAddressOffset + word_align(swcls-&gt;classSize);
}
</pre></div>
<p>上层调用 __NSSetBoolValueAndNotify 里面:</p>
<p>r0 = object_getClass(arg0),arg0 是实例对象,r0 是类对象,因为这里是个 KVO 的调用,那正常情况下r0 是 NSKVONotifying_xxx。</p>
<p>对于 KVO 类,object_getIndexedIvars 返回的地址是 (uint8_t *)obj + sizeof(objc_class)。根据函数的注释,这个地址指向创建类时附在类空间后 extraBytes 大小的一块内存。</p>
<p class="maodian"></p><h2>debug 调试</h2>
<h3>object_getIndexedIvars</h3>
<p>__NSSetBoolValueAndNotify 下的调用</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202301/20230129084949038.png" /></p>
<p>object_getIndexedIvars 入参是 NSKVONotifying_KVObject,object_getClass 获取的是 KVO Class。</p>
<p class="maodian"></p><h3>objc_allocateClassPair</h3>
<p>动态创建 KVO 类的方法。</p>
<div class="jb51code"><pre class="brush:cpp;"> thread #8, queue = 'com.apple.root.default-qos', stop reason = breakpoint 1.1
* frame #0: 0x000000018143a088 libobjc.A.dylib`objc_allocateClassPair
    frame #1: 0x000000018259cd94 Foundation`_NSKVONotifyingCreateInfoWithOriginalClass + 152
    frame #2: 0x00000001825b8fd0 Foundation`_NSKeyValueContainerClassGetNotifyingInfo + 56
    frame #3: 0x000000018254b7dc Foundation`- + 44
    frame #4: 0x000000018254b504 Foundation`- + 88
    frame #5: 0x000000018254b32c Foundation`- + 404
    frame #6: 0x000000018254b054 Foundation`- + 136
    frame #7: 0x00000001040d1860 Test`__29-_block_invoke(.block_descriptor=0x0000000282a55170) at ViewController.m:28:13
    frame #8: 0x00000001043d05a8 libdispatch.dylib`_dispatch_call_block_and_release + 32
    frame #9: 0x00000001043d205c libdispatch.dylib`_dispatch_client_callout + 20
    frame #10: 0x00000001043d4b94 libdispatch.dylib`_dispatch_queue_override_invoke + 1052
    frame #11: 0x00000001043e6478 libdispatch.dylib`_dispatch_root_queue_drain + 408
    frame #12: 0x00000001043e6e74 libdispatch.dylib`_dispatch_worker_thread2 + 196
    frame #13: 0x00000001d515fdbc libsystem_pthread.dylib`_pthread_wqthread + 228
</pre></div>
<p class="maodian"></p><h3>_NSKVONotifyingCreateInfoWithOriginalClass</h3>
<p>objc_allocateClassPair 的上层调用。 allocate 之前的 context w2 是个固定值 0x30,即创建 KVO Class 入参 extraBytes 的大小是 0x30</p>
<div class="jb51code"><pre class="brush:cpp;">    0x18259cd78 &lt;+124&gt;: mov    x1, x21
    0x18259cd7c &lt;+128&gt;: mov    x2, x22
    0x18259cd80 &lt;+132&gt;: bl   0x188097080
    0x18259cd84 &lt;+136&gt;: mov    x0, x20
    0x18259cd88 &lt;+140&gt;: mov    x1, x19
    0x18259cd8c &lt;+144&gt;: mov    w2, #0x30
    0x18259cd90 &lt;+148&gt;: bl   0x1880961f0 // objc_allocateClassPair
    0x18259cd94 &lt;+152&gt;: cbz    x0, 0x18259ce24         ; &lt;+296&gt;
    0x18259cd98 &lt;+156&gt;: mov    x21, x0
    0x18259cd9c &lt;+160&gt;: bl   0x188096410 // objc_registerClassPair
    0x18259cda0 &lt;+164&gt;: mov    x0, x19
    0x18259cda4 &lt;+168&gt;: bl   0x182b45f44               ; symbol stub for: free
    0x18259cda8 &lt;+172&gt;: mov    x0, x21
    0x18259cdac &lt;+176&gt;: bl   0x1880967e0 // object_getIndexedIvars
    0x18259cdb0 &lt;+180&gt;: mov    x19, x0
    0x18259cdb4 &lt;+184&gt;: stp    x20, x21,
</pre></div>
<p>_NSKVONotifyingCreateInfoWithOriginalClass+184 处将 x20 和 x21 写入 ,此时 x0 指向的是大小为 extraBytes 的内存,打印 x20 和 x21 的值</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;x20 = 0x00000001117caa10 &nbsp;(void *)0x00000001117caa38: KVObject(向上回溯这个值取自 _NSKVONotifyingCreateInfoWithOriginalClass 的入参 x0)</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;x21 NSKVONotifying_KVObject</p>
<p>根据这里可以看出 object_getIndexedIvars 返回的地址,依次存储了 KVObject(origin Class) 和 NSKVONotifying_KVObject(KVO Class)。</p>
<p>查看 _NSKVONotifyingCreateInfoWithOriginalClass 的伪代码,对 有 5 次写入的操作,并且最终这个方法返回的是 x0 的地址。</p>
<div class="jb51code"><pre class="brush:cpp;">function __NSKVONotifyingCreateInfoWithOriginalClass {
    r31 = r31 - 0x50;
    stack = r22;
    stack = r21;
    stack = r20;
    stack = r19;
    stack = r29;
    stack = r30;
    r20 = r0;
    if (*(int8_t *)0x993e78 != 0x0) {
            os_unfair_lock_assert_owner(0x993e7c);
    }
    r0 = class_getName(r20);
    r22 = strlen(r0) + 0x10;
    r0 = malloc(r22);
    r19 = r0;
    strlcpy(r0, "NSKVONotifying_", r22);
    strlcat(r19, r21, r22);
    r0 = objc_allocateClassPair(r20, r19, 0x30);
    if (r0 != 0x0) {
            objc_registerClassPair(r0);
            free(r19);
            r0 = object_getIndexedIvars(r21);
            r19 = r0;
            *(int128_t *)r0 = r20; // 第一次写入 Class
            *(int128_t *)(r0 + 0x8) = r21; // 第二次写入 Class
            *(r19 + 0x10) = CFSetCreateMutable(0x0, 0x0, *qword_9592d8); // 第三次写入 CFSet
            *(int128_t *)(r19 + 0x18) = CFDictionaryCreateMutable(0x0, 0x0, 0x0, *qword_959598); // 第四次写入 CFDictionary
            *(int128_t *)(r19 + 0x20) = 0x0; // 第五次写入空值
            if (*qword_9fc560 != -0x1) {
                  dispatch_once(0x9fc560, 0x8eaf98);
            }
            if (class_getMethodImplementation(*r19, @selector(willChangeValueForKey:)) != *qword_9fc568) {
                  r8 = 0x1;
            }
            else {
                  r0 = *r19;
                  r0 = class_getMethodImplementation(r0, @selector(didChangeValueForKey:));
                  r8 = *qword_9fc570;
                  if (r0 != r8) {
                            r8 = *qword_9fc570;
                            if (CPU_FLAGS &amp; NE) {
                                    r8 = 0x1;
                            }
                  }
            }
            *(int8_t *)(r19 + 0x28) = r8;
            _NSKVONotifyingSetMethodImplementation(r19, @selector(_isKVOA), 0x44fab4, 0x0);
            _NSKVONotifyingSetMethodImplementation(r19, @selector(dealloc), 0x44fabc, 0x0);
            _NSKVONotifyingSetMethodImplementation(r19, @selector(class), 0x44fd2c, 0x0);
    }
    else {
            if (*qword_9fc558 != -0x1) {
                  dispatch_once(0x9fc558, 0x8eaf78);
            }
            if (os_log_type_enabled(*0x9fc550, 0x10) != 0x0) {
                  _os_log_error_impl(0x0, *0x9fc550, 0x10, "KVO failed to allocate class pair for name %s, automatic key-value observing will not work for this class", &amp;stack, 0xc);
            }
            free(r19);
            r19 = 0x0;
    }
    if (**qword_9590e8 == **qword_9590e8) {
            r0 = r19;
    }
    else {
            r0 = __stack_chk_fail();
    }
    return r0;
}
</pre></div>
<p>_NSKVONotifyingCreateInfoWithOriginalClass 的上层调用,入参是 ,返回的参数写入 </p>
<div class="jb51code"><pre class="brush:cpp;">    0x1825b8fc0 &lt;+40&gt;: ldr    x0,
    0x1825b8fc4 &lt;+44&gt;: b      0x1825b8fd4               ; &lt;+60&gt;
    0x1825b8fc8 &lt;+48&gt;: ldr    x0,
-&gt;0x1825b8fcc &lt;+52&gt;: bl   0x18259ccfc               ; _NSKVONotifyingCreateInfoWithOriginalClass
    0x1825b8fd0 &lt;+56&gt;: str    x0,
    0x1825b8fd4 &lt;+60&gt;: ldp    x29, x30,
    0x1825b8fd8 &lt;+64&gt;: ldp    x20, x19, , #0x20
</pre></div>
<p>打印 x19 是一个 NSKeyValueContainerClass 类型的实例对象,这个对象类的 ivars layout</p>
<div class="jb51code"><pre class="brush:cpp;">ivars 0x99f3c0 __OBJC_$_INSTANCE_VARIABLES_NSKeyValueContainerClass
            entsize   32
            count   5
            offset    0x9e6048 _OBJC_IVAR_$_NSKeyValueContainerClass._originalClass 8
            name      0x90bd27 _originalClass
            type      0x929ae6 #
            alignment 3
            size      8
            offset    0x9e6050 _OBJC_IVAR_$_NSKeyValueContainerClass._cachedObservationInfoImplementation 16
            name      0x90bd36 _cachedObservationInfoImplementation
            type      0x92bb88 ^?
            alignment 3
            size      8
            offset    0x9e6058 _OBJC_IVAR_$_NSKeyValueContainerClass._cachedSetObservationInfoImplementation 24
            name      0x90bd5b _cachedSetObservationInfoImplementation
            type      0x92bb88 ^?
            alignment 3
            size      8
            offset    0x9e6060 _OBJC_IVAR_$_NSKeyValueContainerClass._cachedSetObservationInfoTakesAnObject 32
            name      0x90bd83 _cachedSetObservationInfoTakesAnObject
            type      0x92a01a B
            alignment 0
            size      1
            offset    0x9e6068 _OBJC_IVAR_$_NSKeyValueContainerClass._notifyingInfo 40
            name      0x90bdaa _notifyingInfo
            type      0x92bdd7 ^{?=##^{__CFSet}^{__CFDictionary}{os_unfair_recursive_lock_s={os_unfair_lock_s=I}I}B}
            alignment 3
            size      8
</pre></div>
<blockquote><p>offset 0x8 name:_originalClass type:Class</p>
<p>offset 0x28 name:_notifyingInfo type:struct</p></blockquote>
<p>_notifyingInfo 结构体</p>
<div class="jb51code"><pre class="brush:cpp;">{
Class,
Class,
__CFSet,
__CFDictionary,
os_unfair_recursive_lock_s
}
</pre></div>
<p>type encoding:</p>
<p>developer.apple.com/library/arc&hellip;</p>
<p>从 context 可以看出_NSKVONotifyingCreateInfoWithOriginalClass 这个方法入参是 OBJC_IVAR_NSKeyValueContainerClass._originalClass。</p>
<p>返回值 x0 是 _OBJC_IVAR__NSKeyValueContainerClass._notifyingInfo。5 次对 的写入是在初始化 _notifyingInfo。</p>
<p>崩溃时的 context:</p>
<div class="jb51code"><pre class="brush:cpp;">    0x1825231f0 &lt;+56&gt;:bl   0x1880967c0 // object_getClass
    0x1825231f4 &lt;+60&gt;:bl   0x1880967e0 // object_getIndexedIvars
    0x1825231f8 &lt;+64&gt;:mov    x23, x0 // x0 == _notifyingInfo
    0x1825231fc &lt;+68&gt;:add    x24, x0, #0x20 // x24 == os_unfair_recursive_lock_s
    0x182523200 &lt;+72&gt;:mov    x0, x24
    0x182523204 &lt;+76&gt;:mov    w1, #0x0
    0x182523208 &lt;+80&gt;:bl   0x188096910 // os_unfair_recursive_lock_lock_with_options crash 调用栈
</pre></div>
<p>调用 object_getClass 获取 Class,调用 object_getIndexedIvars 获取到 _notifyingInfo,_notifyingInfo + 偏移量 0x20 获取 os_unfair_recursive_lock_s,崩溃的原因是这把锁的 owner 损坏了,lock 也是一个结构体,ower 也是根据 offset 获取的。</p>
<p class="maodian"></p><h2>结论</h2>
<p>从崩溃的上下文来看,最可能出问题的是获取 _notifyingInfo,因为只有 KVO &nbsp;Class 才能获取到 _notifyingInfo 这个结构体,如果在调用 __NSSetBoolValueAndNotify 的过程中,在其它线程监听被移除,此时 object_getClass 取到的不是 KVO Class 那后续再根据 offset 去取 lock,这个时候就有可能发生上述崩溃。</p>
<p>线下暴力复现验证了上述猜测。</p>
<div class="jb51code"><pre class="brush:cpp;">- (void)start {
    __block KVObject *obj = ;
    dispatch_async(dispatch_get_global_queue(0, 0x0), ^{
      for (int i = 0; i &lt; 100000; i++) {
            ;
            ;
      }
    });
    dispatch_async(dispatch_get_global_queue(0, 0x0), ^{
      for (int i = 0; i &lt; 100000; i++) {
            obj.value = YES;
            obj.value = NO;
      }
    });
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary&lt;NSKeyValueChangeKey,id&gt; *)change context:(void *)context {}
</pre></div>
<p>解决这个问题的思路就是保证线程安全,我们在线上断点找到了 removeObserver 的代码,将 removeObserver 和触发监听的代码放在了同一个串行队列。当然如果 removeObserver 在 dealloc 里面,理论上也不会出现这类问题。</p>
<p>__NSSetxxxValueAndNotify 系列方法都有可能会触发这个崩溃,类似的问题可以按照相同的思路解决。</p>
<div class="jb51code"><pre class="brush:cpp;">00000000004e05cd t __NSSetBoolValueAndNotify
00000000004e0707 t __NSSetCharValueAndNotify
00000000004e097b t __NSSetDoubleValueAndNotify
00000000004e0abc t __NSSetFloatValueAndNotify
00000000004e0bfd t __NSSetIntValueAndNotify
00000000004e10e7 t __NSSetLongLongValueAndNotify
00000000004e0e6f t __NSSetLongValueAndNotify
00000000004e0491 t __NSSetObjectValueAndNotify
00000000004e15d5 t __NSSetPointValueAndNotify
00000000004e1734 t __NSSetRangeValueAndNotify
00000000004e188a t __NSSetRectValueAndNotify
00000000004e135f t __NSSetShortValueAndNotify
00000000004e19e8 t __NSSetSizeValueAndNotify
00000000004e0841 t __NSSetUnsignedCharValueAndNotify
00000000004e0d36 t __NSSetUnsignedIntValueAndNotify
00000000004e1223 t __NSSetUnsignedLongLongValueAndNotify
00000000004e0fab t __NSSetUnsignedLongValueAndNotify
00000000004e149a t __NSSetUnsignedShortValueAndNotify
00000000004de834 t __NSSetValueAndNotifyForKeyInIvar</pre></div>
<p>以上就是99% iOS开发都不知道的KVO崩溃分析详解的详细内容,更多关于iOS开发KVO崩溃的资料请关注琼殿技术社区其它相关文章!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>IOS中使用 CocoaAsyncSocket​</li><li>iOS开发多线程下全局变量赋值崩溃原理详解</li><li>Android iOS常用APP崩溃日志获取命令方法</li><li>Flutter项目在 iOS14 启动崩溃的解决方法</li><li>iOS APP中保存图片到相册时崩溃的解决方法</li><li>iOS&nbsp;16&nbsp;CocoaAsyncSocket&nbsp;崩溃修复详解</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: 99% iOS开发都不知道的KVO崩溃分析详解