溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

干貨|安卓APP崩潰捕獲方案——xCrash

發(fā)布時(shí)間:2020-08-08 00:56:35 來源:ITPUB博客 閱讀:471 作者:愛奇藝技術(shù)產(chǎn)品團(tuán)隊(duì) 欄目:移動(dòng)開發(fā)

導(dǎo)讀

2019 年,愛奇藝在 GitHub 上開源了 xCrash。這是一個(gè)比較完整的安卓 APP 崩潰捕獲 SDK,它能在 App 進(jìn)程崩潰時(shí),在你指定的目錄中生成 tombstone 文件(格式與系統(tǒng)的 tombstone 文件類似)。它支持捕獲 native 崩潰和 Java 崩潰;支持安卓 4.0 - 9.0;支持 armeabi,armeabi-v7a,arm64-v8a,x86 和 x86_64。

依托于愛奇藝安卓 APP 上億的日活用戶數(shù)據(jù),xCrash 在兼容性、穩(wěn)定性、功能完整性等方面不斷地自我完善。目前 xCrash 已被應(yīng)用于愛奇藝、愛奇藝極速版、愛奇藝動(dòng)畫屋、奇秀、愛奇藝 VR 影院、叭噠漫畫等 20 余款愛奇藝的安卓 APP 中。

問題概述

在移動(dòng)端 APP 的各種質(zhì)量問題中,最嚴(yán)重的可能就是 APP 崩潰閃退了。

從安卓 APP 開發(fā)的角度,Java 崩潰捕獲相對(duì)比較容易,JVM 給 Java 字節(jié)碼提供了一個(gè)受控的運(yùn)行環(huán)境,同時(shí)也提供了完善的 Java 崩潰捕獲機(jī)制。Native 崩潰的捕獲和處理相對(duì)比較困難,安卓系統(tǒng)的debuggerd 守護(hù)進(jìn)程會(huì)為 native 崩潰自動(dòng)生成詳細(xì)的崩潰描述文件(tombstone)。

在開發(fā)調(diào)試階段,可以通過系統(tǒng)提供的 bugreport 工具獲取 tombstone 文件(或者將設(shè)備 root 后也可以拿到)。但是對(duì)于發(fā)布到線上的安卓 APP,如何獲取 tombstone 文件,安卓操作系統(tǒng)本身并沒有提供這樣的功能。這個(gè)問題一直是安卓 native 崩潰分析和移動(dòng)端 APM 系統(tǒng)的痛點(diǎn)之一。

Native 崩潰介紹

信號(hào)

Native 崩潰發(fā)生在機(jī)器指令運(yùn)行的層面。比如:APP 中的 so 庫、系統(tǒng)的 so 庫、JVM 本身等等。如果這部分程序做了 Linux kernel 認(rèn)為不可接受的事情(比如:除數(shù)為零、讓 CPU 執(zhí)行它無法識(shí)別的指令等),kernel 就會(huì)向 APP 中對(duì)應(yīng)的線程發(fā)送相應(yīng)的信號(hào)(signal),這些信號(hào)的默認(rèn)處理方式是殺死整個(gè)進(jìn)程。用戶態(tài)進(jìn)程也可以發(fā)送 signal 終止其他進(jìn)程或自身。這些致命的信號(hào)分為 2 類,主要有:

1、kernel 發(fā)出的:

SIGFPE: 除數(shù)為零。

SIGILL: 無法識(shí)別的 CPU 指令。

SIGSYS: 無法識(shí)別的系統(tǒng)調(diào)用(system call)。

SIGSEGV: 錯(cuò)誤的虛擬內(nèi)存地址訪問。

SIGBUS: 錯(cuò)誤的物理設(shè)備地址訪問。

2、用戶態(tài)進(jìn)程發(fā)出的:

SIGABRT: 調(diào)用 abort() / kill() / tkill() / tgkill() 自殺,或被其他進(jìn)程通過 kill() / tkill() / tgkill() 他殺。

