看似系統Bug的Crash 99%都不是系統問題!本文將與你一塊兒探索Crash分析的科學方法。html
在移動互聯網闖蕩多年的iOS手機管家,通過不斷迭代創新,已經涵蓋了隱私(加密相冊)、安全(騷擾攔截、短信過濾)、工具(網絡檢測、照片清理、極簡提醒等)等等各個方面,爲千萬用戶提供安全專業的服務。但與此同時,工程代碼也愈來愈龐大(近30萬行),一丁點的問題都會影響大量的用戶,因此手管一直在質量上下狠功夫,對Crash率更是追求極致。近幾個迭代對Crash作了專項分析,Crash率在本來0.02%的基礎上穩定降到0.01%,7.7.1版本逼近0.009%,此文將對兩類典型的Crash案例進行分析總結。
ios
Crash主要產生在Objective-C方法調用或系統方法調用,因此本文的兩個典型案例正是針對OC和C方法調用來展開:xcode
Crash堆棧長這樣!Σ(゚д゚lll)安全
圖1網絡
是的,看到這個堆棧我也很方,一眼望去只有一行是工程的代碼堆棧,仍是個main,但深刻分析了Objective-C的消息機制後咱們仍是能找到問題的突破口的。app
Crash類型函數
首先咱們看到這是一個SEGV_ACCERR類型的Crash,訪問了錯誤的地址。工具
其次,經過彙編代碼分析objc_msgSend方法,咱們能夠得知objc_msgSend + 16這一行代碼(以下圖2)是在讀取當前OC方法的receiver的isa指針偏移0x10的處的值(見附錄推薦的objc_msgSend連接文章),因爲對象已經被釋放了,因此讀取該地址致使了讀取錯誤地址,也即產生了野指針Crash。性能
圖2
測試
查找寄存器
因而,咱們查看Crash時各寄存器的值(見圖3),其中x0是發生Crash的函數的第一個參數,針對objc_msgSend來講x0同時表示指向發生Crash的對象的地址,x1是Crash的函數的第二個參數,在objc_msgSend中表示Crash的對象調用的selector,RDM很貼心,已經幫咱們查詢出該selector爲respondsToSelector:。若是x1是咱們工程中本身寫的一個方法就很容易分析問題了,直接查找工程代碼,定位到該函數便可找到緣由,但是respondsToSelector:調用的地方太多了,怎麼辦呢?咱們還要繼續往裏挖。
由於respondsToSelector:的參數是一個selector,因此只要再查出這個selector是什麼(對應查詢x2在符號表中的符號),也能夠立刻定位到問題代碼。可是很遺憾,x2不在Crash報告中Binary Images中的任一個模塊的地址範圍內,那,還有辦法嗎?
圖3
辦法仍是有的,咱們知道lr寄存器是當前函數的上一層函數調用地址,若是能知道lr寄存器執行的方法就能夠進一步肯定問題,很幸運,lr的值恰好就是Binary Images中管家模塊地址範圍內(見圖3,lr是0x000000010508be44,管家模塊範圍是0x104c24000 - 0x1055affff),因而在符號表中搜索lr對應的符號,獲得以下的信息:(下圖中的MQQABC爲你的app的符號表文件,在xcode打包提交時須要保存下來,對應XXX.app.dSYM/Contents/Resources/DWARF/XXX)
圖4
至此,咱們知道圖1那個只有main信息的堆棧產生的Crash是在-[MQQAlertView didDismissWithButtonIndex:]的第530行,產生Crash的緣由是調用了respondsToSelector:,已經十分接近答案了,可是MQQAlertView是管家一個通用的彈窗組件,因此還須要知道是哪一個頁面出現了這個Crash。
定位問題頁面
手管利用RDM的Crash上報組件能夠在Crash產生時上報附件的特性,將一些關鍵的信息存儲到了附件上(當前的ViewController堆棧、上一次釋放的ViewController、applicationState等),能夠在RDM平臺上查看這些附件信息,因而咱們查看附件信息,發現是在用戶退出某頁面A時產生的Crash。
得出問題緣由→→
至此,Crash的路徑已經很清楚了:用戶進入頁面A,頁面A彈出一個彈窗,在彈窗未彈出前用戶快速退出頁面,退出頁面時沒有把彈窗關掉,而後用戶點擊了彈窗,因爲彈窗的delegate是頁面A,而頁面A已經釋放,因此致使了訪問了野指針。
問題緣由查明,問題代碼定位精確,問題也就不難修復了。
注:objc_msgSend + 16是典型的野指針致使的Crash堆棧,遇到這類問題,基本上按照上述思路均可以順利解決。
棘手的Crash一般關鍵堆棧都是落在系統函數上,這也爲咱們把鍋甩給系統找到一個很好的藉口,但想辦法解決問題纔是目標,畢竟系統是沒辦法幫你背這個鍋的¯\_(ツ)_/¯下面這個例子是結合Crash報告提供的信息分析解決問題的典型案例:
圖5
從Crash報告能夠看到幾個關鍵信息:
1)Crash類型一樣是訪問了非法地址SEGV_ACCERR,非法地址是0x68
2)Crash發生在子線程(Thread 7)
3)Crash是落在flockfile + 24的位置上
因而咱們經過Xcode調試到flockfile函數,並定位到 + 24的位置(以下圖6斷點的位置)
圖6
ldr x8, [x19, #0x68] 這句彙編代碼的含義是從x19偏移0x68的地址上加載數據存儲到x8中。結合SEGV_ACCERR,咱們知道這個地址非法了,並且非法地址是0x68,也就是說x19 + 0x68 = 0x68,推出=> x19 = 0,再往上看到第5行:mov x19, x0,能夠知道x19的值是由x0賦值得來的,因此x0 = 0,又由於x0是函數的第一個參數,因此能夠得出flockfile的入參爲0,查看flockfile的定義:
void flockfile(FILE *);
可見,這裏的FILE *指針爲空了。結合堆棧中管家工程中的代碼調用:
- [MQQCBKAsdfUpdater mgPchAsdfCfgFileWithOFP:pFP:toFP:result:error:]
能夠看到,傳入了三個文件路徑,因此問題一定是其中一個文件不存在了。至此就是咱們從Crash報告中能分析出來的信息,再結合查看工程代碼得出:問題代碼最初是在主線程執行,中間dispatch到子線程(從Crash報告得出),線程間狀態沒有控制好致使切換到子線程執行的過程當中文件被刪除了而致使了Crash。
以上分析僅是對過程的回顧,略去了許多細節,這一節進行補充。由於Crash分析主要就是要搞清楚發生Crash時函數調用發生了什麼,因此這一節主要分爲幾個部分:
1)ARM64的函數調用約定
2)經常使用匯編指令
3)Objective-C函數調用的特色
4)查找符號表
5)Crash報告關鍵信息
因爲目前主流機型都是iPhone 5s以上的機型了,因此這裏只介紹ARM64。
圖7(摘自ARM64參考手冊)
ARM64指令集有31個64bit的通用整形寄存器:x0到x30(w0到w30表示只取這些寄存器的低32位)
x0到x7用來作參數傳遞,以及從子函數返回結果(一般經過x0返回,若是是一個比較大的結構體則結果會存在x8的執行地址上)
LR:即x30寄存器,也叫連接寄存器,通常是保存返回上一層調用的地址
FP:即r29,棧底寄存器
外加一個棧頂寄存器SP
棧是從高地址到低地址延伸的,棧底是高地址,棧頂是低地址
fp指向當前棧幀的棧低,即高地址
sp指向當前棧幀的棧頂,即低地址
下圖8是_funcA調用_funcB的棧幀狀況:
圖8(摘自技術博客)
_funcB的前三行代碼如圖8的彙編代碼所示:
第1行stp指令是表示將_funcA的棧底指針fp、連接寄存器lr存到_funcA的棧頂sp - 0x10的地址上,並將sp設置爲sp - 0x10(圖中fp_B),方便後續從_funcB返回_funcA,並恢復_funcA的棧幀
第2行是把sp賦給fp,即設置_funcB的棧底指針(圖中fp_B)
第3行是把sp設置爲sp - 0x30。由此完成了_funcA對_funcB的調用。
下面經過一個實例來分析函數的參數傳遞
圖9
如圖9有兩個方法,OC方法是一個按鈕點擊事件,點擊後調用上面的C方法,爲了調試方便C方法有11個參數,本例中入參的值是1到11,能夠觀察到超過8個參數時是怎麼傳參的。
爲了看到調用過程的彙編代碼,咱們須要在- (IBAction)testCmethodCall1:(id)sender中設置斷點,而後在Xcode中設置Always Show Disaasembly(見圖10),這樣調試過程當中看的就是彙編代碼了
圖10
咱們斷點到OC方法,彙編代碼如圖11
圖11
函數調用狀態切換
第1行:sub sp, sp, #0x40 設置新的棧頂寄存器(sp)
第2行:stp x29, x30, [sp, #0x30] 把棧底寄存器(x29即fp)、連接寄存器(x30即lr)保存起來
第3行:add x29, sp, #0x30 把fp(x29)設置爲sp + 0x30,即設置新的棧底寄存器
這3行,完成了系統對按鈕點擊事件方法的調用所需的狀態切換工做
爲C函數準備入參
接下來直到str w13, [sp, #0x8] 都是在爲調用C方法準備參數,由於沒有通過優化因此顯得很囉嗦。
orr w8, wzr, #0x1 是一個或指令,把零寄存器或上1的值賦給w8寄存器,就是w8 = 1,下面的相似,分別把2到11賦給w9-w十、w3-w七、w11-w13
stur x0, [x29, #-0x8] 把x0保存到x29 - 0x8上
stur x1, [x29, #-0x10] 把x1保存到x29 - 0x10上
str x2, [sp, #0x18] 把x2保存到sp + 0x18上
mov x0, x8把前面賦值爲1的x8(orr w8, wzr, #0x1)賦給x0
mov x1, x9,同理,把2賦給x1
mov x2, x10,同理,把3賦給x2,因而可知前面w八、w九、w10只是中轉用的,至此x0-x7已經將能夠直接傳值的寄存器都賦上了正確的值,接下來的3行則能夠看到是怎麼處理超過8個整形參數的狀況
經過棧傳參
str w11, [sp] 把前面賦值爲9(mov w11, #0x9)的w11存到棧頂位置
str w12, [sp, #0x4] 把前面賦值爲10(mov w12, #0xa)的w12存到棧頂偏移0x4的位置
str w13, [sp, #0x8] 把前面賦值爲11(mov w13, #0xb)的w13存到棧頂偏移0x8的位置
調用C函數
至此,入參所有準備完畢,接下來調用bl 0x104fc237c就能夠調用C函數了
圖12
進入C函數的彙編代碼,咱們先明確下這段C函數的任務是:return a1 + a2 + a11,因此應該是把OC函數中w0、w一、w13(w13存在棧上[sp, #0x8])的值拿出來相加,獲得的結果存到x0上,而後返回,因此:
sub sp, sp, #0x30 把棧頂指針設置爲sp - 0x30,這樣的話,以前w13存在的棧的位置就變成了[sp, #0x38],因此你會看到圖12最後一個紅圈ldr w1, [sp, #0x38]其實就是把以前保存w13的值load到w1中
str w0, [sp, #0x2c]和str w1, [sp, #0x28]把w0、w1的值存到棧上,而後又用ldr w0, [sp, #0x2c]和ldr w1, [sp, #0x28]把w0、w1的值取出來,沒優化的彙編真的很囉嗦
add w0, w0, w1 把w0、w1的值加起來存到w0(即計算了a1 + a2)
ldr w1, [sp, #0x38] 前面說過取出w13的值存到w1
add w0, w0, w1 把w0、w1的值加起來存到w0(即計算了a1 + a2 + a11),如今計算完的結果存到了w0中。
由上面的分析過程咱們能夠看到:
子函數開頭的彙編代碼會調整fp、sp指針
參數傳遞少於8個的使用x0-x7寄存器
超過8個的則使用棧來傳遞
子函數的返回值通常存在x0中
由於x0、x一、x2九、x30等寄存器有特殊含義,因此有時候會把這些寄存器的值先存到棧上,而後再使用它們
2.1節已經接觸了幾個彙編指令,下面整理下經常使用的幾個彙編指令:
mov a, b 即a = b
ldr a, [b] 將b指針所在地址上的內容加載a寄存器中
str a, [b] 將a寄存器存儲到b指針指向地址上
ldr a, [b, #0x10] 從b寄存器地址+0x10的地址上加載內容到a寄存器中
ldr a, [b, #0x10]! 帶感嘆號的意思是把內容加載到a寄存器中,而且修改b寄存器爲b = b + 0x10
cmp a, b 比較a、b寄存器的值,會修改cpsr
cbz xd, addr 判斷xd寄存器是否爲0,是則跳轉到addr地址處執行
cbnz xd, addr 判斷xd寄存器是否不爲0,不爲0則跳轉到addr
b 跳轉指令,不修改lr寄存器,因此子函數調用過程不會出如今堆棧中
bl 跳轉指令,修改lr寄存器,因此子函數調用過程會出如今堆棧中
stp a, b, [c] 從c地址中取出兩個64位值分別存儲到a、b兩個寄存器中
ldp a, b, [c] 把a、b兩個寄存器的值存儲到c地址中
Objective-C函數調用是一種特殊的函數調用,但最終也是轉化爲C函數調用的方式。
咱們都知道Objective-C調用最終都會調用objc_msgSend(id self, SEL selector, ...),而後再用前面的知識分析objc_msgSend便可
能夠看到,x0就是調用的receiver,x1就是調用的selector,後面則是參數。具體能夠查看附錄中相關的文章。
圖13
Crash報告中有Binary Images:
1)模塊的起止地址:好比圖13中MQQABC模塊的起始地址是0x104c24000,結束地址是0x1055affff,因此咱們能夠經過這些模塊的起止地址來判斷一個咱們感興趣的寄存器的地址是屬於哪一個模塊的
2)模塊的UUID,如圖13中MQQABC的UUID是f130b043a0c832d9958d89dab8339961,經過它能夠斷定你的符號文件是正確的,如圖14用dwarfdump
圖14
3)用atos查找地址對應的符號,-l須要提供1)中提到的模塊起始地址
圖15
4)若是用atos查找出來的結果仍然是個地址,還須要在mach-O文件的__TEXT段或__RODATA段的__objc_methname中進一步查找(注意:第一個紅框中查詢出來的0x在otool查找Mach-O文件中要去掉)
圖16
圖17
圖18 結合寄存器值查找關鍵信息
圖19 肯定符號表UUID及起止地址
1.ARM64參考手冊:infocenter.arm.com/help/topic/…
2.技術博客:blog.cnbluebox.com/blog/2017/0…
3. 分析objc_msgSend的彙編代碼:www.cocoachina.com/ios/2017080…
4. ARM64彙編約定:infocenter.arm.com/help/topic/…
騰訊WeTest是由騰訊官方推出的一站式質量開放平臺。十餘年品質管理經驗,致力於質量標準建設、產品質量提高。騰訊WeTest爲移動開發者提供兼容性測試、雲真機、性能測試、安全防禦、企鵝風訊(輿情分析)等優秀研發工具,爲百餘行業提供解決方案,覆蓋產品在研發、運營各階段的測試需求,歷經千款產品磨礪。金牌專家團隊,經過5大維度,41項指標,360度保障您的產品質量。
騰訊互娛爲提升蘋果應用的審覈經過率,專門成立了蘋果審覈測試團隊,打造出iOS預審工具這款產品。通過長時間的內部運營和磨鍊,騰訊蘋果應用審覈經過率從平均35%提高到90%+。點擊連接;wetest.qq.com/product/ios 邀您馬上體驗。
若是使用當中有任何疑問,歡迎聯繫騰訊WeTest企業QQ:800024531