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