AddressSanitizer (ASan) 是一種基於編譯器的快速工具,用於偵測本機程式碼中的記憶體錯誤。
阿桑檢測到:
- 堆疊和堆疊緩衝區上溢/下溢
- 釋放後的堆使用
- 堆疊在範圍外使用
- 雙自由/狂野自由
ASan 可在 32 位元和 64 位元 ARM 以及 x86 和 x86-64 上運作。 ASan 的 CPU 開銷大約是 2x,程式碼大小開銷在 50% 到 2x 之間,記憶體開銷很大(取決於你的分配模式,但大約是 2x)。
Android 10 和 AArch64 上的 AOSP 主分支支援硬體加速 ASan (HWASan) ,這是一種類似的工具,具有較低的 RAM 開銷和更大範圍的檢測到的錯誤。除了 ASan 偵測到的錯誤之外,HWASan 還會偵測返回後的堆疊使用情況。
HWASan 有類似的 CPU 和程式碼大小開銷,但 RAM 開銷要小得多 (15%)。 HWASan 是不確定的。只有 256 個可能的標籤值,因此遺漏任何錯誤的機率為 0.4%。 HWASan 沒有 ASan 的用於檢測溢出的有限大小紅色區域和用於檢測釋放後使用的有限容量隔離區,因此對於 HWASan 來說,溢出有多大或記憶體被釋放多久前都無關緊要。這使得 HWASan 比 ASan 更好。您可以閱讀有關HWASan 設計或HWASan 在 Android 上使用的更多資訊。
除了堆疊溢出之外,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 無法查看 Java 程式碼,但它可以偵測 JNI 庫中的錯誤。為此,您需要使用 ASan 建立可執行文件,在本例中為/system/bin/app_process( 32|64 )
。這會同時在裝置上的所有應用程式中啟用 ASan,這是一個沉重的負載,但具有 2 GB RAM 的裝置應該能夠處理此問題。
將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 快閃記憶體啟動和重新啟動。
使用換行屬性
上一節中的方法將 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-instrumented(第二個 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'd
像這樣的可執行檔在SANITIZE_TARGET
建置中被跳過,並且第一次 make 呼叫的版本保留在/system/bin
中。
像這樣的圖書館是在沒有 ASan 的情況下建造的。它們可以包含來自它們所依賴的靜態庫的一些 ASan 程式碼。