序: 最近在日常開(kāi)發(fā)中遇到了一次Crash引起的Crash的血災(zāi),在5月初的一次發(fā)版把筆者開(kāi)發(fā)的App的Crash率直接從萬(wàn)一干到了接近千二冶共,當(dāng)時(shí)項(xiàng)目負(fù)責(zé)人正好需要向上報(bào)告項(xiàng)目QA相關(guān)情況比默,當(dāng)時(shí)就懵逼了??。
問(wèn)題
由于年初花了大功夫把原來(lái)OC為主體的項(xiàng)目完全遷移到Swift谐岁,由于Swift的安全性伊佃,crash保持的一直不錯(cuò)航揉,忽然這一出搞的也挺懵帅涂,查了一下UMeng的crash追蹤媳友,全是報(bào)Attempted to dereference garbage pointer 0x18ffd63d72d0醇锚,符號(hào)化之后的crash函數(shù)調(diào)用棧也挺迷的焊唬,在不定線程crash赶促,app的函數(shù)符號(hào)定位都是在一個(gè)模型類的0行。
整理了一下相關(guān)的crash log 和umeng上的用戶行為矩屁,知道應(yīng)該是碰到了內(nèi)存問(wèn)題或野指針了,這種傷腦的問(wèn)題如果只是幾個(gè)零星的crash就果斷放后面解決了空幻,但是千分之二的crash直接影響了飯碗問(wèn)題约郁,只能硬著頭皮去解決鬓梅,也是為了重塑曾今的那鐘對(duì)技術(shù)極致追求的精神绽快。
結(jié)果
在整理了Umeng的Crash Log和行為日志以及App Connent用戶上傳的Crash Log紧阔,可以確定是由于野指針/內(nèi)存問(wèn)題引起的隨機(jī)Crash坊罢,并且整理到以下線索
- 隨機(jī)crash出現(xiàn)在app運(yùn)行5分鐘之后的占比很高
- 剛冷啟動(dòng)時(shí)(用戶軌跡只有adviewcontroller)也會(huì)發(fā)生crash的,說(shuō)明有問(wèn)題的代碼應(yīng)該在啟動(dòng)那塊執(zhí)行
- 和idfa的獲取方式變化有關(guān)擅耽,因?yàn)樾掳姹荆?.1.0)由于idfa政策變化被拒過(guò)活孩,因此可以定位到應(yīng)該是集團(tuán)提供的風(fēng)控SDK嫌疑很大,修改提審和風(fēng)控部門對(duì)過(guò)他們sdk有收集idfa乖仇,回憶在集成代碼的時(shí)候發(fā)現(xiàn)風(fēng)控sdk是使用c和c++開(kāi)發(fā)的憾儒,當(dāng)時(shí)改Swift集成的時(shí)候還因此加了橋接文件。
- 在高版本iOS系統(tǒng)和新手機(jī)(arm64e)設(shè)備上搜集到的crash多航夺,低版本和高版本的原始crash的Exception Type是不一樣的:arm64e 是 SIGSEGV arm64 是 SIGBUS
在這里吐槽下Umeng的crash收集,沒(méi)有顯示原始的錯(cuò)誤崔涂,都?xì)w納為Attempted to dereference garbage pointer阳掐,不利于排查和定位錯(cuò)誤
解決步驟:
- 拉取2.1.0發(fā)版代碼
- xcode 打開(kāi) Address Sanitizer(Asan)重新編譯運(yùn)行 -> buggy address 的確可以查到有內(nèi)存使用問(wèn)題
- 注釋啟動(dòng)相關(guān)代碼 風(fēng)控sdk初始化代碼,發(fā)現(xiàn)的確是風(fēng)控sdk導(dǎo)致的
- 聯(lián)系風(fēng)控組,替換SDK缭保,通過(guò)測(cè)試
- 等待上線驗(yàn)證
到這里汛闸,這次Crash問(wèn)題告一段落了,但是在追蹤過(guò)程中查看和回歸了以前很多相關(guān)的底層技術(shù)和工具艺骂,在解決問(wèn)題后再次坐下深入的總結(jié)和記錄诸老。
涉及的技術(shù)點(diǎn)
- iOS內(nèi)存管理機(jī)制: OC C C++的這方面資料很多,可以拓展去看下Swift的坐下總結(jié)
- 符號(hào)文件解析, LLDB高級(jí)調(diào)試和插件編寫(xiě)钳恕,ASDN相關(guān)
- iOS系統(tǒng)crash:Exception(mach oc) 和 unix的bsd 的signal錯(cuò)誤
- bugly的apm工具原理和實(shí)現(xiàn)
- PAC(PAC技術(shù))[https://justinyan.me/post/4129]
我會(huì)以若干篇章去深入探索下相關(guān)技術(shù)點(diǎn)
iOS系統(tǒng)中的Crash
1. Crash的分類
記得在之前文章中探索過(guò)為什么移動(dòng)應(yīng)用會(huì)有crash:內(nèi)存管理别伏,因?yàn)橐苿?dòng)系統(tǒng)為了保護(hù)閃存而舍棄了Swap機(jī)制。
Crash的主要原因是App收到未處理的信號(hào)忧额,iOS的核心操作系統(tǒng)是Darwin厘肮,Darwin內(nèi)核是XNU("X is Not UNIX"),XNU是一個(gè)基于Mach+BSD的混合內(nèi)核睦番,所以引起Crash的信號(hào)可以分為三種:
- Mach異常:Mach負(fù)責(zé)XNU比較底層的任務(wù)类茂,所以Mach異常是指底層的內(nèi)核級(jí)異常,用戶態(tài)的開(kāi)發(fā)者可以直接通過(guò)Mach API設(shè)置thread托嚣、task和host的異常端口來(lái)捕獲Mach異常
- Unix信號(hào):又稱BSD信號(hào)(XNU中的BSD發(fā)出)巩检,如果開(kāi)發(fā)者沒(méi)有捕捉Mach異常,則會(huì)被host層的方法ux_exception()轉(zhuǎn)化為對(duì)應(yīng)的Unix信號(hào)示启,并通過(guò)threadsignal()將信號(hào)投遞到出錯(cuò)的線程兢哭,可以通過(guò)signal(x, SignalHandler)來(lái)捕獲signal
- NSException:應(yīng)用級(jí)異常,也可以認(rèn)為是OC語(yǔ)言層面的異常夫嗓,導(dǎo)致程序向自身發(fā)送了SIGABORT信號(hào)而crash厦瓢,可以try catch捕獲或者通過(guò)NSSetUncaughtExceptionHandler()機(jī)制來(lái)捕獲
Swift的異常機(jī)制這方面的大佬們分享的很少 ,可以研究下Swift的錯(cuò)誤機(jī)制
上面三個(gè)層面的Crash啤月,語(yǔ)言層面的(OC)應(yīng)用級(jí)的Crash是最好解決的,數(shù)組越界劳跃、 runtime的msg_send消息轉(zhuǎn)發(fā)機(jī)制導(dǎo)致的crash谎仲,kvc等OC語(yǔ)言機(jī)制的crash可以通過(guò)crash log中的backtrace很快定位到。而對(duì)于Mach異常和Unix信號(hào)導(dǎo)致的crash則對(duì)于高級(jí)開(kāi)發(fā)來(lái)說(shuō)也是很大的挑戰(zhàn)刨仑。
2. Mach異常和Unix信號(hào)
Mach異常是什么郑诺?它又是如何與Unix信號(hào)建立聯(lián)系的?
// crash log頭部
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0022000000000000 -> 0x0000000000000000 (possible pointer authentication failure)
- Mach異常是XNU的微內(nèi)核核心Mach運(yùn)行中出現(xiàn)的內(nèi)核級(jí)異常杉武,每個(gè)thread辙诞、task、host(
這個(gè)host是什么轻抱?)都有一個(gè)異常端口數(shù)組飞涂,Mach的部分API暴露給用戶態(tài),用戶態(tài)的開(kāi)發(fā)者可以直接通過(guò)Mach API設(shè)置thread、task较店、host的異常端口士八,來(lái)捕獲Mach異常。 - 所有未處理的Mach異常梁呈,都會(huì)通過(guò)ux_exception()轉(zhuǎn)化為Unix信號(hào)婚度,通過(guò)threadsignal將信號(hào)傳遞到出錯(cuò)的線程。iOS的POSIX API就是通過(guò)Mach上層的BSD層實(shí)現(xiàn)的官卡。
注:Mach 最基礎(chǔ)的對(duì)象是“主機(jī)(host)”蝗茁,也就是表示機(jī)器本身的對(duì)象
如上面的貼的Crash Log頭部摘自我這次Crash的日志,EXC_BAD_ACCESS(訪問(wèn)無(wú)效內(nèi)存)異常寻咒,因?yàn)闆](méi)有在Mach層捕獲哮翘,被host層轉(zhuǎn)化為SIGSEGV信號(hào)傳遞給了出錯(cuò)的線程。
所以:
- 未處理Mach異常是會(huì)轉(zhuǎn)為Unix Signal仔涩,應(yīng)用級(jí)異常未捕獲也會(huì)在轉(zhuǎn)為NSException, 然后調(diào)用C的Abort()忍坷,kernel對(duì)App發(fā)出__pthread_kill信號(hào),觸發(fā)Mach異常熔脂,所以只要未捕獲的異常都是會(huì)轉(zhuǎn)化為一條Unix信號(hào)佩研。
- 硬件產(chǎn)生的信號(hào)(通過(guò)CPU的trap機(jī)制:mach_msg_trap(),陷阱這個(gè)概念在 Mach 中等同于系統(tǒng)調(diào)用)被Mach捕獲霞揉,然后轉(zhuǎn)化為Unix信號(hào)旬薯。
- Apple為了統(tǒng)一機(jī)制,操作系統(tǒng)或者用戶產(chǎn)生的信號(hào)(kill和thread_kill)也會(huì)轉(zhuǎn)化為Mach異常适秩,最后轉(zhuǎn)為Unix信號(hào)绊序。
4. Mach異常和Unix信號(hào)的分類
常見(jiàn)的Mach異常
- EXC_CRASH: 進(jìn)程異常退出(SIGABORT) 或者 watch dog超時(shí)殺死App(SIGKILL)
- EXC_BREAKPOINT (SIGTRAP)
- EXC_BAD_ACCESS :內(nèi)存訪問(wèn)無(wú)效
- EXC_BAD_INSTRUCTION:線程試圖訪問(wèn)非法/無(wú)效的指令或?qū)o(wú)效的參數(shù)(操作數(shù))傳遞給指令
- EXC_ARITMETHIC:除以0或整數(shù)溢出/下溢引發(fā)的異常
- EXC_SYSCALL 和 EXC_MACH_SYSCALL:應(yīng)用程序訪問(wèn)內(nèi)核服務(wù)(如文件I/O)或網(wǎng)絡(luò)訪問(wèn)時(shí)發(fā)出
- 其他Mach異常定義在mach/exception_types.h中。與處理器相關(guān)的異常定義在mach/(i386,ppc,...)/exception.h中
在開(kāi)發(fā)中最常見(jiàn)的異常應(yīng)該是EXC_BAD_ACCESS秽荞,就比如這次追蹤到的
Unix信號(hào)
信號(hào)處理函數(shù)可以通過(guò) signal() 系統(tǒng)調(diào)用來(lái)設(shè)置骤公。如果沒(méi)有為一個(gè)信號(hào)設(shè)置對(duì)應(yīng)的處理函數(shù),就會(huì)使用默認(rèn)的處理函數(shù)扬跋,否則信號(hào)就被進(jìn)程截獲并調(diào)用相應(yīng)的處理函數(shù)阶捆。在沒(méi)有處理函數(shù)的情況下,程序可以指定兩種行為:忽略這個(gè)信號(hào) SIG_IGN 或者用默認(rèn)的處理函數(shù) SIG_DFL 钦听。但是有兩個(gè)信號(hào)是無(wú)法被截獲并處理的: SIGKILL洒试、SIGSTOP 。
Signal信號(hào)類型:
- SIGABRT--程序中止命令中止信號(hào)
- SIGALRM--程序超時(shí)信號(hào)
- SIGFPE--程序浮點(diǎn)異常信號(hào)
- SIGILL--程序非法指令信號(hào)
- SIGHUP--程序終端中止信號(hào)
- SIGINT--程序鍵盤中斷信號(hào)
- SIGKILL--程序結(jié)束接收中止信號(hào)
- SIGTERM--程序kill中止信號(hào)
- SIGSTOP--程序鍵盤中止信號(hào)
- SIGSEGV--程序無(wú)效內(nèi)存中止信號(hào)
- SIGBUS--程序內(nèi)存字節(jié)未對(duì)齊中止信號(hào)
- SIGPIPE--程序Socket發(fā)送失敗中止信號(hào)
5. 模擬Mach Message發(fā)送和捕獲Mach異常
5.1 Mach
Mach是XNU的微內(nèi)核
Mach的幾個(gè)基本概念:
Tasks: 擁有一組系統(tǒng)資源的對(duì)象朴上,允許thread
在其中執(zhí)行
Threads: 執(zhí)行的基本單位垒棋,擁有task的上下文,并共享其資源
Ports: task之間通訊的一組受保護(hù)的消息隊(duì)列痪宰,task可以對(duì)任何port發(fā)送/接收數(shù)據(jù)
Message:有類型的數(shù)據(jù)對(duì)象集合叼架,只可以發(fā)送給Host
5.2 模擬Mach Message的發(fā)送
- 創(chuàng)建 post 授權(quán)
+ (mach_port_t)createPortAndListener {
// 在Mach的頭文件中找到的 mach_port_t 完全等價(jià)于 mach_port_name_t
// typedef unsigned int __darwin_natural_t;
// typedef __darwin_natural_t __darwin_mach_port_name_t; /* Used by mach */
// typedef __darwin_mach_port_name_t __darwin_mach_port_t; /* Used by mach */
// typedef __darwin_mach_port_t mach_port_t;
// typedef natural_t mach_port_name_t;
// typedef __darwin_natural_t natural_t;
mach_port_t server_port;
kern_return_t kr = mach_port_allocate(mach_task_self(),
MACH_PORT_RIGHT_RECEIVE,
&server_port);
assert(kr == KERN_SUCCESS);
NSLog(@"Create a port: %d", server_port);
kr = mach_port_insert_right(mach_task_self(),
server_port,
server_port,
MACH_MSG_TYPE_MAKE_SEND);
assert(kr == KERN_SUCCESS);
return server_port;
}
- Mach 端口監(jiān)聽(tīng)
+ (void)setMachPortListener:(mach_port_t)mach_port {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
mach_msg_header_t mach_message;
mach_message.msgh_size = 1024;
mach_message.msgh_local_port = mach_port;
mach_msg_return_t mr;
while (true) {
mr = mach_msg(&mach_message,
MACH_RCV_MSG | MACH_RCV_LARGE,
0,
mach_message.msgh_size,
mach_message.msgh_local_port,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (mr != MACH_MSG_SUCCESS && mr != MACH_RCV_TOO_LARGE) {
NSLog(@"error!");
}
mach_msg_id_t msg_id = mach_message.msgh_id;
mach_port_t remote_port = mach_message.msgh_remote_port;
mach_port_t local_port = mach_message.msgh_local_port;
NSLog(@"Recevie a mach messag:[%d], remote_port: %d, local_port: %d, exception",
msg_id, remote_port, local_port);
}
});
}
- 向創(chuàng)建的 mach port 發(fā)送消息
+ (void)sendMachPostMessage:(mach_port_t)mach_port {
kern_return_t kr;
mach_msg_header_t msg_header;
msg_header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
msg_header.msgh_size = sizeof(mach_msg_header_t);
msg_header.msgh_remote_port = mach_port;
msg_header.msgh_local_port = MACH_PORT_NULL;
msg_header.msgh_id = 100;
NSLog(@"Send a mach message: [%d]", msg_header.msgh_id);
kr = mach_msg(&msg_header,
MACH_SEND_MSG,
msg_header.msgh_size,
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
}
5.3 在Mach層捕獲異常
6. Signal注冊(cè)和處理
7.PAC
在這次的Crash追溯的過(guò)程中畔裕,我發(fā)現(xiàn):
- 在比較新的機(jī)器上(一般iOS系統(tǒng)版本也比較高), Crash的概率比較大
- 通過(guò)App Connect搜集到的原始Crash Log中Crash的Mach異常轉(zhuǎn)化后的Signal是不一樣的
比較老設(shè)備收集到的Crash Log頭部:Unix Signal -> SIGBUS
// 比較老的手機(jī):iPhone 8
Incident Identifier: 9DCFF105-1CBE-4947-B386-68E4375EC340
Hardware Model: iPhone10,1
Process: esport-app [15056]
Path: /private/var/containers/Bundle/Application/86BE49B4-3975-45D9-AC97-CD9CABF4F7D0/esport-app.app/esport-app
Identifier: com.wmzq.esportapp
Version: 2 (2.1.0)
AppStoreTools: 12E262
AppVariant: 1:iPhone10,1:13
Beta: YES
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: com.wmzq.esportapp [2538]
Date/Time: 2021-05-06 15:08:30.2709 +0800
Launch Time: 2021-05-06 15:08:28.6146 +0800
OS Version: iPhone OS 13.7 (17H35)
Release Type: User
Baseband Version: 5.70.01
Report Version: 104
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Subtype: KERN_PROTECTION_FAILURE at 0x000000016c1bfdc0
VM Region Info: 0x16c1bfdc0 is in 0x16c1bc000-0x16c1c0000; bytes after start: 15808 bytes before end: 575
REGION TYPE START - END [ VSIZE] PRT/MAX SHRMOD REGION DETAIL
Stack 000000016c0ec000-000000016c1bc000 [ 832K] rw-/rwx SM=COW thread 21
---> STACK GUARD 000000016c1bc000-000000016c1c0000 [ 16K] ---/rwx SM=NUL ...for thread 22
Stack 000000016c1c0000-000000016c248000 [ 544K] rw-/rwx SM=COW thread 22
Termination Signal: Bus error: 10
Termination Reason: Namespace SIGNAL, Code 0xa
Terminating Process: exc handler [15056]
Triggered by Thread: 22
新設(shè)備收集到的Crash Log頭部:Unix Signal -> SIGSEGV
// iPhone XR
Incident Identifier: 95414C75-D357-4AFC-9951-2EAE098F31B3
Hardware Model: iPhone11,8
Process: esport-app [13681]
Path: /private/var/containers/Bundle/Application/07BF60A9-D0A0-4B29-A9F2-C5E6C99D84EC/esport-app.app/esport-app
Identifier: com.wmzq.esportapp
Version: 2105031 (2.1.0)
AppStoreTools: 12E262
AppVariant: 1:iPhone11,8:14
Beta: YES
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: com.wmzq.esportapp [742]
Date/Time: 2021-05-08 09:41:52.5606 +0800
Launch Time: 2021-05-08 08:38:34.2531 +0800
OS Version: iPhone OS 14.5 (18E199)
Release Type: User
Baseband Version: 3.03.05
Report Version: 104
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0022000000000000 -> 0x0000000000000000 (possible pointer authentication failure)
VM Region Info: 0 is not in any region. Bytes before following region: 4373348352
REGION TYPE START - END [ VSIZE] PRT/MAX SHRMOD REGION DETAIL
UNUSED SPACE AT START
--->
__TEXT 104ac0000-104b24000 [ 400K] r-x/r-x SM=COW ...pp/esport-app
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAL, Code 0xb
Terminating Process: exc handler [13681]
Triggered by Thread: 11
通過(guò)查閱資料知道了Apple在A12開(kāi)始支持了arm64e指令集,提供了指令地址加密功能碉碉,即PAC(Pointer Authentication Code的縮寫(xiě))
7.1 PAC是什么
PAC是ARMv8.3 新增的功能柴钻,因?yàn)殡m然系統(tǒng)是64位的,但是arm64指令地址根本用不滿垢粮,所以把高位的部分(upper bits)拿來(lái)存一個(gè)指針地址的簽名贴届。
PAC指針驗(yàn)證碼就是在CPU執(zhí)行指令前先拿指針的高位簽名和低位的實(shí)際地址部分坐下校驗(yàn),失敗了直接拋出異常
為了實(shí)現(xiàn)PAC, arm64e新增了兩個(gè)指令:
- PACIASP 計(jì)算 PAC 加密并加到指針地址上
- AUTIASP 校驗(yàn)加密部分蜡吧,并還原指針地址
7.2 PAC應(yīng)用舉例
在這里我主要記錄了下這次Cras追溯的過(guò)程和總結(jié)了下iOS系統(tǒng)Crash的產(chǎn)生原理毫蚓,Crash從內(nèi)核態(tài) -> 拋出至用戶態(tài)的過(guò)程,以及PAC等一些概念性的東西昔善。
后面的幾篇文章我會(huì)總結(jié)Xcode的內(nèi)存診斷工具元潘,Zombie Objects、 Address Sanitizer君仆、Malloc Scribble的原理和使用翩概,盡量通過(guò)代碼和WoWCrash示例去實(shí)現(xiàn)一個(gè)搜集定位內(nèi)存問(wèn)題的APM工具。
好久沒(méi)有好好的深入研究一些技術(shù)了返咱,之前一度認(rèn)為iOS的技術(shù)深入不劃算了钥庇,現(xiàn)今的開(kāi)發(fā)都是頁(yè)面黨,替代性太強(qiáng)了咖摹,所以一直猶豫是否轉(zhuǎn)后端或者web评姨。但這次的Crash追蹤過(guò)程,讓我覺(jué)得成為相關(guān)方面資深開(kāi)發(fā)甚至專家對(duì)我還是有誘惑力的萤晴,從Crash Log分析->逆向工具使用->底層原理認(rèn)知->解決問(wèn)題獲取那種喜悅吐句,讓我重新找回方向,加油店读,走出舒適區(qū)??嗦枢。
參考資料
iOS Mach 異常、Unix 信號(hào) 和NSException 異常
iOS Mach異常和signal信號(hào)
為什么 arm64e 的指針地址有空余支持 PAC屯断?