信號(hào)處理函數(shù)

干貨|安卓APP崩潰捕獲方案——xCrash

Naive 崩潰捕獲需要注冊(cè)這些信號(hào)的處理函數(shù)(signal handler),然后在信號(hào)處理函數(shù)中收集數(shù)據(jù)。

因?yàn)樾盘?hào)是以“中斷”的方式出現(xiàn)的,可能中斷任何 CPU 指令序列的執(zhí)行,所以在信號(hào)處理函數(shù)中,只能調(diào)用“異步信號(hào)安全(async-signal-safe)”的函數(shù)。例如malloc()、calloc()、free()、snprintf()、gettimeofday() 等等都是不能使用的,C++ STL / boost 也是不能使用的。

所以,在信號(hào)處理函數(shù)中我們只能不分配堆內(nèi)存,需要使用堆內(nèi)存只能在初始化時(shí)預(yù)分配。如果要使用不在異步信號(hào)安全白名單中的 libc / bionic 函數(shù),只能直接調(diào)用 system call 或者自己實(shí)現(xiàn)。

進(jìn)程崩潰前的極端情況

當(dāng)崩潰捕獲邏輯開始運(yùn)行時(shí),會(huì)面對(duì)很多糟糕的情況,比如:棧溢出、堆內(nèi)存不可用、虛擬內(nèi)存地址耗盡、FD 耗盡、Flash 空間耗盡等。有時(shí),這些極端情況的出現(xiàn),本身就是導(dǎo)致進(jìn)程崩潰的間接原因。

1、棧溢出

我們需要預(yù)先用 sigaltstack() 為 signal handler 分配專門的棧內(nèi)存空間,否則當(dāng)遇到棧溢出時(shí),signal handler 將無法正常運(yùn)行。

2、虛擬內(nèi)存地址耗盡

內(nèi)存泄露很容易導(dǎo)致虛擬內(nèi)存地址耗盡,特別是在 32 位環(huán)境中。這意味著在 signal handler 中也不能使用類似 mmap() 的調(diào)用。

3、FD 耗盡

FD 泄露是常見的導(dǎo)致進(jìn)程崩潰的間接原因。這意味著在 signal handler 中無法正常的使用依賴于 FD 的操作,比如無法 open() + read() 讀取/proc 中的各種信息。為了不干擾 APP 的正常運(yùn)行,我們僅僅預(yù)留了一個(gè) FD,用于在崩潰時(shí)可靠的創(chuàng)建出“崩潰信息記錄文件”。

4、Flash 空間耗盡

在 16G / 32G 存儲(chǔ)空間的安卓設(shè)備中,這種情況經(jīng)常發(fā)生。這意味著 signal handler 無法把崩潰信息記錄到本地文件中。我們只能嘗試在初始化時(shí)預(yù)先創(chuàng)建一些“占坑”文件,然后一直循環(huán)使用這些“占坑”文件來記錄崩潰信息。如果“占坑”文件也創(chuàng)建失敗,我們需要把最重要的一些崩潰信息(比如 backtrace)保存在內(nèi)存中,然后立刻回調(diào)和發(fā)送這些信息。

xCrash 架構(gòu)與實(shí)現(xiàn)

信號(hào)處理函數(shù)與子進(jìn)程

在信號(hào)處理函數(shù)(signal handler)代碼執(zhí)行的開始階段,我們只能“忍辱偷生”:

1、遵守它的各種限制。

2、不使用堆內(nèi)存。

3、自己實(shí)現(xiàn)需要的調(diào)用的“異步信號(hào)安全版本”,比如:snprintf()、gettimeofday()。

4、必要時(shí)直接調(diào)用 system call。

但這并非長久之計(jì),我們要盡快在信號(hào)處理函數(shù)中執(zhí)行“逃逸”,即使用clone() + execl() 創(chuàng)建新的子進(jìn)程,然后在子進(jìn)程中繼續(xù)收集崩潰信息。這樣做的目的是:

