Android ABI

不同的 Android 设备使用不同的 CPU,而这些 CPU 又支持不同的指令集。CPU 和指令集的每种组合都有其自己的应用程序二进制接口 (ABI)。ABI 包括以下信息

  • 可使用的 CPU 指令集(和扩展)。
  • 运行时内存存储和加载的字节序。Android 始终为小端字节序。
  • 在应用和系统之间传递数据的约定,包括对齐约束,以及系统在调用函数时如何使用堆栈和寄存器。
  • 可执行二进制文件(如程序和共享库)的格式,以及它们支持的内容类型。Android 始终使用 ELF。有关更多信息,请参阅 ELF System V 应用程序二进制接口
  • C++ 名称的修饰方式。有关更多信息,请参阅 通用/Itanium C++ ABI

此页面列出了 NDK 支持的 ABI,并提供了有关每个 ABI 工作原理的信息。

ABI 也可以指平台支持的原生 API。有关影响 32 位系统的此类 ABI 问题的列表,请参阅 32 位 ABI 错误

支持的 ABI

表 1. ABI 和支持的指令集。

ABI 支持的指令集 备注
armeabi-v7a
  • armeabi
  • Thumb-2
  • Neon
  • 与 ARMv5/v6 设备不兼容。
    arm64-v8a
  • AArch64
  • 仅限 Armv8.0。
    x86
  • x86 (IA-32)
  • MMX
  • SSE/2/3
  • SSSE3
  • 不支持 MOVBE 或 SSE4。
    x86_64
  • x86-64
  • MMX
  • SSE/2/3
  • SSSE3
  • SSE4.1、4.2
  • POPCNT
  • 完整的 x86-64-v1,但仅部分 x86-64-v2(无 CMPXCHG16B 或 LAHF-SAHF)。

    注意:历史上,NDK 支持 ARMv5 (armeabi) 以及 32 位和 64 位 MIPS,但在 NDK r17 中删除了对这些 ABI 的支持。

    armeabi-v7a

    此 ABI 适用于 32 位 ARM CPU。它包括 Thumb-2 和 Neon。

    有关非 Android 特定的 ABI 部分的信息,请参阅 ARM 架构的应用程序二进制接口 (ABI)

    NDK 的构建系统默认生成 Thumb-2 代码,除非您在用于 ndk-build 的 Android.mk 中使用 LOCAL_ARM_MODE 或在配置 CMake 时使用 ANDROID_ARM_MODE

    有关 Neon 历史记录的更多信息,请参阅 Neon 支持

    出于历史原因,此 ABI 使用 -mfloat-abi=softfp,导致所有 float 值都在整数寄存器中传递,所有 double 值都在进行函数调用时在整数寄存器对中传递。尽管名称如此,但这只会影响浮点调用约定:编译器仍将对算术运算使用硬件浮点指令。

    此 ABI 使用 64 位 long double(与 double 相同,均为 IEEE binary64)。

    arm64-v8a

    此 ABI 适用于 64 位 ARM CPU。

    有关此 ABI 中非 Android 特定部分的完整详细信息,请参阅 Arm 的 了解架构。Arm 还在 64 位 Android 开发 中提供了一些移植建议。

    您可以在 C 和 C++ 代码中使用 Neon 内联函数 来利用高级 SIMD 扩展。有关 Neon 内联函数和 Neon 编程的一般信息,请参阅 Armv8-A 的 Neon 编程指南

    在 Android 上,特定于平台的 x18 寄存器保留用于 ShadowCallStack,您的代码不应触碰它。当前版本的 Clang 在 Android 上默认使用 -ffixed-x18 选项,因此除非您手写汇编程序(或使用非常旧的编译器),否则您无需担心这个问题。

    此 ABI 使用 128 位 long doubleIEEE binary128)。

    x86

    此 ABI 适用于支持通常称为“x86”、“i386”或“IA-32”的指令集的 CPU。

    Android 的 ABI 包括基本指令集以及 MMXSSESSE2SSE3SSSE3 扩展。

    此 ABI 不包括任何其他可选的 IA-32 指令集扩展,例如 MOVBE 或任何 SSE4 变体。您仍然可以使用这些扩展,只要您使用运行时功能探测来启用它们,并为不支持它们的设备提供回退即可。

    NDK 工具链假定在函数调用之前进行 16 字节堆栈对齐。默认工具和选项会强制执行此规则。如果您正在编写汇编代码,则必须确保维护堆栈对齐,并确保其他编译器也遵守此规则。

    有关更多详细信息,请参阅以下文档

    此 ABI 使用 64 位 long double(与 double 相同,均为 IEEE binary64,而不是更常见的 80 位仅 Intel 的 long double)。

    x86_64

    此 ABI 适用于支持通常称为“x86-64”的指令集的 CPU。

    Android 的 ABI 包括基本指令集以及 MMXSSESSE2SSE3SSSE3SSE4.1SSE4.2 和 POPCNT 指令。

    此 ABI 不包括任何其他可选的 x86-64 指令集扩展,例如 MOVBE、SHA 或任何 AVX 变体。您仍然可以使用这些扩展,只要您使用运行时功能探测来启用它们,并为不支持它们的设备提供回退即可。

    有关更多详细信息,请参阅以下文档

    此 ABI 使用 128 位 long doubleIEEE binary128)。

    为特定 ABI 生成代码

    Gradle

    Gradle(无论通过 Android Studio 还是从命令行使用)默认情况下都会为所有未弃用的 ABI 构建。要限制应用程序支持的 ABI 集,请使用 abiFilters。例如,要仅为 64 位 ABI 构建,请在您的 build.gradle 中设置以下配置

    android {
        defaultConfig {
            ndk {
                abiFilters 'arm64-v8a', 'x86_64'
            }
        }
    }
    

    ndk-build

    ndk-build 默认情况下会为所有未弃用的 ABI 构建。您可以通过在 Application.mk 文件中设置 APP_ABI 来定位特定的 ABI。以下代码片段显示了一些使用 APP_ABI 的示例

    APP_ABI := arm64-v8a  # Target only arm64-v8a
    APP_ABI := all  # Target all ABIs, including those that are deprecated.
    APP_ABI := armeabi-v7a x86_64  # Target only armeabi-v7a and x86_64.
    

    有关您可以为 APP_ABI 指定的值的更多信息,请参阅 Application.mk

    CMake

    使用 CMake 时,您一次为单个 ABI 构建,并且必须显式指定您的 ABI。您可以使用 ANDROID_ABI 变量来执行此操作,该变量必须在命令行上指定(不能在您的 CMakeLists.txt 中设置)。例如

    $ cmake -DANDROID_ABI=arm64-v8a ...
    $ cmake -DANDROID_ABI=armeabi-v7a ...
    $ cmake -DANDROID_ABI=x86 ...
    $ cmake -DANDROID_ABI=x86_64 ...
    

    有关必须传递给 CMake 以使用 NDK 构建的其他标志,请参阅 CMake 指南

    构建系统的默认行为是在单个 APK 中包含每个 ABI 的二进制文件,也称为 胖 APK。胖 APK 的大小明显大于仅包含单个 ABI 的二进制文件的 APK;权衡是获得更广泛的兼容性,但以更大的 APK 为代价。强烈建议您利用 应用包APK 分割 来减小 APK 的大小,同时仍然保持最大的设备兼容性。

    在安装时,包管理器仅解压缩目标设备最合适的机器代码。有关详细信息,请参阅 安装时自动提取本地代码

    Android 平台上的 ABI 管理

    本节提供有关 Android 平台如何管理 APK 中的本地代码的详细信息。

    应用程序包中的本地代码

    Play 商店和包管理器都期望在 APK 内部的文件路径上找到 NDK 生成的库,这些文件路径与以下模式匹配

    /lib/<abi>/lib<name>.so
    

    这里,<abi>支持的 ABI 下列出的 ABI 名称之一,<name> 是您在 Android.mk 文件中为 LOCAL_MODULE 变量定义的库的名称。由于 APK 文件只是 zip 文件,因此打开它们并确认共享的本地库位于正确的位置非常简单。

    如果系统未在预期位置找到本地共享库,则无法使用它们。在这种情况下,应用程序本身必须复制库,然后执行 dlopen()

    在胖 APK 中,每个库都位于名称与相应 ABI 匹配的目录下。例如,胖 APK 可能包含

    /lib/armeabi/libfoo.so
    /lib/armeabi-v7a/libfoo.so
    /lib/arm64-v8a/libfoo.so
    /lib/x86/libfoo.so
    /lib/x86_64/libfoo.so
    

    **注意:**如果同时存在 armeabiarmeabi-v7a 目录,则运行 4.0.3 或更早版本的基于 ARMv7 的 Android 设备会从 armeabi 目录而不是 armeabi-v7a 目录安装本地库。这是因为 /lib/armeabi/ 在 APK 中位于 /lib/armeabi-v7a/ 之后。此问题从 4.0.4 开始修复。

    Android 平台 ABI 支持

    Android 系统在运行时知道它支持哪些 ABI,因为特定于构建的系统属性指示

    • 设备的主 ABI,对应于系统映像本身中使用的机器代码。
    • 可选的辅助 ABI,对应于系统映像也支持的其他 ABI。

    此机制确保系统在安装时从包中提取最佳机器代码。

    为了获得最佳性能,您应该直接为主要 ABI 编译。例如,典型的基于 ARMv5TE 的设备只会定义主要 ABI:armeabi。相比之下,典型的基于 ARMv7 的设备会将主要 ABI 定义为 armeabi-v7a,将辅助 ABI 定义为 armeabi,因为它可以运行为每个 ABI 生成的应用程序本地二进制文件。

    64 位设备也支持其 32 位变体。以 arm64-v8a 设备为例,该设备还可以运行 armeabi 和 armeabi-v7a 代码。但是,请注意,如果您的应用程序针对 arm64-v8a 而不是依赖设备运行您的应用程序的 armeabi-v7a 版本,则它在 64 位设备上的性能会更好。

    许多基于 x86 的设备也可以运行 armeabi-v7aarmeabi NDK 二进制文件。对于此类设备,主要 ABI 将为 x86,第二个 ABI 为 armeabi-v7a

    您可以强制安装特定 ABI 的 apk。这对于测试很有用。使用以下命令

    adb install --abi abi-identifier path_to_apk
    

    安装时自动提取本地代码

    在安装应用程序时,包管理器服务会扫描 APK,并查找以下形式的任何共享库

    lib/<primary-abi>/lib<name>.so
    

    如果未找到,并且您已定义辅助 ABI,则服务会扫描以下形式的共享库

    lib/<secondary-abi>/lib<name>.so
    

    当它找到正在寻找的库时,包管理器会将它们复制到应用程序的本地库目录下的 /lib/lib<name>.so (<nativeLibraryDir>/)。以下代码段检索 nativeLibraryDir

    Kotlin

    import android.content.pm.PackageInfo
    import android.content.pm.ApplicationInfo
    import android.content.pm.PackageManager
    ...
    val ainfo = this.applicationContext.packageManager.getApplicationInfo(
            "com.domain.app",
            PackageManager.GET_SHARED_LIBRARY_FILES
    )
    Log.v(TAG, "native library dir ${ainfo.nativeLibraryDir}")
    

    Java

    import android.content.pm.PackageInfo;
    import android.content.pm.ApplicationInfo;
    import android.content.pm.PackageManager;
    ...
    ApplicationInfo ainfo = this.getApplicationContext().getPackageManager().getApplicationInfo
    (
        "com.domain.app",
        PackageManager.GET_SHARED_LIBRARY_FILES
    );
    Log.v( TAG, "native library dir " + ainfo.nativeLibraryDir );
    

    如果根本没有共享对象文件,则应用程序会构建并安装,但在运行时崩溃。

    ARMv9:为 C/C++ 启用 PAC 和 BTI

    启用 PAC/BTI 将提供针对某些攻击向量的保护。PAC 通过在函数的序言中对其进行加密签名来保护返回地址,并在结语中检查返回地址是否仍正确签名。BTI 通过要求每个分支目标都是一个特殊的指令来防止跳转到代码中的任意位置,该指令除了告诉处理器可以安全地到达那里之外什么也不做。

    Android 使用在不支持新指令的旧处理器上不执行任何操作的 PAC/BTI 指令。只有 ARMv9 设备才具有 PAC/BTI 保护,但您也可以在 ARMv8 设备上运行相同的代码:无需多个库变体。即使在 ARMv9 设备上,PAC/BTI 也仅适用于 64 位代码。

    启用 PAC/BTI 会导致代码大小略微增加,通常为 1%。

    有关 PAC/BTI 目标攻击向量以及保护工作原理的详细说明,请参阅 Arm 的 了解架构 - 为复杂软件提供保护PDF)。

    构建更改

    ndk-build

    在 Android.mk 的每个模块中设置 LOCAL_BRANCH_PROTECTION := standard

    CMake

    在您的 CMakeLists.txt 中,对每个目标使用 target_compile_options($TARGET PRIVATE -mbranch-protection=standard)

    其他构建系统

    使用 -mbranch-protection=standard 编译您的代码。此标志仅在为 arm64-v8a ABI 编译时有效。链接时无需使用此标志。

    故障排除

    我们不知道编译器对 PAC/BTI 支持有任何问题,但是

    • 注意在链接时不要混合 BTI 和非 BTI 代码,因为这会导致库未启用 BTI 保护。您可以使用 llvm-readelf 检查生成的库是否具有 BTI 注记。
    $ llvm-readelf --notes LIBRARY.so
    [...]
    Displaying notes found in: .note.gnu.property
      Owner                Data size    Description
      GNU                  0x00000010   NT_GNU_PROPERTY_TYPE_0 (property note)
        Properties:    aarch64 feature: BTI, PAC
    [...]
    $
    
    • OpenSSL 的旧版本(1.1.1i 之前的版本)在手写汇编程序中存在一个错误,会导致 PAC 失败。请升级到最新的 OpenSSL。

    • 某些应用 DRM 系统的旧版本生成的代码违反了 PAC/BTI 要求。如果您正在使用应用 DRM 并在启用 PAC/BTI 时遇到问题,请联系您的 DRM 供应商获取已修复的版本。