Android NDK开发详解之架构和 CPU中的Android ABI
原生开发套件 (NDK) 是一套工具,使您能够在 Android 应用中使用 C 和 C++ 代码,并提供众多平台库,您可使用这些平台库管理原生 activity 和访问实体设备组件,例如传感器和触控输入。NDK 可能不适合大多数 Android 编程初学者,这些初学者只需使用 Java 代码和框架 API 开发应用
Android NDK开发详解之架构和 CPU中的Android ABI
使用原生代码时,硬件很重要。NDK 提供各种 ABI 供您选择,可让您确保针对正确的架构和 CPU 进行编译。
本部分介绍了在构建时如何面向特定的架构和 CPU,如何使用 ARM Neon 扩展指令集,以及在运行时如何使用 CPU 功能库查询可选功能。
不同的 Android 设备使用不同的 CPU,而不同的 CPU 支持不同的指令集。CPU 与指令集的每种组合都有专属的应用二进制接口 (ABI)。ABI 包含以下信息:
可使用的 CPU 指令集(和扩展指令集)。
运行时内存存储和加载的字节顺序。Android 始终是 little-endian。
在应用和系统之间传递数据的规范(包括对齐限制),以及系统调用函数时如何使用堆栈和寄存器。
可执行二进制文件(例如程序和共享库)的格式,以及它们支持的内容类型。Android 始终使用 ELF。如需了解详情,请参阅 ELF System V 应用二进制接口。
如何重整 C++ 名称。如需了解详情,请参阅 Generic/Itanium C++ ABI。
本页列举了 NDK 支持的 ABI,并且介绍了每个 ABI 的运行原理。
ABI 还可以指平台支持的原生 API。如需影响 32 位系统的此类 ABI 问题列表,请参阅 32 位 ABI 错误。
支持的 ABI

