不同的 Android 裝置使用不同的 CPU,而不同的 CPU 支援的指令集各異。每種 CPU 和指令集組合都有專屬的應用程式二進位檔介面 (ABI)。ABI 包含下列資訊:
- 可使用的 CPU 指令集 (和擴充功能)。
- 記憶體在執行階段儲存及載入的字節順序。Android 向來是由小到大排列位元組順序。
- 在應用程式與系統之間傳遞資料的慣例 (包括對齊限制),以及系統在呼叫函式時如何使用堆疊和暫存器。
- 程式及共用程式庫等可執行二進位檔的格式,以及這些二進位檔支援的內容類型。Android 一律使用 ELF。詳情請參閱 ELF System V 應用程式二進位檔介面。
- C++ 名稱如何遭到破壞。詳情請參閱 Generic/Itanium C++ ABI。
本頁面列舉 NDK 支援的 ABI,並介紹每個 ABI 的運作方式。
ABI 也可以指平台支援的原生 API。如需此類會影響 32 位元系統的 ABI 問題清單,請參閱 32 位元 ABI 錯誤。
支援的 ABI
ABI | 支援的指令集 | 附註 |
---|---|---|
armeabi-v7a |
|
與 ARMv5/v6 裝置不相容。 |
arm64-v8a |
僅限 Armv8.0。 | |
x86 |
不支援 MOVBE 或 SSE4。 | |
x86_64 |
|
完整的 x86-64-v1,但僅支援部分 x86-64-v2 (沒有 LAHF-SAHF)。 |
注意:NDK 先前支援 ARMv5 (armeabi) 以及 32 位元和 64 位元 MIPS,但 NDK r17 已不再支援這些 ABI。
armeabi-v7a
此 ABI 適用於 32 位元 ARM CPU,其中包括 Thumb-2 和 Neon。
如要進一步瞭解 ABI 中非 Android 專屬的部分,請參閱 ARM 架構的應用程式二進位檔介面 (ABI)。
若未在 Android.mk
使用 LOCAL_ARM_MODE
做為 ndk-build,或設定 CMake 時選擇 ANDROID_ARM_MODE
,NDK 的建構系統預設會產生 Thumb-2 程式碼。
如要進一步瞭解 Neon 的發展歷程,請參閱 Neon 支援。
基於歷史原因,此 ABI 會使用 -mfloat-abi=softfp
,導致在呼叫函式時,所有 float
值會透過整數暫存器傳遞,而所有 double
值會透過整數暫存器組合傳遞。儘管名稱如此,但這只會影響浮點呼叫慣例:編譯器仍會使用硬體浮點指令進行算術運算。
這個 ABI 會使用 64 位元 long double
(IEEE binary64 與 double
相同)。
arm64-v8a
此 ABI 適用於 64 位元 ARM CPU。
請參閱 Arm 的「學習架構指南」,瞭解 ABI 中非 Android 專屬部分的完整資訊。另外,Arm 也針對 64 位元 Android 開發作業提供移植方面的建議。
您可以在 C 和 C++ 程式碼中使用 Neon 內建函式,就能充分運用進階 SIMD 擴充功能。如要進一步瞭解 Neon 內建函式,以及 Neon 程式設計概覽,請參閱 Armv8-A 架構的 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
(IEEE binary64 與 double
相同,而不是較常見的 80 位元 Intel 專用 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
根據預設,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 指南。
根據預設,建構系統會將每個 ABI 的二進位檔放入單一 APK (也稱為笨重的 APK) 內。與僅含有單一 ABI 二進位檔的 APK 相比,笨重的 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
注意:如果同時有 armeabi
目錄和 armeabi-v7a
目錄,則搭載 4.0.3 以下版本的 ARMv7 型 Android 裝置會從前者安裝原生資料庫,而不會透過後者安裝。這是因為在 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
。
您可以為特定 ABI 強制安裝 APK,這在測試時相當實用。請使用下列指令:
adb install --abi abi-identifier path_to_apk
在安裝時自動擷取原生程式碼
安裝應用程式時,套件管理員服務會掃描 APK,並尋找下列格式的任何共用程式庫:
lib/<primary-abi>/lib<name>.so
如果找不到任何結果,而且您已定義輔助 ABI,套件管理員服務就會掃描下列格式的共用程式庫:
lib/<secondary-abi>/lib<name>.so
找到所需程式庫後,套件管理員會將這些程式庫複製到應用程式的原生資料庫目錄 (<nativeLibraryDir>/
) 下的 /lib/lib<name>.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 會以加密方式在函式的 prolog 中簽署回傳地址,並檢查這些地址是否仍確實在 epilog 中完成簽署,藉此保護上述地址。為了避免跳到程式碼中的任何位置,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 供應商聯絡,取得修正版本。