開發

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

廣告
廣告

導讀

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

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

問題概述

在移動端 APP 的各種質量問題中,最嚴重的可能就是 APP 崩潰閃退了。

從安卓 APP 開發的角度,Java 崩潰捕獲相對比較容易,JVM 給 Java 字節碼提供了一個受控的運行環境,同時也提供了完善的 Java 崩潰捕獲機制。Native 崩潰的捕獲和處理相對比較困難,安卓系統的debuggerd 守護進程會為 native 崩潰自動生成詳細的崩潰描述文件(tombstone)。

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

Native 崩潰介紹

信號

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

1、kernel 發出的:

SIGFPE: 除數為零。

SIGILL: 無法識別的 CPU 指令。

SIGSYS: 無法識別的系統調用(system call)。

SIGSEGV: 錯誤的虛擬內存地址訪問。

SIGBUS: 錯誤的物理設備地址訪問。

2、用戶態進程發出的:

SIGABRT: 調用 abort() / kill() / tkill() / tgkill() 自殺,或被其他進程通過 kill() / tkill() / tgkill() 他殺。

信號處理函數

Naive 崩潰捕獲需要注冊這些信號的處理函數(signal handler),然后在信號處理函數中收集數據。

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

所以,在信號處理函數中我們只能不分配堆內存,需要使用堆內存只能在初始化時預分配。如果要使用不在異步信號安全白名單中的 libc / bionic 函數,只能直接調用 system call 或者自己實現。

進程崩潰前的極端情況

當崩潰捕獲邏輯開始運行時,會面對很多糟糕的情況,比如:棧溢出、堆內存不可用、虛擬內存地址耗盡、FD 耗盡、Flash 空間耗盡等。有時,這些極端情況的出現,本身就是導致進程崩潰的間接原因。

1、棧溢出

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

2、虛擬內存地址耗盡

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

3、FD 耗盡

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

4、Flash 空間耗盡

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

xCrash 架構與實現

信號處理函數與子進程

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

1、遵守它的各種限制。

2、不使用堆內存。

3、自己實現需要的調用的“異步信號安全版本”,比如:snprintf()、gettimeofday()。

4、必要時直接調用 system call。

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

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

2、避開虛擬內存地址耗盡的問題。

3、避開 FD 耗盡的問題。

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

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

6、更安全的讀取內存數據。(ptrace 讀數據失敗會返回錯誤碼,但是在崩潰線程內直接讀內存數據,如果內存地址非法,會導致段錯誤)

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

整體架構

xCrash 整體分為兩部分:運行于崩潰的 APP 進程內的部分,和獨立進程的部分(我們稱為 dumper)。

1、APP 進程內

這部分可以再分為 Java 和 native 兩個部分。

(1)Java 部分:

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

②Native 崩潰捕獲機制的注冊器。通過 JNI 激活 native 層的對應機制。

③Tombstone 文件解析器。可以將 tombstone 文件解析成 json 格式。

④Tombstone 文件管理器。可以檢索設備上已經生成的 tombstone 文件。

(2)Native 部分:

①JNI Bridge。負責與 Java 層的交互。(傳參與回調)

②Signal handlers。負責信號捕獲,以及啟動獨立進程 dumper。

③Fallback mode。負責當 dumper 捕獲崩潰信息失敗時,嘗試在崩潰進行的 signal handler 中收集崩潰信息。

2、Dumper 獨立進程

這部分是純 native 的實現:

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

②Threads。負責崩潰進程中的線程相關數據的收集,比如 registers、backtrace、stack 等等。

③Memory Layout。負責 maps 和 smaps 的解析。

④Memory。負責各種內存數據的讀寫。比如來自本地 buffer、來自mmap() 的 ELF 文件、或者通過 ptrace() 遠程訪問的崩潰進程的內存。

⑤Registers。負責各種處理機架構相關的數據處理。

⑥ELF。負責 ELF 信息的解析。需要解析各種 unwind table 和 symbols 信息,有時需要使用 LZMA 解壓 .gnu_debugdata 中的 mini debug info 信息做進一步的處理。

獲取 backtrace

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

1、libc 對 backtrace 的支持

在 Linux 服務器環境中,當那些致命的 signal 發生時,系統可以為我們產生標準的 core dump 文件,之后我們可以用 gdb 調試和恢復崩潰現場,我們俗稱“驗尸”。

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

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

2、NDK 對 backtrace 的支持

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

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

unwind.h 中的 _Unwind_Backtrace 系列函數對于高版本 Android 系統庫幾乎無效。(NDK 中的 unwind 實現,已經無法跟上 Android 系統快速的迭代優化)

3、Google AOSP 的 backtrace 實現

各版本的 AOSP 都有系統自用的 backtrace 庫,主要作用是配合系統 debuggerd 進程和調試器的工作。

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

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

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

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

(1)系統 debuggerd 是以 root 權限運行的,而我們的 APP 沒有 root 權限,所以某些操作會受到限制。

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

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

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

