做者:段聰,騰訊社交平臺部高級工程師html
商業轉載請聯繫騰訊WeTest得到受權,非商業轉載請註明出處。node
原文連接:wetest.qq.com/lab/view/42…linux
近期測試反饋一個問題,在舊版本微視基礎上覆蓋安裝新版本的微視APP,首次打開拍攝頁錄製視頻合成時高几率出現crash。nginx
那麼咱們直奔主題,看看日誌:shell
另外復現的日誌中還出現以下信息:
bash
'/data/data/com.tencent.weishi/appresArchiveExtra/res1bodydetect/bodydetect/libxnet.so: strtab out of bounds errorapp
後通過測試,發現覆蓋安裝後首次使用美體功能也會出現crash,日誌以下:async
因爲出現問題的場景都是覆蓋安裝首次使用,而且涉及到人體檢測相關的so,彷佛存在某種共同的緣由。函數
所以Abort異常比起fault addr類問題更容易分析,先從前面Linker出現Abort異常的位置開始着手。性能
Linker是so連接和加載的關鍵,屬於系統可執行文件,所以分析起來比較棘手。好在手上正好有一臺剛刷完本身編譯的Android AOSP的Pixel,作一些實驗變得更輕鬆了。
出現異常的Linker代碼linker_soinfo.cpp以下:
const char* soinfo::get_string(ElfW(Word) index) const { if (has_min_version(1) && (index >= strtab_size_)) { async_safe_fatal("%s: strtab out of bounds error; STRSZ=%zd, name=%d", get_realpath(), strtab_size_, index); } return strtab_ + index;}bool soinfo::elf_lookup(SymbolName& symbol_name, const version_info* vi, uint32_t* symbol_index) const { uint32_t hash = symbol_name.elf_hash(); TRACE_TYPE(LOOKUP, "SEARCH %s in %s@%p h=%x(elf) %zd", symbol_name.get_name(), get_realpath(), reinterpret_cast<void*>(base), hash, hash % nbucket_); ElfW(Versym) verneed = 0; if (!find_verdef_version_index(this, vi, &verneed)) { return false; } for (uint32_t n = bucket_[hash % nbucket_]; n != 0; n = chain_[n]) { ElfW(Sym)* s = symtab_ + n; const ElfW(Versym)* verdef = get_versym(n); // skip hidden versions when verneed == 0 if (verneed == kVersymNotNeeded && is_versym_hidden(verdef)) { continue; } if (check_symbol_version(verneed, verdef) && strcmp(get_string(s->st_name), symbol_name.get_name()) == 0 && is_symbol_global_and_defined(this, s)) { TRACE_TYPE(LOOKUP, "FOUND %s in %s (%p) %zd", symbol_name.get_name(), get_realpath(), reinterpret_cast<void*>(s->st_value), static_cast<size_t>(s->st_size)); *symbol_index = n; return true; } } TRACE_TYPE(LOOKUP, "NOT FOUND %s in %s@%p %x %zd", symbol_name.get_name(), get_realpath(), reinterpret_cast<void*>(base), hash, hash % nbucket_); *symbol_index = 0; return true;}複製代碼
從代碼上看,是在so的symtab中查找某個符號時ElfW(Sym)* s的地址出現異常,致使s->st_name獲取到錯誤的數據。
經過復現問題,能夠抓到更完整的 /data/tombstone日誌,獲得以下完整的信息:
儘管從tombstone中咱們能夠看到一些寄存器數據及寄存處地址附近內存數據,同時也能夠看到crash時的虛擬內存映射表,仍然沒法獲取有價值的信息。另外經過幾回覆現,發現並非每次Crash都是SIGABRT,也出現很多SIGSEGV信號,而調用棧和以前都是同樣的,好比這個:
這基本上能夠說明,並非so自己的代碼存在異常,只多是加載的so出現了文件異常。
另外經過在linker中增長日誌,並從新編譯linker替換到/system/lib/linker中:
能夠獲取到以下的地址信息:
經過根據tombstone中的/proc/<poc>/maps的虛擬內存地址與日誌打印的地址進行對比,能夠發現最爲符號表地址的s並無指向so文件在虛擬內存中的地址段,所以能夠懷疑,so加載確實出現了異常。
由於手機root,能夠直接獲取到crash時的so文件(adb pull /data/data/com.tencent.weishi/appresArchiveExtra/res1bodydetect/bodydetect/libxnet.so),導出來對比md5,然而發現與正常狀況下的so是如出一轍的:
既然前面的這些實驗都沒有得出什麼有意義的結論,那麼我回過頭來分析一下,與問題關聯的so加載到底有什麼特殊性。
實際上,微視爲了減包,將一部分so文件進行下發,因爲so也處於不斷迭代的過程當中,新版本的微視可能會在後臺更新so文件,那麼客戶端一旦發現新的版本有新的so,就會去下載so並進行本地替換。
那麼這個過程有什麼問題呢?惟一可能的問題,就是先加載了舊的so,以後下載新的so進行了熱更新。
咱們先看下微視中是否有這種現象。要觀察這種現象,咱們能夠打開linker自身的調試開關,開啓so加載的日誌。經過設置系統屬性,咱們能夠很容易地進行開啓LD_LOG日誌:
adb shell setprop debug.ld.all dlerror,dlopen
固然咱們也能夠只針對某個應用開啓這個日誌(設置系統屬性debug.ld.app.)。另外,爲了開啓linker中更多的日誌,好比DEBUG打印的信息等,咱們只須要在adb shell中設置環境變量:
export LD_DEBUG=10
那麼,咱們從新復現問題,能夠看到以下so加載過程:
這個過程代表:舊的so先被加載了,而後下載了新版本的so,並進行了替換。
這個過程有什麼問題呢?根據《理解inode》一文咱們能夠得知,linux的文件系統使用的inode機制支持了so文件的熱更新(動態更新),即每一個文件都有一個惟一的inode號,打開文件後使用inode號區分文件而不是文件名:
8、inode的特殊做用
因爲inode號碼與文件名分離,這種機制致使了一些Unix/Linux系統特有的現象。
1. 有時,文件名包含特殊字符,沒法正常刪除。這時,直接刪除inode節點,就能起到刪除文件的做用。
2. 移動文件或重命名文件,只是改變文件名,不影響inode號碼。
3. 打開一個文件之後,系統就以inode號碼來識別這個文件,再也不考慮文件名。所以,一般來講,系統沒法從inode號碼得知文件名。
第3點使得軟件更新變得簡單,能夠在不關閉軟件的狀況下進行更新,不須要重啓。由於系統經過inode號碼,識別運行中的文件,不經過文件名。更新的時候,新版文件以一樣的文件名,生成一個新的inode,不會影響到運行中的文件。等到下一次運行這個軟件的時候,文件名就自動指向新版文件,舊版文件的inode則被回收。
可是問題就出在這裏,若是替換文件使用的是cp這樣的操做,會致使原來的so文件截斷,而後從新寫入數據,可是inode並無更新號,磁盤與內存中的信息出現不一致,這種狀況在linux中很常見,好比這篇文章就進行了分析:
1. cp new.so old.so,文件的inode號沒有改變,dentry找到是新的so,可是cp過程當中會把老的so截斷爲0,這時程序再次進行加載的時候,若是須要的文件偏移大於新的so的地址範圍會生成buserror致使程序core掉,或者因爲全局符號表沒有更新,動態庫依賴的外部函數沒法解析,會產生sigsegv從而致使程序core掉,固然也有必定的可能性程序繼續執行,可是十分危險。
2. mv new.so old.so,文件的inode號會發生改變,但老的so的inode號依舊存在,這時程序必須中止重啓服務才能繼續使用新的so,不然程序繼續執行,使用的仍是老的so,因此程序不會core掉,就像咱們在第二部分刪除掉log文件,而依然能用lsof命令看到同樣。
還有更深刻的解釋:
Linux因爲Demand Paging機制的關係,必須確保正在運行中的程序鏡像(注意,並不是文件自己)不被意外修改,所以內核在啓動程序後會綁定 內存頁 到這個so的inode,而一旦此inode文件被open函數O_TRUNC掉,則kernel會把so文件對應在虛存的頁清空,這樣當運行到so裏面的代碼時,由於物理內存中再也不有實際的數據(僅存在於虛存空間內),會產生一次缺頁中斷。Kernel從so文件中copy一份到內存中去,a)可是這時的全局符號表並無通過解析,當調用到時就產生segment fault , b)若是須要的文件偏移大於新的so的地址範圍,就會產生bus error。
那麼問題基本清晰了。咱們在回去看看微視的代碼,這裏下載了so以後直接unzip到原來的路徑,並無先進行rm操做。
更近一步,咱們本身寫個demo測試下剛纔的問題(2個按鈕,一個加載指定so,一個調用so中的native方法):
代碼不能再簡單了:
正常加載so而後執行native方法都是ok的,使用rm+mv替換或者adb push替換也都是ok的,最後再按照錯誤的方法操做,步驟爲:
1. 啓動app,點擊加載so;
2. 經過cp命令替換so;
3. 點擊執行native方法;
結果確實是crash了:
日誌以下,是否是很最開始的日誌信息同樣呢:
到此,咱們有兩種解決辦法:
1. 若是so有升級,先不加載舊的so,等新的so下載完成以後再加載;
2. 能夠先加載舊的so,可是下載了新的so以後,要刪除舊的so,再進行替換。
引文參考:
www.ruanyifeng.com/blog/2011/1…
目前,「自動化兼容測試」 提供雲端自動化兼容服務,提交雲端百臺真機,並行測試。快速發現遊戲/應用兼容性和性能問題,覆蓋安卓主流機型。
點擊:wetest.qq.com/product/aut… 便可體驗。
若是使用當中有任何疑問,歡迎聯繫騰訊WeTest企業QQ:2852350015