【騰訊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
通過
setenv
方法,可以設定環境變數,修改MallocZoneNano=0
。然而並沒有效果,因為dylib的初始化在微信之前,此時微信還未啟動。根據蘋果的文件,Info.plist的
LSEnvironment
欄位,可以設定環境變數,然而這個只適用於macOS。
在Xcode的Schema裡設定
MallocZoneNano=0
後,本地不再出現crash。但schema只適用於除錯階段,不能編進app裡。
看來這個方法也行不通,但起碼驗證了,關掉nano zone是可以解決問題。
嘗試四:hook
既然無法完全關閉nano zone,那就嘗試跳過它。
因為我們自己通過malloc_zone_create
建立的zone都屬於scalable zone,不會導致crash。因此我們可以
- 通過
malloc_zone_create
建立一個新的zone,並命名為guard zone - 用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。
- 修改nano zone的函式指標,重定向到guard zone。
a.對於沒有傳入指標的函式,直接重定向到guard zone。
b.對於有傳入指標的函式,先用size判斷所屬的zone,再進行分發。
這裡需要特別注意的是,因為在修改函式指標前,已經有一部分指標在nano zone中申請了。因此對於每個傳入的指標,我們都需要找到它所屬的zone。程式碼示例為:
注:
- 該問題不止有一種方式解決,可自行發散思維。
- 這種方式目前還在灰度中,若要使用,請搭配適當的灰度和回退措施。
更多精彩內容歡迎關注騰訊 Bugly的微信公眾賬號:
騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智慧合併功能幫助開發同學把每天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響使用者數最多的崩潰,精準定位功能幫助開發同學定位到出問題的程式碼行,實時上報可以在釋出後快速的瞭解應用的質量情況,適配最新的 iOS, Android 官方作業系統,鵝廠的工程師都在使用,快來加入我們吧!