1、避開 async-signal-safe 的限制。

2、避開虛擬內(nèi)存地址耗盡的問題。

3、避開 FD 耗盡的問題。

4、使用 ptrace() suspend 崩潰進(jìn)程中所有的線程。與 iOS 不同,Linux / Android 不支持 suspend 本進(jìn)程內(nèi)的線程。(如果不做 suspend,則其他未崩潰的線程還在繼續(xù)執(zhí)行,還在繼續(xù)寫 logcat,當(dāng)我們收集 logcat 時(shí),崩潰時(shí)間點(diǎn)附近的 logcat 可能早已被淹沒。類似的,其他的業(yè)務(wù) log buffers 也存在被淹沒的問題。)

5、除了崩潰線程本身的 registers、backtrace 等,還能用 ptrace()收集到進(jìn)程中其他所有線程的 registers、backtrace 等信息,這對(duì)于某些崩潰問題的分析是有意義的。

6、更安全的讀取內(nèi)存數(shù)據(jù)。(ptrace 讀數(shù)據(jù)失敗會(huì)返回錯(cuò)誤碼,但是在崩潰線程內(nèi)直接讀內(nèi)存數(shù)據(jù),如果內(nèi)存地址非法,會(huì)導(dǎo)致段錯(cuò)誤)

由此可以看出“逃逸”是必然的選擇,整個(gè)過程如下圖所示:

干貨|安卓APP崩潰捕獲方案——xCrash

整體架構(gòu)

xCrash 整體分為兩部分:運(yùn)行于崩潰的 APP 進(jìn)程內(nèi)的部分,和獨(dú)立進(jìn)程的部分(我們稱為 dumper)。

干貨|安卓APP崩潰捕獲方案——xCrash

1、APP 進(jìn)程內(nèi)

這部分可以再分為 Java 和 native 兩個(gè)部分。

(1)Java 部分:

①Java 崩潰捕獲。直接使用 JVM 提供的機(jī)制來完成,最后生成兼容 tombstone 格式的 dump 文件。

②Native 崩潰捕獲機(jī)制的注冊(cè)器。通過 JNI 激活 native 層的對(duì)應(yīng)機(jī)制。

③Tombstone 文件解析器??梢詫?tombstone 文件解析成 json 格式。

④Tombstone 文件管理器??梢詸z索設(shè)備上已經(jīng)生成的 tombstone 文件。

(2)Native 部分:

①JNI Bridge。負(fù)責(zé)與 Java 層的交互。(傳參與回調(diào))

②Signal handlers。負(fù)責(zé)信號(hào)捕獲,以及啟動(dòng)獨(dú)立進(jìn)程 dumper。

③Fallback mode。負(fù)責(zé)當(dāng) dumper 捕獲崩潰信息失敗時(shí),嘗試在崩潰進(jìn)行的 signal handler 中收集崩潰信息。

2、Dumper 獨(dú)立進(jìn)程

這部分是純 native 的實(shí)現(xiàn):

①Process。負(fù)責(zé)崩潰進(jìn)程中各個(gè)線程的控制(attach 和 detach),以及進(jìn)程層面的信息收集,比如 FD 列表、logcat 等等。

②Threads。負(fù)責(zé)崩潰進(jìn)程中的線程相關(guān)數(shù)據(jù)的收集,比如 registers、backtrace、stack 等等。

③Memory Layout。負(fù)責(zé) maps 和 smaps 的解析。

④Memory。負(fù)責(zé)各種內(nèi)存數(shù)據(jù)的讀寫。比如來自本地 buffer、來自mmap() 的 ELF 文件、或者通過 ptrace() 遠(yuǎn)程訪問的崩潰進(jìn)程的內(nèi)存。

