阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680
一、简介
使用原生代码时,硬件很重要。NDK 提供各种 ABI 供您选择,可让您确保针对正确的架构和 CPU 进行编译。
本部分介绍了在编译时如何面向特定的架构和 CPU,如何使用 ARM NEON 扩展指令集,以及在运行时如何使用 cpufeatures 库查询可选功能。
二、ABI 管理
不同的 Android 手机使用不同的 CPU,而不同的 CPU 支持不同的指令集。CPU 与指令集的每种组合都有专属的应用二进制接口,即 ABI。ABI 可以非常精确地定义应用的机器代码在运行时如何与系统交互。您必须为应用要使用的每个 CPU 架构指定 ABI。
典型的 ABI 包含以下信息:
- 机器代码应使用的 CPU 指令集。
- 运行时内存存储和加载的字节顺序。
- 可执行二进制文件(例如程序和共享库)的格式,以及它们支持的内容类型。
- 在代码与系统之间传递数据的各种规范。这些规范包括对齐限制,以及系统调用函数时如何使用堆栈和寄存器。
- 运行时可用于机器代码的函数符号列表 - 通常来自非常具体的库集。
本页列举了 NDK 支持的 ABI,并且介绍了每个 ABI 的运行原理。如需 32 位系统上的 ABI 问题列表,请参阅 32 位 ABI 错误
每个 ABI 支持一个或多个指令集。表 1 概述了每个 ABI 支持的指令集。
表 1. ABI 和支持的指令集。
![19956127-06d41123a3a0a5e4.png](https://i-blog.csdnimg.cn/blog_migrate/098ee59c20c5b305264270facc9b4009.png)
注意:NDK 以前支持 32 位和 64 位 MIPS,但这项支持已在 NDK r17 中移除。
下面提供了关于每个 ABI 的更多详细信息。
armeabi
注意:此 ABI 已在 NDK r17 中移除。
此 ABI 适用于基于 ARM、至少支持 ARMv5TE 指令集的 CPU。详情请参阅以下文档:
- ARM 架构参考手册
- ARM 架构的过程调用标准
- ARM ELF 文件格式
- ARM 架构的应用二进制接口 (ABI)
- ARM 架构的基本平台 ABI
- ARM 架构的 C 库 ABI
- ARM 架构的 C++ ABI
- ARM 架构的运行时 ABI
- ELF System V 应用二进制接口
- Generic/Itanium C++ ABI
AAPCS 标准将 EABI 定义为类似但不同 ABI 的系列。此外,Android 还采用小字节序 ARM GNU/Linux ABI。
此 ABI 不支持硬件辅助的浮点运算。相反,所有浮点运算都使用编译器的 libgcc.a
静态库中的软件辅助函数。
armeabi ABI 支持 ARM 的 Thumb(亦称 Thumb-1)指令集。NDK 默认生成 Thumb 代码,除非您在 Android.mk
文件中使用 LOCAL_ARM_MODE
变量指定不同的行为。
armeabi-v7a
此 ABI 可扩展 armeabi 以包含多个 CPU 扩展指令集。此 Android 特定 ABI 支持的扩展指令包括:
- Thumb-2 扩展指令集,其性能堪比 32 位 ARM 指令,简洁性类似于 Thumb-1。
- VFP 硬件 FPU 指令。更具体一点,是指 VFPv3-D16,它除了 ARM 核心中的 16 个 32 位寄存器之外,还包含 16 个专用 64 位浮点寄存器。
v7-a ARM 规范描述的其他扩展指令集,包括高级 SIMD(亦称 NEON)、VFPv3-D32 和 ThumbEE,都是此 ABI 的可选扩展指令集。由于不能保证它们存在,因此系统在运行时应检查扩展指令集是否可用。如果不可用,您必须使用替代代码路径。此检查类似于系统在检查或使用 MMX、SSE2 及 x86 CPU 上其他专用指令集时所执行的检查。
要了解如何执行这些运行时检查,请参阅 cpufeatures
库。此外,如需有关 NDK 支持为 NEON 编译机器代码的信息,请参阅 NEON 支持。
armeabi-v7a
ABI 使用 -mfloat-abi=softfp
开关强制实施以下规则:编译器在函数调用期间必须传递核心寄存器对中的所有双精度值,而不是专用浮点值。系统可以使用 FP 寄存器执行所有内部计算。这样可极大地提高计算速度。
arm64-v8a
此 ABI 适用于基于 ARMv8、支持 AArch64 的 CPU。它还包含 NEON 和 VFPv4 指令集。
如需了解更多信息,请参阅 ARMv8 技术预览,并联系 ARM 进一步了解详情。
x86
此 ABI 适用于支持通常称为“x86”或“IA-32”的指令集的 CPU。此 ABI 的特性包括:
- 指令一般由具有编译器标记的 GCC 生成,如下所示:
-march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32
这些标记指向 Pentium Pro 指令集,以及 MMX、SSE、SSE2、SSE3 及 SSSE3 扩展指令集。生成的代码在顶层 Intel 32 位 CPU 之间进行了均衡优化。
如需了解有关编译器标记的更多信息,特别是与性能优化相关的信息,请参阅 GCC x86 性能提示。
- 使用标准 Linux x86 32 位调用规范,与 SVR 使用的规范相反。详情请参阅不同 C++ 编译器和操作系统的调用规范的第 6 部分“寄存器的使用”。
ABI 不含任何其他可选 IA-32 扩展指令集,例如:
- MOVBE
- SSE4 的任何变体。
您仍可使用这些扩展指令集,只要您使用运行时功能探测来启用它们,并且为不支持它们的设备提供回退机制。
NDK 工具链假设在函数调用之前进行 16 字节堆栈对齐。默认工具和选项会强制实施此规则。如果编写的是汇编代码,必须确保堆栈对齐,而且其他编译器也遵守此规则。
请参阅以下文档了解更多详情:
- GCC 在线文档:Intel 386 和 AMD x86-64 选项
- 不同 C++ 编译器和操作系统的调用规范
- Intel IA-32 Intel 架构软件开发者手册第 2 卷:指令集参考
- Intel IA-32 Intel 架构软件开发者手册第 3 卷:系统编程指南
- System V 应用二进制接口:Intel386 处理器架构补充
x86_64
此 ABI 适用于支持通常称为“x86-64”的指令集的 CPU。它支持 GCC 通常使用以下编译器标记生成的指令:
-march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel
这些标记指向 x86-64 指令集(根据 GCC 文档),以及 MMX、SSE、SSE2、SSE3、SSSE3、SSE4.1、SSE4.2 和 POPCNT 扩展指令集。生成的代码在顶层 Intel 64 位 CPU 之间进行了均衡优化。
如需了解有关编译器标记的更多信息,特别是与性能优化相关的信息,请参阅 GCC x86 性能。
此 ABI 不含任何其他可选的 x86-64 扩展指令集,例如:
- MOVBE
- SHA
- AVX
- AVX2
您仍可使用这些扩展指令集,只要您使用运行时功能探测来启用它们,并且为不支持它们的设备提供回退机制。
请参阅以下文档了解更多详情:
为特定 ABI 生成代码
默认情况下,NDK 指向所有非弃用 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
变量指定的值,请参阅 Android.mk。
编译系统的默认行为是将每个 ABI 的二进制文件包括在单个 APK(也称为胖 APK)内。与仅包含单个 ABI 的二进制文件的 APK 相比,胖 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
注意:搭载 4.0.3 或更早版本、基于 ARMv7 的 Android 设备从 armeabi
目录(而非 armeabi-v7a
目录,如果两个目录都存在)安装原生库。这是因为在 APK 中,/lib/armeabi/
在 /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-v7a
和 armeabi
NDK 二进制文件。对于这些设备,主要 ABI 将是 x86
,辅助 ABI 是 armeabi-v7a
。
安装时自动解压缩原生代码
安装应用时,软件包管理器服务将扫描 APK,并查找以下形式的任何共享库:
lib/<primary-abi>/lib<name>.so
如果未找到,并且您已定义辅助 ABI,该服务将扫描以下形式的共享库:
lib/<secondary-abi>/lib<name>.so
找到所需的库时,软件包管理器会将它们复制到应用的 data
目录 (data/data/<package_name>/lib/
) 下的 /lib/lib<name>.so
。
如果根本没有共享对象文件,应用也会编译并安装,但在运行时会崩溃。
三、处理CPU功能
ABI:使用预处理器的预定义宏
通常,在编译时使用 #ifdef
及以下各项确定 ABI 最为方便:
- 对于 32 位 ARM,使用
__arm__
- 对于 64 位 ARM,使用
__aarch64__
- 对于 32 位 X86,使用
__i386__
- 对于 64 位 X86,使用
__x86_64__
请注意:32 位 X86 称为 __i386__
,而不是 __x86__
,这可能与您预想的有所不同!
CPU 核心计数:使用 libc 的 sysconf(3)
sysconf(3) 既可以查询 _SC_NPROCESSORS_CONF
(系统中的 CPU 核心数),又可以查询 _SC_NPROCESSORS_ONLN
(当前在线的 CPU 核心数)。
功能:使用 libc 的 getauxval(3)
自 API 级别 18 开始,Android 的 C 库开始支持 etauxval(3)。AT_HWCAP
和 AT_HWCAP2
参数会返回列出特定 CPU 功能的位掩码。请参阅 NDK 中的各种 hwcap.h
头文件以获取要进行比较的常量,如用于 arm64 SHA512 指令的 HWCAP_SHA512
,或用于 arm Thumb 整数除法指令的 HWCAP_IDIVT
。
Google cpu_features 库
AT_HWCAP
的一个问题是有时设备会出错。例如,一些旧设备宣称拥有整数除法指令,但实际上并没有。
Google 的 cpu_features 库凭借其对特定 SoC 的了解(通过解析 /proc/cpuinfo
掌握相应的 SoC),解决了此类问题。
另一个解决方案是为 SIGILL
安装信号处理程序,然后直接尝试执行相关指令。例如,BoringSSL/OpenSSL 使用的就是这种方法。
NDK cpufeatures 库
NDK 提供了一个名为 cpufeatures
的小型库,其功能类似于 getauxval(3),但它也可用于 API 级别 18 之前的版本。与其他 cpu_features 库不同,它对特定 SoC 并无额外的了解。
NDK cpufeatures API
uint64_t android_getCpuFeatures();
返回一组位标记,每个标记代表一个 CPU 系列特定的功能。本部分其余内容介绍了各个系列的功能。
32 位 ARM CPU 系列
以下标记适用于 32 位 ARM CPU 系列:
ANDROID_CPU_ARM_FEATURE_VFPv2
表示设备的 CPU 支持 VFPv2 指令集。大多数 ARMv6 CPU 都支持此指令集。
ANDROID_CPU_ARM_FEATURE_ARMv7
表示设备的 CPU 支持 armeabi-v7a ABI 所支持的 ARMv7-A 指令集。此指令集同时支持 Thumb-2 和 VFPv3-D16 指令。此返回值还表示支持 VFPv3 硬件 FPU 扩展指令集。
ANDROID_CPU_ARM_FEATURE_VFPv3
表示设备的 CPU 支持 VFPv3 硬件 FPU 扩展指令集。
此值等同于 VFPv3-D16 指令集,后者只提供 16 个硬件双精度 FP 寄存器。
ANDROID_CPU_ARM_FEATURE_VFP_D32
表示设备的 CPU 支持 32 个(而不是 16 个)硬件双精度 FP 寄存器。即使有 32 个硬件双精度 FP 寄存器,也只有 32 个单精度寄存器映射至同一寄存器组。
ANDROID_CPU_ARM_FEATURE_NEON
表示设备的 CPU 支持 ARM Advanced SIMD (NEON) 向量扩展指令集。请注意,ARM 要求这些 CPU 也要实现 VFPv3-D32,VFPv3-D32 提供 32 个硬件 FP 寄存器(与 NEON 单元共享)。
ANDROID_CPU_ARM_FEATURE_VFP_FP16
表示设备的 CPU 支持在 16 位寄存器上执行浮点运算的指令。该功能是 VFPv4 规范的一部分。
ANDROID_CPU_ARM_FEATURE_VFP_FMA
表示设备的 CPU 支持 VFP 指令集的融合乘积累加扩展指令集。它也是 VFPv4 规范的一部分。
ANDROID_CPU_ARM_FEATURE_NEON_FMA
表示设备的 CPU 支持 NEON 指令集的融合乘积累加扩展指令集。它也是 VFPv4 规范的一部分。
ANDROID_CPU_ARM_FEATURE_IDIV_ARM
表示设备的 CPU 在 ARM 模式下支持整数除法。仅适用于更高型号的 CPU,例如 Cortex-A15。
ANDROID_CPU_ARM_FEATURE_IDIV_THUMB2
表示设备的 CPU 在 Thumb-2 模式下支持整数除法。仅适用于更高型号的 CPU,例如 Cortex-A15。
ANDROID_CPU_ARM_FEATURE_iWMMXt
表示设备的 CPU 支持可添加 MMX 寄存器和指令的扩展指令集。该功能仅适用于少数几个基于 XScale 的 CPU。
ANDROID_CPU_ARM_FEATURE_LDREX_STREX
表示设备的 CPU 支持自 ARMv6 后可用的 LDREX 和 STREX 指令。借助专用监视器,这些指令结合起来可在内存中提供原子级更新。
64 位 ARM CPU 系列
以下标记适用于 64 位 ARM CPU 系列:
ANDROID_CPU_ARM64_FEATURE_FP
表示设备的 CPU 具有浮点单元 (FPU)。所有 Android ARM64 设备都必须支持此功能。
ANDROID_CPU_ARM64_FEATURE_ASIMD
表示设备的 CPU 具有高级 SIMD (ASIMD) 单元。所有 Android ARM64 设备都必须支持此功能。
ANDROID_CPU_ARM64_FEATURE_AES
表示设备的 CPU 支持 AES 指令。
ANDROID_CPU_ARM64_FEATURE_CRC32
表示设备的 CPU 支持 CRC32 指令。
ANDROID_CPU_ARM64_FEATURE_SHA1
表示设备的 CPU 支持 SHA1 指令。
ANDROID_CPU_ARM64_FEATURE_SHA2
表示设备的 CPU 支持 SHA2 指令。
ANDROID_CPU_ARM64_FEATURE_PMULL
表示设备的 CPU 支持 64 位 PMULL 和 PMULL2 指令。
32 位 x86 CPU 系列
以下标记适用于 32 位 x86 CPU 系列。
ANDROID_CPU_X86_FEATURE_SSSE3
表示设备的 CPU 支持 SSSE3 扩展指令集。
ANDROID_CPU_X86_FEATURE_POPCNT
表示设备的 CPU 支持 POPCNT 指令。
ANDROID_CPU_X86_FEATURE_MOVBE
表示设备的 CPU 支持 MOVBE 指令。此指令特定于某些 Intel IA-32 CPU,例如 Atom。
android_getCpuFeatures() 针对没有列出扩展指令集的 CPU 系列返回 0。
int android_getCpuCount(void);
注意:考虑到 sysconf(3),此函数通常无用。
返回系统中的 CPU 核心数,可能高于实际在线的核心数。
AndroidCpuFamily android_getCpuFamily();
注意:考虑到预处理器宏,此函数通常无用。
返回以下其中一个常量,代表设备所支持的 CPU 系列/架构:
ANDROID_CPU_FAMILY_ARM
ANDROID_CPU_FAMILY_X86
ANDROID_CPU_FAMILY_ARM64
ANDROID_CPU_FAMILY_X86_64
对于 64 位系统上的 32 位可执行文件,此函数返回 32 位架构。
将 NDK cpufeatures 与 ndk-build 搭配使用
cpufeatures
库可用作导入模块。要使用此库,请执行以下操作:
将
cpufeatures
添加到LOCAL_STATIC_LIBRARIES
。在源代码中
#include <cpu-features.h>
。在 Android.mk 的末尾导入
android/cpufeatures
。
以下是导入 cpufeatures
的 Android.mk
示例:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := your-module-name
LOCAL_SRC_FILES := ...
LOCAL_STATIC_LIBRARIES := cpufeatures
include $(BUILD_SHARED_LIBRARY)
$(call import-module,android/cpufeatures)
三、NEON 支持
NDK 支持 ARM Advanced SIMD(通常称为 NEON),一种适用于 ARMv7 和 ARMv8 的可选扩展指令集。NEON 提供标量/向量指令和寄存器(与 FPU 共享),堪比 x86 中的 MMX/SSE/3DNow!。要运行 NEON,需要 VFPv3-D32(32 个硬件 FPU 64 位寄存器,而非最小值 16 个)。
并非所有基于 ARMv7 的 Android 设备都支持 NEON,但支持的设备可能会因 NEON 支持标量/向量指令而受益匪浅。
NDK 支持模块编译,甚至可以编译支持 NEON 的特定源文件。
使用 LOCAL_ARM_NEON
要让 NDK 编译支持 NEON 的所有源文件,请在模块定义中添加以下代码行:makefile LOCAL_ARM_NEON := true
如果您要编译仅包含 NEON 代码的静态或共享库,编译支持 NEON 的所有源文件可能特别有用。
使用 .neon 后缀
为 LOCAL_SRC_FILES 变量列出源文件时,您可以选择使用 .neon 后缀表示要编译支持 NEON 的单个文件。例如,以下示例会编译一个支持 NEON 的文件 (foo.c),以及另一个不支持 NEON 的文件 (bar.c):makefile LOCAL_SRC_FILES := foo.c.neon bar.c
您可将 .neon 后缀与 .arm 后缀相结合,后者指定用于非 NEON 指令的 32 位 ARM 指令集(而非 Thumb2)。在这种情况下,.arm 必须在 .neon 之前。例如:foo.c.arm.neon 可行,但 foo.c.neon.arm 不可行。
编译要求
NEON 支持适用于 [armeabi-v7a](https://developer.android.google.cn/ndk/guides/abis.html#v7a)
和 [arm64-v8a](https://developer.android.google.cn/ndk/guides/abis.html#arm64-v8a)
ABI。如果 NDK 编译脚本在尝试借助 NEON 支持进行编译时遇到其他 ABI,NDK 编译脚本将退出。务必在 Android.mk
文件中添加如下检查代码:
# define a static library containing our NEON code
ifeq ($(TARGET_ARCH_ABI),armeabi-v7a)
include $(CLEAR_VARS)
LOCAL_MODULE := mylib-neon
LOCAL_SRC_FILES := mylib-neon.c
LOCAL_ARM_NEON := true
include $(BUILD_STATIC_LIBRARY)
endif # TARGET_ARCH_ABI == armeabi-v7a
运行时检测
应用必须执行运行时检测,以确认支持 NEON 的机器代码能够在目标设备上运行。这是因为并非所有基于 ARMv7 的 Android 设备都支持 NEON。应用可以使用 NDK 随附的 cpufeatures
库执行此检查。
您应检查 android_getCpuFamily()
是否返回 ANDROID_CPU_FAMILY_ARM
,以及 android_getCpuFeatures()
是否返回一个值,并且设置了 ANDROID_CPU_ARM_FEATURE_NEON
标记。例如:
#include <cpu-features.h>
...
...
if (android_getCpuFamily() == ANDROID_CPU_FAMILY_ARM &&
(android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0)
{
// use NEON-optimized routines
...
}
else
{
// use non-NEON fallback routines instead
...
}
...
对 x86 提供跨平台支持
NDK 支持使用第三方 NEON_2_SSE.h 将您现有的 ARM SIMD (NEON) 内建函数跨平台编译为 x86 SSE 代码。要详细了解此内容,请参阅从 ARM NEON 到 Intel SSE:自动移植解决方案、提示与技巧。
示例代码
hello-neon 示例举例说明了如何同时使用 cpufeatures
库和 NEON 内建函数。此示例实现了 C 版本的微小 FIR 过滤器循环基准,并针对支持 NEON 的设备实现了经 NEON 优化的基准。
原文链接 https://developer.android.google.cn/ndk/guides/cpu-arm-neon
阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680