iOS13 一次Crash定位 – 被释放的NSURL.host

作者 : 开心源码 本文共4089个字,预计阅读时间需要11分钟 发布时间: 2022-05-13 共195人阅读

每年一次的iOS更新,都会给开发者带来少量适配工作,少量本来工作正常的代码可能就会发生崩溃。 本文讲到了一种 CoreFoundation 对象的内存管理方式在iOS13上遇到的问题。

1. 问题

iOS 13 Beta 版本上,手淘出现了一个必现的崩溃:

Thread 0 name:  Dispatch queue: com.apple.main-threadThread 0 Crashed:0   libobjc.A.dylib                 0x00000001d6f9af20 objc_retain + 161   CFNetwork                       0x00000001d7843f60 0x1d77b0000 + 6060482   CFNetwork                       0x00000001d780cec8 0x1d77b0000 + 3806163   CFNetwork                       0x00000001d77dff24 _CFSocketStreamCreatePair + 564   xxxxxxxxxxxxxxxxx               0x000000010c2a44b4 0x10b46c000 + 149106445   xxxxxxxxxxxxxxxxx               0x000000010c2a6238 0x10b46c000 + 149182006   xxxxxxxxxxxxxxxxx               0x000000010c2a661c 0x10b46c000 + 14919196

崩溃在了 _CFSocketStreamCreatePair 方法里面, 而后崩溃在了 objc_retain 里面,推测是传入的某个ObjC的对象野指针了导致的。

通过追溯源码,发现调用的是 CFStreamCreatePairWithSocketToHost 这个方法,而后找到这个方法的定义:

void CFStreamCreatePairWithSocketToHost(    CFAllocatorRef _Null_unspecified alloc,     CFStringRef _Null_unspecified host,     UInt32 port,    CFReadStreamRef _Null_unspecified * _Null_unspecified readStream,     CFWriteStreamRef _Null_unspecified * _Null_unspecified writeStream);

根据上下文判断,是第二个参数 CFStringRef _Null_unspecified host 野指针了。

而后找到这个 host 对象的初始化:

NSURL *serverUrl = [NSURL URLWithString:@"xxxxx"];CFStringRef hostRef = (__bridge CFStringRef)serverUrl.host;

这段代码看起来如同并没有问题,怎样会导致野指针,而后Crash呢?

这要从iOS的内存管理上找答案。

2. 苹果的autorelease内存管理优化

我们都知道苹果使用 “引用计数” 技术来管理内存, 使用 “自动释放池AutoreleasePool” 技术来处理方法返回值的内存管理问题。 相关技术原理网上都有很多文章。但是本文中遇到的Crash是由苹果对使用 ARC 代码进行的编译优化从而引发的。所以先讲一下这个优化是什么。

考虑一个内存管理的最简单的case:

在最初的 ARC 机制下,上图中的左边代码会编译成右边这样的代码,从而保证了对象 b 的生命周期完整。

但是我们再详细分析下这个代码,是不是去掉 [b autorelease][b retain] 这两步操作的话,代码也是可以正常执行的呢? 答案是一定的, 那么这个操作其实就是可以优化掉的。苹果考虑到了这一点。

那么要怎样样做到这个优化呢? 由于这个优化是需要同时考虑 被调用方: funcB调用方: funcA 这两个方法配合来完成,由于需要根据调用方的内存管理代码才能决定我被调用方要不要真的去掉autorelease操作。 而且还要在ABI上向下适配。 苹果是这样做的:

代码:

