受 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 兼容性问题,欢迎留言,我们将尽力寻找答案,并在新的文章中给予解答。