聊聊苹果的Bug - iOS 10 nano_free Crash

背景

iOS 10.0-10.1.1上,新出现了一类堆栈为nano_free字样的crash问题,困扰了我们一段时间,这里主要分享解决这个问题的思路,最后尝试提出一个解决方案可供参考。

它的crash堆栈大致为:

  • 这种crash我们并不陌生,一般野指针的问题,也是这样的堆栈。但在iOS 10发布之后,这类crash就嗖地窜到了微信的crash排行榜的前列,而此时微信并没有发布新版本。
  • 通过和一些内部、外部团队的交流,发现这是个共性问题,例如:https://forums.developer.apple.com/thread/63546

这两种迹象表明,这很可能是苹果的bug。按流程,我们向苹果提了bug report,并得到回复:“iOS 10.2 Beta有稳定性提升”。

终于等到iOS 10.2 Beta发布,我们重新统计了此类crash的系统版本分布。发现不仅在10.2 Beta正常,而且iOS 9也没有crash。

苹果给我们的建议是:“引导用户升级系统”。这当然能解决问题,但用户升级系统是个漫长的周期。

而其实我们非常关注这个问题的原因,不仅是线上版本的crash,更是在我们的开发分支,它的crash概率异常的高。如果不搞清楚触发crash的原因,那这将是一颗定时炸弹,不知道何时就会被我们合入主线,发布出去。因此我们着手开始做一些尝试。

尝试

首先我们的切入点是iOS 9和10.2 Beta没有crash。既然如此,能否将正常的代码合入微信,替换掉系统的呢?

尝试一:替换dylib

各版本的dylib可以在macOS的~/Library/Developer/Xcode/iOS DeviceSupport/找到,我们选了iOS 9.3.5的libsystem_malloc.dylib。尝试编入时却报链接错误:

ld: cannot link directly with /Users/sanhuazhang/Desktop/TestNanoCrash/libsystem_malloc.dylib. Link against the umbrella framework 'System.framework' instead. for architecture arm64 clang: error: linker command failed with exit code 1 (use -v to see invocation)

这个是因为dylib的LY_SUB_FRAEWORK段指明它属于System.framework,直接被编译器拒绝了。看来没有办法。(如果有同学知道如何绕过这个保护,烦请赐教。)

尝试二:编入源码

libsystem_malloc.dylib的源码可以在 https://opensource.apple.com/tarballs/libmalloc/ 找到。这里有多个版本,用otool找到iOS 9.3.5对应的源码是libmalloc-67.40.1.tar.gz。

然而这份源码是不完整的,只能读不能编译。看来这个方法也行不通。

阅读源码

上述两个方法不行,就有点束手无策了,只能阅读源码,尝试找突破口。

libsystem_malloc.dylib中,对内存的管理有两个实现:nano zone和scalable zone。他们分别管理不同大小的内存块:

其中nano zone分配nano类型的指针,而scalable zone则分配其他三种类型。nano zone的管理区间和scalable zone是有重叠的,可以理解为nano zone是scalable在小内存下的一个优化。

这两种方法通过MallocZoneNano的环境变量进行配置:

  • MallocZoneNano=1时,default zone为nano zone,不满足nano zone的内存会fall through到它的helper zone,而helper zone是一个scalable zone。
  • MallocZoneNano=0时,deafult zone为scalable zone。

通过getenv("MallocZoneNano")可以拿到环境变量的值,我们发现,在iOS 9和iOS 10.2 Beta中,MallocZoneNano=0,而其他系统MallocZoneNano=1

换句话说,苹果并不是修复了这个问题,而只是屏蔽了。因此其实我们在尝试一中提到替换dylib,即使替换成功,也是不解决问题的。

结合最初的crash堆栈,我们知道crash是发生在nano zone内的,那是否可以关掉nano zone呢?

尝试三:修改环境变量MallocZoneNano=0

  1. 通过setenv方法,可以设置环境变量,修改MallocZoneNano=0。然而并没有效果,因为dylib的初始化在微信之前,此时微信还未启动。
  2. 根据苹果的文档,Info.plist的LSEnvironment字段,可以设置环境变量,然而这个只适用于macOS。
  1. 在Xcode的Schema里设置MallocZoneNano=0后,本地不再出现crash。但schema只适用于调试阶段,不能编进app里。

看来这个方法也行不通,但起码验证了,关掉nano zone是可以解决问题。

尝试四:hook

既然无法完全关闭nano zone,那就尝试跳过它。

因为我们自己通过malloc_zone_create创建的zone都属于scalable zone,不会导致crash。因此我们可以

  1. 通过malloc_zone_create创建一个新的zone,并命名为guard zone
  2. 用fishhook,将malloc和malloc_zone_malloc等一众常用的内存管理的方法,转发到guard zone

使用这个方案后,crash的概率确实降了一些。但并不彻底解决问题。

因为fishhook无法hook掉其他dylib的调用,也就是说,系统的调用(如Cocoa、CoreFoundation等)依然是走nano zone。

尝试五:跳过nano zone

从上面我们知道,nano zone管理的是0-256字节的内存,如果内存不在这个区间,则会fall through到helper zone。而zone的结构是公开的:

