xCrash 詳解與源碼分析

一、前言

工欲擅其事,必先利其器。當(dāng)我們的應(yīng)用發(fā)生錯誤或者崩潰時,如果有一款趁手的日志捕獲工具,那將會得心應(yīng)手的多。今天要學(xué)習(xí)的是來自 IQiYi 的 xCrash 日志捕獲工具。這款工具不管是從質(zhì)量上還是功能上,都是上乘之作。

二、xCrash 敘述

xCrash 能捕獲的異常日志包括了 Java Crash、Native Crash 以及 ANR 日志,而我們在 Android 上所發(fā)生的異常,其歸結(jié)起來無非就是這三種。關(guān)于這個庫,按官方的解釋,其主要的優(yōu)點如下:

支持 Android 4.0 - 10(API level 14 - 29)。
支持 armeabi,armeabi-v7a,arm64-v8a,x86 和 x86_64。
捕獲 java 崩潰,native 崩潰和 ANR。
獲取詳細的進程、線程、內(nèi)存、FD、網(wǎng)絡(luò)統(tǒng)計信息。
通過正則表達式設(shè)置需要獲取哪些線程的信息。
不需要 root 權(quán)限或任何系統(tǒng)權(quán)限。

而站在開發(fā)的角度來看,其架構(gòu)也是十分清晰的。下面是官方所提供的架構(gòu)圖。

image.png

三、初始化分析

1.初始化

初始化的代碼如下,似乎 so easy。

public class MyCustomApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        
        xcrash.XCrash.init(this);
    }
}

這里就不進一步貼代碼了,只文字說明一下,初始化主要是獲取AppId、AppVersion 等基礎(chǔ)信息。當(dāng)然,除此之外,最重要當(dāng)然是對 JavaCrash Handler、NativeCrash Handler 以及 AnrCrash Handler 的初始化。

2. JavaCrash Handler 的初始化

JavaCrashHandler.jpg

如上圖 JavaCrashHandler 實現(xiàn)了接口 UncaughtExceptionHandler,而它的初始化也簡單。

Thread.setDefaultUncaughtExceptionHandler(this);

這樣也算利用虛擬機所提供的接口,開始監(jiān)控 Java Crash 了。另外比較主要的便是其實現(xiàn)的方法uncaughtException,后面再來說。

3. AnrHandler 的初始化

AnrHandler 的初始化除了一些參數(shù)的設(shè)定,然后就是監(jiān)聽 /data/anr 目錄的變化。

fileObserver = new FileObserver("/data/anr/", CLOSE_WRITE) {
            public void onEvent(int event, String path) {
                try {
                    if (path != null) {
                        String filepath = "/data/anr/" + path;
                        if (filepath.contains("trace")) {
                            handleAnr(filepath);
                        }
                    }
                } catch (Exception e) {
                    XCrash.getLogger().e(Util.TAG, "AnrHandler fileObserver onEvent failed", e);
                }
            }
        };

        try {
            fileObserver.startWatching();
        } catch (Exception e) {
            fileObserver = null;
            XCrash.getLogger().e(Util.TAG, "AnrHandler fileObserver startWatching failed", e);
        }

當(dāng)然,我們都知道,在高版本的 Android 系統(tǒng)中,應(yīng)用已經(jīng)訪問不到 /data/anr 了。xCrash 是不是有提供了其他的實現(xiàn)方案呢?實際上它上捕獲了 SIGQUIT 信號,這個是 Android App 發(fā)生 ANR 時由 ActivityMangerService 向 App 發(fā)送的信號。具體的,在后面再來分析。

4.NativeHandler 的初始化

NativeHandler 的初始化要相對復(fù)雜一些了,其分為 Java 層和 Native 層。

4.1 Java 層

Java 層相對簡單,主要是加載 libxcrash.so ,以及進一步調(diào) nativeInit() 進行 native 層的初始化。

System.loadLibrary("xcrash");

4.2 Native 層

nativeInit() 所映射的 jni 實現(xiàn)是 xc_jni_init()。在 xc_jni_init 又分了 3 個小步驟來進行初始化。

xc_common_init

這里面初始化了一些公共參數(shù),如 os-kernel-version、app_version、appid、log 目錄等。其中最重要的是初始化了兩個文件 fd ,以應(yīng)對文件 fd 被耗盡的情況。

    //create prepared FD for FD exhausted case
    xc_common_open_prepared_fd(1);
    xc_common_open_prepared_fd(0);

這兩個 fd 分別給了 xc_common_crash_prepared_fd 和 xc_common_trace_prepared_fd。但是這里要注意,它們目前打開的都是 "/dev/null"。