⑤Registers。負(fù)責(zé)各種處理機(jī)架構(gòu)相關(guān)的數(shù)據(jù)處理。

⑥ELF。負(fù)責(zé) ELF 信息的解析。需要解析各種 unwind table 和 symbols 信息,有時(shí)需要使用 LZMA 解壓 .gnu_debugdata 中的 mini debug info 信息做進(jìn)一步的處理。

獲取 backtrace

獲取 backtrace 是崩潰捕獲中比較復(fù)雜和重要的部分,這也恰恰是安卓 native 開發(fā)中最混亂和不一致的地方之一。

1、libc 對(duì) backtrace 的支持

在 Linux 服務(wù)器環(huán)境中,當(dāng)那些致命的 signal 發(fā)生時(shí),系統(tǒng)可以為我們產(chǎn)生標(biāo)準(zhǔn)的 core dump 文件,之后我們可以用 gdb 調(diào)試和恢復(fù)崩潰現(xiàn)場(chǎng),我們俗稱“驗(yàn)尸”。

在 Linux 嵌入式環(huán)境中,由于 flash 空間有限,我們一般可以注冊(cè) signal handler,然后調(diào)用 libc 的 backtrace() 和backtrace_symbols_fd() 獲取 backtrace。

(注意:不能使用backtrace_symbols(),它不是異步信號(hào)安全的)

2、NDK 對(duì) backtrace 的支持

NDK 中目前沒有提供可靠的 unwind API。

安卓使用 bionic 替代了 libc,bionic 中沒有 backtrace() 和backtrace_symbols_fd()。(它們不在 POSIX 標(biāo)準(zhǔn)中)

unwind.h 中的 _Unwind_Backtrace 系列函數(shù)對(duì)于高版本 Android 系統(tǒng)庫幾乎無效。(NDK 中的 unwind 實(shí)現(xiàn),已經(jīng)無法跟上 Android 系統(tǒng)快速的迭代優(yōu)化)

3、Google AOSP 的 backtrace 實(shí)現(xiàn)

各版本的 AOSP 都有系統(tǒng)自用的 backtrace 庫,主要作用是配合系統(tǒng) debuggerd 進(jìn)程和調(diào)試器的工作。

(1)libcorkscrew:只用于 Android 4.1 - 4.4W。

(2)libunwind:只用于 Android 5.0 - 7.1.1。

(3)libunwindstack:只用于 Android 8.0 及以上版本。

如果 APP 直接使用這些庫,會(huì)遇到以下的問題:

(1)系統(tǒng) debuggerd 是以 root 權(quán)限運(yùn)行的,而我們的 APP 沒有 root 權(quán)限,所以某些操作會(huì)受到限制。

(2)NDK 沒有暴露這些系統(tǒng)庫的對(duì)外調(diào)用接口。Android 7.0 以后 APP 無法直接 dlopen() 系統(tǒng)庫,所以其中的 libunwind 和 libunwindstack 只能自己編譯源碼后放到 APP 中使用。

(3)使用這些庫的 local unwind 接口比較容易,但是使用 remote unwind 接口時(shí)適配比較復(fù)雜。(原因還是這些庫是為了 debuggerd 和調(diào)試器設(shè)計(jì)的,不是為 APP 設(shè)計(jì)的)

(4)高版本系統(tǒng)的 backtrace 庫無法直接編譯用于低版本的系統(tǒng),libunwind 和 libunwindstack 中使用了大量低版本系統(tǒng)所沒有的系統(tǒng)函數(shù)。所以作為 APP 只能分別編譯這些系統(tǒng) backtrace 庫,然后在運(yùn)行時(shí)根據(jù)系統(tǒng) API level 動(dòng)態(tài)判斷需要使用哪個(gè)庫。這顯著的增加了 APP 包體積。

4、xCrash 的 backtrace 實(shí)現(xiàn)