那么可以用tricky一点的方法:修改nano zone和helper zone的函数指针,让nano zone的内存申请虚增,超过256字节,以骗过nano zone,而fall through到helper zone后,再恢复为真正的大小。以malloc为例,具体实现为:

由于内存有限,size的最高位一般不会被使用,因此我们可以用这一位来标记。

当我满心以为终于解决问题时,却发现,crash概率不仅没有降低,反而到了几乎必现的程度。而此时除了少数在替换前就申请的内存是走的nano zone,其他内存都是在scalable zone内被管理。这一现象不禁让人怀疑,nano_free的crash,很可能是zone判断错误。即在scalable zone申请的内存,却在nano zone中释放。

重现问题

为了验证,我们还得从源码中搞清楚怎么区分一个指针属于nano zone还是scalable zone:

可以看到,在x86下,是通过获取指针地址所属的段来判断zone的。当signature满足0x00006这个段时,则属于nano zone。

虽然这份代码里没有提供arm下的判断方式,但可以结合源码中对signature判断的函数,并通过符号断点,很快就能找到arm下比较signature的汇编。

即:当ptr>>28==0x17时,属于nano zone。

通过测试代码可以发现,小于256字节的指针确实在0x17段。然而,代码跑了一阵子之后,大于256字节的指针也落在了0x17段。

似乎我们已经很接近问题的核心了。再来一段测试代码验明真身。

先通过循环不断地申请257字节的内存,并保存起来。这些内存应该都落在scalable zone中。当出现0x17段的内存时,我们break掉。

可以假设在此之后scalable zone内申请的内存,都在0x17段,具体代码为:

我们新建了一个iOS的Single View Application,除了这段代码,没有做其他任何的修改。问题重现了:

解决方案

从重现的代码来看,要真正规避nano_free类型的crash出现,只能是减少内存的使用,但这并不好操作。因此,解决思路还是回到保护上。

结合上面提到尝试3和4,我们进行了这样的修改。

  1. 创建一个自己的zone,命名为guard zone。
  2. 修改nano zone的函数指针,重定向到guard zone。
    1. 对于没有传入指针的函数,直接重定向到guard zone。
    2. 对于有传入指针的函数,先用size判断所属的zone,再进行分发。

这里需要特别注意的是,因为在修改函数指针前,已经有一部分指针在nano zone中申请了。因此对于每个传入的指针,我们都需要找到它所属的zone。代码示例为:

注:

  • 该问题不止有一种方式解决,可自行发散思维。
  • 这种方式目前还在灰度中,若要使用,请搭配适当的灰度和回退措施。

原文发布于微信公众号 - WeMobileDev(WeMobileDev)

原文发表时间:2016-12-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Grace development

电商系统设计之商品接口

我应该是少数在文章中直接展示接口文档的人。本篇我思考了很久到底要不要解析下商品接口开发的注意点。

18210
来自专栏逸鹏说道

模仿百度新闻列表底部的“加载更多”

前言   自从上个月来到了学校的信息化中心实习后自由安排的时间越来越少,遂好久没来更新博客了。   昨天在完成一个模仿手机端百度新闻列表底“点击加载更多”的功能...

35780
来自专栏小樱的经验随笔

BugkuCTF web基础$_GET

前言 写了这么久的web题,算是把它基础部分都刷完了一遍,以下的几天将持续更新BugkuCTF WEB部分的题解,为了不影响阅读,所以每道题的题解都以单独一篇文...

330100
来自专栏小程序之家

如何在小程序中实现拍照功能

在小程序使用的过程中,难免会用到相机组件,本文将教大家配置入门小程序camera组件的使用,并自己制作一个小程序相机的demo出来。

2.6K40
来自专栏熊二哥

企业模式和设计模式快速入门

相信大家对GOF的23个设计模式和Martin Fowler的企业应用架构模式都有过了解,这部分的内容和知识非常驳杂,不过真正常用的模式并不多,比如单例模式、策...

20870
来自专栏更流畅、简洁的软件开发方式

三层架构之我见 —— 不同于您见过的三层架构。

       我从02年开始了编程的工作,开始接触一些简单的网站,下半年写了个小的自助建站程序(asp和asp.net),比较简陋没有使用。03年开始正式做网站...

20670
来自专栏Fundebug

配置Tree Shaking来减少JavaScript的打包体积

译者按: 用Tree Shaking技术来减少JavaScript的Payload大小

13050
来自专栏大数据挖掘DT机器学习

爬取淘宝/天猫评论数据的过程

要做数据分析首先得有数据才行。对于我等平民来说,最廉价的获取数据的方法,应该是用爬虫在网络上爬取数据了。本文记录一下笔者爬取天猫某商品的全过程,淘宝上面的店铺...

44870
来自专栏Keegan小钢

Android项目重构之路:界面篇

在前一篇文章《Android项目重构之路:架构篇》中已经简单说明了项目的架构,将项目分为了四个层级:模型层、接口层、核心层、界面层。其中,最上层的界面,是变化最...

17940
来自专栏互联网杂技

Nodejs学习路线图

Nodejs框架是基于V8的引擎,是目前速度最快的Javascript引擎。chrome浏览器就基于V8,同时打开20-30个网页都很流畅。Nodejs标准的w...

1.2K80

扫码关注云+社区

领取腾讯云代金券