// Prepare a value at +1 for return through a +0 autoreleasing convention.id objc_autoreleaseReturnValue(id obj){    // 判断能否需要优化, 假如可以,就直接return,不做autorelease    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;    return objc_autorelease(obj);}idobjc_retainAutoreleasedReturnValue(id obj){    // 判断能否走了优化逻辑,假如走了就不用retain    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;    return objc_retain(obj);}static ALWAYS_INLINE bool prepareOptimizedReturn(ReturnDisposition disposition){    assert(getReturnDisposition() == ReturnAtPlus0);    // 判断方法返回地址是不是某个值,是的话就认为可以优化    if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {        // 可以优化就把ReturnAtPlus1 存起来,存到了tls里面        if (disposition) setReturnDisposition(disposition);        return true;    }    return false;}static ALWAYS_INLINE bool callerAcceptsOptimizedReturn(const void *ra){    // fd 03 1d aa    mov fp, fp    // arm64 instructions are well-aligned    // 判断return address是不是 0xaa1d03fd, 在arm64上就是 `mov fp, fp` 指令    if (*(uint32_t *)ra == 0xaa1d03fd) {        return true;    }    return false;}static ALWAYS_INLINE ReturnDisposition acceptOptimizedReturn(){    ReturnDisposition disposition = getReturnDisposition();    setReturnDisposition(ReturnAtPlus0);  // reset to the unoptimized state    return disposition;}// 存在当 tls中,当前线程相关的static ALWAYS_INLINE ReturnDisposition getReturnDisposition(){    return (ReturnDisposition)(uintptr_t)tls_get_direct(RETURN_DISPOSITION_KEY);}static ALWAYS_INLINE void setReturnDisposition(ReturnDisposition disposition){    tls_set_direct(RETURN_DISPOSITION_KEY, (void*)(uintptr_t)disposition);}

从上面的分析中,我们可以得出,只需看到调用 objc_msgSend 之后的一条指令是 mov x29, x29 , 那么一定就是开启了这个优化。

所以,大家汇编调试的时候看到这样一行指令,不要觉得奇怪 mov x29,x29 不是啥都没做么?其实是用于这里的优化。

3. Crash根因

理解了 ObjC的 autorelease优化之后,再回到我们遇到的crash问题。有理由怀疑 [NSURL host] 这个方法在旧版本系统上不会走这个优化,因而返回值被放入了 AutoreleasePool 所以后面继续使用是正常的。但是iOS13 上走到了这个优化逻辑,实际上返回的 host 是没有加入 AutoreleasePool 的。 而这个时候刚好又没有 objc 对象接收,直接用 __bridge 转移到了 CF对象上。导致这个 host 直接释放了。

通过查看 对 [NSURL host] 的调用代码证实了这个猜想:

  1. +312 行调用 [NSURL host] 获取host.
  2. 由于 +316的指令是 mov x29, x29 所以假如[NSURL host] 里的实现是相似上述 funcB 则会走到autorelease优化。也就是返回的 host 没有加入autoreleasePool
  3. +320 行中,由于开启优化,也捕获做retain
  4. +328 行,直接release, 这个时候 host就释放了
  5. 后续继续对它进行访问,就Crash了。

还需要证实的就是 [NSURL host]本身的实现了。于是比照了iOS12 和 iOS13 上的实现:

iOS12 上内部通过调用了 [NSURL _cfurl] 获取,已经加入了autoreleasePool。

在iOS13上,就是正常的取值做autorelease, 因而会走到优化逻辑:

4. 小结

慎用 __bridge 来进行 OC对象和 CF对象直接的强转。 由于Autorelease优化的存在,这种用法可能让你的代码不安全,因而尽可能使用 CFBridgeRetain __bridge_retained 来转换管理CF对象,避免由于作用域不一致的情况导致对象呗提前释放的问题。

本文源码来自:https://opensource.apple.com/tarballs/objc4/

本文作者:念纪,来自淘客户端iOS架构组
淘宝基础平台团队正在举行2019实习生(2020年毕业)和社招招聘,岗位有iOS Android用户端开发工程师、Java研发工程师、C/C++研发工程师、前台开发工程师、算法工程师,欢迎投递简历至junzhan.yzw@taobao.com
假如你想更详细理解淘宝基础平台团队,欢迎观看团队详情视频


本文作者:念纪

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » iOS13 一次Crash定位 – 被释放的NSURL.host

发表回复