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

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

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

1. 问题

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

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libobjc.A.dylib                 0x00000001d6f9af20 objc_retain + 16
1   CFNetwork                       0x00000001d7843f60 0x1d77b0000 + 606048
2   CFNetwork                       0x00000001d780cec8 0x1d77b0000 + 380616
3   CFNetwork                       0x00000001d77dff24 _CFSocketStreamCreatePair + 56
4   xxxxxxxxxxxxxxxxx               0x000000010c2a44b4 0x10b46c000 + 14910644
5   xxxxxxxxxxxxxxxxx               0x000000010c2a6238 0x10b46c000 + 14918200
6   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:
image

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

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

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

image

代码:

// 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);
}

id
objc_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 , 那么肯定就是开启了这个优化。

image

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

3. Crash根因

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

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

image

  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。

image

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

image

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
如果你想更详细了解淘宝基础平台团队,欢迎观看团队介绍视频
更多淘宝基础平台团队的技术分享,可关注淘宝技术微信公众号AlibabaMTT

目录
相关文章
|
4月前
|
文字识别 安全 API
iOS Crash 治理:淘宝VisionKitCore 问题修复(下)
iOS Crash 治理:淘宝VisionKitCore 问题修复(下)
|
7月前
|
iOS开发
我给 iOS 系统打了个补丁——修复 iOS 16 系统键盘重大 Crash(下)
我给 iOS 系统打了个补丁——修复 iOS 16 系统键盘重大 Crash(下)
235 1
|
4月前
|
双11 Android开发 数据安全/隐私保护
iOS Crash 治理:淘宝VisionKitCore 问题修复(上)
iOS Crash 治理:淘宝VisionKitCore 问题修复(上)
|
7月前
|
存储 安全 编译器
我给 iOS 系统打了个补丁——修复 iOS 16 系统键盘重大 Crash(上)
我给 iOS 系统打了个补丁——修复 iOS 16 系统键盘重大 Crash(上)
248 0
|
Unix Linux C#
iOS开发:Crash异常总结与捕获
说到异常捕获,就必须要提到Crash问题,iOS中,Crash一般分为两种: 1、一种是由EXC_BAD_ACCESS引起的,原因是访问了不属于本进程的内存地址,有可能是访问已被释放的内存; 2、一种是未被捕获的目标C异常(NSException)记录,导致程序向自身发送了SIGABRT信号而崩溃。
678 0
iOS开发:Crash异常总结与捕获
|
存储 运维 监控
mPass iOS崩溃与Crash⽇志符号化详解
在日常mPaas客户端运维中,经常遇到一些iOS闪退,无法直接从闪退堆栈看到原因。主要是因为iOS客户端上传的崩溃日志里的调用栈信息都是通过内存地址记录的,无法直接看到闪退的调用栈信息。如果需要定位到调用栈,需要使用符号表对闪退日志进行符号化。本文从日志收集、日志符号化原理、符号化工具等方向介绍下iOS下crash日志符号化方案。
1832 1
mPass iOS崩溃与Crash⽇志符号化详解
|
存储 JSON iOS开发
[iOS研习记]——记MJExtension多线程Crash的解决历程
[iOS研习记]——记MJExtension多线程Crash的解决历程
428 0
[iOS研习记]——记MJExtension多线程Crash的解决历程
|
监控 Unix API
Dokit支持iOS本地crash查看功能
一、前言 在日常开发中或者测试过程中,我们的应用可能会出现Crash的问题。对于这类问题我们要抱着零容忍的态度,因为如果线上出现了这类问题,将会严重影响用户的体验。 如果Crash出现的时候恰好是在开发过程中,那么开发者可以根据Xcode的调用堆栈或者控制台输出的信息来定位问题的原因。
iOS你不知道的事--Crash分析
原文作者:Cooci_和谐学习_不急不躁原文地址:https://www.jianshu.com/p/56f96167a6e9 大家平时在开发过程中,经常会遇到Crash,那也是在正常不过的事,但是作为一个优秀的iOS开发人员,必将这些用户不良体验降到最低。
1161 0
|
API iOS开发
iOS KVO crash 自修复技术实现与原理解析
【前言】KVO API设计非常不合理,于是有很多的KVO三方库,比如 KVOController 用更优的API来规避这些crash,但是侵入性比较大,必须编码规范来约束所有人都要使用该方式。有没有什么更优雅,无感知的接入方式?
6468 0