ASAN實戰(zhàn):從編譯到跨系統(tǒng)內(nèi)存問題定位)
簡介這份資源面向在高通8155p平臺上進行QNXAndroid9雙系統(tǒng)開發(fā)的工程師聚焦于開啟ASANAddressSanitizer以定位Native層內(nèi)存越界、泄漏等疑難問題。包內(nèi)同時提供Android.mk與Android.bp兩套構(gòu)建腳本示例覆蓋libAsanDebug、asan_debugd及其vnd變體、libclang_rt.asan運行時庫等模塊幫助開發(fā)者解決因構(gòu)建配置不當(dāng)導(dǎo)致ASAN無法使能、編譯報錯等常見障礙。資源共18個文件包含4個mk、4個bp構(gòu)建腳本4個cpp源碼2個so預(yù)編譯庫2個h頭文件及2個mk~備份文件壓縮包約1.21MB結(jié)構(gòu)清晰便于對照移植。目前已有1994人學(xué)習(xí)下載適合不熟悉Android.mk與Android.bp編寫、或反復(fù)遭遇編譯問題的開發(fā)者參考可快速掌握ASAN在車機雙系統(tǒng)環(huán)境下的集成方式與排錯思路。1. 高通8155p上QNXAndroid9雙系統(tǒng)ASAN到底能幫你抓到什么在高通8155p這類座艙平臺上QNX負責(zé)儀表與安全域Android9跑IVI應(yīng)用兩邊通過Hypervisor共享內(nèi)存、GPU和音頻通道。這種架構(gòu)下最讓人頭疼的不是崩潰而是那種“偶發(fā)花屏、隨機重啟、日志里什么都沒有”的內(nèi)存問題。ASANAddressSanitizer就是用來把這類玄學(xué)問題變成可定位堆棧的利器。它能在運行時檢測越界讀寫、釋放后使用、雙重釋放、內(nèi)存泄漏等直接告訴你哪一行代碼踩了哪塊內(nèi)存。本文面向的是在8155p上做Android9系統(tǒng)開發(fā)、需要開啟ASAN定位native內(nèi)存問題的工程師。不管你的模塊用的是Android.mk還是Android.bp核心思路一致讓目標(biāo)模塊帶上ASAN運行時并在QNXAndroid雙系統(tǒng)環(huán)境下正確加載。下面從原理到落地一步步拆。2. ASAN在Android9上的編譯鏈路從Android.mk到Android.bp2.1 為什么Android9的ASAN不能直接照搬高版本做法Android9API 28的編譯系統(tǒng)正處于Android.mk向Android.bp過渡的階段ASAN的支持方式和Android10以后有明顯差異。高版本上你可以在Android.bp里寫sanitize: { address: true }但在Android9的Soong里這個字段雖然存在對部分模塊類型支持并不完整尤其是涉及JNI、HAL和可執(zhí)行文件時。更穩(wěn)妥的做法是走LOCAL_SANITIZE : address這條老路或者用全局開關(guān)SANITIZE_TARGETaddress做整包構(gòu)建。但整包ASAN在8155p上不現(xiàn)實——QNX側(cè)不參與Android構(gòu)建Android側(cè)全量ASAN會導(dǎo)致鏡像巨大、啟動極慢Hypervisor分區(qū)根本放不下。所以實際落地一定是按模塊開啟只對懷疑有內(nèi)存問題的native庫或可執(zhí)行文件做ASAN插樁。另一個坑是Android9默認(rèn)的ASAN運行時庫路徑。libclang_rt.asan-aarch64-android.so需要和你的模塊一起推到設(shè)備上并且要在init階段通過LD_PRELOAD或者鏈接依賴的方式加載。如果只編譯不部署運行時運行時會直接報ASAN: failed to load runtime日志里只有一行很容易被忽略。2.2 Android.mk模塊開啟ASAN的最小改動假設(shè)你有一個native可執(zhí)行文件或共享庫用Android.mk管理。最小改動是在模塊定義里加三行# Android.mk 中為單個模塊開啟ASAN LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : my_memory_test LOCAL_SRC_FILES : main.cpp LOCAL_SANITIZE : address LOCAL_SANITIZE_DIAG : address # 確保ASAN運行時被打包 LOCAL_REQUIRED_MODULES : libclang_rt.asan-aarch64-android include $(BUILD_EXECUTABLE)LOCAL_SANITIZE : address告訴編譯器插入ASAN插樁代碼LOCAL_SANITIZE_DIAG : address讓診斷信息更完整。LOCAL_REQUIRED_MODULES保證運行時庫被編進system分區(qū)。注意如果你的模塊是BUILD_SHARED_LIBRARY同樣適用但需要確認(rèn)依賴它的上層模塊不會因為ASAN的符號沖突而崩潰。實際項目中我遇到過JNI庫開ASAN后Java層調(diào)用直接閃退原因是ASAN運行時和Android Runtime的malloc調(diào)試鉤子沖突解決辦法是在BoardConfig.mk里加MALLOC_DEBUG相關(guān)配置或者只對獨立可執(zhí)行文件開ASANJNI庫用LOCAL_SANITIZE的hwaddress替代——但Android9對HWASAN支持更弱所以多數(shù)時候還是回到獨立進程驗證。2.3 Android.bp模塊開啟ASAN的寫法與限制Android.bp在Android9里對ASAN的支持分兩種情況。對于cc_binary和cc_library可以寫// Android.bp 中為cc_binary開啟ASAN cc_binary { name: my_memory_test_bp, srcs: [main.cpp], sanitize: { address: true, diag: { address: true, }, }, required: [libclang_rt.asan-aarch64-android], }這段配置在Android9的Soong里能通過編譯但有一個隱藏問題required字段在部分Android9分支上不會自動把運行時庫加入system.img需要你在device.mk里手動加PRODUCT_PACKAGES libclang_rt.asan-aarch64-android。另外cc_library_shared開ASAN后如果被未開ASAN的模塊依賴鏈接階段會報undefined reference to __asan_init。這是因為ASAN插樁后的庫需要運行時符號而未插樁的模塊不會鏈接ASAN運行時。解決辦法是讓依賴鏈上的所有模塊都開ASAN或者把ASAN庫做成獨立可執(zhí)行文件通過dlopen加載。我一般會優(yōu)先選獨立可執(zhí)行文件方案因為對系統(tǒng)其他部分影響最小。2.4 編譯參數(shù)與運行時庫的對應(yīng)關(guān)系A(chǔ)SAN在Android9上依賴幾個關(guān)鍵編譯參數(shù)-fsanitizeaddress、-fno-omit-frame-pointer、-fsanitize-recoveraddress可選。-fno-omit-frame-pointer必須加否則堆棧回溯會缺幀定位精度大打折扣。-fsanitize-recoveraddress讓ASAN在檢測到錯誤后繼續(xù)運行適合抓多個問題但默認(rèn)建議關(guān)掉因為繼續(xù)運行可能掩蓋后續(xù)錯誤。運行時庫方面64位用libclang_rt.asan-aarch64-android.so32位用libclang_rt.asan-arm-android.so。8155p的Android9通常是64位為主但如果有32位兼容進程兩個都要推。推庫的位置一般是/system/lib64/和/system/lib/權(quán)限644selinux上下文要設(shè)為u:object_r:system_lib_file:s0否則init加載時會拒絕。3. QNXAndroid雙系統(tǒng)下ASAN的部署與觸發(fā)3.1 跨系統(tǒng)內(nèi)存共享場景下ASAN的局限性QNX和Android通過Hypervisor共享內(nèi)存時Android側(cè)的ASAN只能檢測Android進程內(nèi)部的地址空間問題。如果內(nèi)存越界發(fā)生在共享內(nèi)存區(qū)域且寫壞的是QNX側(cè)的數(shù)據(jù)ASAN在Android側(cè)可能完全無感——因為那塊內(nèi)存在Android的地址空間里是合法映射ASAN不會攔截。這種情況下需要配合QNX側(cè)的內(nèi)存保護工具或者在共享內(nèi)存邊界加哨兵值guard page來輔助判斷。實際項目中我遇到過Android側(cè)音頻服務(wù)寫共享內(nèi)存越界導(dǎo)致QNX儀表進程崩潰但Android側(cè)ASAN日志干凈。最后是在共享內(nèi)存分配時前后各加4KB guard page才定位到越界寫。所以ASAN不是萬能藥跨系統(tǒng)問題要結(jié)合邊界防護。3.2 把ASAN可執(zhí)行文件推上8155p并運行編譯出帶ASAN的可執(zhí)行文件后部署步驟如下# 推ASAN運行時庫和測試程序到設(shè)備 adb root adb remount adb push out/target/product/xxx/system/bin/my_memory_test /system/bin/ adb push prebuilts/clang/host/linux-x86/clang-xxx/lib64/libclang_rt.asan-aarch64-android.so /system/lib64/ adb shell chmod 644 /system/lib64/libclang_rt.asan-aarch64-android.so adb shell chmod 755 /system/bin/my_memory_test # 設(shè)置ASAN選項并運行 adb shell ASAN_OPTIONSdetect_leaks0:abort_on_error1:symbolize1 /system/bin/my_memory_testdetect_leaks0在Android9上建議關(guān)掉因為LeakSanitizer在Android的init進程環(huán)境下經(jīng)常誤報而且需要ptrace權(quán)限selinux默認(rèn)不允許。abort_on_error1讓ASAN檢測到錯誤立即abort方便抓core dump或tombstone。symbolize1讓ASAN嘗試符號化堆棧但需要llvm-symbolizer在設(shè)備上通常沒有所以實際符號化要在host側(cè)用ndk-stack或llvm-symbolizer離線做。運行后如果ASAN報錯日志會出現(xiàn)在logcat或stderr格式是ERROR: AddressSanitizer: heap-buffer-overflow on address...后面跟著調(diào)用棧。3.3 用wrap.sh在Android9上給JNI庫開ASAN如果問題出在JNI庫且不想重編整個APK可以用wrap.sh機制。在APK的lib/arm64-v8a/目錄下放一個wrap.sh#!/system/bin/sh # wrap.sh 為JNI庫加載ASAN運行時 HERE$(cd $(dirname $0) pwd) export LD_PRELOAD$HERE/libclang_rt.asan-aarch64-android.so export ASAN_OPTIONSdetect_leaks0:abort_on_error1 exec $同時把ASAN運行時庫放到同一目錄。wrap.sh需要可執(zhí)行權(quán)限且APK要設(shè)android:debuggabletrue。這個方案在Android9上對zygote啟動的進程有效但對init啟動的native服務(wù)無效。另外LD_PRELOAD加載ASAN運行時后所有malloc/free都會被攔截性能下降明顯只適合調(diào)試階段。我一般會在驗證完問題后立即移除wrap.sh避免影響正常功能。3.4 符號化堆棧從設(shè)備日志到代碼行ASAN在設(shè)備上輸出的堆棧通常是地址偏移沒有行號。符號化需要把out/target/product/xxx/symbols/下的帶符號文件拿到host側(cè)用llvm-symbolizer處理# 在host側(cè)符號化ASAN日志 llvm-symbolizer --objout/target/product/xxx/symbols/system/bin/my_memory_test asan_log.txt如果日志里是#0 0x1234 in my_func /path/to/file.cpp:42這種格式說明編譯時帶了-g且符號表沒被strip。Android9的release構(gòu)建默認(rèn)strip所以開ASAN的模塊要加LOCAL_STRIP_MODULE : falseAndroid.mk或strip: { none: true }Android.bp。否則符號化出來全是??。這個坑我踩過不止一次血淚經(jīng)驗是開ASAN的同時一定關(guān)strip否則等于白抓。4. 避坑與常見問題排查4.1 ASAN報錯但堆棧全是問號現(xiàn)象ASAN日志里調(diào)用棧顯示#0 0x0000 in ??沒有任何函數(shù)名和行號。原因通常是編譯時strip了符號或者llvm-symbolizer找不到對應(yīng)的帶符號文件。解決在模塊定義里關(guān)掉stripAndroid.mk加LOCAL_STRIP_MODULE : falseAndroid.bp加strip: { none: true }。同時確認(rèn)out/target/product/xxx/symbols/下有對應(yīng)模塊的未strip版本。如果還是不行檢查編譯時是否帶了-gAndroid9默認(rèn)-g在eng構(gòu)建下開啟userdebug下可能關(guān)閉需要手動加LOCAL_CFLAGS -g。4.2 開啟ASAN后模塊加載失敗報undefined symbol現(xiàn)象dlopen加載ASAN庫時失敗logcat報undefined reference to __asan_init或__asan_report_load8。原因是被ASAN插樁的庫依賴ASAN運行時符號但運行時庫沒有被正確加載。解決確保libclang_rt.asan-aarch64-android.so在/system/lib64/下且權(quán)限正確同時檢查依賴鏈上所有模塊是否都開了ASAN。如果只有部分模塊開未開的模塊調(diào)用開了ASAN的模塊時符號解析會失敗。最穩(wěn)妥的做法是讓ASAN模塊獨立成可執(zhí)行文件不與其他模塊共享鏈接。4.3 ASAN誤報LeakSanitizer導(dǎo)致進程被殺現(xiàn)象進程啟動后不久被init殺掉日志顯示LeakSanitizer: detected memory leaks。原因Android9的init進程和部分系統(tǒng)服務(wù)在啟動階段有大量一次性分配LeakSanitizer會把這些當(dāng)成泄漏。解決在ASAN_OPTIONS里加detect_leaks0。如果確實需要查泄漏改用LSAN_OPTIONSsuppressions...加抑制文件把系統(tǒng)庫的分配排除掉。但Android9上LSAN對ptrace的依賴經(jīng)常和selinux沖突所以多數(shù)時候直接關(guān)掉泄漏檢測只查越界和釋放后使用。4.4 雙系統(tǒng)共享內(nèi)存越界ASAN抓不到現(xiàn)象QNX側(cè)進程崩潰Android側(cè)ASAN無任何輸出。原因越界寫發(fā)生在共享內(nèi)存區(qū)域該區(qū)域在Android地址空間內(nèi)是合法映射ASAN不攔截。解決在共享內(nèi)存分配時前后加guard page或者用mprotect把邊界頁設(shè)為PROT_READ寫越界會觸發(fā)SIGSEGV再配合debuggerd抓tombstone。另外可以在共享內(nèi)存結(jié)構(gòu)里加magic number定期校驗發(fā)現(xiàn)被改寫就報錯。這個方案比ASAN更原始但在跨系統(tǒng)場景下更可靠。4.5 Android.bp的sanitize字段不生效現(xiàn)象在Android.bp里寫了sanitize: { address: true }但編譯出的二進制沒有ASAN插樁運行時也不報ASAN錯誤。原因Android9的Soong對部分模塊類型的sanitize支持不完整尤其是cc_library_shared和cc_binary在特定分支上會忽略該字段。解決改用LOCAL_SANITIZE的Android.mk寫法或者用全局SANITIZE_TARGETaddress構(gòu)建后從out/里取插樁后的二進制手動推設(shè)備。驗證是否插樁成功可以用readelf -s看二進制里有沒有__asan_init符號有就是插了沒有就是沒生效。5. 進階用ASAN自定義分配器定位共享內(nèi)存踩踏ASAN默認(rèn)只攔截標(biāo)準(zhǔn)malloc/free和棧/全局變量對自定義分配器比如共享內(nèi)存池?zé)o能為力。在8155p上很多內(nèi)存問題出在自定義的共享內(nèi)存分配器上。一個進階做法是給自定義分配器加ASAN的poison/unpoison接口。ASAN提供了__asan_poison_memory_region和__asan_unpoison_memory_region可以在分配時把內(nèi)存標(biāo)記為可訪問釋放時標(biāo)記為不可訪問。這樣越界寫或釋放后寫就會觸發(fā)ASAN報錯。具體實現(xiàn)在你的共享內(nèi)存分配器里alloc時調(diào)用__asan_unpoison_memory_region(ptr, size)free時調(diào)用__asan_poison_memory_region(ptr, size)。編譯時需要鏈接ASAN運行時并且確保這些接口在Android9的ASAN運行時里存在。我實測過libclang_rt.asan-aarch64-android.so確實導(dǎo)出了這兩個符號。但要注意poison的粒度是8字節(jié)所以size要按8字節(jié)對齊否則會有誤報。另一個技巧是用ASAN的start_deactivated模式。在進程啟動時先不激活A(yù)SAN等關(guān)鍵初始化完成后再通過__asan_init激活。這樣可以避免ASAN在init階段和系統(tǒng)庫的沖突。Android9上可以通過ASAN_OPTIONSstart_deactivated1開啟然后在代碼里調(diào)用__asan_init()。這個模式對調(diào)試啟動階段的內(nèi)存問題特別有用因為默認(rèn)ASAN在init階段就會攔截所有分配導(dǎo)致啟動失敗。驗證ASAN是否真正生效可以寫一個故意越界的小程序// asan_check.cpp 驗證ASAN是否生效 #include cstring int main() { char *buf new char[10]; memset(buf, 0, 20); // 故意越界寫 delete[] buf; return 0; }編譯時加-fsanitizeaddress -fno-omit-frame-pointer -g推設(shè)備運行。如果ASAN生效會立刻報heap-buffer-overflow并給出堆棧。如果沒有報錯說明插樁或運行時加載有問題回到第2章檢查編譯參數(shù)和庫路徑。最后說一個我自己的習(xí)慣每次開ASAN之前先把模塊的strip關(guān)掉、-g加上、運行時庫路徑確認(rèn)一遍這三件事做完再編譯。否則大概率是白抓一輪日志里全是問號后悔藥都沒得吃。ASAN在8155p的QNXAndroid9雙系統(tǒng)上不是一鍵開關(guān)而是一套需要按模塊、按場景調(diào)整的調(diào)試鏈路。把編譯、部署、符號化、跨系統(tǒng)邊界這幾步走通它就能把最玄學(xué)的內(nèi)存問題變成一行明確的代碼位置。希望幫到你。本文還有配套的精品資源點擊獲取