教你 Debug 的正確姿式——記一次 CoreMotion 的 Crash

做者:林藍東java

最近的一個手機 QQ 版本發出去後收到比較多關於 CoreMotion 的 crash 上報,案發現場以下:git

1-crash-stack-frame

可是看看這個堆棧發現它徹底不按照套路出牌啊!github

2-blackman

乍一看是掛在 CoreMotion 裏面的CLStartStopAdvertisingBeacon函數,看似是 iBeacon 相關的問題,但其實是具體函數的符號解不出來,注意 CLStartStopAdvertisingBeacon + 175940 這個巨大的偏移量,通常的函數不可能這麼大,因此這個地址對應的確定是另外的一個函數!正則表達式

拋開錯誤的函數名,看看堆棧的調用順序,看上去是像是 CoreMotion 在子線程起了一個 Runloop,而後在這個 Runloop 處理來自 IOKit 的回調。安全

再看看 crash 的 Exception Codes: BUS_ADRALN at 0x006575716572205d,能夠知道這是訪問了一個未對齊的地址 0x006575716572205d 致使的崩潰;同時留意到上報上來的寄存器狀態,這個地址正是當前 pcx8 寄存器的值!:微信

3-register

通常 PC 寄存器保存的是下一條指令的地址,而且要求地址最後的兩個比特位是 00 ,這個地址很明顯不能知足要求;這種狀況一般是由於數據被破壞,致使讀取到的函數指針值異常多線程

有了上面幾點發現,咱們能夠到真機上去探一探究竟。這個上報上來的 crash 是發生在安裝了 iOS 10.3.1 (14E304 的一臺 64 位機器上,因此咱們找來一臺符合這兩個條件的設備;由於這是發生在系統框架裏面,知足這兩個條件才能保證 CoreMotion 的二進制內容和 crash 的機器是一致的(能夠經過 framework 的 UUID 來驗證這一點)。併發

在真機上咱們要去找到這幾個解錯的函數名,而咱們的依據就是下圖中紅色框的地址:框架

4-stack-frame

這些是 crash 所在指令的地址,但這些地址因爲 ASLR(地址空間配置隨機載入) 的緣由是不固定的,因此咱們不能在本身的機器上直接用這些地址,而是要利用 crash 時 CoreMotion 框架的載入地址來計算出一個相對的偏移量。一般一個 crash 日誌上報上來都會帶有一個Binary Images信息:dom

5-binary-images

能夠看到當時 CoreMotion 的載入起始地址是 0x199543000,而後咱們用 crash 堆棧頂部指令的地址 0x00000001995ab62c 減去它獲得一個偏移量 0x6862c0x1995ab62c - 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,不過遺憾的是輸出結果並無什麼卵用:

6-image-lookup

___lldb_unnamed_symbol2303說明 CoreMotion 把這個符號裁掉了... 不過咱們能夠在這個地址打個斷點 br set -a 0x19863362c,而後跑進去看一下;進入手機QQ的好友動態頁面 (QQ空間),發現這個斷點被觸發了:

7-breakpoint

注意斷點位置的上一句 blr x8跳轉到 x8 寄存器中的地址,並把 lr 寄存器設置爲 pc + 4 的值,若是此處 x8 的值出現問題,那麼就會出現上報堆棧中的現象: BUS_ADRALN,而且 x8 和 pc 的值都是這個出錯的地址。

然而到這一步後彷佛遇到死衚衕,函數符號都被裁剪掉,並且這裏的回調都是 C 函數,沒法從 selector 獲取方法名,操做的也不是 OC 對象,惟一能夠肯定的是進入手機QQ的 好友動態 頁面時該函數會被調用。經過查看此頁面代碼,確實會啓動一個 CMMotionManager 而後經過回調監聽陀螺儀的回調,可是此段代碼並不是新增功能,以前版本一直穩定工做,檢查後沒有發現可疑點。因此進一步推測:有沒有其它業務代碼也在使用 CMMotionManager ?

爲此,咱們查看了上報信息中這些 crash 的發生場景,發現集中發生在兩個地方:

TBStoryViewControllerMQZoneVideoRecordViewController ,這兩個類都是提供攝像功能 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:

8-accelerometer

進一步經過 iOSRuntimeHeader 能夠確認 UIAccelerometer有一個 CMMotionManager 做爲實例變量:

9-runtime-header

看看業務代碼,對 UIAccelerometer 的使用也是很簡單,彷佛沒有什麼不妥,難道又冤枉了好人?可是仔細看看斷點處的堆棧發現一個可疑的地方:調用發生在 Thread 139,而 UIAccelerometer 是一個 UIKit 的類,通常 UIKit 的方法只能在主線程使用!查看官方文檔並無說明 UIAccelerometer 是不是線程安全,因此咱們須要驗證一下,若是不是,這裏多是一個突破口。

查看代碼發現是經過 -[UIAccelerometer sharedAccelerometer] 獲取一個單例對象進行使用,若是這個類是線程安全的,那麼 sharedAccelerometer 的實現也應該是線程的,因爲這種單例方法通常實現比較簡單,因此不妨查看下彙編代碼看看實現:

10-shared-accelerometer

翻譯成ARC代碼大概是:

11-shared-accelerometer

能夠看到整段代碼沒有任何鎖的保護,若是有兩個線程同時獲取單例,就可能發生 sharedInstance 變量被重複賦值的狀況,並且第二次賦值會將第一次構造的對象進行 release,讓該對象野掉,而咱們知道 UIAccelerometer 有一個 CMMotionManager 的成員變量,它也會隨之一塊兒野掉!

同時還發現 -[UIAccelerometer _motionManager] 這個私有方法:

12-motion-manager

一樣用判斷是否爲空的形式對 _motionManager 變量進行惰性初始化,一樣沒有加任何鎖的保護,若是多個線程同時調用這個方法也會形成 _motionManager 野掉!

驗證是否在多線程使用很簡單了,[UIAccelerometer sharedAccelerometer][UIAccelerometer _motionManager] 分別打個斷點,而後運行:

13-thread-safety

從斷點觸發的位置能夠發現該兩個方法會在不一樣線程進行訪問,並且時機很是接近。最後追溯緣由,是以前有同窗爲了不 UIAccelerometer 在主線程啓動形成卡頓,直接將加速劑的開始和借宿操做經過 dispatch_async 放到了一個 global_queue 裏面,都放到了一個 global_queue 裏面,屬於併發隊列,UIAccelerometer 的回調又是在主線程,因此形成了上面的問題:快速開關界面形成多線程同時調用 -[UIAccelerometer sharedAccelerometer]

因此,最終的解決方案是將 UIAccelerometer 的操做所有移動回主線程。

總結

林子大了什麼鳥都有,一個大型的應用總會遇到各類奇葩的 BUG,具體解決的手段可能各有不一樣,可是有一個 科學方法 很值得參考,經過觀察收集一個 crash 上報的細節信息,而後提出假設,驗證假設;這個過程當中輔助以各類工具和經驗,最後經過幾個這樣的迭代定位出問題所在:

14-scientific-method


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

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

相關文章
相關標籤/搜索