眾里尋他千百度,驀然回首,那人卻在燈火闌珊處。--《青玉案·元夕》
要學(xué)會看crash崩潰和報告
一個應(yīng)用程序并不總會一直運行的很好,它總會有出現(xiàn)crash崩潰的情況。如果在應(yīng)用程序中接入了一些第三方的crash收集工具或者自建crash收集報告平臺的話將會很好的幫助開發(fā)者去分析和解決應(yīng)用程序在線上運行的問題,當(dāng)出現(xiàn)的崩潰問題能得到及時的解決和快速的修復(fù)時必將會大大的提升應(yīng)用程序的用戶體驗。
當(dāng)前比較流行的crash收集分析工具很多都是基于開源的KSCrash代碼來進(jìn)行封裝和改進(jìn)的。蘋果自身也構(gòu)建了一套crash采集和分析的機制,你可以從真機的聯(lián)機日志或者從開發(fā)者賬號中去查看對應(yīng)的crash信息。網(wǎng)絡(luò)上也有很多關(guān)于crash分析的文章,以及crash堆棧符號化處理的文章。這里假定你已經(jīng)了解了一些查看crash報告的方法和技巧以及一些簡單的crash分析技巧,因為這些是作為開發(fā)者需要具備的技能之一。
一個objc_msgSend+16崩潰棧
應(yīng)用程序出現(xiàn)的crash崩潰異常有一些能夠簡單的被分析和解決,往往這些crash崩潰異常都會帶有明確的上下文信息和函數(shù)調(diào)用層級堆棧。但并不是所有的crash崩潰異常都能被簡單的解決,尤其是那些沒有明確上下文信息的函數(shù)調(diào)用堆棧或者那些調(diào)用堆棧中沒有一個函數(shù)或者方法能夠被直接定位到源代碼的場景,就如下面這個崩潰的函數(shù)調(diào)用棧(部分信息):
Incident Identifier: 85BE3461-D7FD-4043-A4B9-1C0D9A33F63D
CrashReporter Key: 9ec5a1d3b8d5190024476c7068faa58d8db0371f
Hardware Model: iPhone7,2
Code Type: ARM-64
Parent Process: ? [1]
Date/Time: 2018-08-06 16:36:58.000 +0800
OS Version: iOS 10.3.3 (14G60)
Report Version: 104
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Codes: 0x00000000 at 0x00000005710bbeb8
Crashed Thread: 2
Thread 2 name: WebThread
Thread 2 Crashed:
0 libobjc.A.dylib objc_msgSend + 16
1 UIKit -[UIWebDocumentView _updateSubviewCaches] + 40
2 UIKit -[UIWebDocumentView subviews] + 92
3 UIKit -[UIView(CALayerDelegate) _wantsReapplicationOfAutoLayoutWithLayoutDirtyOnEntry:] + 72
4 UIKit -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1256
5 QuartzCore -[CALayer layoutSublayers] + 148
6 QuartzCore CA::Layer::layout_if_needed(CA::Transaction*) + 292
7 QuartzCore CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 32
8 QuartzCore CA::Context::commit_transaction(CA::Transaction*) + 252
9 QuartzCore CA::Transaction::commit() + 504
10 QuartzCore CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) + 120
11 CoreFoundation __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
12 CoreFoundation __CFRunLoopDoObservers + 372
13 CoreFoundation CFRunLoopRunSpecific + 456
14 WebCore RunWebThread(void*) + 456
15 libsystem_pthread.dylib _pthread_body + 240
16 libsystem_pthread.dylib _pthread_body + 0
Thread 2 crashed with ARM-64 Thread State:
cpsr: 0x0000000020000000 fp: 0x000000016e18d7c0 lr: 0x000000018e2765fc pc: 0x0000000186990150
sp: 0x000000016e18d7b0 x0: 0x0000000174859740 x1: 0x000000018eb89b7b x10: 0x0000000102ffc000
x11: 0x00000198000003ff x12: 0x0000000102ffc290 x13: 0xbadd8a65710bbead x14: 0x0000000000000000
x15: 0x000000018caeb48c x16: 0x00000005710bbea8 x17: 0x000000018e2765d4 x18: 0x0000000000000000
x19: 0x0000000103a52800 x2: 0x0000000000000000 x20: 0x00000000000002a0 x21: 0x0000000000000000
x22: 0x0000000000000000 x23: 0x0000000000000000 x24: 0x0000000000000098 x25: 0x0000000000000000
x26: 0x000000018ebade52 x27: 0x00000001ad018624 x28: 0x0000000000000000 x29: 0x000000016e18d7c0
x3: 0x000000017463db60 x4: 0x0000000000000000 x5: 0x0000000000000000 x6: 0x0000000000000000
x7: 0x0000000000000000 x8: 0x00000001acfb9000 x9: 0x000000018ebf8829
Binary Images:
0x100030000 - 0x1022cbfff +xxxx arm64 <6b98f446542b3de5818256a8f2dc9ebf> /var/containers/Bundle/Application/441619EF-BD56-4738-B6CF-854492CDFAC9/xxxx.app/xxxx
0x1063f8000 - 0x106507fff MacinTalk arm64 <0890ce05452130bb9af06c0a04633cbb> /System/Library/TTSPlugins/MacinTalk.speechbundle/MacinTalk
0x107000000 - 0x1072e3fff TTSSpeechBundle arm64 <d583808dd4b9361b99a911b40688ffd0> /System/Library/TTSPlugins/TTSSpeechBundle.speechbundle/TTSSpeechBundle
...
0x18e03d000 - 0x18ede3fff UIKit arm64 <314063bdf85f321d88d6e24a0de464a2> /System/Library/Frameworks/UIKit.framework/UIKit
0x18ede4000 - 0x18ee0cfff CoreBluetooth arm64 <ced176702d7c37e6a9027eeb3fbf7f66> /System/Library/Frameworks/CoreBluetooth.framework/CoreBluetooth
這是一個在iOS10.3.3版本的64位設(shè)備上的一條crash異常報告的片段信息,要記住這些信息,它對定位crash崩潰異常有很大的幫助。從崩潰的函數(shù)調(diào)用棧中可以看出異常是出現(xiàn)在最頂層的函數(shù)調(diào)用objc_msgSend+16處,也就是在objc_msgSend函數(shù)的第5條指令處(通常情況下arm體系結(jié)構(gòu)中每條指令占用4個字節(jié),上述的信息表明是崩潰在函數(shù)的第16個字節(jié)的偏移地址處,也就是函數(shù)的第5條指令處)。崩潰異常類型顯示為EXC_BAD_ACCESS表明是產(chǎn)生了無效的地址的讀寫訪問,整個崩潰函數(shù)調(diào)用棧中沒應(yīng)用程序中的任何上下文信息。objc_msgSend函數(shù)是runtime方法執(zhí)行的核心引擎而且調(diào)用如此的頻繁,函數(shù)內(nèi)部是不可能有BUG的。 那么為什么會崩潰在這呢?
當(dāng)異常出現(xiàn)在沒有源代碼的函數(shù)內(nèi)部時,唯一的方法就是去看它內(nèi)部的“源代碼”實現(xiàn)
既然出現(xiàn)問題是在objc_msgSend函數(shù)的第5條指令處,可以來看看這個函數(shù)實現(xiàn)的匯編代碼指令開頭片段:
;iOS10以后的objc_msgSend的部分實現(xiàn)代碼。
_objc_msgSend:
00000001800bc140<+0> cmp x0, #0x0 ;判斷對象receiver和0進(jìn)行比較
00000001800bc144<+4> b.le 0x1800bc1ac ;如果對象指針為0或者高位為1則執(zhí)行特殊處理跳轉(zhuǎn)。
00000001800bc148<+8> ldr x13, [x0] ;取出對象的isa指針賦值給x13
00000001800bc14c<+12> and x16, x13, #0xffffffff8 ;得到對象的Class對象指針賦值給x16
00000001800bc150<+16> ldp x10, x11, [x16, #0x10] ;取出Class對象的cache成員分別保存到x10,x11寄存器中
-----------------------------------------------------------上面的指令就是代碼崩潰處。
00000001800bc154<+20> and w12, w1, w11
無論是真機還是模擬器,XCODE都支持在運行時來查看任何調(diào)用的函數(shù)的匯編代碼實現(xiàn),你可以通過設(shè)置符號斷點或者進(jìn)入?yún)R編調(diào)試模式以及單指令跳轉(zhuǎn)的方式來查看函數(shù)的匯編代碼實現(xiàn)。
從代碼中可以看出是在讀取對象的Class對象指針的數(shù)據(jù)成員cache時出現(xiàn)了無效的地址訪問異常。但是對象的Class對象這部分定義數(shù)據(jù)是存儲在進(jìn)程內(nèi)存的數(shù)據(jù)區(qū)段中,并且伴隨著整個應(yīng)用的生命周期而存在,是不可能被釋放和銷毀的,因此正常情況下是不可能存在非法內(nèi)存地址訪問異常的。會出現(xiàn)這種問題的原因就是調(diào)用方法的OC對象被銷毀了,再說具體一點就是對一個已經(jīng)被釋放掉的OC對象繼續(xù)調(diào)用了實例方法而導(dǎo)致的。因此當(dāng)出現(xiàn)這種類型的崩潰時,不管是否有明確上下文,其原因都是一致的。下面這張圖就能很清楚的說明其中的原因了:
實際上在arm64位系統(tǒng)中isa中保存的并不是對象的Class對象地址,上面的圖目的是為了更加直觀的顯示問題原因。
一個OC對象obj在被銷毀前,其中的isa指針會指向正確的Class對象所在的內(nèi)存地址。因此調(diào)用objc_msgSend方法將會正常的運行,而一旦obj對象被銷毀后,為其分配的堆內(nèi)存將被回收用作其他用途,因此有可能這部分內(nèi)存區(qū)域的數(shù)據(jù)會被覆寫。當(dāng)對一個已經(jīng)釋放了的OC對象繼續(xù)調(diào)用實例方法時,在objc_msgSend函數(shù)內(nèi)部讀取到obj的isa指針得到的將是一個未知或者有可能無效的指針值。所以當(dāng)對這個未知地址指向的內(nèi)存進(jìn)行訪問時就出現(xiàn)了上面的EXC_BAD_ACCESS的異常崩潰了。
CPU指令中操作寄存器和常數(shù)的指令一般不會產(chǎn)生崩潰異常,比如上面的第1,2,4,6條指令;而一般產(chǎn)生訪問異常的指令是發(fā)生在那些訪問內(nèi)存地址的指令當(dāng)中,比如第3條和5條。
也許你會好奇既然obj對象已經(jīng)被釋放了,為什么崩潰會出現(xiàn)在objc_msgSend函數(shù)的第5條指令,其中的第3條指令是訪問對象的isa數(shù)據(jù)的,為什么不崩潰在這呢? 其實答案很簡單,因為幾乎所有的OC對象都是從堆內(nèi)存區(qū)域中分配內(nèi)存的,所以當(dāng)某個OC對象被銷毀后,其所占用的內(nèi)存仍然會放回堆內(nèi)存區(qū)域中進(jìn)行管理,而堆內(nèi)存區(qū)域的地址是可以進(jìn)行任意的讀寫訪問的,所以即使對象被銷毀釋放,仍然是可以訪問對象所指向的內(nèi)存區(qū)域的數(shù)據(jù)的。
應(yīng)用程序出現(xiàn)崩潰異常時除了函數(shù)調(diào)用棧可提供分析參考外,還可以從寄存器中的值來進(jìn)行一步分析。根據(jù)上述的函數(shù)指令實現(xiàn)中可以看出:
x0 寄存器中的保存的就是那個被銷毀了的對象指針。
x1 寄存器中保存的就是產(chǎn)生崩潰的對象的方法名稱的地址。
x13 寄存器中保存的就是對象的isa指針值。
x16 寄存器中保存的就是對象的Class指針對象。
函數(shù)崩潰處指令為:
ldp x10, x11, [x16, #0x10]
這時候因為x16中其實保存的是一個非法的Class對象指針地址了,所以當(dāng)執(zhí)行l(wèi)dp指令來從x16所指向地址的偏移0x10處讀取內(nèi)存數(shù)據(jù)時就產(chǎn)生了崩潰,而崩潰的異常代碼:
Exception Codes: 0x00000000 at 0x00000005710bbeb8
中的地址值也剛好和x16寄存器中的值是一致的。也就是表明x16中所保存的Class對象指針就是一個非法和無效的內(nèi)存地址。
在所有的OC方法中如果你設(shè)置了符號斷點那么在方法開始執(zhí)行時x0中保存的總是執(zhí)行方法的對象,也是第一個方法的參數(shù);x1中總是保存的執(zhí)行的方法的名稱字符串,也是第二個方法的參數(shù);然后x2到x15有可能依次是方法的其他參數(shù)。因此通常情況下你可以在調(diào)試控制臺中輸入:
po $x0
來顯示對象信息,p (char*)$x1
來顯示方法名稱。 具體的詳細(xì)介紹可以參考我的另外一篇文章:寄存器介紹
上面的崩潰調(diào)用棧中,所有的函數(shù)和方法都是系統(tǒng)函數(shù)并沒有程序自身的源代碼,因此很難跟蹤或者發(fā)現(xiàn)問題產(chǎn)生的原因,因為此時是無法知道是哪個類的對象執(zhí)行方法調(diào)用而產(chǎn)生的crash了,唯一的線索就是x1寄存器中的值了。這個寄存器中的值保存的是調(diào)用的方法名, 它是一個SEL類型的數(shù)據(jù),因此可以根據(jù)x1中保存的方法名來進(jìn)行反推,也就是從方法名來反推出產(chǎn)生崩潰的對象的類名。
x1寄存器中保存的方法的內(nèi)存地址是存在于某個加載的庫Image的代碼段中,因此可以在崩潰日志的Binary Images列表中找到定義方法名的庫Image信息,Binary Images列表中的每個庫Image都有這個庫加載的開始和結(jié)束地址以及路徑名稱,可以很容易就從這些區(qū)間列表中找到x1寄存器所指的方法名到底屬于哪個庫。就上面的例子來說可以很明確的看到方法地址0x18eb89b7b是屬于:
0x18e03d000 - 0x18ede3fff UIKit arm64 <314063bdf85f321d88d6e24a0de464a2> /System/Library/Frameworks/UIKit.framework/UIKit
也就是UIKit庫中定義的某個對象在執(zhí)行x1所指的方法而產(chǎn)生了崩潰。有了這個更進(jìn)一步的信息后就可以在源代碼中進(jìn)行檢查看看哪部分代碼調(diào)用到了產(chǎn)生崩潰的庫中所定義的對象了(當(dāng)然UIKit這里不具備代表性,實際中崩潰時方法名也許會在其他的庫中)。這樣就從一定程度上能夠縮小排查問題的范圍。
常見的崩潰異常分析定位方法
當(dāng)出現(xiàn)了沒有上下文的崩潰異常調(diào)用棧時,并不是對它束手無策。除了可以根據(jù)異常類型(signal的類型)分析外,還可以借助搜索引擎以及一些常見的問題解答站點來尋找答案,當(dāng)然還可以借助下面列出幾種定位和分析的方法:
1.開源代碼法
這個方法其實很簡單,蘋果其實開源了非常多的基礎(chǔ)庫的源代碼,因此當(dāng)程序崩潰在這些開源的基礎(chǔ)庫上時就可以去下載對應(yīng)的基礎(chǔ)庫的源代碼進(jìn)行閱讀。然后從源代碼上進(jìn)行問題的分析,從而找到產(chǎn)生異常崩潰的原因。你可以從https://opensource.apple.com處去下載開源的最新的源代碼。這種方法的缺點是并不是所有的代碼都是開源的,而且開源的代碼并不一定是你真機設(shè)備上運行的iOS版本。因此這種方法只能是一種輔助方法。
2.方法符號斷點法
采用這種方法時,確保你手頭上要有一臺和產(chǎn)生崩潰異常問題的操作系統(tǒng)版本相同的真機設(shè)備,以方便聯(lián)機調(diào)試和運行。你可以在崩潰異常報告的:
OS Version: iOS 10.3.3 (14G60)
部分看到產(chǎn)生異常的操作系統(tǒng)版本號,就如本文的例子里面產(chǎn)生異常的操作系統(tǒng)版本號為iOS 10.3.3。因為相同的操作系統(tǒng)版本號中所有庫中代碼實現(xiàn)的都是一樣的。如果實在沒有對應(yīng)的版本號的設(shè)備則可以試圖找一臺版本號最相近的設(shè)備。明確了操作系統(tǒng)版本和真機設(shè)備后再從代碼倉庫中檢出和你線上相同版本的應(yīng)用程序的源代碼(假如崩潰調(diào)用棧中沒有任何我們編寫的函數(shù)代碼則這個條件要求不必那么嚴(yán)格)。并打開項目工程,然后為產(chǎn)生崩潰的函數(shù)調(diào)用棧的棧頂函數(shù)或者方法名添加一個符號斷點。如果你不知道如何添加符號斷點請參考文章:https://blog.csdn.net/xuhen/article/details/77747456, 或者查找關(guān)鍵字:“XCODE 符號斷點"。
設(shè)置符號斷點的方法或者函數(shù)名時可以有如下的選擇:
- 如果產(chǎn)生崩潰的棧頂是一個OC對象的方法則可以直接用這個類名和方法名來設(shè)置符號斷點。
- 如果產(chǎn)生崩潰的棧頂是一個通用的C函數(shù)比如objc_msgSend、free、objc_release則考慮用函數(shù)調(diào)用棧的第二層函數(shù)和方法名來設(shè)置符號斷點。比如文本例子中的-[UIWebDocumentView _updateSubviewCaches]方法。
- 如果產(chǎn)生崩潰的函數(shù)調(diào)用棧頂是一個沒有對外暴露的C函數(shù),因為這種函數(shù)設(shè)置符號斷點的難度比交大,所以往往考慮采用函數(shù)調(diào)用棧的第二層函數(shù)或者方法名來做為符號斷點。
設(shè)置符號斷點的目的是為了在崩潰函數(shù)調(diào)用堆棧重現(xiàn)時,能在運行時的斷點處進(jìn)行動態(tài)分析。當(dāng)你設(shè)置了符號斷點后,如果程序邏輯運行到這個函數(shù)或者方法時,系統(tǒng)就會在設(shè)置的方法或者函數(shù)的第一條指令處停止下來。這時候就可以查看此時的函數(shù)調(diào)用棧是否和產(chǎn)生崩潰時的調(diào)用棧相符,如果相符合那么表明能夠重現(xiàn)可能發(fā)生問題的邏輯了,如果斷點處的調(diào)用棧和產(chǎn)生崩潰的調(diào)用棧不相同,則可能需要讓程序繼續(xù)運行,以便下次在同樣斷點處時進(jìn)行調(diào)用棧的比較,因為設(shè)置斷點的方法名并不一定只在一處被調(diào)用。
當(dāng)程序停在了設(shè)置符號斷點的函數(shù)或者方法的開始地址后,接下來就需要在這個方法內(nèi)進(jìn)行第二個斷點的設(shè)置,設(shè)置的地方就是崩潰函數(shù)調(diào)用棧中函數(shù)調(diào)用上層函數(shù)的偏移處,這個可以在崩潰的報告中看到:
0 libobjc.A.dylib objc_msgSend + 16
1 UIKit -[UIWebDocumentView _updateSubviewCaches] + 40
也就是需要在_updateSubviewCaches函數(shù)的第11條指令或者函數(shù)的第40個偏移字節(jié)附近處添加一個斷點。這樣當(dāng)程序運動到斷點處時就可以在函數(shù)調(diào)用上層函數(shù)前查看各寄存器的值從而進(jìn)行問題的定位和分析。
一般情況下崩潰函數(shù)棧報告中除棧頂函數(shù)外的每一層函數(shù)名后 + 的數(shù)字表明是在當(dāng)前函數(shù)的對應(yīng)的地址偏移處附近進(jìn)行了上層函數(shù)的調(diào)用,也就是對應(yīng)的地址偏移附近一般都會存在一條bl指令或者blr這兩條指令,這兩條指令的作用就是執(zhí)行函數(shù)的調(diào)用。
通過二次斷點的設(shè)置,程序運行到斷點時的指令是:
0x18c0248fc <+36>: bl 0x1893042dc ;0x1893042dc 這個地址就是objc_msgSend的函數(shù)地址
本例子的異常崩潰的原因是對一個已經(jīng)釋放的對象繼續(xù)調(diào)用方法而產(chǎn)生的崩潰。所以當(dāng)斷點停在指令處時,我們可以在右下角的lldb控制臺中打印指令:
(lldb)po $x0
<__NSArrayM 0x1c044c2a0>(
<UIWebOverflowScrollView: 0x1281d7e00; frame = (0 0; 375 603); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x1c0851190>; layer = <WebLayer: 0x1c4426ba0>; contentOffset: {0, 0}; contentSize: {375, 12810}; adjustedContentInset: {0, 0, 0, 0}>
)
(lldb) p (char*)$x1
(char *) $6 = 0x000000018cb9dd70 "release"
(lldb)
可以看出x0是一個數(shù)組對象,而x1中則是release方法。這樣就進(jìn)一步明確了是對一個已經(jīng)釋放了的數(shù)組對象調(diào)用了release方法而導(dǎo)致異常崩潰了。至于x0是一個什么數(shù)組以及保存在哪里,則可以通過匯編指令中的x0寄存器的使用進(jìn)行回溯往上查找指令來進(jìn)一步分析了。其實這個問題如果進(jìn)一步觀察就可以看出:崩潰的線程并不是出現(xiàn)在主線程,而是在一個工作線程中。而視圖的操作基本都應(yīng)該放在主線程進(jìn)行,因此當(dāng)主線程的某些子視圖數(shù)組對象被釋放后,這里又在輔助線程中進(jìn)行讀取訪問,就出現(xiàn)了上面的異常崩潰問題了。
在函數(shù)調(diào)用bl或者blr指令處設(shè)置斷點后,因為根據(jù)ABI規(guī)則所有非浮點數(shù)的參數(shù)分別依次保存在x0,x1,....這些寄存器中。所以可以在斷點處分別打印出這些寄存器的值就可以知道函數(shù)調(diào)用前所傳遞的參數(shù)值了。這個方法非常有助于進(jìn)行問題的定位和分析。
3.手動重現(xiàn)法
有時候即使你設(shè)置了符號斷點,場景依然無法重現(xiàn),這時候就需要采用一些特殊的手段,那就是手動的執(zhí)行方法調(diào)用。實現(xiàn)方式很簡單就是在某個演示代碼中人為的進(jìn)行崩潰棧頂函數(shù)的調(diào)用。就比如上面的例子當(dāng)[UIWebDocumentView _updateSubviewCaches]
方法一直不被執(zhí)行時,就可以自己手動的去創(chuàng)建一個UIWebDocumentView對象,并手動的調(diào)用對應(yīng)的方法_updateSubviewCaches即可。這里存在的兩個問題是有可能這個類并沒有對外進(jìn)行聲明,或者我們并不知道方法的參數(shù)類型或者需要傳遞的值。對于第一個問題解決的方法可以采用NSClassFromString來得到類信息并進(jìn)行對象創(chuàng)建。而第二個問題則可以借助一些工具比如class-dump或者一些其他的手段來確認(rèn)方法的參數(shù)個數(shù)和參數(shù)類型。總之,目的就是為了能夠進(jìn)入函數(shù)的斷點,甚至都可以在不知道如何傳遞參數(shù)時將所有的參數(shù)都傳值為0或者nil來臨時解決問題。下面就是模擬崩潰函數(shù)的調(diào)用實現(xiàn)代碼:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
//因為類名和方法名都未對外公開,我們可以借助一些技術(shù)手段來讓某個特定的方法執(zhí)行,目的是為了能夠進(jìn)入到方法的內(nèi)部實現(xiàn)。
Class cls = NSClassFromString(@"UIWebDocumentView");
id obj = [[cls alloc] init];
SEL sel = sel_registerName("_updateSubviewCaches");
[obj performSelector:sel];
//...
}
測試代碼可以寫在任何一個地方,這里為了方便就在程序啟動處加上測試代碼。等代碼編寫完畢后,就可以為方法設(shè)置符號斷點。這樣當(dāng)程序一運行時就一定能夠進(jìn)入到這個函數(shù)的內(nèi)部去。一旦函數(shù)被執(zhí)行后出現(xiàn)了斷點,就可以按照第2種方法中的介紹進(jìn)行崩潰分析了。
其實第3種方法的原則就是只要能讓產(chǎn)生崩潰異常的方法被調(diào)用,這其中可以嘗試著采用各種手段將對象和方法run起來。
4.第三方工具靜態(tài)分析法
前面兩種介紹的都是動態(tài)分析法, 有時候還可以借助一些反編譯的工具來對程序代碼進(jìn)行靜態(tài)分析。比如像Hopper或者IDA之類的工具。缺點就是這些工具是收費的,而且效果沒有動態(tài)分析那么的好。在使用上個人覺得IDA分析工具更加友好和強大一些。
采用第三方工具時需要找到產(chǎn)生崩潰的函數(shù)所在的庫,函數(shù)所在的庫在崩潰的函數(shù)調(diào)用棧列表中就能找到了。如果崩潰函數(shù)是在應(yīng)用程序本身中被定義,那么需要將上傳到appstore的ipa文件解壓縮并提取出其中的可執(zhí)行程序用工具打開即可。如果崩潰函數(shù)是在某個系統(tǒng)庫中被定義,那么可從如下的路徑:
~/Library/Developer/Xcode/iOS DeviceSupport/
iOS DeviceSupport這個文件夾下的內(nèi)容將展示你所有曾經(jīng)聯(lián)機調(diào)試過的各種操作系統(tǒng)版本的庫的一份拷貝,如果你沒有真機調(diào)試過出現(xiàn)崩潰的操作系統(tǒng)版本,請找一個安裝了這個操作系統(tǒng)版本的真機設(shè)備,并聯(lián)機,這樣你的文件夾中就會有對應(yīng)的操作系統(tǒng)版本下的系統(tǒng)庫的拷貝信息了。
中找到對應(yīng)的產(chǎn)生崩潰的手機操作系統(tǒng)版本號的庫文件:10.3.3(14G60)/Symbols/System/Library/Frameworks/UIKit.framework/UIKit
當(dāng)用IDA工具打開對應(yīng)的庫文件或者可執(zhí)行文件時你看到的將是這個庫文件的所有匯編形式的代碼和數(shù)據(jù)。因此你可以通過搜索菜單來查找產(chǎn)生崩潰的函數(shù)或者方法名。這時候你就可以進(jìn)一步對產(chǎn)生問題的函數(shù)的匯編代碼進(jìn)行分析了。采用IDA工具進(jìn)行匯編代碼分析的缺點是靜態(tài)分析無法看到運行時的各個寄存器的真實的值,因此采用這種方法可能更需要考慮你對匯編代碼的理解能力。下面就是本文例子中的[UIWebDocumentView _updateSubviewCaches]方法的實現(xiàn)匯編代碼:
采用IDA工具進(jìn)行分析時,需要了解一些比如庫基地址和代碼數(shù)據(jù)偏移地址以及地址重定向相關(guān)的知識。蘋果系統(tǒng)為安全對每個庫的加載都采用了ASLR的方式,也就是庫所加載的基地址每次運行時都是隨機的,這樣當(dāng)某次崩潰發(fā)生時需要將產(chǎn)生崩潰時的地址轉(zhuǎn)化為我們通過IDA工具打開的地址。 轉(zhuǎn)換公式為:
轉(zhuǎn)換后的地址 = 崩潰時寄存器中保存的原始地址值 - 崩潰時地址所在的庫的基地址值 + 工具打開庫時所設(shè)定的基地址。
就以上面崩潰異常為例,當(dāng)我們用IDA工具看看x1寄存器中的值到底是一個什么方法名,那么只需要把x1的值(0x018eb89b7b),減去其所在的庫UIKit的基地址值(0x18e03d000),在加上IDA工具打開庫時的基地址(要想看基地址則滾動到IDA視圖的最開始部分,本次打開的基地址為:0x187769000)。所以x1寄存器中的地址值被轉(zhuǎn)化后應(yīng)該為:
0x018eb89b7b - 0x18e03d000 + 0x187769000 = 0x1882B5B7B
在IDA工具中將地址跳轉(zhuǎn)到0x1882B5B7B就可以看到本例子中產(chǎn)生崩潰的方法名是叫release:
當(dāng)然IDA工具是可以手動進(jìn)行基地址的自定義設(shè)置的,這樣就不需要進(jìn)行計算以便和線上崩潰的基地址對齊。
如果你手頭上沒有第三方工具,其實系統(tǒng)內(nèi)置的otools工具也可以幫我們進(jìn)行問題的定位以及匯編代碼的查看和分析了,具體的方法大家就去查找相關(guān)的對otools使用的教程即可,這里就不展開了。
總結(jié)
上面列出的所有分析方法中有靜態(tài)分析的也有動態(tài)分析。當(dāng)出現(xiàn)了崩潰時除了從崩潰函數(shù)調(diào)用棧去分析問題,還可以從寄存器,以及加載的鏡像列表,以及崩潰棧頂部的函數(shù)的匯編代碼等等進(jìn)行綜合的分析和判斷。當(dāng)然即使這樣也不能保證所有問題就一定能夠得到解決,本文中列舉的例子只是在實際中的一種非常常見的崩潰異常,希望通過這個示例來起到一個拋磚引玉的效果,畢竟不同的崩潰異常的差異是比較大的。遇到問題需要具體分析,走進(jìn)函數(shù)的內(nèi)部實現(xiàn)就一定能夠找到產(chǎn)生問題的根源。
??【返回目錄】