xCrash 參考了一部分 AOSP 和 BreakPad 的實(shí)現(xiàn)思路,在不需要 root 權(quán)限和兼容 Android 4.0 - 9.0 的前提下,自己實(shí)現(xiàn)了 unwind 邏輯。這樣做的好處是 unwind 過程不再是一個(gè)黑盒,細(xì)節(jié)完全可控,遇到問題完全可調(diào)試。

Backtrace unwind 依賴于三部分?jǐn)?shù)據(jù):寄存器、棧內(nèi)存、各 ELF 中的 unwind table。xCrash 目前能處理 Android 4.0 - 9.0 中可能出現(xiàn)的所有格式的 unwind table,它們來自于 ELF 中的以下 section:

(1).ARM.exidx(只存在于 32 位 ARM 架構(gòu))

(2).eh_frame 和 .eh_frame_hdr

(3).debug_frame

(4).gnu_debugdata(LZMA 壓縮的 mini debug info,其中可能包含其他的 unwind table,比如:.debug_frame)

xCrash 的其他功能

除了獲取常見的設(shè)備信息、registers、backtrace、stack、memory near、maps、logcat 等基本信息,xCrash 還提供以下的功能:

1、完整的 FD 列表

讓你知道崩潰時(shí)進(jìn)程中的每一個(gè) FD 具體都用在了哪里。

2、詳細(xì)的內(nèi)存使用統(tǒng)計(jì)

獲取了操作系統(tǒng)全局的物理內(nèi)存使用統(tǒng)計(jì)、崩潰進(jìn)程的虛擬內(nèi)存使用統(tǒng)計(jì)、崩潰進(jìn)程的內(nèi)存詳細(xì)使用信息(類似 dumpsys meminfo)。讓你對(duì)進(jìn)程崩潰時(shí)的內(nèi)存狀態(tài)有全面的了解。

3、用正則白名單設(shè)置需要獲取哪些線程的信息

APP 的線程數(shù)超過 100 個(gè)是很常見的,如果像系統(tǒng) tombstone 那樣總是獲取全部線程的 registers、backtrace 等信息,在大多數(shù)情況下是沒有必要的;這也容易導(dǎo)致 unwind 時(shí)間過長,崩潰捕獲邏輯還沒有走完,APP 就被系統(tǒng)強(qiáng)殺了。xCrash 讓你能通過一組正則表達(dá)式白名單來設(shè)置需要獲取哪些線程的信息。

4、零權(quán)限需求

xCrash 不需要 root 權(quán)限,也不需要任何的 APP 系統(tǒng)權(quán)限,這讓使用 xCrash 的 APP 沒有任何權(quán)限方面的負(fù)擔(dān)。

5、監(jiān)測(cè)設(shè)備是否已被 root

監(jiān)測(cè)的過程是完全透明和無感知的。在后期分析數(shù)據(jù)時(shí),如果發(fā)現(xiàn)某個(gè)崩潰只發(fā)生在已被 root 的設(shè)備上,就有理由懷疑是否是一些特別的原因造成的。

6、極高的崩潰信息捕獲成功率

xCrash 通過 FD 預(yù)留;Flash “占坑”文件;寫文件失敗時(shí)通過預(yù)分配內(nèi)存保存 backtrace 等重要信息做緊急回調(diào)、clone() + execl() 失敗后進(jìn)入 fallback 模式執(zhí)行本地 unwind 等一系列保護(hù)措施,最大程度的保證了崩潰信息捕獲的成功率。

7、擴(kuò)展性支持

xCrash 支持崩潰后附加用戶自定義信息。目前在愛奇藝 APP 中,已經(jīng)通過 xCrash 的擴(kuò)展能力,在崩潰時(shí)投遞了大播放日志、彈幕日志、NLE視頻編輯日志、APP Life Cycle Trace等信息。為排查特定業(yè)務(wù)的崩潰問題提供支持。

xCrash 與 BreakPad 比較

