做者:林藍東java
最近的一個手機 QQ 版本發出去後收到比較多關於 CoreMotion 的 crash 上報,案發現場以下:git
可是看看這個堆棧發現它徹底不按照套路出牌啊!github
乍一看是掛在 CoreMotion
裏面的CLStartStopAdvertisingBeacon
函數,看似是 iBeacon 相關的問題,但其實是具體函數的符號解不出來,注意 CLStartStopAdvertisingBeacon + 175940
這個巨大的偏移量,通常的函數不可能這麼大,因此這個地址對應的確定是另外的一個函數!正則表達式
拋開錯誤的函數名,看看堆棧的調用順序,看上去是像是 CoreMotion
在子線程起了一個 Runloop,而後在這個 Runloop 處理來自 IOKit 的回調。安全
再看看 crash 的 Exception Codes: BUS_ADRALN at 0x006575716572205d
,能夠知道這是訪問了一個未對齊的地址 0x006575716572205d
致使的崩潰;同時留意到上報上來的寄存器狀態,這個地址正是當前 pc
和 x8
寄存器的值!:微信
通常 PC
寄存器保存的是下一條指令的地址,而且要求地址最後的兩個比特位是 00
,這個地址很明顯不能知足要求;這種狀況一般是由於數據被破壞,致使讀取到的函數指針值異常。多線程
有了上面幾點發現,咱們能夠到真機上去探一探究竟。這個上報上來的 crash 是發生在安裝了 iOS 10.3.1 (14E304
的一臺 64 位機器上,因此咱們找來一臺符合這兩個條件的設備;由於這是發生在系統框架裏面,知足這兩個條件才能保證 CoreMotion
的二進制內容和 crash 的機器是一致的(能夠經過 framework 的 UUID 來驗證這一點)。併發
在真機上咱們要去找到這幾個解錯的函數名,而咱們的依據就是下圖中紅色框的地址:框架
這些是 crash 所在指令的地址,但這些地址因爲 ASLR(地址空間配置隨機載入) 的緣由是不固定的,因此咱們不能在本身的機器上直接用這些地址,而是要利用 crash 時 CoreMotion
框架的載入地址來計算出一個相對的偏移量。一般一個 crash 日誌上報上來都會帶有一個Binary Images
信息:dom
能夠看到當時 CoreMotion
的載入起始地址是 0x199543000
,而後咱們用 crash 堆棧頂部指令的地址 0x00000001995ab62c
減去它獲得一個偏移量 0x6862c( 0x1995ab62c - 0x199543000 = 0x6862c)
。
接下來在真機上編譯運行手機QQ,啓動後暫停進入 lldb
,執行命令:image list
命令能夠獲得當前 CoreMotion
的載入地址:
[ 36] 1EE3BF50-5BBD-3BB1-B441-6468626F84D6 0x00000001985cb000 /.../Library/Frameworks/CoreMotion.framework/CoreMotion
咱們把 0x00000001985cb000
加上以前計算出來的偏移量 0x6862c
就得出一個新地址: 0x1985cb000 + 0x6862c = 0x19863362c
這個就是當前機器上對應的地址。有了這個地址咱們能夠嘗試解下真實的函數名:image lookup -a 0x19863362c
,不過遺憾的是輸出結果並無什麼卵用:
___lldb_unnamed_symbol2303
說明 CoreMotion
把這個符號裁掉了... 不過咱們能夠在這個地址打個斷點 br set -a 0x19863362c
,而後跑進去看一下;進入手機QQ的好友動態頁面 (QQ空間),發現這個斷點被觸發了:
注意斷點位置的上一句 blr x8
:跳轉到 x8 寄存器中的地址,並把 lr 寄存器設置爲 pc + 4 的值,若是此處 x8 的值出現問題,那麼就會出現上報堆棧中的現象: BUS_ADRALN
,而且 x8 和 pc 的值都是這個出錯的地址。
然而到這一步後彷佛遇到死衚衕,函數符號都被裁剪掉,並且這裏的回調都是 C 函數,沒法從 selector 獲取方法名,操做的也不是 OC 對象,惟一能夠肯定的是進入手機QQ的 好友動態 頁面時該函數會被調用。經過查看此頁面代碼,確實會啓動一個 CMMotionManager
而後經過回調監聽陀螺儀的回調,可是此段代碼並不是新增功能,以前版本一直穩定工做,檢查後沒有發現可疑點。因此進一步推測:有沒有其它業務代碼也在使用 CMMotionManager
?
爲此,咱們查看了上報信息中這些 crash 的發生場景,發現集中發生在兩個地方:
TBStoryViewController
和 MQZoneVideoRecordViewController
,這兩個類都是提供攝像功能 ViewController,並且繼承自一樣的父類,界面展現出來以後確實也會觸發以前 crash 的函數;可是找遍這幾個類的代碼,沒有發現直接使用 CMMotionManager
的地方,因而推測是間接使用了 CMMotionManager
。
爲了找到誰間接使用了 CMMotionManager
,首先想到的是給全部的 CMMotionManager
方法打上斷點,這樣一調用就會停住,而後從堆棧上就能看出誰使用了它
(lldb) br set -r "CMMotionManager"
這裏使用了 -r
選項來傳入一個正則表達式,用於匹配全部 CMMotionManager
的方法,而後打上符號斷點。當是最後仍是行不通,由於 CMMotionManager
的幾乎全部的符號都被裁掉了,因此打不上.... 這時候 Frida 這個工具就派上用場了,將它提供的 framework 編譯到本身的工程裏後,咱們就能夠在命令行監控到全部的 Objective-C 方法調用記錄:
frida-trace -U -f re.frida.Gadget -m "-[CMMotionManager \*]"
經過這個方法發現那兩個 controller 一旦展現,就會出現包括 -[CMMotionManager isAccelerometerActive]
的幾個調用。那麼給-[CMMotionManager isAccelerometerActive]
打個斷點看看誰在使用,符號斷點咱們打不上,那麼咱們就直接打到函數地址上,利用運行時 API 取出該方法的 IMP 值:
(lldb) po method_getImplementation((Method)class_getInstanceMethod([CMMotionManager class], @selector(isAccelerometerActive))) 0x0000000198612918 (lldb) br set -a 0x0000000198612918
運行後果真逮到了,一個業務代碼會使用 ![9-runtime-header](/Users/derek/Desktop/CoreMotionCrash/9-runtime-header.png)
,而後 UIAccelerometer
使用了 CMMotionManager
:
進一步經過 iOSRuntimeHeader 能夠確認 UIAccelerometer
有一個 CMMotionManager
做爲實例變量:
看看業務代碼,對 UIAccelerometer
的使用也是很簡單,彷佛沒有什麼不妥,難道又冤枉了好人?可是仔細看看斷點處的堆棧發現一個可疑的地方:調用發生在 Thread 139
,而 UIAccelerometer
是一個 UIKit
的類,通常 UIKit 的方法只能在主線程使用!查看官方文檔並無說明 UIAccelerometer
是不是線程安全,因此咱們須要驗證一下,若是不是,這裏多是一個突破口。
查看代碼發現是經過 -[UIAccelerometer sharedAccelerometer]
獲取一個單例對象進行使用,若是這個類是線程安全的,那麼 sharedAccelerometer
的實現也應該是線程的,因爲這種單例方法通常實現比較簡單,因此不妨查看下彙編代碼看看實現:
翻譯成ARC代碼大概是:
能夠看到整段代碼沒有任何鎖的保護,若是有兩個線程同時獲取單例,就可能發生 sharedInstance
變量被重複賦值的狀況,並且第二次賦值會將第一次構造的對象進行 release,讓該對象野掉,而咱們知道 UIAccelerometer
有一個 CMMotionManager
的成員變量,它也會隨之一塊兒野掉!
同時還發現 -[UIAccelerometer _motionManager]
這個私有方法:
一樣用判斷是否爲空的形式對 _motionManager
變量進行惰性初始化,一樣沒有加任何鎖的保護,若是多個線程同時調用這個方法也會形成 _motionManager
野掉!
驗證是否在多線程使用很簡單了,[UIAccelerometer sharedAccelerometer]
和 [UIAccelerometer _motionManager]
分別打個斷點,而後運行:
從斷點觸發的位置能夠發現該兩個方法會在不一樣線程進行訪問,並且時機很是接近。最後追溯緣由,是以前有同窗爲了不 UIAccelerometer
在主線程啓動形成卡頓,直接將加速劑的開始和借宿操做經過 dispatch_async
放到了一個 global_queue
裏面,都放到了一個 global_queue
裏面,屬於併發隊列,UIAccelerometer
的回調又是在主線程,因此形成了上面的問題:快速開關界面形成多線程同時調用 -[UIAccelerometer sharedAccelerometer]
!
因此,最終的解決方案是將 UIAccelerometer
的操做所有移動回主線程。
林子大了什麼鳥都有,一個大型的應用總會遇到各類奇葩的 BUG,具體解決的手段可能各有不一樣,可是有一個 科學方法 很值得參考,經過觀察收集一個 crash 上報的細節信息,而後提出假設,驗證假設;這個過程當中輔助以各類工具和經驗,最後經過幾個這樣的迭代定位出問題所在:
更多精彩內容歡迎關注騰訊 Bugly的微信公衆帳號:
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!