AddressSanitizer

AddressSanitizer (ASan) 是一款以編譯器為基礎的快速工具,用於偵測原生程式碼中的記憶體錯誤。

ASan 可偵測到:

  • 堆疊和堆積緩衝區溢位/反向溢位
  • 釋放後的堆積使用情況
  • 超出範圍的堆疊使用情況
  • 重複釋放/錯誤釋放

ASan 可以在 32 位元和 64 位元 ARM 以及 x86 和 x86-64 上執行。ASan 的 CPU 負擔約為 2 倍,程式碼大小負擔介於 50% 到 2 倍之間,記憶體負擔也相當大 (取決於您的配置模式,但約為 2 倍)。

Android 10 和 AArch64 上的 AOSP 最新發布分支版本支援 Hardware-assisted AddressSanitizer (HWASan),這項類似工具的 RAM 負擔較低,可偵測的錯誤範圍也較廣。除了 ASan 偵測到的錯誤,HWASan 還能偵測回傳後的堆疊使用情形。

HWASan 的 CPU 和程式碼大小負擔與 ASan 類似,但 RAM 負擔小得多 (15%)。 HWASan 屬於非決定性工具,標記值只有 256 種可能,因此遺漏任何錯誤的機率只有 0.4%。HWASan 沒有 ASan 的有限大小紅色區域,因此無法偵測溢位,也沒有有限容量的隔離區,因此無法偵測釋放後使用,所以溢位大小或記憶體解除分配的時間長度對 HWASan 來說並不重要。因此 HWASan 比 ASan 更適合用於偵測堆疊溢位。如要進一步瞭解 HWASan 的設計,或瞭解 Android 上的 HWASan 用法,請參閱相關說明。

ASan 除了能偵測堆積溢位,還能偵測堆疊/全域溢位,而且速度快,記憶體負擔極小。

本文說明如何使用 ASan 建構及執行部分/所有 Android 項目。如果您要使用 ASan 建構 SDK/NDK 應用程式,請改為參閱「Address Sanitizer」。

使用 ASan 清理個別可執行檔

在可執行檔的建構規則中加入 LOCAL_SANITIZE:=addresssanitize: { address: true }。您可以搜尋程式碼中的現有範例,或尋找其他可用的清除器。

偵測到錯誤時,ASan 會將詳細報表列印到標準輸出和 logcat,然後讓程序當機。

使用 ASan 清理共用程式庫

由於 ASan 的運作方式,以 ASan 建構的程式庫只能由以 ASan 建構的可執行檔使用。

如要清除多個執行檔中使用的共用程式庫,但並非所有執行檔都是使用 ASan 建構,您需要兩個程式庫副本。建議您在相關模組的 Android.mk 中加入下列內容:

LOCAL_SANITIZE:=address
LOCAL_MODULE_RELATIVE_PATH := asan

這會將程式庫放在 /system/lib/asan 中,而非 /system/lib。接著,使用下列指令執行可執行檔:

LD_LIBRARY_PATH=/system/lib/asan

如果是系統精靈,請在 /init.rc/init.$device$.rc 的適當區段中新增下列內容。

setenv LD_LIBRARY_PATH /system/lib/asan

如果存在,請讀取 /proc/$PID/maps,確認程序是否使用 /system/lib/asan 中的程式庫。如果不是,您可能需要停用 SELinux:

adb root
adb shell setenforce 0
# restart the process with adb shell kill $PID
# if it is a system service, or may be adb shell stop; adb shell start.

更完善的堆疊追蹤

ASan 會使用快速影格指標型解開器,記錄程式中每個記憶體配置和取消配置事件的堆疊追蹤。大多數 Android 都是在沒有框架指標的情況下建構而成。因此,您通常只會獲得一或兩個有意義的影格。如要修正這個問題,請使用 ASan (建議!) 或下列項目重建程式庫:

LOCAL_CFLAGS:=-fno-omit-frame-pointer
LOCAL_ARM_MODE:=arm

或者,在程序環境中設定 ASAN_OPTIONS=fast_unwind_on_malloc=0。後者可能會耗用大量 CPU 資源,視負載而定。

符號化

ASan 報表一開始會包含二進位檔和共用程式庫中的偏移參照。取得來源檔案和行號資訊的方式有兩種:

  • 確認 llvm-symbolizer 二進位檔位於 /system/bin 中。 llvm-symbolizer 是以 third_party/llvm/tools/llvm-symbolizer 中的來源建構而成。
  • 透過 external/compiler-rt/lib/asan/scripts/symbolize.py 指令碼篩選報表。

由於主機上提供符號化程式庫,第二種方法可提供更多資料 (即file:line位置)。

應用程式中的 ASan

ASan 無法查看 Java 程式碼,但可以偵測 JNI 程式庫中的錯誤。為此,您需要使用 ASan 建構可執行檔,在本例中為 /system/bin/app_process(32|64)。這會同時在裝置上的所有應用程式中啟用 ASan,負載較重,但 2 GB RAM 的裝置應該可以處理。

frameworks/base/cmds/app_process 的建構規則中新增 LOCAL_SANITIZE:=addressapp_process

編輯適當 system/core/rootdir/init.zygote(32|64).rc 檔案的 service zygote 區段,在包含 class main 的縮排行區塊中新增下列幾行,縮排量也相同:

    setenv LD_LIBRARY_PATH /system/lib/asan:/system/lib
    setenv ASAN_OPTIONS allow_user_segv_handler=true

建構、adb 同步、fastboot 刷入開機,然後重新啟動。

使用 wrap 屬性

上一節的方法會將 ASan 放入系統中的每個應用程式 (實際上是放入 Zygote 程序的每個子項)。您可以使用 ASan 執行單一 (或多個) 應用程式,但會犧牲部分記憶體負荷,換取較慢的應用程式啟動速度。

方法是使用 wrap. 屬性啟動應用程式。以下範例會在 ASan 下執行 Gmail 應用程式:

adb root
adb shell setenforce 0  # disable SELinux
adb shell setprop wrap.com.google.android.gm "asanwrapper"

在這個情況下,asanwrapper 會將 /system/bin/app_process 重新編譯為 /system/bin/asan/app_process,後者是以 ASan 建構而成。此外,系統也會在動態程式庫搜尋路徑的開頭新增 /system/lib/asan。這樣一來,使用 asanwrapper 執行時,系統會優先使用 /system/lib/asan 中的 ASan 檢測程式庫,而非 /system/lib 中的一般程式庫。

如果發現錯誤,應用程式會異常終止,並將報告列印到記錄檔。

SANITIZE_TARGET

Android 7.0 以上版本支援使用 ASan 一次建構整個 Android 平台。(如果您要建構高於 Android 9 的版本,建議使用 HWASan)。

在相同的建構樹狀結構中執行下列指令。

make -j42
SANITIZE_TARGET=address make -j42

在這個模式下,userdata.img 包含額外程式庫,也必須刷入裝置。使用下列指令列:

fastboot flash userdata && fastboot flashall

這會建構兩組共用程式庫:/system/lib 中的一般程式庫 (第一次叫用 make),以及 /data/asan/lib 中的 ASan 檢測程式庫 (第二次叫用 make)。第二個建構作業的可執行檔會覆寫第一個建構作業的可執行檔。ASan 檢測的可執行檔會取得不同的程式庫搜尋路徑,其中包含 /data/asan/lib 前的 /system/lib (透過 PT_INTERP 中的 /system/bin/linker_asan)。

$SANITIZE_TARGET 值變更時,建構系統會清除中繼物件目錄。這會強制重建所有目標,同時保留 /system/lib 底下安裝的二進位檔。

ASan 無法建構部分目標:

  • 靜態連結的可執行檔
  • LOCAL_CLANG:=false 個目標
  • LOCAL_SANITIZE:=false 不適用於 ASan SANITIZE_TARGET=address

系統會在 SANITIZE_TARGET 建構作業中略過這類可執行檔,並將第一次呼叫 make 時的版本保留在 /system/bin 中。

這類程式庫的建構作業不會使用 ASan。其中可能包含所依附靜態程式庫的部分 ASan 程式碼。

佐證文件