4、xCrash 的 backtrace 實現

xCrash 參考了一部分 AOSP 和 BreakPad 的實現思路,在不需要 root 權限和兼容 Android 4.0 – 9.0 的前提下,自己實現了 unwind 邏輯。這樣做的好處是 unwind 過程不再是一個黑盒,細節完全可控,遇到問題完全可調試。

Backtrace unwind 依賴于三部分數據:寄存器、棧內存、各 ELF 中的 unwind table。xCrash 目前能處理 Android 4.0 – 9.0 中可能出現的所有格式的 unwind table,它們來自于 ELF 中的以下 section:

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

(2).eh_frame 和 .eh_frame_hdr

(3).debug_frame

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

xCrash 的其他功能

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

1、完整的 FD 列表

讓你知道崩潰時進程中的每一個 FD 具體都用在了哪里。

2、詳細的內存使用統計

獲取了操作系統全局的物理內存使用統計、崩潰進程的虛擬內存使用統計、崩潰進程的內存詳細使用信息(類似 dumpsys meminfo)。讓你對進程崩潰時的內存狀態有全面的了解。

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

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

4、零權限需求

xCrash 不需要 root 權限,也不需要任何的 APP 系統權限,這讓使用 xCrash 的 APP 沒有任何權限方面的負擔。

5、監測設備是否已被 root

監測的過程是完全透明和無感知的。在后期分析數據時,如果發現某個崩潰只發生在已被 root 的設備上,就有理由懷疑是否是一些特別的原因造成的。

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

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

7、擴展性支持

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

xCrash 與 BreakPad 比較

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

BreakPad 是一種“以后期調試為目的的崩潰捕獲方案”,BreakPad 的崩潰捕獲結果是一個二進制的 minidump 文件,需要后期拿到崩潰相關的所有 ELF 原始文件(包括系統動態庫文件),然后開始進行類似 gdb 的調試過程,才能定位問題。

拿到每個崩潰機型上需要的系統庫文件,這會是一個耗時的過程;再加上復雜的 APP 自身可能包含數十個 native 庫,這些 native 庫由不同的業務團隊開發,并且在 APP 發版后還可能熱更新。如果要把這整個過程自動化的完成,需要一個非常復雜的系統來支持。

在我們開發移動端 APM 系統和 xCrash SDK 的初期,曾經短暫的試用過 BreakPad,最后覺得這種方式對于我們來說后期的維護成本太高了,而收益看起來比較有限。

1、BreakPad 的優勢

對于特定的疑難問題,可以通過調試來獲取到更多的寄存器和內存信息,也許有助于這些問題的解決。

2、BreakPad 的弱點

(1)后期的自動化處理比較復雜耗時,維護成本非常高。

(2)后期處理時如果遇到對應系統庫缺失、或者庫版本錯誤的情況,就會無法拿到正確的 backtrace。這會影響到突發線上崩潰的報警,以及對突發崩潰的及時熱修。

(3)BreakPad 自身的跨平臺屬性,以及較長的開發歷史,導致了它的代碼結構比較龐大而復雜,維護和二次開發的難度較大。

3、相對于 BreakPad,xCrash 的優勢

(1)完全在設備本地執行崩潰信息提取,生成系統標準的 tombstone 文本格式的 dump 信息。后期只要在服務端做簡單的文本解析和聚合,就能快速發現線上的突發崩潰。

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

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

xCrash 的未來計劃

伴隨著安卓本身以及移動端各項技術的快速發展,xCrash 未來還有很多事情可以做,例如:

(1)ANR 監控。

(2)強化 fallback 模式。

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

(4)崩潰次數和時間的本地記錄和統計。

(5)與 BreakPad 如何互補。

我們真誠的歡迎您和我們一起開發和維護 xCrash。

xCrash 在 GitHub 的項目地址:

https://github.com/iqiyi/xCrash

愛奇藝技術產品團隊公眾號,由愛奇藝技術產品團隊發起,秉持高效、開放、創新的理念,分享前沿技術,傳達愛奇藝生態理念及技術進展。

互聯網公司為啥都不用MySQL分區表?

上一篇

架構演進實踐:從0到4000高并發請求背后的努力!

下一篇

你也可能喜歡

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

長按儲存圖像,分享給朋友

ITPUB 每周精要將以郵件的形式發放至您的郵箱


微信掃一掃

微信掃一掃
重庆快乐10分苹果版本 金福彩票安卓 大家赢足球即时比分半一 江苏什么团最赚钱 新疆时时彩 下载江西微乐南昌麻将 新浪体育比分 机构调研预测未来两月从哪赚钱 山东时时彩 哪些公司用免费模式赚钱了 人人发彩票网址 现在网上怎么赚钱 因特娱乐网址 现在干啥买卖能赚钱 广东时时彩 2015年0元开加盟店赚钱 浙江20选5