KSCrash 是一個異常收集的開源框架。
它可以捕獲到Mach級內核異常、信號異常、C++異常、Objective-C異常、主線程死鎖;當捕獲到異常后,KSCrash可以在設備上完成符號化崩潰日志(前提是編譯的時候將符號表編譯到可執(zhí)行程序中);日志的格式你也可以定制,可以是JSON格式的,也可以是Apple crash日志風格。另外,還有僵尸對象查找、內存自省等特性。
目前異常收集的框架非常多,有集成了收集、統(tǒng)計功能的一條龍產品,如,友盟,鵝廠的Bugly 等等;也有幾個開源框架,如,KSCrash,plcrashreporter,CrashKit。
基于我們項目的安全性考慮,即,不希望第三方SDK看到崩潰日志,我選取了開源框架這條路??v覽這幾個開源框架,只有KSCrash一直在更新。所以,毫不猶豫的選用了它。
APP Crash后,獲取崩潰線程的程調用堆棧的過程,是程序執(zhí)行過程的逆向過程。那么,了解APP的執(zhí)行正向過程,對獲取崩潰線程的調用堆棧是非常非常有益的。所以,在分析KSCrash原理前,依照APP正向執(zhí)行過程先推導下 異常收集、符號化的原理。
推導異常收集、符號化的原理
本節(jié)描述的內容只是按照自己的理解編寫的,由于道行尚淺,理解不深,所以具體安排的內容不一定合理。依據APP執(zhí)行的過程,主要囊括了:編譯生成可執(zhí)行APP、內核加載并啟動APP、調用堆棧等,并穿插了一點理解KSCrash的必備知識。
編譯生成可執(zhí)行APP
開發(fā)者通過IDE集成開發(fā)環(huán)境(例如Xcode),將源碼文件轉化為臨時中間文件(這種文件應該是機器語言了),然后使用鏈接器(/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í)行代碼和數據??梢詤⒖肌禣S X ABI Mach-O File Format Reference》官方文檔。這個文檔在官網打不來了,我就鏈接到我自己的pdf地址了。
概述
Mach-O文件包括三個組成部分,分別如下:
1.header:指定了文件的基本信息,如CUP類型、加載命令個數等。
2.Load commands:加載命令,指定了文件的邏輯結構、在虛擬內存(virtual memory)中文件的布局。你可以理解為一片文章的目錄。
3.Raw segment data:數據部分。

這個是官網上的結構示意圖。
header
Mach-O文件的開頭部分是就是Header—文件頭。Header的數據結構定義在XNU微內核的loader.h文件中。loader.h也可以在IOS SDK的/usr/include/mach-o目錄下找到,header的數據結構定義如下:
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 */
};
可以看出包括了 :魔數、cup的類型、子類型、文件的類型、load commend個數、load commend大小等數據。
load commend
緊跟在Header后面的是load commend。load commend指定了文件的布局。具體指定了以下內容:
? The initial layout of the file in virtual memory 文件在虛擬內存中的初始布局
? 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都有對應的數據結構,可以在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中定義的各種數據都存儲在這部分中。
查看Mach-O實用工具
在終端中有幾個工具是可以查看Mach-O文件內容的。另外位于usr/include/mach-o/dyld.h中的函數可以在程序中訪問Mach-O文件內容。
文件類型展示工具-file。The file-type displaying tool, 位于/usr/bin/file,顯示文件的類型,對于多構架的文件,它顯示每個構架下的鏡像類型。在終端中輸入:
~/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文件的各種數據。查看Mach-O header內容,在終端中輸入:
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í)行
根據上面分析的可執(zhí)行文件的結構,我們可以看到,可執(zhí)行文件中已經包含了符號表 ,這個符號表是可執(zhí)行代碼的虛擬地址和代碼中符號的對應表。符號表是綁定過程中建立的,程序的綁定有很多種,可以參看下面的文檔:Mach-O Programming Topics - Binding Symbols,里面詳細介紹了綁定和查找符號的過程。
看到符號表,那么我們可以做這樣的設想:如果程序崩潰,只要我們獲取到了崩潰調用堆棧的回溯地址,然后從這個符號表中查找對應的符號,就完成了調用堆棧的符號化工作? 還有就是我們如何獲取程序的調用堆棧呢?還有很多需要我們接著往下看。為了知道如何獲取調用堆棧的回溯,我們了解下程序的執(zhí)行過程:
程序的執(zhí)行過程:內核首先加載可執(zhí)行文件,并且檢測程序文件的起始部分的mach_header結構,內核驗證是否合法的Macj-O文件,解析header中的load commands。加載Load Commond中指定依賴鏡像到內存中,然后啟動進程,執(zhí)行程序的入口函數,進入正常的run loop。
調用堆棧
首先介紹一下什么叫調用堆棧:假設我們?yōu)榱送瓿梢粋€任務1,任務1的完成需要完成任務2…. 分別定義為幾個函數:function1,function2,function3,funtion4。即,function1調用function2,function2調用function3,function3調用function4。在function4運行過程中,我們可以從線程當前堆棧中了解到調用他的那幾個函數分別是誰。function4、function3、function2、function1呈現(xiàn)出一種“堆?!钡奶卣?,最后被調用的函數出現(xiàn)在最上方。因此稱呼這種關系為調用堆棧(call stack)。 下面有一個圖展示下:

