AddressSanitizer (ASan) 是一種基於編譯器的快速工具,用於檢測本機代碼中的內存錯誤。
ASan 檢測到:
- 堆棧和堆緩衝區溢出/下溢
- 釋放後的堆使用
- 堆棧使用範圍外
- 雙重免費/狂野免費
ASan 在 32 位和 64 位 ARM 以及 x86 和 x86-64 上運行。 ASan 的 CPU 開銷大約是 2 倍,代碼大小開銷在 50% 到 2 倍之間,並且內存開銷很大(取決於您的分配模式,但大約是 2 倍)。
Android 10 和 AArch64 上的 AOSP 主分支支持硬件加速 ASan (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,這是一個繁重的負載,但具有 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
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
app_process 重寫為 /system/bin/asan /system/bin/asan/app_process
,它是用 ASan 構建的。它還在動態庫搜索路徑的開頭添加/system/lib/asan
。這樣,當使用asanwrapper
運行時,來自/system/lib/asan
asan 的 ASan 檢測庫優於 /system /system/lib
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 調用)。第二個構建的可執行文件會覆蓋第一個構建的可執行文件。通過在 PT_INTERP 中使用/system/bin/linker_asan
, PT_INTERP
檢測的可執行文件獲得了一個不同的庫搜索路徑,該路徑在/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 代碼。