注意:NDK 以前支持 ARMv5 (armeabi) 以及 32 位和 64 位 MIPS,但 NDK r17 已不再支持。
armeabi-v7a
此 ABI 适用于 32 位 ARM CPU。它包含 Thumb-2 和 Neon (VFP) 硬件浮点指令,具体是指具有 16 个专用 64 位浮点寄存器的 VFPv3-D16。
如需详细了解 ABI 中并非特定于 Android 的部分,请参阅 ARM 架构的应用二进制接口 (ABI)
默认情况下,NDK 构建系统会生成 Thumb-2 代码,除非您在 Android.mk 中针对 ndk-build 使用 LOCAL_ARM_MODE,或在配置 CMake 时使用 ANDROID_ARM_MODE。
包括高级 SIMD (Neon) 和 VFPv3-D32 在内的其他扩展程序都是可选的。如需了解详情,请参阅 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 扩展指令集。针对 Armv8-A 的 Neon 程序员指南详细介绍了 Neon 固有特性和 Neon 程序设计的概况。
在 Android 中,特定于平台的 x18 寄存器专用于 ShadowCallStack,不应由您的代码使用。当前的 Clang 版本默认使用 Android 中的 -ffixed-x18 选项,因此除非您使用的是手写汇编程序(或非常旧的编译器),否则无需担心这一点。
此 ABI 使用 128 位 long double (IEEE binary128)。
x86
此 ABI 适用于支持通常称为“x86”“i386”或“IA-32”的指令集的 CPU。
Android 的 ABI 包括基础指令集以及 MMX、SSE、SSE2 和 SSE3 和 SSSE3 扩展指令集。
ABI 不含任何其他可选 IA-32 扩展指令集,例如 MOVBE 或 SSE4 的任何变体。您仍可使用这些扩展指令集,只要您使用运行时功能探测来启用它们,并且为不支持它们的设备提供回退机制。
NDK 工具链假设在函数调用之前进行 16 字节堆栈对齐。默认工具和选项会强制实施此规则。如果编写的是汇编代码,必须确保堆栈对齐,而且其他编译器也遵守此规则。
请参阅以下文档了解更多详情:
不同 C++ 编译器和操作系统的调用规范
Intel IA-32 Intel 架构软件开发者手册第 2 卷:指令集参考
Intel IA-32 Intel 架构软件开发者手册第 3 卷:系统编程指南
System V 应用二进制接口:Intel386 处理器架构补充
此 ABI 使用 64 位 long double(与 double 相同的 IEEE binary64,而不是更为常见的仅限 Intel 的 80 位 long double)。
x86_64
此 ABI 适用于支持通常称为“x86-64”的指令集的 CPU。
Android 的 ABI 包括基础指令集以及 MMX、SSE、SSE2、SSE3、SSSE3、SSE4.1、SSE4.2 和 POPCNT 指令。
ABI 不含任何其他可选 x86-64 扩展指令集,例如 MOVBE、SHA 或 AVX 的任何变体。 您仍可使用这些扩展指令集,只要您使用运行时功能探测来启用它们,并且为不支持它们的设备提供回退机制。
请参阅以下文档了解更多详情:
不同 C++ 编译器和操作系统的调用规范
Intel64 和 IA-32 架构软件开发者手册第 2 卷:指令集参考
Intel64 和 IA-32 Intel 架构软件开发者手册第 3 卷:系统编程
此 ABI 使用 128 位 long double (IEEE binary128)。
为特定 ABI 生成代码
默认情况下,Gradle(无论是通过 Android Studio 使用,还是从命令行使用)会针对所有非弃用 ABI 进行构建。要限制应用支持的 ABI 集,请使用 abiFilters。例如,要仅针对 64 位 ABI 进行构建,请在 build.gradle 中设置以下配置:
Gradle
android {
defaultConfig {
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
}
ndk-build
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.
CMake
$ cmake -DANDROID_ABI=arm64-v8a ...
$ cmake -DANDROID_ABI=armeabi-v7a ...
$ cmake -DANDROID_ABI=x86 ...
$ cmake -DANDROID_ABI=x86_64 ...
构建系统的默认行为是将每个 ABI 的二进制文件包括在单个 APK(也称为胖 APK)内。与仅包含单个 ABI 的二进制文件的 APK 相比,胖 APK 要大得多;要权衡的是兼容性更广,但 APK 更大。强烈建议您利用 app bundle 和 APK 拆分减小 APK 的大小,同时仍保持最大限度的设备兼容性。
在安装时,软件包管理器只解压缩最适合目标设备的机器代码。如需了解详情,请参阅安装时自动解压缩原生代码。
Android 平台上的 ABI 管理
本部分详细说明了 Android 平台如何管理 APK 中的原生代码。
应用软件包中的原生代码
Play 商店和软件包管理器都希望能在 APK 中符合以下格式的文件路径上找到 NDK 生成的库:
/lib/<abi>/lib<name>.so
其中, 是支持的 ABI 下列出的 ABI 名称之一, 是您为 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,因为 build 特定的系统属性会指示:
设备的主要 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。
您可以为特定 ABI 强制安装 apk。这在测试时非常有用。请使用以下命令:
adb install --abi abi-identifier path_to_apk
安装时自动解压缩原生代码
安装应用时,软件包管理器服务将扫描 APK,并查找以下形式的任何共享库:
lib//lib.so
如果未找到,并且您已定义辅助 ABI,该服务将扫描以下形式的共享库:
lib//lib.so
找到所需的库时,软件包管理器会将它们复制到应用的原生库目录 (/) 下的 /lib/lib.so。以下代码段会收到 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 可防范一些攻击途径。PAC 通过以下方式来保护返回地址:在函数 prolog 中以加密形式对这些地址进行签名,并检查返回地址是否仍在 epilog 中进行了正确签名。为了避免跳转到代码中的任意位置,BTI 要求每个分支目标都是一条特殊指令,除了告知处理器可以到达这个位置之外,该指令不会执行任何其他操作。
Android 使用 PAC/BTI 指令,这些指令在不支持新指令的旧版处理器上不会执行任何操作。只有 ARMv9 设备具有 PAC/BTI 保护功能,但您也可以在 ARMv8 设备上运行相同的代码:无需库的多个变体。即使在 ARMv9 设备上,PAC/BTI 也仅适用于 64 位代码。
启用 PAC/BTI 后,代码大小会略有增加,通常为 1%。
如需详细了解攻击途径 PAC/BTI 目标以及保护功能的运作方式,请参阅 Arm 的了解架构 - 为复杂软件提供保护 (PDF)。
build 变更
注意:在将代码视为攻击者的目标时,我们建议您也使用 CFI 进行构建。CFI 和 PAC/BTI 类似,但互补。
ndk-build
CMake
其他构建系统
在 Android.mk 的每个模块中设置 LOCAL_BRANCH_PROTECTION := standard。
问题排查
我们不了解编译器对 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 之前的版本)在手写汇编程序中存在一个 bug,会导致 PAC 失败。升级到当前的 OpenSSL。
某些应用 DRM 系统的旧版本会生成违反 PAC/BTI 要求的代码。如果您使用的是应用 DRM 并且在启用 PAC/BTI 时遇到问题,请与您的 DRM 供应商联系以获取修复的版本。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐

所有评论(0)