xc_crash_init
xcc_unwind_init 初始化 unwinder。

api_level >= 16 && api_level <= 20 則加載 libcorkscrew.so
api_level >= 21 && api_level <= 23 則加載 libunwind.so

xc_crash_init_callback 初始化 jni call back。這里主要是初始化了一個 native 的線程,然后通過 eventfd 阻塞等待 native 發(fā)生 crash 時向上層 java 發(fā)出通知。

接下來是比較重要的信號注冊,通過xcc_signal_crash_register 進行。

int xcc_signal_crash_register(void (*handler)(int, siginfo_t *, void *))
{
    stack_t ss;
    .......
    if(0 != sigaltstack(&ss, NULL)) return XCC_ERRNO_SYS;
    ......
    for(i = 0; i < sizeof(xcc_signal_crash_info) / sizeof(xcc_signal_crash_info[0]); i++)
        if(0 != sigaction(xcc_signal_crash_info[i].signum, &act, &(xcc_signal_crash_info[i].oldact)))
            return XCC_ERRNO_SYS;

    return 0;
}

這里看關(guān)鍵的幾行,其中 sigalstack 是用于替換信號處理函數(shù)棧,有的說法是設(shè)置緊急函數(shù)棧。其原因是一般情況下,信號處理函數(shù)被調(diào)用時,內(nèi)核會在進程的棧上為其創(chuàng)建一個棧幀。但是這里就會有一個問題,如果棧的增長到達了棧的資源限制值(RLIMIT_STACK,使用 ulimit 命令可以查看,一般為 8M),或是棧已經(jīng)長得太大(沒有 RLIMIT_STACK 的限制),以致到達了映射內(nèi)存(mapped memory)邊界,那么此時信號處理函數(shù)就沒法得到棧幀的分配。
然后就是通過 sigaction() 進行信號的安裝,這里只關(guān)注一下它安裝哪一些信號。

     {.signum = SIGABRT},abort發(fā)出的信號
    {.signum = SIGBUS},非法內(nèi)存訪問
    {.signum = SIGFPE},浮點異常
    {.signum = SIGILL},非法指令
    {.signum = SIGSEGV},無效內(nèi)存訪問
    {.signum = SIGTRAP},斷點或陷阱指令
    {.signum = SIGSYS},系統(tǒng)調(diào)用異常
    {.signum = SIGSTKFLT}棧溢出

信號的處理函數(shù)在 xc_crash_signal_handler。這個在后面再來分析。還有,這里有也準(zhǔn)備了一個文件 fd , xc_crash_prepared_fd , 暫時還不清楚與前面 2 個的區(qū)別與關(guān)系。

xc_trace_init
trace 只是針對 Android 5.0 以上,因為其主要是用來獲取 ANR 的 trace。xc_trace_init_callback() 只是獲取 Java 的 methodId,進一步的主要操作在 xcc_signal_trace_register()。

int xcc_signal_trace_register(void (*handler)(int, siginfo_t *, void *))
{
    ......
    //un-block the SIGQUIT mask for current thread, hope this is the main thread
    sigemptyset(&set);
    sigaddset(&set, SIGQUIT);
    if(0 != (r = pthread_sigmask(SIG_UNBLOCK, &set, &xcc_signal_trace_oldset))) return r;
    //register new signal handler for SIGQUIT
    ......
    if(0 != sigaction(SIGQUIT, &act, &xcc_signal_trace_oldact))
    {
        pthread_sigmask(SIG_SETMASK, &xcc_signal_trace_oldset, NULL);
        return XCC_ERRNO_SYS;
    }
    ......
}

用來處理 SIG_QUIT 的響應(yīng)函數(shù)是 xc_trace_handler() ,這個也是后面再來分析。函數(shù)的最后還會啟動一個線程,并在線程響應(yīng)函數(shù)xc_trace_dumper中等待 ANR 的發(fā)生。這里的等待機制同樣是用的 eventfd。

5.初始化小結(jié)

  1. 初始化 JavaCrashHandler,其實現(xiàn)機制是通過 Thread.setDefaultUncaughtExceptionHandler() 注冊一個自己的 UncaughtExceptionHandler。

  2. 初始化 AnrHandler,其實現(xiàn)機制是監(jiān)聽 "/data/anr" 文件夾的變化。同時對于 5.0 以上的版本,通過監(jiān)聽 SIGQUIT 來實現(xiàn)。

  3. 初始化 NativeHandler,預(yù)留 FD、安裝一系列 signal、初始化用于 unwind 的
    libcorkscrew.so 和 libunwind.so ,以及獲取相關(guān)的函數(shù)。

四、異常處理分析

