KSCrash崩潰收集原理淺析

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:數據部分。

KSCrash崩潰原理淺析1.png

這個是官網上的結構示意圖。

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)。 下面有一個圖展示下:

KSCrash崩潰原理淺析4.jpg

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

KSCrash崩潰原理淺析5.jpg
KSCrash崩潰原理淺析6.jpg

所以獲取到崩潰時線程的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獲取崩潰日志的原理。下面是主要的流程:

KSCrash崩潰原理淺析3.png

獲取崩潰日志主要流程有:

注冊異常處理函數
等待異常發(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 的注冊流程圖

KSCrash崩潰原理淺析_mach 異常安裝.png

基本流程是:

首先調用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 異常處理函數的注冊過程:

KSCrash崩潰原理淺析_sinal 異常安裝.png

替換信號處理函數棧

    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崩潰原理淺析_符號過程.png

下面只用代碼講解,代碼只保留主要邏輯,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,放心用吧。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容