一葬毫、前言
移動(dòng)端性能優(yōu)化相關(guān)的技術(shù)已經(jīng)發(fā)展到了深水區(qū),微信移動(dòng)端技術(shù)團(tuán)隊(duì)出品的Matrix APM套件就是對(duì)性能優(yōu)化的一個(gè)全集解決方案靠瞎。高途移動(dòng)端團(tuán)隊(duì)對(duì)微信移動(dòng)端團(tuán)隊(duì)一直敬佩有佳箱残,著手落地Matrix APM套件,在眾多的套件里揩懒,有一個(gè)套件叫MemGuard什乙,Matrix的GitHub主頁(yè)有說明式的介紹,但在落地上的內(nèi)容卻少的可憐已球,本篇內(nèi)容正是對(duì)MemGuard的重鑄改造臣镣,并基于此的落地實(shí)踐。
二和悦、背景
高途移動(dòng)端最核心的場(chǎng)景是上課,上課主要形式是直播和錄播渠缕,底層大量使用C/C++鸽素,我們?cè)谙硎芨咝阅艿耐瑫r(shí),也在承受不穩(wěn)定的侵襲亦鳞,而在這不穩(wěn)定中馍忽,內(nèi)存問題一直是困擾C/C++開發(fā)的一大難題,Google一直致力于探索并解決C/C++的內(nèi)存問題燕差,并在Chromium上了落地了GWP-Asan這個(gè)開發(fā)套件遭笋,目前已經(jīng)在Chrome上得到了充分的線上驗(yàn)證,微信客戶端技術(shù)團(tuán)隊(duì)基于GWP-Asan推出了MemGuard套件徒探,我們著手落地瓦呼,但卻遇到了不少問題。
三测暗、MemGuard的實(shí)現(xiàn)原理
MemGuard是基于GWP-Asan修改的堆內(nèi)存檢測(cè)工具央串,通過閱讀源碼可以知道磨澡,微信的改造力度還是非常徹底的,但原理基本是一致的质和,下圖為MemGuard實(shí)現(xiàn)原理圖稳摄。
上面三層比較簡(jiǎn)單,MemGuard的Java API->JNI API->C++ API饲宿,提供初始化API和Builder的參數(shù)設(shè)置以及順序初始化底層的6個(gè)模塊厦酬,對(duì)于這6個(gè)模塊可以把它分為兩層,應(yīng)用處理層和操作系統(tǒng)處理層瘫想。
應(yīng)用處理層
1.PagePool
PagePool的功能仗阅,主要包含兩部分
【1】申請(qǐng)一塊虛擬地址空間,用來進(jìn)行后續(xù)的內(nèi)存檢查殿托,通過syscall調(diào)用mmap來進(jìn)行內(nèi)存映射(這是一種kernel層的調(diào)用方式霹菊,正常我們使用C函數(shù)mmap來進(jìn)行內(nèi)存映射),mmap的使用有標(biāo)準(zhǔn)的API支竹,這里不贅述旋廷,但有幾個(gè)技術(shù)細(xì)節(jié)需要關(guān)注
①通過sysconf(_SC_PAGESIZE)獲取以字節(jié)為單位的Page的大小
②mmap的flag設(shè)置了MAP_PRIVATE和MAP_ANONYMOUS(用戶空間內(nèi)存分配分為兩類,一類是file-backed礼搁,一類是 anonymous)饶碘,前者是防止其他進(jìn)程訪問到這塊虛擬地址空間,后者因?yàn)槭莂nonymous內(nèi)存空間馒吴,所以為了方便調(diào)試以及排查問題扎运,使用如下去設(shè)置anonymous內(nèi)存空間的名稱。
PagePool申請(qǐng)四類內(nèi)存地址空間饮戳,這個(gè)策略與GWP-ASan是一致的豪治,四類內(nèi)存地址空間包括GUARD_PAGE,ALLOC_PAGE扯罐,F(xiàn)REE_SLOT负拟,META_AREA,下圖描述了每類內(nèi)存地址空間的意義歹河。
【2】申請(qǐng)完虛擬地址空間后掩浙,要對(duì)地址空間做可讀可修改的保護(hù),這樣后續(xù)可操作這塊地址空間秸歧。
2.Interception
Interception在MemGuard里的核心功能主要是用xhook攔截libc.so里的free函數(shù)厨姚,攔截下來后,如果是在MemGuard的監(jiān)控范圍內(nèi)键菱,則記錄在FREE_SLOTS內(nèi)存地址空間谬墙,用于后續(xù)檢測(cè)Use-After-Free的問題。
3.Allocation
Allocation的邏輯相對(duì)比較簡(jiǎn)單,主要用于觸發(fā)左對(duì)齊芭梯,右對(duì)齊险耀,內(nèi)存free的邏輯,但核心邏輯還是會(huì)調(diào)用PagePool的方法玖喘。
操作系統(tǒng)處理層
1.Unwind
Matrix內(nèi)部用于native backtrace的技術(shù)甩牺,具體原理可以參考文章【5】。?
2.SoLoadMonitor
SoLoadMonitor的功能累奈,主要包含兩部分
【1】自實(shí)現(xiàn)了系統(tǒng)dlopen贬派,dlsym,dlclose等函數(shù)的功能澎媒,名叫semi_dlfcn搞乏,為什么要自實(shí)現(xiàn)呢?原因是從Android7以后系統(tǒng)會(huì)阻止動(dòng)態(tài)鏈接非公開的C/C++庫(kù)戒努,標(biāo)準(zhǔn)的解決方案都是自實(shí)現(xiàn)请敦,下面讓我們看看各個(gè)函數(shù)是如何實(shí)現(xiàn)的?
semi_dlopen:Android5以上依賴系統(tǒng)dl_iterate_phdr函數(shù)來查詢應(yīng)用程序已加載的共享對(duì)象储玫,Android5以下通過一行一行讀取/proc/thread-self/maps文件來查詢應(yīng)用程序已加載的共享對(duì)象侍筛,其中會(huì)做一些異常校驗(yàn),比如ELF文件合法性校驗(yàn)撒穷,是否是linker加載匣椰,文件權(quán)限檢查等。
semi_dlsym:通過指定的符號(hào)名字端礼,從semi_dlopen獲取的共享對(duì)象列表里查找禽笑,如果ELF的類型是FUNC或者OBJECT,并且符號(hào)名字相等蛤奥,則視為找到佳镜。
【2】有了以上兩個(gè)重要函數(shù)的實(shí)現(xiàn)后,首先通過semi_dlsym查找系統(tǒng)dlopen凡桥,android_dlopen_ext和dlclose函數(shù)蟀伸,然后利用xhook去接管系統(tǒng)dlopen,android_dlopen_ext和dlclose函數(shù)唬血,進(jìn)而走到自實(shí)現(xiàn)的dlopen望蜡,android_dlopen_ext和dlclose函數(shù)里唤崭,這里需要注意一點(diǎn)拷恨,SoLoadMonitor并未適配24和25,下面我們看看dlopen谢肾,android_dlopen_ext和dlclose函數(shù)系統(tǒng)兼容性的問題腕侄。
dlopen:大于等于24,dlopen對(duì)應(yīng)的Linker符號(hào)表是__dl___loader_dlopen或__dl__Z8__dlopenPKciPKv,小于24對(duì)應(yīng)的Linker符號(hào)表是__dl_dlopen冕杠。
android_dlopen_ext:大于等于24微姊,android_dlopen_ext對(duì)應(yīng)的Linker符號(hào)表是__dl___loader_android_dlopen_ext或__dl__Z20__android_dlopen_extPKciPK17android_dlextinfoPKv,小于24對(duì)應(yīng)的Linker符號(hào)表是__dl_android_dlopen_ext分预。
dlclose:大于等于24兢交,dlclose對(duì)應(yīng)的Linker符號(hào)表是__dl___loader_dlclose或__dl__Z9__dlclosePv,小于24對(duì)應(yīng)的Linker符號(hào)表是__dl_dlclose笼痹。
注:以上符號(hào)可以在/system/bin/linker或/system/bin/linker64可執(zhí)行程序中查看配喳。
3.SignalHandler
SignalHandler主要功能是通過系統(tǒng)的sigaction函數(shù)注冊(cè)監(jiān)聽SIGSEGV(是POSIX上的標(biāo)準(zhǔn),代表段錯(cuò)誤)信號(hào)量凳干,細(xì)分code是SEGV_ACCERR(代表訪問內(nèi)存時(shí)發(fā)生的錯(cuò)誤)晴裹,要在Allocation的內(nèi)存分配地址內(nèi),滿足以上條件才會(huì)將dump的文件路徑回傳給應(yīng)用層救赐。
四涧团、落地實(shí)踐遇到的問題
雖然我們已經(jīng)搞清楚了MemGuard的核心原理和代碼實(shí)現(xiàn)細(xì)節(jié),但在落地實(shí)踐中還是遇到了一些問題经磅。
【1】在閱讀源碼的時(shí)候泌绣,發(fā)現(xiàn)MemGuard組件和MemHook組件的初始化是互斥的,原因是兩者都進(jìn)行了C/C++函數(shù)內(nèi)存創(chuàng)建和銷毀API的hook馋贤,兩遍初始化會(huì)造成性能和穩(wěn)定性的問題赞别,所以進(jìn)行了初始化互斥操作。
【2】SEGV_ACCERR發(fā)生在堆內(nèi)存的異常情況配乓,SEGV_MAPERR發(fā)生在棧內(nèi)存的異常情況仿滔,SignalHandler模塊處理的是SEGV_ACCERR的異常情況,所以棧內(nèi)存的異常情況MemGuard不會(huì)捕捉處理犹芹,實(shí)際測(cè)試已驗(yàn)證崎页。
【3】每次檢測(cè)到內(nèi)存問題,MemGuard都會(huì)將問題記錄在cache目錄下memguard目錄下的一個(gè)文件里腰埂,一個(gè)內(nèi)存崩潰會(huì)生成一個(gè)文件飒焦,再次生成文件會(huì)覆蓋原文件,那如何保證日志都采集上來呢屿笼?因?yàn)樵跈z測(cè)出內(nèi)存問題的時(shí)候處于一個(gè)極其不穩(wěn)定的狀態(tài)牺荠,立即上報(bào)存在失敗情況,所以高途選擇在下一次冷啟動(dòng)的時(shí)候進(jìn)行上報(bào)驴一,上報(bào)源是bugly休雌,上報(bào)的時(shí)候帶上關(guān)鍵字,在bugly后臺(tái)可以非常方便的查詢肝断。
【4】MemGuard的思想是源于GWP-Asan杈曲,而GWP-Asan為了性能驰凛,每次發(fā)生SEGV_ACCERR內(nèi)存崩潰,都會(huì)采樣進(jìn)行文件的生成担扑,但高途是在Debug環(huán)境下使用恰响,故需要添加外部可以100%采樣的接口。
【5】MemGuard每次遇到SEGV_ACCERR的異常涌献,App都會(huì)直接崩潰胚宦,為了提升用戶體驗(yàn),高途進(jìn)行了規(guī)避崩潰操作燕垃,目前采用文件保護(hù)的方式间唉,但在不同系統(tǒng)上實(shí)驗(yàn)發(fā)現(xiàn)并不能完全起到作用。
【6】上邊我們提到MemGuard的SoLoadMonitor模塊存在兼容性問題利术,故高途只在Android6以上開啟MemGuard,保證App整體的穩(wěn)定性印叁。
【7】MemGuard堆棧解析出來的是符號(hào)表的信息,無法分析轮蜕,故我們?cè)诰€下使用addr2line命令還原帶有符號(hào)表的so文件。?
【8】因?yàn)槲覀儗?duì)源碼進(jìn)行了二次訂制開發(fā)跃洛,所以需要對(duì)源碼進(jìn)行二次打包發(fā)布率触,通過gradlew assembleRelease和gradlew assembleDebug可打出aar的二進(jìn)制產(chǎn)物,再將二進(jìn)制產(chǎn)物發(fā)布到高途的Maven上去(包括matrix-hooks組件葱蝗,matrix-android-lib組件,matrix-android-commons組件两曼,matrix-backtrace組件),最后在App里直接依賴集成玻驻。
五悼凑、結(jié)語(yǔ)
Meta從IDE開發(fā)環(huán)境階段,代碼審查差異階段璧瞬,生產(chǎn)環(huán)境發(fā)布上線階段出發(fā),對(duì)修復(fù)問題的時(shí)效性進(jìn)行了總結(jié)渔欢,如下圖所示
高途對(duì)MemGuard的重鑄改造档冬,主要是在Diff階段前的實(shí)踐落地,結(jié)果發(fā)現(xiàn)了很多潛在的C/C++的內(nèi)存問題酷誓,也算是一個(gè)不大不小的里程碑。
六棒拂、參考資料
【1】GWP-ASan
【2】字節(jié)Android Native Crash治理之Memory Corruption工具原理與實(shí)踐