1.Java 異常處理

Java 的異常處理機制比較簡單,只要 uncaughtException() 方法中等待異常的回調(diào),然后收集相應(yīng)的信息即可。這些都比較簡單,這里就不詳細分析了,感興趣的可以自己去看。另外,其實現(xiàn)了一個 Util 類用來讀取系統(tǒng)的文件,里面有很多值的學(xué)習(xí)的東西,如獲取 meminfo 、獲取文件所占用的 fds 等。

2.ANR 異常處理

2.1 Java 層的處理

Java 層的處理在 AnrHandler#handleAnr() 方法中,其也比較簡單,就是解析 data/anr/trace.txt 文件,看看有沒有自己進程的信息。感興趣的也可以自己去分析。

2.2 Native 層的處理

關(guān)于Native 層的 anr 處理,官方有給了具體的實現(xiàn)架構(gòu)圖。那么,對照圖,我們來具體看看它是如何實現(xiàn)的。

image.png

在 Native 初始化時,我們知道其監(jiān)聽了 SIGQUIT 信號來處理 ANR 的發(fā)生,并在 xc_trace_handler() 方法中來進行處理。

XCC_UTIL_TEMP_FAILURE_RETRY(write(xc_trace_notifier, &data, sizeof(data)));

其主要的實現(xiàn)很簡單,就是通過 eventfd 發(fā)送一個通知,那這個通知的響應(yīng)函數(shù)是 xc_trace_dumper(),下面來看看它的具體實現(xiàn)。

前面 2 步打開日志文件 xc_common_open_trace_log() 和 寫入頭信息 xc_trace_write_header() 感興趣的可以自己分析。我們重點是要關(guān)注其怎么 dump art 的 trace。

xc_trace_load_symbols 加載符號表
xc_dl_create() 和 xc_dl_sym() 是里面比較重要的兩個函數(shù)實現(xiàn)。xc_dl_create 是尋找到 so 被 mmap 所加載的虛擬地址,xc_dl_sym 是計算 so 中相應(yīng)符號(函數(shù))的虛擬地址。
其主要是從 libc++.so 中查找符號 _ZNSt3__14cerrE,對的,就是 cerr ;從 libart.so 中查找符號 _ZN3art7Runtime9instance_E 以及 _ZN3art7Runtime14DumpForSigQuitERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE 在進程虛擬空間中的地址。針對 L 還需要 _ZN3art3Dbg9SuspendVMEv 和 _ZN3art3Dbg8ResumeVMEv。

xc_dl_create() 的具體實現(xiàn)在 xc_dl_find_map_start() 獲取 so 的基地址、xc_dl_file_open() 通過 mmap 加載 so、xc_dl_parse_elf() 解析 so。這里的解析 so ,其實就是解析 elf 文件,這個比較復(fù)雜,需要對 elf 文件格式熟悉。這里就不深分析了。

xc_trace_libart_runtime_dump 開始 dump

相關(guān)代碼如下:

        if(xc_trace_is_lollipop)
            xc_trace_libart_dbg_suspend();
        xc_trace_libart_runtime_dump(*xc_trace_libart_runtime_instance, xc_trace_libcpp_cerr);
        if(xc_trace_is_lollipop)
            xc_trace_libart_dbg_resume();

xc_trace_libart_runtime_dump 就是_ZN3art7Runtime14DumpForSigQuitERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE。也就是調(diào)用 dump 將對 SIGQUIT 的處理輸出到 cerr 中。這里有一個細節(jié),就是在 dump 節(jié),其通過 dup2() 函數(shù)將標(biāo)準(zhǔn)的錯誤輸出重定向到了自己的 fd 中。就在這段代碼的上面,如下。

        if(dup2(fd, STDERR_FILENO) < 0)
        {
            if(0 != xcc_util_write_str(fd, "Failed to duplicate FD.\n")) goto end;
            goto skip;
        }

接下來就是其他日志的處理了,感興趣的也可以看一下,比如 logcat 日志的獲取、文件 fd、網(wǎng)絡(luò)日志等。至此,就完成了對 trace 的抓取了。

3.Native 異常處理

關(guān)于 Native 異常處理,官方給的架構(gòu)圖如下,流程上是很清晰的。


image.png

在初始化的時候我們分析到,當(dāng)發(fā)生 native 崩潰時,會在信號處理函數(shù) xc_crash_signal_handler() 進行處理。那么就從這個函數(shù)開始分析吧。

static void xc_crash_signal_handler(int sig, siginfo_t *si, void *uc)
{
  ......
  pid_t dumper_pid = xc_crash_fork(xc_crash_exec_dumper);
  ......
  int wait_r = XCC_UTIL_TEMP_FAILURE_RETRY(waitpid(dumper_pid, &status, __WALL));
}

