- iOS Crash 流程化3:Crash 產(chǎn)生和符號(hào)化的原理
- 異常類(lèi)型
- Mach異常
- Unix信號(hào)
- 異常的產(chǎn)生
- 線(xiàn)程回溯
- 符號(hào)化回溯線(xiàn)程
- 符號(hào)在二進(jìn)制中的偏移量
- atos
- 符號(hào)化回溯線(xiàn)程
- 符號(hào)化內(nèi)幕
- 小小結(jié)
- 線(xiàn)程的狀態(tài)寄存器
- Binary Images
- 小結(jié)
- 異常類(lèi)型
iOS 的異常類(lèi)型(Exception Type)由兩部分構(gòu)成:Mach異常、Unix信號(hào)異常。
異常類(lèi)型
Mach異常
蘋(píng)果系統(tǒng)有一個(gè)微內(nèi)核,叫做XNU,它的源碼可以在opensource上下載到。Mach是XNU的核心,因而,Mach異常就指Mach內(nèi)核異常。Mach包含三部分內(nèi)容:thread,task,host。后續(xù)的章節(jié)中很多地方都會(huì)用到Mach。不妨移步到Mach IPC Interface,了解下Mach暴露給用戶(hù)的API。
Mach暴露給了用戶(hù)部分API,允許用戶(hù)和內(nèi)核交互。用戶(hù)態(tài)的開(kāi)發(fā)者可以通過(guò)Mach API設(shè)置thread、task、host的異常端口,來(lái)捕獲Mach異常,抓取Crash事件。
Mach異常包括:
#define EXC_BAD_ACCESS 1 /* Could not access memory */
/* Code contains kern_return_t describing error. */
/* Subcode contains bad memory address. */
#define EXC_BAD_INSTRUCTION 2 /* Instruction failed */
/* Illegal or undefined instruction or operand */
#define EXC_ARITHMETIC 3 /* Arithmetic exception */
/* Exact nature of exception is in code field */
#define EXC_EMULATION 4 /* Emulation instruction */
/* Emulation support instruction encountered */
/* Details in code and subcode fields */
#define EXC_SOFTWARE 5 /* Software generated exception */
/* Exact exception is in code field. */
/* Codes 0 - 0xFFFF reserved to hardware */
/* Codes 0x10000 - 0x1FFFF reserved for OS emulation (Unix) */
#define EXC_BREAKPOINT 6 /* Trace, breakpoint, etc. */
/* Details in code field. */
#define EXC_SYSCALL 7 /* System calls. */
#define EXC_MACH_SYSCALL 8 /* Mach system calls. */
#define EXC_RPC_ALERT 9 /* RPC alert */
#define EXC_CRASH 10 /* Abnormal process exit */
#define EXC_RESOURCE 11 /* Hit resource consumption limit */
Unix信號(hào)
信號(hào)是通知進(jìn)程已發(fā)生某種情況的軟中斷技術(shù)。例如:某個(gè)進(jìn)程執(zhí)行了除法操作,其除數(shù)為0,則將名為SIGFPE(浮點(diǎn)異常)的信號(hào)發(fā)送給該進(jìn)程。
異常的產(chǎn)生
那么,怎么會(huì)有兩種異常信息呢?
念茜的漫談iOS Crash收集框架闡述了兩者的關(guān)系,這里再重復(fù)下。
蘋(píng)果系統(tǒng)是基于Unix系統(tǒng)的,蘋(píng)果的大牛們?yōu)榱思嫒軺nix信號(hào),將Mach異常轉(zhuǎn)化為Unix信號(hào),并投射到異常的線(xiàn)程,這樣做的目的是:對(duì)于不懂Mach異常的人,也可以使用Unix信號(hào)捕獲異常。所以,Crash日志有兩種異常信息。
Mach和Unix關(guān)系圖:
所有Mach異常都在host層被ux_exception轉(zhuǎn)換為相應(yīng)的Unix信號(hào),并通過(guò)threadsignal將信號(hào)投遞到出錯(cuò)的線(xiàn)程。
捕獲Mach異常或者Unix信號(hào)都可以抓到crash事件,這兩種方式哪個(gè)更好呢?
優(yōu)選Mach異常,因?yàn)镸ach異常的處理會(huì)先于Unix信號(hào)處理,如果Mach異常的handler讓程序exit了,那么Unix信號(hào)就永遠(yuǎn)不會(huì)到達(dá)這個(gè)進(jìn)程了。
所以,Crash日志中的EXC_BAD_ACCESS 是Mach異常信息,SIGSEGV是Unix信號(hào)異常信息。
小貼士:
因?yàn)橛布a(chǎn)生的信號(hào)(通過(guò)CPU陷阱)被Mach層捕獲,然后才轉(zhuǎn)換為對(duì)應(yīng)的Unix信號(hào);蘋(píng)果為了統(tǒng)一機(jī)制,于是操作系統(tǒng)和用戶(hù)產(chǎn)生的信號(hào)(通過(guò)調(diào)用kill和pthread_kill)也首先沉下來(lái)被轉(zhuǎn)換為Mach異常,再轉(zhuǎn)換為Unix信號(hào)。
線(xiàn)程回溯
符號(hào)化回溯線(xiàn)程
線(xiàn)程的回溯是APP Crash瞬間,程序中所有線(xiàn)程的逆向調(diào)用堆棧。線(xiàn)程回溯對(duì)我們修復(fù)Crash非常有用,根據(jù)線(xiàn)程回溯,可以分析、定位程序崩潰的原因。
下面將崩潰的代碼、未符號(hào)化崩潰日志、符號(hào)化崩潰日志貼出來(lái),做個(gè)對(duì)比性的理解。
@implementation ViewController
- (IBAction)onCrash:(__unused id)sender {
char* ptr = (char*)-1;
*ptr = 10; ///這里程序崩潰了
}
@end
未符號(hào)化的崩潰日志
符號(hào)化的崩潰日志
符號(hào)在二進(jìn)制中的偏移量
未符號(hào)化的崩潰日志中紅色文字展示了幾個(gè)名詞:鏡像文件、加載地址、堆棧地址。以及沒(méi)有展示出來(lái)的一個(gè)名詞:符號(hào)在二進(jìn)制中的偏移量。他們的含義分別為:
- 鏡像文件:是可執(zhí)行二進(jìn)制文件和二進(jìn)制文件依賴(lài)的動(dòng)態(tài)庫(kù)的總稱(chēng)。
- 堆棧地址:是代碼在內(nèi)存中執(zhí)行的內(nèi)存地址。
- 鏡像的加載地址:程序執(zhí)行時(shí),內(nèi)核會(huì)將包含程序代碼的鏡像加載到內(nèi)存中,鏡像在內(nèi)存中的基地址就是加載地址。程序每次啟動(dòng)時(shí),鏡像的加載地址是隨機(jī)的。所以,同一代碼在不同的設(shè)備中執(zhí)行時(shí),堆棧地址是不一樣的。
- 符號(hào)在二進(jìn)制中的偏移量:按照字面意思理解吧。它以通過(guò)下面的公式得到:
符號(hào)在二進(jìn)制中的偏移量 = 堆棧地址 - 鏡像的加載地址
符號(hào)在二進(jìn)制中的偏移量非常有用,我們就是根據(jù)它,從符號(hào)文件中查找出地址對(duì)應(yīng)的代碼符號(hào)。這里的符號(hào)文件指的是:帶有符號(hào)表的可執(zhí)行二進(jìn)制文件、dSYM文件,這兩種文件在后續(xù)章節(jié)中都統(tǒng)稱(chēng)為符號(hào)文件。
那么怎么將未符號(hào)化的崩潰日志符號(hào)化呢?
atos
蘋(píng)果自帶的atos命令行工具可以查找地址對(duì)應(yīng)的符號(hào),在終端中輸入:
/usr/bin/atos -o [符號(hào)文件] -arch arm64 -l 0x100030000 0x000000010003522c
輸出結(jié)果如下:
-[ViewController onCrash:] (in Simple-Example) (ViewController.m:10)
是不是很簡(jiǎn)單的就將地址轉(zhuǎn)換為符號(hào)?是的,只需將符號(hào)文件(-o指定)、代碼構(gòu)架(-arch指定)、加載地址(-l指定)、堆棧地址傳入atos命令,就能解析出符號(hào)。
atos命令解析出了堆棧地址為0x000000010003522c、加載地址為0x100030000對(duì)應(yīng)的符號(hào)。符號(hào)為[ViewController onCrash:],也驗(yàn)證了崩潰發(fā)生在onCrash函數(shù)中,也驗(yàn)證了崩潰日志中的地址是可以符號(hào)化的。
符號(hào)化原理是什么?怎么就通過(guò)地址找到了Crash代碼的符號(hào),這就涉及符號(hào)化內(nèi)幕。
符號(hào)化內(nèi)幕
符號(hào)化的內(nèi)幕就是:==在符號(hào)文件中,通過(guò)偏移量查找符號(hào)==。下面,一步步的來(lái)分析,首先計(jì)算Crash地址在符號(hào)文件中的偏移量,為000000010000522c。
符號(hào)在二進(jìn)制中的偏移量 = 堆棧地址 - 鏡像的加載地址 = 0x000000010003522c - 0x100030000 = 000000010000522c
在符號(hào)文件中直接找地址000000010000522c,應(yīng)該是找不到,在后續(xù)你可以理解。我們使用逆向方法,根據(jù)符號(hào)-[ViewController onCrash:],找對(duì)應(yīng)的地址,比較是不是000000010000522c,如果是,就充分說(shuō)明了,通過(guò)偏移量是可以查找到內(nèi)存地址對(duì)應(yīng)的符號(hào)的。在終端中輸入下面的命令:
nm [符號(hào)文件] | grep "ViewController onCrash:"
輸出如下
00008320 t -[ViewController onCrash:]
0000000100005224 t -[ViewController onCrash:]
輸出的第一行是armv7s構(gòu)架的符號(hào),第二行是arm64構(gòu)架的符號(hào),Crash日志顯示的代碼構(gòu)架是arm64,使用第二行,符號(hào)-[ViewController onCrash:]對(duì)應(yīng)的偏移量是0000000100005224,而不是 000000010000522c,這個(gè)公式是在stack overflow上找到的,就相差8!后來(lái)寫(xiě)日志組織測(cè)試用例的時(shí)候,忽然明白了為什么差那一點(diǎn)點(diǎn)。
原來(lái),我們通過(guò)nm命令查找出的符號(hào)地址對(duì),是函數(shù)入口地址和對(duì)應(yīng)的函數(shù)調(diào)用的符號(hào)對(duì),僅僅是函數(shù)調(diào)用的符號(hào),沒(méi)有函數(shù)內(nèi)部代碼的符號(hào),而程序是崩潰到函數(shù)內(nèi)部,崩潰到*ptr = 10
這句話(huà),內(nèi)部代碼的地址怎么可能和入口地址一樣呢!相差一點(diǎn)點(diǎn)!
下面根據(jù)偏移量000000010000522c和代碼推算函數(shù)的入口地址吧,看看是什么。崩潰代碼*ptr = 10
前面只有一個(gè)語(yǔ)句—定義初始化指針char* ptr = (char*)-1
,在64位系統(tǒng)上指針的地址占8個(gè)字節(jié),000000010000522c - 8= 0000000100005224,果然是0000000100005224。
這個(gè)不就是函數(shù)的入口地址嘛。原來(lái)那一點(diǎn)點(diǎn)的原因在這里。那么偏移量0000000100005224 對(duì)應(yīng)的符號(hào)正是-[ViewController onCrash:]
。
上面通過(guò)nm 命令查找符號(hào)可能不直觀(guān),可以通過(guò)可視化工具MachOView查看。驗(yàn)證下吧,選擇 Debug Symbols(ARM64_ALL)->Symbol Table->Symbols
,然后在右上角的搜索框中輸入符號(hào):-[ViewController onCrash:]
,結(jié)果如下:
通過(guò)這個(gè)工具可以直觀(guān)的查看到符號(hào)和地址的對(duì)應(yīng)關(guān)系。
小小結(jié)
終于把符號(hào)化和符號(hào)化原理闡述完了簡(jiǎn)單回顧下:
- 可以通過(guò)系統(tǒng)的atos符號(hào)化崩潰日志的單個(gè)符號(hào)
- 符號(hào)化內(nèi)部原理就是:根據(jù)符號(hào)在二進(jìn)制中的偏移量,在符號(hào)文件中查找對(duì)應(yīng)的符號(hào)。其中:==符號(hào)在二進(jìn)制中的偏移量 = 堆棧地址 - 鏡像的加載地址==。
線(xiàn)程的狀態(tài)寄存器
Thread 0 crashed with ARM Thread State (64-bit):
x0: 0x000000010050b460 x1: 0x0000000100102cea x2: 0x00000001004339d0 x3: 0x00000001740f8f00
x4: 0x00000001740f8f00 x5: 0x00000001740f8f00 x6: 0x0000000000000001 x7: 0x0000000000000000
x8: 0xffffffffffffffff x9: 0x000000000000000a x10: 0x00000001b3ad0018 x11: 0x00c1580100c15880
x12: 0x0000000000c15800 x13: 0x0000000000c15900 x14: 0x0000000000c158c0 x15: 0x0000000000c15801
x16: 0x0000000000000000 x17: 0x00000001000c1224 x18: 0x0000000000000000 x19: 0x00000001740f8f00
x20: 0x00000001004339d0 x21: 0x0000000100102cea x22: 0x000000010050b460 x23: 0x0000000170240bd0
x24: 0x000000017400db90 x25: 0x0000000000000001 x26: 0x0000000000000000 x27: 0x00000001b2822000
x28: 0x0000000000000040 fp: 0x000000016fd41ab0 lr: 0x0000000194aea7b0
sp: 0x000000016fd41a90 pc: 0x00000001000c122c cpsr: 0x60000000
這是APP crash的時(shí)候,ARM64 構(gòu)架CPU的32個(gè)寄存器的值, 其中 fp 幀指針、sp 堆棧指針,lr 是返回地址指針,這三個(gè)都比較有用,用來(lái)逐級(jí)回溯線(xiàn)程調(diào)用棧。
Binary Images
鏡像文件就是上面講的可執(zhí)行程序 和 依賴(lài)的所有動(dòng)態(tài)庫(kù)。
鏡像文件中包括鏡像的加載地址,和線(xiàn)程回溯中的鏡像加載地址指的是一個(gè)地址。加載地址后面有個(gè)UUID,符號(hào)文件中也有個(gè)UUID,只有這兩個(gè)地址一致,才能解析出地址對(duì)應(yīng)的符號(hào)。符號(hào)文件中的UUID可以通過(guò)終端中輸入下面的命令得到:
dwarfdump —u [符號(hào)文件]
輸出如下:
UUID: C8E0E6E4-F761-3A19-B231-A31C1BB9037A (armv7)
UUID: 39BBB8F4-CCB0-3193-8491-C007931CA05E (arm64)
第二行的arm64構(gòu)架的UUID居然和圖8中的紅色矩形框中UUID必須一致,這才表示代碼對(duì)應(yīng)的符號(hào)能在這個(gè)符號(hào)文件中找到,如果不一致,就沒(méi)法解析出地址對(duì)應(yīng)的符號(hào)。不論是Xcode,還是symbolicatecrash,都解析不了。
也可以通過(guò)MachOView查看符號(hào)文件的UUID,結(jié)果如下:
小結(jié)
這里闡述了日志的產(chǎn)生原因和符號(hào)化崩潰日志的原理。同時(shí)提及了幾個(gè)有用的工具:
- file 文件類(lèi)型顯示工具(The file-type displaying tool,位于/usr/bin/file);
- atos (將數(shù)字地址轉(zhuǎn)換為鏡像或可執(zhí)行程序中的符號(hào)工具,convert numeric addresses to symbols of binary images or processes,位于/usr/bin/atos);
- nm(符號(hào)表展示工具,The symbol table display tool,位于 /usr/bin/nm);
- 可視化查看Mach-O工具,MachOView