KSCrash 是一個異常收集的開源框架惰帽。
它可以捕獲到Mach級內(nèi)核異常、信號異常召噩、C++異常母赵、Objective-C異常、主線程死鎖具滴;當捕獲到異常后凹嘲,KSCrash可以在設(shè)備上完成符號化崩潰日志(前提是編譯的時候?qū)⒎柋砭幾g到可執(zhí)行程序中);日志的格式你也可以定制构韵,可以是JSON格式的周蹭,也可以是Apple crash日志風(fēng)格。另外疲恢,還有僵尸對象查找凶朗、內(nèi)存自省等特性。
目前異常收集的框架非常多显拳,有集成了收集棚愤、統(tǒng)計功能的一條龍產(chǎn)品,如杂数,友盟宛畦,鵝廠的Bugly 等等;也有幾個開源框架揍移,如次和,KSCrash,plcrashreporter那伐,CrashKit踏施。
基于我們項目的安全性考慮石蔗,即,不希望第三方SDK看到崩潰日志畅形,我選取了開源框架這條路抓督。縱覽這幾個開源框架束亏,只有KSCrash一直在更新。所以阵具,毫不猶豫的選用了它碍遍。
APP Crash后,獲取崩潰線程的程調(diào)用堆棧的過程阳液,是程序執(zhí)行過程的逆向過程怕敬。那么,了解APP的執(zhí)行正向過程帘皿,對獲取崩潰線程的調(diào)用堆棧是非常非常有益的东跪。所以,在分析KSCrash原理前鹰溜,依照APP正向執(zhí)行過程先推導(dǎo)下 異常收集虽填、符號化的原理。
推導(dǎo)異常收集曹动、符號化的原理
本節(jié)描述的內(nèi)容只是按照自己的理解編寫的斋日,由于道行尚淺,理解不深墓陈,所以具體安排的內(nèi)容不一定合理恶守。依據(jù)APP執(zhí)行的過程,主要囊括了:編譯生成可執(zhí)行APP贡必、內(nèi)核加載并啟動APP兔港、調(diào)用堆棧等,并穿插了一點理解KSCrash的必備知識仔拟。
編譯生成可執(zhí)行APP
開發(fā)者通過IDE集成開發(fā)環(huán)境(例如Xcode)衫樊,將源碼文件轉(zhuǎn)化為臨時中間文件(這種文件應(yīng)該是機器語言了),然后使用鏈接器(/usr/bin/ld)將臨時的對象文件(object file)合并為可執(zhí)行文件理逊。不過上面的編譯橡伞、鏈接步驟都集成到Xcode中了。我們在Xcode中編譯的時候晋被,體會不到這個過程兑徘。在蘋果系統(tǒng)中,可執(zhí)行APP的存儲格式是Mach-O格式羡洛。所以我們先了解下Mach-O文件格式挂脑。
Mach-O文件存儲格式
Mach-O (Mach object的縮寫) 是蘋果系統(tǒng)上存儲可執(zhí)行程序和庫(libraries)的標準格式藕漱。它是BSD系統(tǒng)中.a文件格式的替代物,它封裝著程序的可執(zhí)行代碼和數(shù)據(jù)崭闲±吡可以參考《OS X ABI Mach-O File Format Reference》官方文檔。這個文檔在官網(wǎng)打不來了刁俭,我就鏈接到我自己的pdf地址了橄仍。
概述
Mach-O文件包括三個組成部分,分別如下:
1.header:指定了文件的基本信息牍戚,如CUP類型侮繁、加載命令個數(shù)等。
2.Load commands:加載命令如孝,指定了文件的邏輯結(jié)構(gòu)宪哩、在虛擬內(nèi)存(virtual memory)中文件的布局。你可以理解為一片文章的目錄第晰。
3.Raw segment data:數(shù)據(jù)部分锁孟。
這個是官網(wǎng)上的結(jié)構(gòu)示意圖。
header
Mach-O文件的開頭部分是就是Header—文件頭茁瘦。Header的數(shù)據(jù)結(jié)構(gòu)定義在XNU微內(nèi)核的loader.h文件中品抽。loader.h也可以在IOS SDK的/usr/include/mach-o目錄下找到,header的數(shù)據(jù)結(jié)構(gòu)定義如下:
struct mach_header
{
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
};
可以看出包括了 :魔數(shù)腹躁、cup的類型桑包、子類型、文件的類型纺非、load commend個數(shù)哑了、load commend大小等數(shù)據(jù)。
load commend
緊跟在Header后面的是load commend烧颖。load commend指定了文件的布局弱左。具體指定了以下內(nèi)容:
? The initial layout of the file in virtual memory 文件在虛擬內(nèi)存中的初始布局
? The location of the symbol table (used for dynamic linking) 符號表的位置
? The initial execution state of the main thread of the program 程序主線程的入口地址
? The names of shared libraries that contain definitions for the main executable’s imported symbols 主執(zhí)行文件依賴的分享庫
load commend 的種類非常多,loader.h 中的定義了各種所有的類型炕淮。我們僅以LC_SEGMENT拆火、LC_SYMTAB(符號表)為例了解load commend。每種類型的load commend都有對應(yīng)的數(shù)據(jù)結(jié)構(gòu)涂圆,可以在loader.h文件中查看们镜。下面是部分類型Load Commond:
#define LC_SEGMENT 0x1 ///代碼段
#define LC_SYMTAB 0x2 /// 符號表
#define LC_SYMSEG 0x3 /* link-edit gdb symbol table info (obsolete) */
#define LC_THREAD 0x4 /* thread */
#define LC_UNIXTHREAD 0x5 /* unix thread (includes a stack) */
#define LC_LOADFVMLIB 0x6 /* load a specified fixed VM shared library */
#define LC_IDFVMLIB 0x7 /* fixed VM shared library identification */
#define LC_IDENT 0x8 /* object identification info (obsolete) */
#define LC_FVMFILE 0x9 /* fixed VM file inclusion (internal use) */
#define LC_PREPAGE 0xa /* prepage command (internal use) */
#define LC_DYSYMTAB 0xb /* dynamic link-edit symbol table info */
#define LC_LOAD_DYLIB 0xc /* load a dynamically linked shared library */
#define LC_ID_DYLIB 0xd /* dynamically linked shared lib ident */
#define LC_LOAD_DYLINKER 0xe /* load a dynamic linker */
#define LC_ID_DYLINKER 0xf /* dynamic linker identification */
#define LC_PREBOUND_DYLIB 0x10 /* modules prebound for a dynamically */
...................
Data
Data緊跟在Load Commond后面。load commend中定義的各種數(shù)據(jù)都存儲在這部分中润歉。
查看Mach-O實用工具
在終端中有幾個工具是可以查看Mach-O文件內(nèi)容的模狭。另外位于usr/include/mach-o/dyld.h中的函數(shù)可以在程序中訪問Mach-O文件內(nèi)容。
文件類型展示工具-file踩衩。The file-type displaying tool, 位于/usr/bin/file嚼鹉,顯示文件的類型贩汉,對于多構(gòu)架的文件,它顯示每個構(gòu)架下的鏡像類型锚赤。在終端中輸入:
~/Desktop/收集匹舞、解析IOS崩潰日式/Exception/UncaughtException_archive
輸出:
/Users/lijian/Desktop/收集、解析IOS崩潰日式/Exception/UncaughtException_archive: Mach-O universal binary with 2 architectures
/Users/lijian/Desktop/收集线脚、解析IOS崩潰日式/Exception/UncaughtException_archive (for architecture armv7): Mach-O executable arm
/Users/lijian/Desktop/收集赐稽、解析IOS崩潰日式/Exception/UncaughtException_archive (for architecture arm64): Mach-O 64-bit executable
對象文件展示工具otool。The object-file displaying tool浑侥,位于/usr/bin/otool又憨,顯示Mach-O文件的各種數(shù)據(jù)。查看Mach-O header內(nèi)容锭吨,在終端中輸入:
otool -hV ~/Desktop/收集、解析IOS崩潰日式/Exception/UncaughtException_archive
輸出:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC ARM V7 0x00 EXECUTE 23 2432 NOUNDEFS DYLDLINK TWOLEVEL PIE
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 ARM64 ALL 0x00 EXECUTE 23 2872 NOUNDEFS DYLDLINK TWOLEVEL PIE
可以使用otool 查看load commend寒匙。在終端中輸入:
otool -lV ~/Desktop/收集零如、解析IOS崩潰日式/Exception/UncaughtException_archive
輸出:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedface 12 9 0x00 2 23 2432 0x00200085
Load command 0
cmd LC_SEGMENT
cmdsize 56
segname __PAGEZERO
vmaddr 0x00000000
vmsize 0x00004000
fileoff 0
filesize 0
maxprot 0x00000000
initprot 0x00000000
nsects 0
flags 0x0
……….
符號展示工具-nm,The symbol table display tool,位于 /usr/bin/nm, allows you to view the contents of an object file’s symbol table锄弱。查看符號表考蕾,在終端中輸入:
nm ~/Desktop/收集、解析IOS崩潰日式/Exception/UncaughtException_archive
輸出:
/Users/lijian/Desktop/收集会宪、解析IOS崩潰日式/Exception/UncaughtException_archive (for architecture arm64):
U _NSGetUncaughtExceptionHandler
U _NSLog
U _NSSearchPathForDirectoriesInDomains
U _NSSetUncaughtExceptionHandler
U _NSStringFromClass
U _objc_msgSend
U _objc_msgSendSuper2
U _objc_release
U _objc_retain
U _objc_retainAutorelease
U _objc_retainAutoreleasedReturnValue
U _objc_setProperty_nonatomic_copy
U _objc_storeStrong
U _strstr
U dyld_stub_binder
...........
綁定和執(zhí)行
根據(jù)上面分析的可執(zhí)行文件的結(jié)構(gòu)肖卧,我們可以看到,可執(zhí)行文件中已經(jīng)包含了符號表 掸鹅,這個符號表是可執(zhí)行代碼的虛擬地址和代碼中符號的對應(yīng)表塞帐。符號表是綁定過程中建立的,程序的綁定有很多種巍沙,可以參看下面的文檔:Mach-O Programming Topics - Binding Symbols葵姥,里面詳細介紹了綁定和查找符號的過程。
看到符號表句携,那么我們可以做這樣的設(shè)想:如果程序崩潰榔幸,只要我們獲取到了崩潰調(diào)用堆棧的回溯地址,然后從這個符號表中查找對應(yīng)的符號矮嫉,就完成了調(diào)用堆棧的符號化工作削咆? 還有就是我們?nèi)绾潍@取程序的調(diào)用堆棧呢?還有很多需要我們接著往下看蠢笋。為了知道如何獲取調(diào)用堆棧的回溯拨齐,我們了解下程序的執(zhí)行過程:
程序的執(zhí)行過程:內(nèi)核首先加載可執(zhí)行文件,并且檢測程序文件的起始部分的mach_header結(jié)構(gòu)挺尿,內(nèi)核驗證是否合法的Macj-O文件奏黑,解析header中的load commands炊邦。加載Load Commond中指定依賴鏡像到內(nèi)存中,然后啟動進程熟史,執(zhí)行程序的入口函數(shù)馁害,進入正常的run loop。
調(diào)用堆棧
首先介紹一下什么叫調(diào)用堆棧:假設(shè)我們?yōu)榱送瓿梢粋€任務(wù)1蹂匹,任務(wù)1的完成需要完成任務(wù)2…. 分別定義為幾個函數(shù):function1,function2,function3,funtion4碘菜。即,function1調(diào)用function2限寞,function2調(diào)用function3忍啸,function3調(diào)用function4。在function4運行過程中履植,我們可以從線程當前堆棧中了解到調(diào)用他的那幾個函數(shù)分別是誰计雌。function4、function3玫霎、function2凿滤、function1呈現(xiàn)出一種“堆棧”的特征庶近,最后被調(diào)用的函數(shù)出現(xiàn)在最上方翁脆。因此稱呼這種關(guān)系為調(diào)用堆棧(call stack)。 下面有一個圖展示下:
函數(shù)調(diào)用經(jīng)常是嵌套的鼻种,在同一時刻反番,堆棧中會有多個函數(shù)的信息。每個未完成運行的函數(shù)占用一個獨立的連續(xù)區(qū)域叉钥,稱作棧幀(Stack Frame)罢缸。棧幀是堆棧的邏輯片段,當調(diào)用函數(shù)時邏輯棧幀被壓入堆棧, 當函數(shù)返回時邏輯棧幀被從堆棧中彈出投队。棧幀存放著函數(shù)參數(shù)祖能,局部變量及恢復(fù)前一棧幀所需要的數(shù)據(jù)等。理解了入棧和出棧蛾洛,基本能理解調(diào)用堆棧养铸,下面兩個圖,一個是入棧轧膘,一個是出棧钞螟,圖中描述的很清楚。
所以獲取到崩潰時線程的ebp和esp 就能回溯到上一個調(diào)用谎碍,依次類推鳞滨,回溯出所有的調(diào)用堆棧。下面了解下寄存器蟆淀。
寄存器
為了線程獲取BP和SP拯啦,我們需要了解一點點寄存器澡匪。因為他們保存在CPU的寄存器中。
arm64構(gòu)架的寄存器在Procedure Call Standard for the ARM 64-bit Architecture (AArch64)有詳細的說明褒链。不過都是英文的唁情,我沒有看,我從代碼中也找到了它的定義甫匹,位于IOS SDK的usr/include/arm目錄下的_mcontext.h文件中甸鸟。其中幾個關(guān)鍵的定義的代碼我摘錄下來了,如下:
_STRUCT_MCONTEXT64
{
_STRUCT_X86_EXCEPTION_STATE64 __es; ///異常寄存器
_STRUCT_X86_THREAD_STATE64 __ss; ///線程狀態(tài)寄存器
_STRUCT_X86_FLOAT_STATE64 __fs; ///浮點寄存器
};
這個結(jié)構(gòu)體
定義了所有的寄存器兵迅。其中_STRUCT_MCONTEXT64結(jié)構(gòu)體定義了三大類寄存器抢韭,根據(jù)字面意思理解為:異常寄存器、線程狀態(tài)寄存器恍箭、浮點寄存器刻恭。我們只關(guān)注線程狀態(tài)寄存器。
_STRUCT_ARM_THREAD_STATE64
{
__uint64_t __x[29]; ///General purpose registers x0-x28
__uint64_t __fp; ///這里就是BP,x29
__uint64_t __lr; /// Link register x30
__uint64_t __sp; ///這里就是SP x31
__uint64_t __pc; Program counter
__uint32_t __cpsr; Current program status register
__uint32_t __pad; /* Same size for 32-bit or 64-bit clients */
};
不管你見或者不見我我就在那里扯夭,BP就在 _STRUCT_MCONTEXT64->ss.fp里吠各,SP就在_STRUCT_MCONTEXT64->ss->sp里。不知不覺的問題已經(jīng)轉(zhuǎn)化了勉抓,轉(zhuǎn)化為獲取線程的_STRUCT_X86_THREAD_STATE64數(shù)據(jù),即候学,獲取線程的狀態(tài)結(jié)構(gòu)體藕筋。
XNU微內(nèi)核的核心部分Mach,里面暴露了一些線程的接口函數(shù)梳码,我們應(yīng)該能獲取到線程的狀態(tài)結(jié)構(gòu)體隐圾。了解這些函數(shù)的接口定義可以參考:Mach IPC Interface、IPC 原理講解掰茶。
獲取線程狀態(tài)
IPC 接口文檔的線程接口部分(Thread Interface)的 thread_get_state函數(shù)可以獲取線程的狀態(tài)暇藏。他的定義如下:
kern_return_t thread_get_state
(thread_act_t target_thread,
thread_state_flavor_t flavor,
thread_state_t old_state,
mach_msg_type_number_t old_state_count);
thread_get_state函數(shù)返回target_thread的執(zhí)行狀態(tài),存儲在flavor參數(shù)里濒蒋⊙渭睿看著上面的定義,是不是一點感覺都沒有沪伙,一頭霧水瓮顽,摸不著頭腦?我也是围橡,幸好KSCrash中有這部分代碼暖混,貼出來瞅瞅:
bool ksmach_threadState(const thread_t thread,
STRUCT_MCONTEXT_L* const machineContext)
{
return ksmach_fillState(thread,
(thread_state_t)&machineContext->__ss,
ARM_THREAD_STATE,
ARM_THREAD_STATE_COUNT);
}
bool ksmach_fillState(const thread_t thread,
const thread_state_t state,
const thread_state_flavor_t flavor,
const mach_msg_type_number_t stateCount)
{
mach_msg_type_number_t stateCountBuff = stateCount;
kern_return_t kr;
kr = thread_get_state(thread, flavor, state, &stateCountBuff);
if(kr != KERN_SUCCESS)
{
KSLOG_ERROR("thread_get_state: %s", mach_error_string(kr));
return false;
}
return true;
}
上面代碼說明了thread_get_state函數(shù)可以根據(jù)線程ID(thread_t thread),獲取到線程狀態(tài)(_STRUCT_ARM_THREAD_STATE64)翁授,也就是通過線程ID拣播,就能獲取到線程當前執(zhí)行狀態(tài)的BP 和SP晾咪。
思路回溯
上面講了,那么多贮配,目的只有一個谍倦,就是理出一個思路—–獲取程序崩潰時線程的調(diào)用堆棧∧良担現(xiàn)在大概是這樣的:
程序發(fā)生崩潰剂跟,我們獲取到崩潰的線程,取出線程的threadID酣藻。
通過thread_get_state函數(shù)曹洽, 獲取線程ID為threadID的線程的 當前執(zhí)行狀態(tài),目的是獲攘删纭:幀指針BP送淆、棧指針SP;
依據(jù)《1.4 調(diào)用堆椗陆危》原理偷崩、BP、SP撞羽,循環(huán)取出線程的調(diào)用堆棧阐斜。
依據(jù)《1.2 Mach-O文件存儲格式》原理,將調(diào)用堆棧中的地址轉(zhuǎn)換為代碼中的符號诀紊。
總體邏輯現(xiàn)在通了谒出,但是,還有好多好多的細節(jié)邻奠,等待我們?nèi)ネ晟企栽热纾粋€關(guān)鍵的邏輯碌宴,我是怎么知道程序崩潰了呢杀狡?從而讓程序執(zhí)行到崩潰處理函數(shù)里,完成線程回溯功能贰镣。
通過分析KS的代碼呜象,得知,可以在程序啟動的時候注冊崩潰的處理函數(shù)碑隆,程序崩潰發(fā)生時董朝,會執(zhí)行崩潰處理函數(shù)。
其實干跛,捕獲異常的方式多種多樣子姜,不同捕獲方式,捕獲的原理不同。捕獲原理請參看《二哥捕、KSCrash異常捕獲原理》牧抽。這里只掃盲下經(jīng)典的捕獲方式。
捕獲崩潰方式
捕獲崩潰的方式有:
捕獲Mach 異常
捕獲Unix 信號
其實遥赚,這部分內(nèi)容在漫談 iOS Crash 收集框架中闡述的非常明白扬舒。為了表示寫的好,這里再重復(fù)的闡述下凫佛。
iOS 系統(tǒng)自帶的Apple’s Crash Reporter 記錄在設(shè)備中的 Crash 日志讲坎,Exception Type項通常會包含兩個元素:Mach 異常 和 Unix 信號。
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x041a6f3
Mach 異常是什么愧薛?它又是如何與 Unix 信號建立聯(lián)系的晨炕?
Mach 是一個 XNU 的微內(nèi)核核心,Mach 異常是指最底層的內(nèi)核級異常毫炉,被定義在 下 瓮栗。每個 thread,task瞄勾,host 都有一個異常端口數(shù)組费奸,Mach 的部分 API 暴露給了用戶態(tài),用戶態(tài)的開發(fā)者可以直接通過 Mach API 設(shè)置 thread进陡,task愿阐,host 的異常端口,來捕獲 Mach 異常趾疚,抓取 Crash 事件缨历。
所有 Mach 異常都在 host 層被ux_exception轉(zhuǎn)換為相應(yīng)的 Unix 信號,并通過threadsignal將信號投遞到出錯的線程盗蟆。iOS 中的 POSIX API 就是通過 Mach 之上的 BSD 層實現(xiàn)的。
因此舒裤,EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach 層的EXC_BAD_ACCESS異常喳资,在 host 層被轉(zhuǎn)換成 SIGSEGV 信號投遞到出錯的線程。既然最終以信號的方式投遞到出錯的線程腾供,那么就可以通過注冊 signalHandler 來捕獲信號:
signal(SIGSEGV,signalHandler);
捕獲 Mach 異称偷耍或者 Unix 信號都可以抓到 crash 事件,這兩種方式哪個更好呢伴鳖?優(yōu)選 Mach 異常节值,因為 Mach 異常處理會先于 Unix 信號處理發(fā)生,如果 Mach 異常的 handler 讓程序 exit 了榜聂,那么 Unix 信號就永遠不會到達這個進程了搞疗。轉(zhuǎn)換 Unix 信號是為了兼容更為流行的 POSIX 標準 (SUS 規(guī)范),這樣不必了解 Mach 內(nèi)核也可以通過 Unix 信號的方式來兼容開發(fā)须肆。
KSCrash異常捕獲原理
KSCrash是一個完備的異常捕獲開源框架匿乃,它不僅可以捕獲到各種異常桩皿,并可在設(shè)備上完成符號化工作。同時幢炸,還有很多高級的特性泄隔,例如查找僵尸對象(Zombie)、 內(nèi)存自释鸹病(Introspection)佛嬉、 主線程死鎖檢測。
捕獲日志流程
這里只分析KSCrash獲取崩潰日志的原理闸天。下面是主要的流程:
獲取崩潰日志主要流程有:
注冊異常處理函數(shù)
等待異常發(fā)生
異常發(fā)生
回調(diào)到異常處理函數(shù)
在異常處理函數(shù)中獲取異常發(fā)生時刻的所有線程
循環(huán)獲取每個線程的調(diào)用堆棧
符號化調(diào)用堆棧
保存異常日志
程序結(jié)束
下次啟動發(fā)送上次的崩潰日志
捕獲的異常種類
根據(jù)KSCrash的官網(wǎng)介紹暖呕,它可以捕獲多種異常,包括:
Mach kernel exceptions
Fatal signals
C++ exceptions
Objective-C exceptions
Main thread deadlock (experimental)
Custom crashes (e.g. from scripting languages)
下面主要介紹下 Mach kernel exceptions、Fatal signals号枕、C++ exceptions異常的注冊異常處理函數(shù)原理缰揪。
Mach異常注冊原理
下面是mach exceptions 的注冊流程圖
基本流程是:
首先調(diào)用task_get_exception_ports 保存先前的異常處理端口。
調(diào)用mach_port_allocate 創(chuàng)建異常處理端口g_exceptionPort葱淳。
調(diào)用 mach_port_insert_right 獲取端口的權(quán)限
設(shè)置異常處理端口
創(chuàng)建線程钝腺,線程中不停的調(diào)用mach_msg ,讀取g_exceptionPort端口上的數(shù)據(jù)赞厕,如果異常發(fā)生艳狐,mach_msg成功,進入異常處理流程。
恢復(fù)先前的異常處理端口
調(diào)用ksmachexc_i_fetchMachineState 獲取線程狀態(tài)。
保存狀態(tài)并完成符號化功能三椿。
卸載異常處理函數(shù)沥割。
千言萬語,不如幾行代碼的說服力蹲诀,所以后面的內(nèi)容都使用代碼+注釋的形式表述。
bool kscrashsentry_installMachHandler(KSCrash_SentryContext* const context)
{
bool attributes_created = false;
pthread_attr_t attr;
kern_return_t kr;
int error;
const task_t thisTask = mach_task_self();
exception_mask_t mask = EXC_MASK_BAD_ACCESS |
EXC_MASK_BAD_INSTRUCTION |
EXC_MASK_ARITHMETIC |
EXC_MASK_SOFTWARE |
EXC_MASK_BREAKPOINT;
if(g_installed)
{
return true;
}
g_installed = 1;
g_context = context;
///獲取先前異常捕獲的端口
kr = task_get_exception_ports(thisTask,
mask,
g_previousExceptionPorts.masks,
&g_previousExceptionPorts.count,
g_previousExceptionPorts.ports,
g_previousExceptionPorts.behaviors,
g_previousExceptionPorts.flavors);
if(g_exceptionPort == MACH_PORT_NULL)
{
///創(chuàng)建異常捕獲端口
kr = mach_port_allocate(thisTask,
MACH_PORT_RIGHT_RECEIVE,
&g_exceptionPort);
///獲取端口的權(quán)限
kr = mach_port_insert_right(thisTask,
g_exceptionPort,
g_exceptionPort,
MACH_MSG_TYPE_MAKE_SEND);
}
///設(shè)置異常捕獲端口
kr = task_set_exception_ports(thisTask,
mask,
g_exceptionPort,
EXCEPTION_DEFAULT,
THREAD_STATE_NONE);
///啟動讀異常端口數(shù)據(jù)的線程
pthread_attr_init(&attr);
attributes_created = true;
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
error = pthread_create(&g_secondaryPThread,
&attr,
&ksmachexc_i_handleExceptions,
kThreadSecondary);
g_secondaryMachThread = pthread_mach_thread_np(g_secondaryPThread);
context->reservedThreads[KSCrashReservedThreadTypeMachSecondary] = g_secondaryMachThread;
error = pthread_create(&g_primaryPThread,
&attr,
&ksmachexc_i_handleExceptions,
kThreadPrimary);
pthread_attr_destroy(&attr);
g_primaryMachThread = pthread_mach_thread_np(g_primaryPThread);
context->reservedThreads[KSCrashReservedThreadTypeMachPrimary] = g_primaryMachThread;
failed:
return false;
}
這里完了展示主要邏輯,去掉了很多日志和錯誤判斷的代碼刮便。下面是異常處理函數(shù)
void* ksmachexc_i_handleExceptions(void* const userData)
{
MachExceptionMessage exceptionMessage = {{0}};
MachReplyMessage replyMessage = {{0}};
const char* threadName = (const char*) userData;
pthread_setname_np(threadName);
if(threadName == kThreadSecondary)
{
thread_suspend(ksmach_thread_self());
}
for(;;)
{
///讀取異常端口
kern_return_t kr = mach_msg(&exceptionMessage.header,
MACH_RCV_MSG,
0,
sizeof(exceptionMessage),
g_exceptionPort,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if(kr == KERN_SUCCESS)
{
break;
}
}
///讀取到異常信息,證明崩潰發(fā)生
if(g_installed)
{
bool wasHandlingCrash = g_context->handlingCrash;
kscrashsentry_beginHandlingCrash(g_context);
///掛起所有的線程
kscrashsentry_suspendThreads();
// Switch to the secondary thread if necessary, or uninstall the handler
// to avoid a death loop.
if(ksmach_thread_self() == g_primaryMachThread)
{
KSLOG_DEBUG("This is the primary exception thread. Activating secondary thread.");
if(thread_resume(g_secondaryMachThread) != KERN_SUCCESS)
{
KSLOG_DEBUG("Could not activate secondary thread. Restoring original exception ports.");
ksmachexc_i_restoreExceptionPorts();
}
}
else
{
KSLOG_DEBUG("This is the secondary exception thread. Restoring original exception ports.");
ksmachexc_i_restoreExceptionPorts();
}
///是否正在處理異常
if(wasHandlingCrash)
{
KSLOG_INFO("Detected crash in the crash reporter. Restoring original handlers.");
// The crash reporter itself crashed. Make a note of this and
// uninstall all handlers so that we don't get stuck in a loop.
g_context->crashedDuringCrashHandling = true;
kscrashsentry_uninstall(KSCrashTypeAsyncSafe);
}
/// 填充異常信息
STRUCT_MCONTEXT_L machineContext;
if(ksmachexc_i_fetchMachineState(exceptionMessage.thread.name, &machineContext))
{
if(exceptionMessage.exception == EXC_BAD_ACCESS)
{
g_context->faultAddress = ksmach_faultAddress(&machineContext);
}
else
{
g_context->faultAddress = ksmach_instructionAddress(&machineContext);
}
}
g_context->crashType = KSCrashTypeMachException;
g_context->offendingThread = exceptionMessage.thread.name;
g_context->registersAreValid = true;
g_context->mach.type = exceptionMessage.exception;
g_context->mach.code = exceptionMessage.code[0];
g_context->mach.subcode = exceptionMessage.code[1];
g_context->onCrash();
kscrashsentry_uninstall(KSCrashTypeAsyncSafe);
kscrashsentry_resumeThreads();
}
// Send a reply saying "I didn't handle this exception".
replyMessage.header = exceptionMessage.header;
replyMessage.NDR = exceptionMessage.NDR;
replyMessage.returnCode = KERN_FAILURE;
mach_msg(&replyMessage.header,
MACH_SEND_MSG,
sizeof(replyMessage),
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
return NULL;
}
signals異常注冊
下圖是signals exceptions 異常處理函數(shù)的注冊過程:
替換信號處理函數(shù)棧
int sigaltstack(const stack_t *ss, stack_t *oss);</signal.h>
int sigaction(int signo,const struct sigaction *restrict act,
struct sigaction *restrict oact);
給信號signum設(shè)置新的信號處理函數(shù)act绽慈, 同時保留該信號原有的信號處理函數(shù)oldact
安裝的信號句柄是g_signalStack恨旱,信號的種類包括如下:
SIGABRT, /* abort() */
SIGBUS, /* bus error */
SIGFPE, /* floating point exception */
SIGILL, /* illegal instruction (not reset when caught) */
SIGPIPE, /* write on a pipe with no one to read it */
SIGSEGV, /* segmentation violation */
SIGSYS, /* bad argument to system call */
SIGTRAP, /* trace trap (not reset when caught) */
C++ exceptions 異常注冊
這個比較簡單,直接調(diào)用了標注庫的std::set_terminate(CPPExceptionTerminate)函數(shù)坝疼,設(shè)置CPPExceptionTerminate為C++ exceptions 的異常處理函數(shù)搜贤。
Object C 異常注冊
具體看代碼Sentry 目錄下的KSCrashSentry_NSException.m文件
獲取線程的調(diào)用堆棧、符號化調(diào)用堆棧
獲取線程的調(diào)用堆棧钝凶、符號化調(diào)用堆棧原理
下面只用代碼講解仪芒,代碼只保留主要邏輯,kscrash_i_onCrash符號化的入口函數(shù):
{
...
///根據(jù)崩潰上下文context,寫崩潰日志
kscrashreport_writeMinimalReport(context, g_recrashReportFilePath);
......
}
void kscrashreport_writeStandardReport(KSCrash_Context* const crashContext,
const char* const path)
{
......
/// 寫崩潰時刻所有線程的 回溯
kscrw_i_writeAllThreads(writer,
KSCrashField_Threads,
&crashContext->crash,
crashContext->config.introspectionRules.enabled,
crashContext->config.searchThreadNames,
crashContext->config.searchQueueNames);
.....
}
void kscrw_i_writeAllThreads(const KSCrashReportWriter* const writer,
const char* const key,
const KSCrash_SentryContext* const crash,
bool writeNotableAddresses,
bool searchThreadNames,
bool searchQueueNames)
{
const task_t thisTask = mach_task_self();
thread_act_array_t threads;
mach_msg_type_number_t numThreads;
kern_return_t kr;
///獲取所有線程
if((kr = task_threads(thisTask, &threads, &numThreads)) != KERN_SUCCESS)
{
KSLOG_ERROR("task_threads: %s", mach_error_string(kr));
return;
}
// Fetch info for all threads.
writer->beginArray(writer, key);
{
for(mach_msg_type_number_t i = 0; i < numThreads; i++)
{
kscrw_i_writeThread(writer, NULL, crash, threads[i], (int)i, writeNotableAddresses, searchThreadNames,
searchQueueNames);
}
}
....
}
void kscrw_i_writeThread(const KSCrashReportWriter* const writer,
const char* const key,
const KSCrash_SentryContext* const crash,
const thread_t thread,
const int index,
const bool writeNotableAddresses,
const bool searchThreadNames,
const bool searchQueueNames)
{
bool isCrashedThread = thread == crash->offendingThread;
char nameBuffer[128];
STRUCT_MCONTEXT_L machineContextBuffer;
uintptr_t backtraceBuffer[kMaxBacktraceDepth];
int backtraceLength = sizeof(backtraceBuffer) / sizeof(*backtraceBuffer);
int skippedEntries = 0;
/// 獲取線程狀態(tài)桌硫、 異常狀態(tài)
STRUCT_MCONTEXT_L* machineContext = kscrw_i_getMachineContext(crash,
thread,
&machineContextBuffer);
///獲取異常線程的回溯
uintptr_t* backtrace = kscrw_i_getBacktrace(crash,
thread,
machineContext,
backtraceBuffer,
&backtraceLength,
&skippedEntries);
if(backtrace != NULL)
{
///符號化線程回溯
kscrw_i_writeBacktrace(writer,
KSCrashField_Backtrace,
backtrace,
backtraceLength,
skippedEntries);
}
......
}```
代碼分析到目前夭咬,關(guān)鍵的代碼已經(jīng)出現(xiàn)了,三部分:
獲取線程狀態(tài)铆隘、 異常狀態(tài)
獲取異常線程的回溯
符號化線程回溯
獲取線程狀態(tài) 代碼分析
```STRUCT_MCONTEXT_L* kscrw_i_getMachineContext(const KSCrash_SentryContext* const crash,
const thread_t thread,
STRUCT_MCONTEXT_L* const machineContextBuffer)
{
if(!kscrw_i_fetchMachineState(thread, machineContextBuffer))
{
return NULL;
}
return machineContextBuffer;
}
bool kscrw_i_fetchMachineState(const thread_t thread,
STRUCT_MCONTEXT_L* const machineContextBuffer)
{
if(!ksmach_threadState(thread, machineContextBuffer))
{
return false;
}
if(!ksmach_exceptionState(thread, machineContextBuffer))
{
return false;
}
return true;
}
bool ksmach_threadState(const thread_t thread,
STRUCT_MCONTEXT_L* const machineContext)
{
return ksmach_fillState(thread,
(thread_state_t)&machineContext->__ss,
ARM_THREAD_STATE64,
ARM_THREAD_STATE64_COUNT);
}
bool ksmach_fillState(const thread_t thread,
const thread_state_t state,
const thread_state_flavor_t flavor,
const mach_msg_type_number_t stateCount)
{
mach_msg_type_number_t stateCountBuff = stateCount;
kern_return_t kr;
kr = thread_get_state(thread, flavor, state, &stateCountBuff);
if(kr != KERN_SUCCESS)
{
return false;
}
return true;
}
bool ksmach_exceptionState(const thread_t thread,
STRUCT_MCONTEXT_L* const machineContext)
{
return ksmach_fillState(thread,
(thread_state_t)&machineContext->__es,
ARM_EXCEPTION_STATE64,
ARM_EXCEPTION_STATE64_COUNT);
}
獲取異常線程的回溯 代碼分析
uintptr_t* kscrw_i_getBacktrace(const KSCrash_SentryContext* const crash,
const thread_t thread,
const STRUCT_MCONTEXT_L* const machineContext,
uintptr_t* const backtraceBuffer,
int* const backtraceLength,
int* const skippedEntries)
{
int actualSkippedEntries = 0;
int actualLength = ksbt_backtraceLength(machineContext);
*backtraceLength = ksbt_backtraceThreadState(machineContext,
backtraceBuffer,
actualSkippedEntries,
*backtraceLength);
return backtraceBuffer;
}
int ksbt_backtraceThreadState(const STRUCT_MCONTEXT_L* const machineContext,
uintptr_t*const backtraceBuffer,
const int skipEntries,
const int maxEntries)
{
int i = 0;
if(skipEntries == 0)
{
const uintptr_t instructionAddress = ksmach_instructionAddress(machineContext);
backtraceBuffer[i] = instructionAddress;
i++;
}
KSFrameEntry frame = {0};
const uintptr_t framePtr = ksmach_framePointer(machineContext);
if(framePtr == 0 ||
ksmach_copyMem((void*)framePtr, &frame, sizeof(frame)) != KERN_SUCCESS)
{
return 0;
}
for(; i < maxEntries; i++)
{
backtraceBuffer[i] = frame.return_address;
if(backtraceBuffer[i] == 0 ||
frame.previous == 0 ||
ksmach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS)
{
break;
}
}
return i;
}
uintptr_t ksmach_instructionAddress(const STRUCT_MCONTEXT_L* const machineContext)
{
return machineContext->__ss.__pc;
}```
##符號化的代碼 代碼分析
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};
typedef struct dl_info
{
const char *dli_fname; /* Pathname of shared object */
void *dli_fbase; /* Base address of shared object */
const char *dli_sname; /* Name of nearest symbol */
void *dli_saddr; /* Address of nearest symbol */
} Dl_info;
void kscrw_i_writeBacktrace(const KSCrashReportWriter* const writer,
const char* const key,
const uintptr_t* const backtrace,
const int backtraceLength,
const int skippedEntries)
{
Dl_info symbolicated[backtraceLength];
ksbt_symbolicate(backtrace, symbolicated, backtraceLength, skippedEntries);
}
#define CALL_INSTRUCTION_FROM_RETURN_ADDRESS(A) (DETAG_INSTRUCTION_ADDRESS((A)) - 1)
void ksbt_symbolicate(const uintptr_t* const backtraceBuffer,
Dl_info* const symbolsBuffer,
const int numEntries,
const int skippedEntries)
{
int i = 0;
for(; i < numEntries; i++)
{
ksdl_dladdr(CALL_INSTRUCTION_FROM_RETURN_ADDRESS(backtraceBuffer[i]), &symbolsBuffer[i]);
}
}
bool ksdl_dladdr(const uintptr_t address, Dl_info* const info)
{
info->dli_fname = NULL;
info->dli_fbase = NULL;
info->dli_sname = NULL;
info->dli_saddr = NULL;
const uint32_t idx = ksdl_imageIndexContainingAddress(address);
if(idx == UINT_MAX)
{
return false;
}
const struct mach_header* header = _dyld_get_image_header(idx);
const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
/// 符號在鏡像的偏移量 = 堆棧地址 - 鏡像的加載地址
const uintptr_t addressWithSlide = address - imageVMAddrSlide;
const uintptr_t segmentBase = ksdl_segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
if(segmentBase == 0)
{
return false;
}
info->dli_fname = _dyld_get_image_name(idx);
info->dli_fbase = (void*)header;
// Find symbol tables and get whichever symbol is closest to the address.
const STRUCT_NLIST* bestMatch = NULL;
uintptr_t bestDistance = ULONG_MAX;
uintptr_t cmdPtr = ksdl_firstCmdAfterHeader(header);
if(cmdPtr == 0)
{
return false;
}
for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++)
{
const struct load_command* loadCmd = (struct load_command*)cmdPtr;
///查找LC_SYMTAB load command
if(loadCmd->cmd == LC_SYMTAB)
{
const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPtr;
const STRUCT_NLIST* symbolTable = (STRUCT_NLIST*)(segmentBase + symtabCmd->symoff);
const uintptr_t stringTable = segmentBase + symtabCmd->stroff;
///在符號表中循環(huán)查找卓舵,直到首次達到 鏡像偏移量imageVMAddrSlide
for(uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++)
{
// If n_value is 0, the symbol refers to an external object.
if(symbolTable[iSym].n_value != 0)
{
uintptr_t symbolBase = symbolTable[iSym].n_value;
uintptr_t currentDistance = addressWithSlide - symbolBase;
if((addressWithSlide >= symbolBase) &&
(currentDistance <= bestDistance))
{
bestMatch = symbolTable + iSym;
bestDistance = currentDistance;
}
}
}
///取出符號信息,符號信息存儲在
if(bestMatch != NULL)
{
info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);
info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
if(*info->dli_sname == '_')
{
info->dli_sname++;
}
// This happens if all symbols have been stripped.
if(info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3)
{
info->dli_sname = NULL;
}
break;
}
}
cmdPtr += loadCmd->cmdsize;
}
return true;
}
上面是所有的關(guān)鍵代碼膀钠。用到了一些Mach 的API掏湾,單不是蘋果私有API,放心用吧肿嘲。