這個函數(shù)除了做一些打開文件 fd 等基本的操作之外,其最主要做的事就是通過 xc_crash_fork() 創(chuàng)建一個子進程并等待子進程返回。

創(chuàng)建的子進程的響應(yīng)函數(shù)是 xc_crash_exec_dumper()。這個函數(shù)首先通過 pipe 將一系列的參數(shù),比如進程 pid ,崩潰線程 tid 等,寫入到標(biāo)準(zhǔn)的輸入當(dāng)中,其目的是為了子進程從標(biāo)準(zhǔn)的輸入當(dāng)中去讀取參數(shù)。然后通過 execl() 進入到真正的 dumper 程序。

static int xc_crash_exec_dumper(void *arg)
{
  ......
  execl(xc_crash_dumper_pathname, XCC_UTIL_XCRASH_DUMPER_FILENAME, NULL);
}

這個其實就是通過 execl() 來運行 libxcrash_dumper.so ,當(dāng)然,它不會再創(chuàng)建新的進程。而 libxcrash_dumper.so 的入口在 xcd_core.c 中的 main() 。可能很多人第一次在 Android 中見到我們熟悉的 C 語言中的 main() 函數(shù)吧。

下面我把 main() 函數(shù)都貼出來,整個實現(xiàn)言簡意賅,基本反應(yīng)了上面 dump 架構(gòu)圖的核心邏輯。

int main(int argc, char** argv)
{
    (void)argc;
    (void)argv;
    
    //don't leave a zombie process
    alarm(30);

    //read args from stdin
    if(0 != xcd_core_read_args()) exit(1);

    //open log file
    if(0 > (xcd_core_log_fd = XCC_UTIL_TEMP_FAILURE_RETRY(open(xcd_core_log_pathname, O_WRONLY | O_CLOEXEC)))) exit(2);

    //register signal handler for catching self-crashing
    xcc_unwind_init(xcd_core_spot.api_level);
    xcc_signal_crash_register(xcd_core_signal_handler);

    //create process object
    if(0 != xcd_process_create(&xcd_core_proc,
                               xcd_core_spot.crash_pid,
                               xcd_core_spot.crash_tid,
                               &(xcd_core_spot.siginfo),
                               &(xcd_core_spot.ucontext))) exit(3);

    //suspend all threads in the process
    xcd_process_suspend_threads(xcd_core_proc);

    //load process info
    if(0 != xcd_process_load_info(xcd_core_proc)) exit(4);

    //record system info
    if(0 != xcd_sys_record(xcd_core_log_fd,
                           xcd_core_spot.time_zone,
                           xcd_core_spot.start_time,
                           xcd_core_spot.crash_time,
                           xcd_core_app_id,
                           xcd_core_app_version,
                           xcd_core_spot.api_level,
                           xcd_core_os_version,
                           xcd_core_kernel_version,
                           xcd_core_abi_list,
                           xcd_core_manufacturer,
                           xcd_core_brand,
                           xcd_core_model,
                           xcd_core_build_fingerprint)) exit(5);

    //record process info
    if(0 != xcd_process_record(xcd_core_proc,
                               xcd_core_log_fd,
                               xcd_core_spot.logcat_system_lines,
                               xcd_core_spot.logcat_events_lines,
                               xcd_core_spot.logcat_main_lines,
                               xcd_core_spot.dump_elf_hash,
                               xcd_core_spot.dump_map,
                               xcd_core_spot.dump_fds,
                               xcd_core_spot.dump_network_info,
                               xcd_core_spot.dump_all_threads,
                               xcd_core_spot.dump_all_threads_count_max,
                               xcd_core_dump_all_threads_whitelist,
                               xcd_core_spot.api_level)) exit(6);

    //resume all threads in the process
    xcd_process_resume_threads(xcd_core_proc);

#if XCD_CORE_DEBUG
    XCD_LOG_DEBUG("CORE: done");
#endif
    return 0;
}

里面的每一個過程就不再進行分析了,這里只說最重要的一點,其最核心的獲取線程的 regs、backtrace 等信息是通過 ptrace 技術(shù)來獲取的。這里面關(guān)于 ptrace,關(guān)于 elf 都相對比較復(fù)雜,因此不在這里獻丑了。

五、總結(jié)

xCrash 的代碼看起來非常簡潔,層次也十分的清晰,感嘆作者的功力之強。而由于個人水平有限,有些地方分析的可能也不是特別深入到位。有什么錯誤之處也請幫忙指出改正,感謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容