Android 相容性 | NDK 工具集更新須知

谷歌開發者_發表於2017-04-14

640?wx_fmt=gif


640?wx_fmt=jpeg


受 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 上正常執行。我們建議您修改本機程式碼構建配置,以便生成正確的二進位制檔案。


640?wx_fmt=gif


Android 的相容性一直是很多開發者所關心的問題,我們將持續關注 Android 相容性的變化,併發布一系列相關文章幫助大家及時瞭解。如果您在使用 NDK 工具集的過程中發現了我們尚未收錄的 Android 相容性問題,歡迎留言,我們將盡力尋找答案,並在新的文章中給予解答。


推薦閱讀:

四月刊 | Google Play 開發者 FAQ

移動應用設計:應用內搜尋和業務轉化

Gameloft通過玩家邀請機制獲得爆發式下載

領取《出海寶典》,邁出成功出海的第一步!


640?wx_fmt=gif


相關文章