BreakPad 是 Google 開發(fā)的跨平臺(tái)崩潰捕獲方案,目前主要用于 Chromium。安卓 APP 也可以使用 BreakPad 來捕獲異常。

BreakPad 是一種“以后期調(diào)試為目的的崩潰捕獲方案”,BreakPad 的崩潰捕獲結(jié)果是一個(gè)二進(jìn)制的 minidump 文件,需要后期拿到崩潰相關(guān)的所有 ELF 原始文件(包括系統(tǒng)動(dòng)態(tài)庫文件),然后開始進(jìn)行類似 gdb 的調(diào)試過程,才能定位問題。

拿到每個(gè)崩潰機(jī)型上需要的系統(tǒng)庫文件,這會(huì)是一個(gè)耗時(shí)的過程;再加上復(fù)雜的 APP 自身可能包含數(shù)十個(gè) native 庫,這些 native 庫由不同的業(yè)務(wù)團(tuán)隊(duì)開發(fā),并且在 APP 發(fā)版后還可能熱更新。如果要把這整個(gè)過程自動(dòng)化的完成,需要一個(gè)非常復(fù)雜的系統(tǒng)來支持。

在我們開發(fā)移動(dòng)端 APM 系統(tǒng)和 xCrash SDK 的初期,曾經(jīng)短暫的試用過 BreakPad,最后覺得這種方式對(duì)于我們來說后期的維護(hù)成本太高了,而收益看起來比較有限。

1、BreakPad 的優(yōu)勢(shì)

對(duì)于特定的疑難問題,可以通過調(diào)試來獲取到更多的寄存器和內(nèi)存信息,也許有助于這些問題的解決。

2、BreakPad 的弱點(diǎn)

(1)后期的自動(dòng)化處理比較復(fù)雜耗時(shí),維護(hù)成本非常高。

(2)后期處理時(shí)如果遇到對(duì)應(yīng)系統(tǒng)庫缺失、或者庫版本錯(cuò)誤的情況,就會(huì)無法拿到正確的 backtrace。這會(huì)影響到突發(fā)線上崩潰的報(bào)警,以及對(duì)突發(fā)崩潰的及時(shí)熱修。

(3)BreakPad 自身的跨平臺(tái)屬性,以及較長的開發(fā)歷史,導(dǎo)致了它的代碼結(jié)構(gòu)比較龐大而復(fù)雜,維護(hù)和二次開發(fā)的難度較大。

3、相對(duì)于 BreakPad,xCrash 的優(yōu)勢(shì)

(1)完全在設(shè)備本地執(zhí)行崩潰信息提取,生成系統(tǒng)標(biāo)準(zhǔn)的 tombstone 文本格式的 dump 信息。后期只要在服務(wù)端做簡(jiǎn)單的文本解析和聚合,就能快速發(fā)現(xiàn)線上的突發(fā)崩潰。

(2)tombstone 文本格式是安卓系統(tǒng) debuggerd 的標(biāo)準(zhǔn)崩潰信息輸出格式,無需再向開發(fā)人員解釋該格式的具體含義。

(3)專為安卓 APP 量身定制,接入使用的過程已經(jīng)做到極簡(jiǎn)。

xCrash 的未來計(jì)劃

伴隨著安卓本身以及移動(dòng)端各項(xiàng)技術(shù)的快速發(fā)展,xCrash 未來還有很多事情可以做,例如:

(1)ANR 監(jiān)控。

(2)強(qiáng)化 fallback 模式。

(3)減少 dump 過程中崩潰進(jìn)程的卡頓。

(4)崩潰次數(shù)和時(shí)間的本地記錄和統(tǒng)計(jì)。

(5)與 BreakPad 如何互補(bǔ)。

我們真誠的歡迎您和我們一起開發(fā)和維護(hù) xCrash。

xCrash 在 GitHub 的項(xiàng)目地址:

https://github.com/iqiyi/xCrash

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI