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 和程式碼大小負擔相似,但 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:=address
或 sanitize: { 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,這會造成大量負載,但裝置的 RAM 若有 2 GB,應該可以處理這項作業。
將 LOCAL_SANITIZE:=address
新增至 frameworks/base/cmds/app_process
中的 app_process
建構規則。暫時忽略位於相同檔案中的 app_process__asan
目標 (如果讀取時仍存在)。
編輯適當 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 檢測的執行檔會透過 PT_INTERP
中的 /system/bin/linker_asan
取得不同的程式庫搜尋路徑,該路徑會在 /system/lib
之前納入 /data/asan/lib
。
當 $SANITIZE_TARGET
值變更時,建構系統會覆寫中繼物件目錄。這會強制重新建構所有目標,同時保留 /system/lib
底下已安裝的二進位檔。
某些目標無法使用 ASan 建構:
- 靜態連結的可執行檔
LOCAL_CLANG:=false
目標LOCAL_SANITIZE:=false
不是SANITIZE_TARGET=address
的 ASan
這類可執行檔會在 SANITIZE_TARGET
版本中略過,而第一個 make 叫用版本會保留在 /system/bin
中。
這類程式庫是在沒有 ASan 的情況下建構。這些檔案可能包含部分 ASan 程式碼,這些程式碼來自其依附的靜態資料庫。