iOS13-一次Crash定位-被释放的NSURLhost

6次阅读

共计 4149 个字符,预计需要花费 11 分钟才能阅读完成。

每年一次的 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:

在最初的 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);
}

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

所以,大家汇编调试的时候看到这样一行指令,不要觉得奇怪 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
如果你想更详细了解淘宝基础平台团队,欢迎观看团队介绍视频


本文作者:念纪

阅读原文

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

正文完
 0