Android ABI

Android デバイスによって、使用している 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 / ARMv6 デバイスと互換性なし。
    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、4.2
  • POPCNT
  • 注: これまで NDK は ARMv5(armeabi)、32 ビットおよび 64 ビットの MIPS をサポートしていましたが、こうした ABI のサポートは NDK r17 で削除されました。

    armeabi-v7a

    この ABI は 32 ビットの ARM ベース CPU 向けです。Android バリアントには Thumb-2 と、16 個の専用 64 ビット浮動小数点レジスタを搭載した VFP ハードウェア浮動小数点命令(具体的には VFPv3-D16)が含まれています。

    ABI の Android 固有ではない部分については、Application Binary Interface (ABI) for the ARM Architecture をご覧ください。

    NDK のビルドシステムはデフォルトで Thumb-2 コードを生成します。ただし、ndk-build 用の Android.mkLOCAL_ARM_MODE を使用する場合や、CMake の設定時に ANDROID_ARM_MODE を使用する場合は除きます。

    Advanced SIMD(Neon)や VFPv3-D32 など、その他の拡張はオプションです。 詳細については、NEON サポートをご覧ください。

    armeabi-v7a ABI は -mfloat-abi=softfp を使用することで、システムに浮動小数点コードの実行を許可する一方、コンパイラが関数を呼び出す際にはすべての float 値を整数レジスタで、すべての double 値を整数レジスタペアで渡すようにするルールを適用します。

    arm64-v8a

    この ABI は、64 ビットの AArch64 アーキテクチャをサポートする ARMv8-A ベースの CPU 向けです。Advanced SIMD(Neon)アーキテクチャ拡張が含まれています。

    C および C++ コードで NEON イントリンシックを使用して、Advanced SIMD 拡張機能を利用できます。NEON イントリンシックと NEON プログラミングの全般について詳しくは、Armv8-A 向け NEON プログラマー ガイドをご覧ください。

    Android 固有ではない ABI の部分について詳しくは、Arm のアーキテクチャについての記事をご覧ください。また 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 パフォーマンスのヒントをご覧ください。

    • SVR 向けの規則ではなく、標準 Linux x86 32 ビット呼び出し規則を使用します。詳細については、各種 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
    

    こうしたフラグは、GCC のドキュメントにあるように、x86-64 命令セットをターゲットにしています。また、MMXSSESSE2SSE3SSSE3SSE4.1SSE4.2POPCNT の命令セット拡張機能もターゲットにしています。生成されるコードは、上位の Intel 32 ビット CPU 全体でバランスを取った形で最適化されます。

    コンパイラ フラグの詳細、特にパフォーマンスの最適化に関するフラグについては、GCC x86 パフォーマンスをご覧ください。

    この ABI には、次のような必須でない他の x86-64 命令セット拡張機能は含まれていません。

    • MOVBE
    • SHA
    • AVX
    • AVX2

    このような拡張機能を利用するには、ランタイム機能プロービングを使用して対象の拡張機能を有効にし、サポートしていないデバイス向けにフォールバックを提供する必要があります。

    詳細については、以下のドキュメントをご覧ください。

    特定の ABI 向けのコードを生成する

    Gradle

    Android Studio 経由とコマンドラインからのどちらで使用する場合でも、Gradle はデフォルトで、サポートが終了していないすべての ABI 向けにビルドします。アプリがサポートする ABI のセットを制限するには、abiFilters を使用します。たとえば、64 ビット ABI のみを対象にビルトするには、build.gradle で以下のように設定します。

    android {
        defaultConfig {
            ndk {
                abiFilters 'arm64-v8a', 'x86_64'
            }
        }
    }
    

    ndk-build

    ndk-build はデフォルトで、サポートが終了していないすべての ABI 向けにビルドします。特定の ABI をターゲットにするには、Application.mk ファイルで APP_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 では、1 つの 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 ...
    

    NDK を使用してビルドするために CMake に渡す必要があるその他のフラグについては、CMake ガイドをご覧ください。

    ビルドシステムのデフォルト動作では、各 ABI のバイナリが単一の APK に組み込まれます。このような APK をファット APK と呼びます。ファット APK は、単一の ABI のバイナリだけを含む APK と比べて、サイズがかなり大きくなります。互換性が拡大するというメリットがありますが、サイズが大きくなるのは大きなデメリットとなります。したがって、App Bundle または APK 分割を利用して、APK のサイズを縮小しつつ、デバイス互換性を最大限維持していく方法を強くおすすめします。

    インストール時、Package Manager は、ターゲット デバイスに最も適したマシンコードだけを解凍します。詳細については、インストール時のネイティブ コードの自動抽出をご覧ください。

    Android プラットフォーム上での ABI 管理

    このセクションでは、Android プラットフォームが APK 内のネイティブ コードを管理する方法について説明します。

    アプリ パッケージ内のネイティブ コード

    Play ストアも Package Manager も、次のパターンに合致する APK 内のファイルパスに基づいて、NDK が生成したライブラリを検索します。

    /lib/<abi>/lib<name>.so
    

    この例において、<abi>サポート対象 ABI のリストに含まれる 1 つの 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
    

    注: Android 4.0.3 以前を搭載している ARMv7 ベースの Android デバイスは、armeabi ディレクトリと armeabi-v7a ディレクトリの両方が存在する場合、armeabi ディレクトリからネイティブ ライブラリをインストールします。これは、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 コードも実行することができます。ただし、アプリを 64 ビットデバイス上で実行する場合は、arm64-v8a をターゲットにした方が armeabi-v7a バージョンを実行するよりも優れたパフォーマンスを発揮します。

    x86 ベースデバイスも多くの場合、armeabi-v7a NDK バイナリと armeabi NDK バイナリを実行できます。このようなデバイスの場合、プライマリ ABI を x86 に設定し、セカンダリ ABI を armeabi-v7a に設定します。

    特定の ABI 向けの APK を強制的にインストールすることもできます。 これはテストに便利です。次のコマンドを使用します。

    adb install --abi abi-identifier path_to_apk
    

    インストール時のネイティブ コードの自動抽出

    アプリのインストール時、Package Manager サービスは APK をスキャンし、次の形式の共有ライブラリを検索します。

    lib/<primary-abi>/lib<name>.so
    

    ライブラリが見つからず、かつ、セカンダリ ABI が定義されていた場合、このサービスは次の形式の共有ライブラリがないかスキャンします。

    lib/<secondary-abi>/lib<name>.so
    

    探しているライブラリが見つかると、Package Manager はそのライブラリを、アプリのネイティブ ライブラリのディレクトリ(<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 );
    

    共有オブジェクト ファイルがまったく存在しない場合、アプリのビルドとインストールは行われますが、実行時にクラッシュします。