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
  • VFPv3-D16
  • 與 ARMv5/v6 裝置不相容。
    arm64-v8a
  • AArch64
  • x86
  • x86 (IA-32)
  • MMX
  • SSE/2/3
  • SSSE3
  • 不支援 MOVBE 或 SSE4。
    x86_64
  • x86-64
  • MMX
  • SSE/2/3
  • SSSE3
  • SSE4.1、SSE4.2
  • POPCNT
  • 注意:NDK 先前支援 ARMv5 (armeabi)、32 位元及 64 位元 MIPS,但 NDK r17 已不再支援這些 ABI。

    armeabi-v7a

    此 ABI 適用於 32 位元 ARM 架構 CPU。Android 變化版本包含 Thumb-2 和 VFP 硬體浮點指令 (具體來說就是 VFPv3-D16),其中包含 16 個專屬 64 位元浮點暫存器。

    如要進一步瞭解 ABI 中並非 Android 專屬的部分,請參閱「ARM 架構的應用程式二進位檔介面 (ABI)

    除非您在 Android.mk 中針對 ndk-build 使用 LOCAL_ARM_MODE,或者在設定 CMake 時使用 ANDROID_ARM_MODE,否則根據預設,NDK 的建構系統會產生 Thumb-2 程式碼。

    進階 SIMD (Neon) 及 VFPv3-D32 等其他擴充功能都是選用項目。 詳情請參閱 Neon 支援

    armeabi-v7a ABI 使用 -mfloat-abi=softfp 來強制執行以下規則:雖然系統可以執行浮點程式碼,但是編譯器在呼叫函式時必須傳遞整數暫存器中的所有 float 值,以及整數暫存器對中的所有 double 值。

    arm64-v8a

    此 ABI 適用於支援 64 位元 AArch64 架構的 ARMv8-A 型 CPU,而且具有進階 SIMD (Neon) 架構擴充功能。

    您可以在 C 和 C++ 程式碼中使用 Neon 內建函式,就能充分運用進階 SIMD 擴充功能。針對 Armv8-A 的 Neon 程式設計師指南中詳細介紹 Neon 內建函式,並提供 Neon 程式設計概覽。

    請參閱 Arm 的瞭解架構,瞭解 ABI 中並非 Android 專屬部分的完整資訊。另外,Arm 也針對 64 位元 Android 開發提供移植方面的建議。

    在 Android 中,平台專用的 x18 暫存器僅供 ShadowCallStack 使用,而不應供程式碼使用。目前的 Clang 版本預設使用 Android 中的 -ffixed-x18 選項,因此,除非您有使用手寫組譯工具 (或版本過舊的編譯器),否則不需要擔心這一點。

    x86

    此 ABI 適用於支援「x86」、「i386」或「IA-32」指令集的 CPU。此 ABI 的特性包括:

    • 指令一般由具有編譯器旗標的 GCC 產生,如下所示:
      -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32
      

      這些旗標的目標為 Pentium Pro 指令集,以及 MMXSSESSE2SSE3SSSE3 指令集擴充功能。 產生的程式碼在頂層 Intel 32 位元 CPU 之間進行了平衡最佳化。

      如要進一步瞭解編譯器旗標 (尤其是與效能最佳化相關的資訊),請參閱 GCC x86 效能提示

    • 使用標準 Linux x86 32 位元呼叫慣例,而不是 SVR 所用的慣例。詳情請參閱不同 C++ 編譯器和作業系統的呼叫慣例的第 6 節「暫存器使用方式」。

    此 ABI 不包含其他任何選用的 IA-32 指令集擴充功能,例如:

    • MOVBE
    • SSE4 的任何變化版本。

    您仍可以使用這些擴充功能,前提是您必須使用執行階段功能探測來啟用這些擴充功能,並為不支援這些擴充功能的裝置提供備用選項。

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

    請參閱下列文件,瞭解更多詳細資訊:

    x86_64

    此 ABI 適用於支援「x86-64」指令集的 CPU,並支援 GCC 通常透過以下編譯器旗標產生的指令:

    -march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel
    

    這些旗標的目標為 x86-64 指令集 (根據 GCC 說明文件所述),以及 MMXSSESSE2SSE3SSSE3SSE4.1SSE4.2 和 POPCNT 指令集擴充功能。產生的程式碼在頂層 Intel 64 位元 CPU 之間進行了平衡最佳化。

    如要進一步瞭解編譯器旗標 (尤其是與效能最佳化相關的資訊),請參閱 GCC x86 效能提示

    此 ABI 不包含其他任何選用的 x86-64 指令集擴充功能,例如:

    • MOVBE
    • SHA
    • AVX
    • AVX2

    您仍可以使用這些擴充功能,前提是您必須使用執行階段功能探測來啟用這些擴充功能,並為不支援這些擴充功能的裝置提供備用選項。

    請參閱下列文件,瞭解更多詳細資訊:

    為特定 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 裝置會從 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-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 供應商聯絡,取得修正版本。