函數調用經常是嵌套的,在同一時刻,堆棧中會有多個函數的信息。每個未完成運行的函數占用一個獨立的連續(xù)區(qū)域,稱作棧幀(Stack Frame)。棧幀是堆棧的邏輯片段,當調用函數時邏輯棧幀被壓入堆棧, 當函數返回時邏輯棧幀被從堆棧中彈出。棧幀存放著函數參數,局部變量及恢復前一棧幀所需要的數據等。理解了入棧和出棧,基本能理解調用堆棧,下面兩個圖,一個是入棧,一個是出棧,圖中描述的很清楚。


所以獲取到崩潰時線程的ebp和esp 就能回溯到上一個調用,依次類推,回溯出所有的調用堆棧。下面了解下寄存器。
寄存器
為了線程獲取BP和SP,我們需要了解一點點寄存器。因為他們保存在CPU的寄存器中。
arm64構架的寄存器在Procedure Call Standard for the ARM 64-bit Architecture (AArch64)有詳細的說明。不過都是英文的,我沒有看,我從代碼中也找到了它的定義,位于IOS SDK的usr/include/arm目錄下的_mcontext.h文件中。其中幾個關鍵的定義的代碼我摘錄下來了,如下:
_STRUCT_MCONTEXT64
{
_STRUCT_X86_EXCEPTION_STATE64 __es; ///異常寄存器
_STRUCT_X86_THREAD_STATE64 __ss; ///線程狀態(tài)寄存器
_STRUCT_X86_FLOAT_STATE64 __fs; ///浮點寄存器
};
這個結構體
定義了所有的寄存器。其中_STRUCT_MCONTEXT64結構體定義了三大類寄存器,根據字面意思理解為:異常寄存器、線程狀態(tài)寄存器、浮點寄存器。我們只關注線程狀態(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里。不知不覺的問題已經轉化了,轉化為獲取線程的_STRUCT_X86_THREAD_STATE64數據,即,獲取線程的狀態(tài)結構體。
XNU微內核的核心部分Mach,里面暴露了一些線程的接口函數,我們應該能獲取到線程的狀態(tài)結構體。了解這些函數的接口定義可以參考:Mach IPC Interface、IPC 原理講解。
獲取線程狀態(tài)
IPC 接口文檔的線程接口部分(Thread Interface)的 thread_get_state函數可以獲取線程的狀態(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函數返回target_thread的執(zhí)行狀態(tài),存儲在flavor參數里??粗厦娴亩x,是不是一點感覺都沒有,一頭霧水,摸不著頭腦?我也是,幸好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函數可以根據線程ID(thread_t thread),獲取到線程狀態(tài)(_STRUCT_ARM_THREAD_STATE64),也就是通過線程ID,就能獲取到線程當前執(zhí)行狀態(tài)的BP 和SP。
思路回溯
上面講了,那么多,目的只有一個,就是理出一個思路—–獲取程序崩潰時線程的調用堆?!,F(xiàn)在大概是這樣的:
程序發(fā)生崩潰,我們獲取到崩潰的線程,取出線程的threadID。
通過thread_get_state函數, 獲取線程ID為threadID的線程的 當前執(zhí)行狀態(tài),目的是獲?。簬羔楤P、棧指針SP;
依據《1.4 調用堆?!吩怼P、SP,循環(huán)取出線程的調用堆棧。
依據《1.2 Mach-O文件存儲格式》原理,將調用堆棧中的地址轉換為代碼中的符號。
總體邏輯現(xiàn)在通了,但是,還有好多好多的細節(jié),等待我們去完善,比如,一個關鍵的邏輯,我是怎么知道程序崩潰了呢?從而讓程序執(zhí)行到崩潰處理函數里,完成線程回溯功能。
通過分析KS的代碼,得知,可以在程序啟動的時候注冊崩潰的處理函數,程序崩潰發(fā)生時,會執(zhí)行崩潰處理函數。
其實,捕獲異常的方式多種多樣,不同捕獲方式,捕獲的原理不同。捕獲原理請參看《二、KSCrash異常捕獲原理》。這里只掃盲下經典的捕獲方式。
捕獲崩潰方式
捕獲崩潰的方式有:
捕獲Mach 異常
捕獲Unix 信號
其實,這部分內容在漫談 iOS Crash 收集框架中闡述的非常明白。為了表示寫的好,這里再重復的闡述下。
iOS 系統(tǒng)自帶的Apple’s Crash Reporter 記錄在設備中的 Crash 日志,Exception Type項通常會包含兩個元素:Mach 異常 和 Unix 信號。
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x041a6f3
Mach 異常是什么?它又是如何與 Unix 信號建立聯(lián)系的?
Mach 是一個 XNU 的微內核核心,Mach 異常是指最底層的內核級異常,被定義在 下 。每個 thread,task,host 都有一個異常端口數組,Mach 的部分 API 暴露給了用戶態(tài),用戶態(tài)的開發(fā)者可以直接通過 Mach API 設置 thread,task,host 的異常端口,來捕獲 Mach 異常,抓取 Crash 事件。
所有 Mach 異常都在 host 層被ux_exception轉換為相應的 Unix 信號,并通過threadsignal將信號投遞到出錯的線程。iOS 中的 POSIX API 就是通過 Mach 之上的 BSD 層實現(xiàn)的。
因此,EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach 層的EXC_BAD_ACCESS異常,在 host 層被轉換成 SIGSEGV 信號投遞到出錯的線程。既然最終以信號的方式投遞到出錯的線程,那么就可以通過注冊 signalHandler 來捕獲信號:
signal(SIGSEGV,signalHandler);
捕獲 Mach 異?;蛘?Unix 信號都可以抓到 crash 事件,這兩種方式哪個更好呢?優(yōu)選 Mach 異常,因為 Mach 異常處理會先于 Unix 信號處理發(fā)生,如果 Mach 異常的 handler 讓程序 exit 了,那么 Unix 信號就永遠不會到達這個進程了。轉換 Unix 信號是為了兼容更為流行的 POSIX 標準 (SUS 規(guī)范),這樣不必了解 Mach 內核也可以通過 Unix 信號的方式來兼容開發(fā)。
KSCrash異常捕獲原理
KSCrash是一個完備的異常捕獲開源框架,它不僅可以捕獲到各種異常,并可在設備上完成符號化工作。同時,還有很多高級的特性,例如查找僵尸對象(Zombie)、 內存自?。↖ntrospection)、 主線程死鎖檢測。
捕獲日志流程
這里只分析KSCrash獲取崩潰日志的原理。下面是主要的流程:

獲取崩潰日志主要流程有:
注冊異常處理函數
等待異常發(fā)生
異常發(fā)生
回調到異常處理函數
在異常處理函數中獲取異常發(fā)生時刻的所有線程
循環(huán)獲取每個線程的調用堆棧
符號化調用堆棧
保存異常日志
程序結束
下次啟動發(fā)送上次的崩潰日志
捕獲的異常種類
根據KSCrash的官網介紹,它可以捕獲多種異常,包括:
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異常的注冊異常處理函數原理。
Mach異常注冊原理
下面是mach exceptions 的注冊流程圖

基本流程是:
首先調用task_get_exception_ports 保存先前的異常處理端口。
調用mach_port_allocate 創(chuàng)建異常處理端口g_exceptionPort。
調用 mach_port_insert_right 獲取端口的權限
設置異常處理端口
創(chuàng)建線程,線程中不停的調用mach_msg ,讀取g_exceptionPort端口上的數據,如果異常發(fā)生,mach_msg成功,進入異常處理流程。
恢復先前的異常處理端口
調用ksmachexc_i_fetchMachineState 獲取線程狀態(tài)。
保存狀態(tà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);
///獲取端口的權限
kr = mach_port_insert_right(thisTask,
g_exceptionPort,
g_exceptionPort,
MACH_MSG_TYPE_MAKE_SEND);
}
///設置異常捕獲端口
kr = task_set_exception_ports(thisTask,
mask,
g_exceptionPort,
EXCEPTION_DEFAULT,
THREAD_STATE_NONE);
///啟動讀異常端口數據的線程
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;
}
這里完了展示主要邏輯,去掉了很多日志和錯誤判斷的代碼。下面是異常處理函數
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 異常處理函數的注冊過程:

替換信號處理函數棧
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設置新的信號處理函數act, 同時保留該信號原有的信號處理函數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 異常注冊
這個比較簡單,直接調用了標注庫的std::set_terminate(CPPExceptionTerminate)函數,設置CPPExceptionTerminate為C++ exceptions 的異常處理函數。
Object C 異常注冊
具體看代碼Sentry 目錄下的KSCrashSentry_NSException.m文件
獲取線程的調用堆棧、符號化調用堆棧
獲取線程的調用堆棧、符號化調用堆棧原理

下面只用代碼講解,代碼只保留主要邏輯,kscrash_i_onCrash符號化的入口函數:
{
...
///根據崩潰上下文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);
}
......
}```
代碼分析到目前,關鍵的代碼已經出現(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;
}
上面是所有的關鍵代碼。用到了一些Mach 的API,單不是蘋果私有API,放心用吧。