1. 程式人生 > >【騰訊Bugly乾貨分享】聊聊蘋果的Bug

【騰訊Bugly乾貨分享】聊聊蘋果的Bug

作者:張三華

導語

精神哥最近發現, 很多開發者在 iOS10 上遇到了一類堆疊為nano_free字樣的Crash,也有很多人向我們Bugly客服反饋遇到了這類問題,但並沒有好的解決方案。正當大家都束手無策的時候,微信強大的技術團隊針對這類Crash進行了深度研究,並提出了一個解決方案。原來微信也遇到了這個問題呢,我們一起來看看他們是如何幹掉這個Crash的吧!

背景

iOS 10.0-10.1.1上,新出現了一類堆疊為nano_free字樣的crash問題,困擾了我們一段時間,這裡主要分享解決這個問題的思路,最後嘗試提出一個解決方案可供參考。

它的crash堆疊大致為:

  • 這種crash我們並不陌生,一般野指標的問題,也是這樣的堆疊。但在iOS 10釋出之後,這類crash就嗖地竄到了微信的crash排行榜的前列,而此時微信並沒有釋出新版本。

這兩種跡象表明,這很可能是蘋果的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。

  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。

  1. 修改nano zone的函式指標,重定向到guard zone。
    a.對於沒有傳入指標的函式,直接重定向到guard zone。
    b.對於有傳入指標的函式,先用size判斷所屬的zone,再進行分發。

這裡需要特別注意的是,因為在修改函式指標前,已經有一部分指標在nano zone中申請了。因此對於每個傳入的指標,我們都需要找到它所屬的zone。程式碼示例為:

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

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

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