【騰訊Bugly乾貨分享】聊聊蘋果的Bug - iOS 10 nano_free Crash

本文來自於騰訊Bugly公衆號(weixinBugly),未經做者贊成,請勿轉載,原文地址:https://mp.weixin.qq.com/s/hnwj24xqrtOhcjEt_TaQ9w微信

做者:張三華app

導語

精神哥最近發現, 不少開發者在 iOS10 上遇到了一類堆棧爲nano_free字樣的Crash,也有不少人向咱們Bugly客服反饋遇到了這類問題,但並無好的解決方案。正當你們都一籌莫展的時候,微信強大的技術團隊針對這類Crash進行了深度研究,並提出了一個解決方案。原來微信也遇到了這個問題呢,咱們一塊兒來看看他們是如何幹掉這個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。操作系統

蘋果給咱們的建議是:「引導用戶升級系統」。這固然能解決問題,但用戶升級系統是個漫長的週期。scala

而其實咱們很是關注這個問題的緣由,不只是線上版本的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。

  3. 在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,將mallocmalloc_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。 a.對於沒有傳入指針的函數,直接重定向到guard zone。 b.對於有傳入指針的函數,先用size判斷所屬的zone,再進行分發。

這裏須要特別注意的是,由於在修改函數指針前,已經有一部分指針在nano zone中申請了。所以對於每一個傳入的指針,咱們都須要找到它所屬的zone。代碼示例爲:

注:

  • 該問題不止有一種方式解決,可自行發散思惟。
  • 這種方式目前還在灰度中,若要使用,請搭配適當的灰度和回退措施。

更多精彩內容歡迎關注騰訊 Bugly的微信公衆帳號:

騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!

相關文章
相關標籤/搜索