Android ABI

不同的 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

表 1. ABI 及支援的指令集。

ABI 支援的指令集 附註
armeabi-v7a
  • armeabi
  • Thumb-2
  • 霓虹
  • 與 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。

    注意: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 binary64double 相同)。

    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 包含基礎指令集,以及 MMXSSESSE2SSE3SSSE3 擴充功能。

    此 ABI 不含任何其他選用的 IA-32 指令集擴充功能,例如 MOVBE 或任何 SSE4 變化版本。您仍可使用這些擴充功能,前提是您必須透過執行階段功能探測來啟用上述擴充功能,並為不支援此類擴充功能的裝置提供備用選項。

    NDK 工具鏈假設在呼叫函式之前 16 位元組堆疊已對齊。預設工具和選項會強制執行這項規則。如要編寫組譯程式碼,請務必確保堆疊對齊,並確定其他編譯器也遵循這項規則。

    詳情請參閱下列文件:

    此 ABI 會使用 64 位元 long double (IEEE binary64double 相同,而不是較常見的 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 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-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
    

    找到所需程式庫後,套件管理員會將這些程式庫複製到應用程式的原生資料庫目錄 (<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 供應商聯絡,取得修正版本。