Android兼容性 | NDK工具集更新須知
受 Android 平臺(tái)其他改進(jìn)的影響,Android M 和 N 中的動(dòng)態(tài)鏈接器對(duì)于編寫整潔且具有跨平臺(tái)兼容性的本機(jī)代碼提出了更為嚴(yán)格的要求;滿足這些要求的本機(jī)代碼才能順利完成加載。為確保平穩(wěn)過(guò)渡到較新的 Android 版本,應(yīng)用的本機(jī)代碼必須遵循這些規(guī)則和建議。
下面,我們將重申并詳細(xì)說(shuō)明與本機(jī)代碼加載有關(guān)的各項(xiàng)變更及其影響,以及您可以采取哪些措施來(lái)避免出現(xiàn)問(wèn)題。
所需工具:在 NDK 中,每個(gè)架構(gòu)都有一個(gè) <arch>-linux-android-readelf 二進(jìn)制文件(如 arm-linux-androideabi-readelf 或 i686-linux-android-readelf,位于 toolchains/ 下),但您可以對(duì)任何架構(gòu)使用 readelf,因?yàn)槲覀儗⒅贿M(jìn)行基本檢查。在 Linux 上,您需要為 readelf 安裝“binutils”程序包,為 scanelf 安裝“pax-utils”程序包。
私有 API(從 API 24 開(kāi)始實(shí)施)
本機(jī)庫(kù)只能使用公共 API,且不得鏈接到非 NDK 平臺(tái)庫(kù)。此規(guī)則從 API 24 開(kāi)始實(shí)施,此后應(yīng)用便無(wú)法再加載非 NDK 平臺(tái)庫(kù)。此規(guī)則由動(dòng)態(tài)鏈接器執(zhí)行,因此無(wú)論代碼使用何種方式加載,都無(wú)法訪問(wèn)非公共庫(kù):System.loadLibrary(...)、DT_NEEDED 條目以及直接調(diào)用 dlopen(...) 都會(huì)同樣失敗。
對(duì)于各項(xiàng)更新,用戶獲得的應(yīng)用體驗(yàn)應(yīng)該是一致的,而開(kāi)發(fā)者應(yīng)不必進(jìn)行緊急更新應(yīng)用以應(yīng)對(duì)平臺(tái)變更。因此,我們建議不要使用私有 C/C++ 符號(hào)。所有 Android 設(shè)備都必須通過(guò)的兼容性測(cè)試套件 (CTS) 并不包含對(duì)私有符號(hào)進(jìn)行測(cè)試。此類符號(hào)可能不存在,也可能會(huì)采用不同的行為方式。這可能導(dǎo)致使用私有符號(hào)的應(yīng)用在某些設(shè)備上,或在未來(lái)發(fā)布的新版本系統(tǒng)中無(wú)法使用。當(dāng) Android 6.0 Marshmallow 從 OpenSSL 切換到 BoringSSL 后,很多開(kāi)發(fā)者都發(fā)現(xiàn)了這種問(wèn)題。
為了減少這種過(guò)渡對(duì)用戶的影響,我們確定了 Google Play 上安裝量最大的應(yīng)用中頗為常用且我們短期內(nèi)仍可提供支持的一些庫(kù)(包括 libandroid_runtime.so、libcutils.so、libcrypto.so 和 libssl.so)。為了給您留出更多時(shí)間進(jìn)行過(guò)渡,我們會(huì)暫時(shí)支持這些庫(kù);因此,如果看到表示您的代碼在將來(lái)發(fā)布的版本中會(huì)無(wú)效的警告信息,請(qǐng)立即予以更正!
- $ readelf --dynamic libBroken.so | grep NEEDED
- 0x00000001 (NEEDED) Shared library: [libnativehelper.so]
- 0x00000001 (NEEDED) Shared library: [libutils.so]
- 0x00000001 (NEEDED) Shared library: [libstagefright_foundation.so]
- 0x00000001 (NEEDED) Shared library: [libmedia_jni.so]
- 0x00000001 (NEEDED) Shared library: [liblog.so]
- 0x00000001 (NEEDED) Shared library: [libdl.so]
- 0x00000001 (NEEDED) Shared library: [libz.so]
- 0x00000001 (NEEDED) Shared library: [libstdc++.so]
- 0x00000001 (NEEDED) Shared library: [libm.so]
- 0x00000001 (NEEDED) Shared library: [libc.so]
潛在問(wèn)題:從 API 24 開(kāi)始,動(dòng)態(tài)鏈接器將無(wú)法加載私有庫(kù),從而導(dǎo)致應(yīng)用無(wú)法加載。
解決方案:重寫本機(jī)代碼,使其僅依賴公共 API。短期解決方案是:將沒(méi)有復(fù)雜依存關(guān)系的平臺(tái)庫(kù) (libcutils.so) 復(fù)制到項(xiàng)目;長(zhǎng)期解決方案是將相關(guān)代碼復(fù)制到項(xiàng)目樹(shù)。SSL/Media/JNI internal/binder API 不得通過(guò)本機(jī)代碼訪問(wèn)。必要時(shí),本機(jī)代碼應(yīng)調(diào)用適當(dāng)?shù)墓?Java API 方法。
NDK 的 platforms/android-API/usr/lib 下列出了所有的公共庫(kù)。
注意:SSL/crypto 是一種特殊情況,應(yīng)用不得直接使用平臺(tái) libcrypto 和 libssl 庫(kù),即使在較早版本的平臺(tái)上也不可以。所有應(yīng)用都應(yīng)使用 GMS 安全提供程序,以確保應(yīng)用免遭已知漏洞攻擊。
缺少節(jié)標(biāo)頭(從 API 24 開(kāi)始實(shí)施)
每個(gè) ELF 文件的節(jié)標(biāo)頭中都包含附加信息。現(xiàn)在,文件中必須有這些節(jié)標(biāo)頭,因?yàn)閯?dòng)態(tài)鏈接器要使用它們來(lái)進(jìn)行健全性檢查。有些開(kāi)發(fā)者嘗試通過(guò)刪除這些節(jié)標(biāo)頭對(duì)二進(jìn)制文件進(jìn)行混淆處理,防止遭到反向工程。(這樣做實(shí)際上并沒(méi)有用,因?yàn)榭梢允褂霉ぞ邅?lái)重建已刪除的信息,而這類工具到處都有。)
- $ readelf --header libBroken.so | grep 'section headers'
- Start of section headers: 0 (bytes into file)
- Size of section headers: 0 (bytes)
- Number of section headers: 0
- $
解決方案:從您的版本中移除用于刪除節(jié)標(biāo)頭的額外步驟。
文本重定位(從 API 23 開(kāi)始實(shí)施)
從 API 23 開(kāi)始,共享對(duì)象不得包含文本重定位。也就是說(shuō),必須按原樣加載代碼,不得對(duì)其進(jìn)行修改。這種方法可縮短加載時(shí)間并提高安全性。
文本重定位的常見(jiàn)原因是使用了與非位置無(wú)關(guān)的手寫編譯器。這種情況并不常見(jiàn)。請(qǐng)使用我們的文檔中所述的 scanelf 工具進(jìn)一步診斷:
- $ scanelf -qT libTextRel.so
- libTextRel.so: (memory/data?) [0x15E0E2] in (optimized out: previous simd_broken_op1) [0x15E0E0]
- libTextRel.so: (memory/data?) [0x15E3B2] in (optimized out: previous simd_broken_op2) [0x15E3B0]
- [skipped the rest]
如果您沒(méi)有可用的 scanelf 工具,可以改用 readelf 進(jìn)行基本檢查,查找 TEXTREL 條目或 TEXTREL 標(biāo)記。查找其中一項(xiàng)就已足夠。(TEXTREL 條目對(duì)應(yīng)的值無(wú)關(guān)緊要且通常為 0,存在 TEXTREL 條目即表明 .so 包含文本重定位)。以下示例中同時(shí)存在這兩種指示符:
注意:從技術(shù)上來(lái)講,可能存在帶有 TEXTREL 條目/標(biāo)記卻不包含任何實(shí)際文本重定位的共享對(duì)象。NDK 中不會(huì)出現(xiàn)這種情況,但如果您要自行生成 ELF 文件,請(qǐng)確保不要生成聲明包含文本重定位的 ELF 文件,因?yàn)?Android 動(dòng)態(tài)鏈接器信任該條目/標(biāo)記。
潛在問(wèn)題:重定位會(huì)強(qiáng)制使代碼頁(yè)面可寫入,并會(huì)增加內(nèi)存中的臟頁(yè)數(shù)量,這非常浪費(fèi)內(nèi)存。從 Android K (API 19) 開(kāi)始,動(dòng)態(tài)鏈接器發(fā)布了有關(guān)文本重定位的警告,而在 API 23 及更高版本中,它拒絕加載包含文本重定位的代碼。
解決方案:重寫編譯器使其與位置無(wú)關(guān),以確保不需要任何文本重定位。有關(guān)詳細(xì)信息,請(qǐng)查看 Gentoo 文檔。
無(wú)效的 DT_NEEDED 條目(從 API 23 開(kāi)始實(shí)施)
雖然庫(kù)依賴項(xiàng)(ELF 標(biāo)頭中的 DT_NEEDED 條目)可以是絕對(duì)路徑,但在 Android 平臺(tái)上卻毫無(wú)意義,因?yàn)槟鸁o(wú)法控制系統(tǒng)將在何處安裝庫(kù)。DT_NEEDED 條目應(yīng)與所需庫(kù)的 SONAME 相同,將在運(yùn)行時(shí)查找?guī)斓娜蝿?wù)留給動(dòng)態(tài)鏈接器。
在 API 23 之前,Android 的動(dòng)態(tài)鏈接器在查找所需庫(kù)時(shí)會(huì)忽略完整路徑,僅使用基本名稱(最后一個(gè)“/”之后的部分)。從 API 23 開(kāi)始,運(yùn)行時(shí)鏈接器將完全遵循 DT_NEEDED,因此,如果設(shè)備的特定位置不存在庫(kù),鏈接器將無(wú)法加載相應(yīng)庫(kù)。
更糟的是,有些構(gòu)建系統(tǒng)存在漏洞,這會(huì)導(dǎo)致它們插入指向構(gòu)建主機(jī)上的文件的 DT_NEEDED 條目,而在設(shè)備上卻無(wú)法找到相應(yīng)文件。
- $ readelf --dynamic libSample.so | grep NEEDED
- 0x00000001 (NEEDED) Shared library: [libm.so]
- 0x00000001 (NEEDED) Shared library: [libc.so]
- 0x00000001 (NEEDED) Shared library: [libdl.so]
- 0x00000001 (NEEDED) Shared library:
- [C:\Users\build\Android\ci\jni\libBroken.so]
- $
潛在問(wèn)題:在 API 23 之前使用的是 DT_NEEDED 條目的基本名稱,但從 API 23 開(kāi)始,Android 運(yùn)行時(shí)將嘗試使用指定路徑加載庫(kù),但設(shè)備上卻不存在該路徑。有些已損壞的第三方工具鏈/構(gòu)建系統(tǒng)使用的是構(gòu)建主機(jī)而非 SONAME 上的路徑。
解決方案:確保所有所需的庫(kù)僅由 SONAME 引用。最好讓運(yùn)行時(shí)鏈接器查找和加載這些庫(kù),因?yàn)閹?kù)在不同設(shè)備上的位置可能有所不同。
缺少 SONAME(從 API 23 開(kāi)始使用)
每個(gè) ELF 共享對(duì)象(“本機(jī)庫(kù)”)都必須具備 SONAME(共享對(duì)象名稱)屬性。NDK 工具鏈會(huì)默認(rèn)添加此屬性,如果此屬性不存在,則表明備用工具鏈配置有誤或構(gòu)建系統(tǒng)中存在錯(cuò)誤配置。缺少 SONAME 可能會(huì)導(dǎo)致運(yùn)行時(shí)問(wèn)題,例如加載錯(cuò)誤的庫(kù):缺少此屬性時(shí)會(huì)改為使用文件名。
- $ readelf --dynamic libWithSoName.so | grep SONAME
- 0x0000000e (SONAME) Library soname: [libWithSoName.so]
- $
潛在問(wèn)題:命名空間沖突可能會(huì)導(dǎo)致在運(yùn)行時(shí)加載錯(cuò)誤的庫(kù),進(jìn)而導(dǎo)致在未找到所需符號(hào)時(shí)或您嘗試使用非預(yù)期且不兼容 ABI 的庫(kù)時(shí)系統(tǒng)崩潰。
解決方案:最新版 NDK 會(huì)默認(rèn)生成正確的 SONAME。請(qǐng)確保您使用的是最新版 NDK,且未將構(gòu)建系統(tǒng)配置為生成不正確的 SONAME 條目(使用 -soname 鏈接器選項(xiàng))。
請(qǐng)注意,使用最新版 NDK 構(gòu)建的整潔的跨平臺(tái)代碼應(yīng)當(dāng)可以在 Android N 上正常運(yùn)行。我們建議您修改本機(jī)代碼構(gòu)建配置,以便生成正確的二進(jìn)制文件。
Android 的兼容性一直是很多開(kāi)發(fā)者所關(guān)心的問(wèn)題,我們將持續(xù)關(guān)注 Android 兼容性的變化,并發(fā)布一系列相關(guān)文章幫助大家及時(shí)了解。如果您在使用 NDK 工具集的過(guò)程中發(fā)現(xiàn)了我們尚未收錄的 Android 兼容性問(wèn)題,歡迎留言,我們將盡力尋找答案,并在新的文章中給予解答。