Android兼容性 | NDK工具集更新須知
受 Android 平臺其他改進的影響,Android M 和 N 中的動態鏈接器對于編寫整潔且具有跨平臺兼容性的本機代碼提出了更為嚴格的要求;滿足這些要求的本機代碼才能順利完成加載。為確保平穩過渡到較新的 Android 版本,應用的本機代碼必須遵循這些規則和建議。
下面,我們將重申并詳細說明與本機代碼加載有關的各項變更及其影響,以及您可以采取哪些措施來避免出現問題。
所需工具:在 NDK 中,每個架構都有一個 <arch>-linux-android-readelf 二進制文件(如 arm-linux-androideabi-readelf 或 i686-linux-android-readelf,位于 toolchains/ 下),但您可以對任何架構使用 readelf,因為我們將只進行基本檢查。在 Linux 上,您需要為 readelf 安裝“binutils”程序包,為 scanelf 安裝“pax-utils”程序包。
私有 API(從 API 24 開始實施)
本機庫只能使用公共 API,且不得鏈接到非 NDK 平臺庫。此規則從 API 24 開始實施,此后應用便無法再加載非 NDK 平臺庫。此規則由動態鏈接器執行,因此無論代碼使用何種方式加載,都無法訪問非公共庫:System.loadLibrary(...)、DT_NEEDED 條目以及直接調用 dlopen(...) 都會同樣失敗。
對于各項更新,用戶獲得的應用體驗應該是一致的,而開發者應不必進行緊急更新應用以應對平臺變更。因此,我們建議不要使用私有 C/C++ 符號。所有 Android 設備都必須通過的兼容性測試套件 (CTS) 并不包含對私有符號進行測試。此類符號可能不存在,也可能會采用不同的行為方式。這可能導致使用私有符號的應用在某些設備上,或在未來發布的新版本系統中無法使用。當 Android 6.0 Marshmallow 從 OpenSSL 切換到 BoringSSL 后,很多開發者都發現了這種問題。
為了減少這種過渡對用戶的影響,我們確定了 Google Play 上安裝量最大的應用中頗為常用且我們短期內仍可提供支持的一些庫(包括 libandroid_runtime.so、libcutils.so、libcrypto.so 和 libssl.so)。為了給您留出更多時間進行過渡,我們會暫時支持這些庫;因此,如果看到表示您的代碼在將來發布的版本中會無效的警告信息,請立即予以更正!
- $ 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]
潛在問題:從 API 24 開始,動態鏈接器將無法加載私有庫,從而導致應用無法加載。
解決方案:重寫本機代碼,使其僅依賴公共 API。短期解決方案是:將沒有復雜依存關系的平臺庫 (libcutils.so) 復制到項目;長期解決方案是將相關代碼復制到項目樹。SSL/Media/JNI internal/binder API 不得通過本機代碼訪問。必要時,本機代碼應調用適當的公共 Java API 方法。
NDK 的 platforms/android-API/usr/lib 下列出了所有的公共庫。
注意:SSL/crypto 是一種特殊情況,應用不得直接使用平臺 libcrypto 和 libssl 庫,即使在較早版本的平臺上也不可以。所有應用都應使用 GMS 安全提供程序,以確保應用免遭已知漏洞攻擊。
缺少節標頭(從 API 24 開始實施)
每個 ELF 文件的節標頭中都包含附加信息。現在,文件中必須有這些節標頭,因為動態鏈接器要使用它們來進行健全性檢查。有些開發者嘗試通過刪除這些節標頭對二進制文件進行混淆處理,防止遭到反向工程。(這樣做實際上并沒有用,因為可以使用工具來重建已刪除的信息,而這類工具到處都有。)
- $ 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
- $
解決方案:從您的版本中移除用于刪除節標頭的額外步驟。
文本重定位(從 API 23 開始實施)
從 API 23 開始,共享對象不得包含文本重定位。也就是說,必須按原樣加載代碼,不得對其進行修改。這種方法可縮短加載時間并提高安全性。
文本重定位的常見原因是使用了與非位置無關的手寫編譯器。這種情況并不常見。請使用我們的文檔中所述的 scanelf 工具進一步診斷:
- $ 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]
如果您沒有可用的 scanelf 工具,可以改用 readelf 進行基本檢查,查找 TEXTREL 條目或 TEXTREL 標記。查找其中一項就已足夠。(TEXTREL 條目對應的值無關緊要且通常為 0,存在 TEXTREL 條目即表明 .so 包含文本重定位)。以下示例中同時存在這兩種指示符:
注意:從技術上來講,可能存在帶有 TEXTREL 條目/標記卻不包含任何實際文本重定位的共享對象。NDK 中不會出現這種情況,但如果您要自行生成 ELF 文件,請確保不要生成聲明包含文本重定位的 ELF 文件,因為 Android 動態鏈接器信任該條目/標記。
潛在問題:重定位會強制使代碼頁面可寫入,并會增加內存中的臟頁數量,這非常浪費內存。從 Android K (API 19) 開始,動態鏈接器發布了有關文本重定位的警告,而在 API 23 及更高版本中,它拒絕加載包含文本重定位的代碼。
解決方案:重寫編譯器使其與位置無關,以確保不需要任何文本重定位。有關詳細信息,請查看 Gentoo 文檔。
無效的 DT_NEEDED 條目(從 API 23 開始實施)
雖然庫依賴項(ELF 標頭中的 DT_NEEDED 條目)可以是絕對路徑,但在 Android 平臺上卻毫無意義,因為您無法控制系統將在何處安裝庫。DT_NEEDED 條目應與所需庫的 SONAME 相同,將在運行時查找庫的任務留給動態鏈接器。
在 API 23 之前,Android 的動態鏈接器在查找所需庫時會忽略完整路徑,僅使用基本名稱(最后一個“/”之后的部分)。從 API 23 開始,運行時鏈接器將完全遵循 DT_NEEDED,因此,如果設備的特定位置不存在庫,鏈接器將無法加載相應庫。
更糟的是,有些構建系統存在漏洞,這會導致它們插入指向構建主機上的文件的 DT_NEEDED 條目,而在設備上卻無法找到相應文件。
- $ 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]
- $
潛在問題:在 API 23 之前使用的是 DT_NEEDED 條目的基本名稱,但從 API 23 開始,Android 運行時將嘗試使用指定路徑加載庫,但設備上卻不存在該路徑。有些已損壞的第三方工具鏈/構建系統使用的是構建主機而非 SONAME 上的路徑。
解決方案:確保所有所需的庫僅由 SONAME 引用。最好讓運行時鏈接器查找和加載這些庫,因為庫在不同設備上的位置可能有所不同。
缺少 SONAME(從 API 23 開始使用)
每個 ELF 共享對象(“本機庫”)都必須具備 SONAME(共享對象名稱)屬性。NDK 工具鏈會默認添加此屬性,如果此屬性不存在,則表明備用工具鏈配置有誤或構建系統中存在錯誤配置。缺少 SONAME 可能會導致運行時問題,例如加載錯誤的庫:缺少此屬性時會改為使用文件名。
- $ readelf --dynamic libWithSoName.so | grep SONAME
- 0x0000000e (SONAME) Library soname: [libWithSoName.so]
- $
潛在問題:命名空間沖突可能會導致在運行時加載錯誤的庫,進而導致在未找到所需符號時或您嘗試使用非預期且不兼容 ABI 的庫時系統崩潰。
解決方案:最新版 NDK 會默認生成正確的 SONAME。請確保您使用的是最新版 NDK,且未將構建系統配置為生成不正確的 SONAME 條目(使用 -soname 鏈接器選項)。
請注意,使用最新版 NDK 構建的整潔的跨平臺代碼應當可以在 Android N 上正常運行。我們建議您修改本機代碼構建配置,以便生成正確的二進制文件。
Android 的兼容性一直是很多開發者所關心的問題,我們將持續關注 Android 兼容性的變化,并發布一系列相關文章幫助大家及時了解。如果您在使用 NDK 工具集的過程中發現了我們尚未收錄的 Android 兼容性問題,歡迎留言,我們將盡力尋找答案,并在新的文章中給予解答。