Android ABI

Android デバイスによって、使用している CPU が異なるため、サポートされる命令セットも異なります。そのため、CPU と命令セットの組み合わせごとに、それぞれ専用のアプリケーション バイナリ インターフェース(ABI)が用意されています。ABI には次の情報が格納されています。

  • 使用可能な CPU 命令セット(および拡張)。
  • 実行時のメモリストアとメモリロードのエンディアン。Android は常にリトルエンディアンです。
  • アプリとシステムとの間でデータを受け渡す際の規則。たとえば、アライメントに関する制約や、システムが関数の呼び出し時にスタックやレジスタを使用する方法などです。
  • 実行バイナリのフォーマット(プログラム、共有ライブラリ、サポートするコンテンツのタイプなど)。Android は常に ELF を使用します。詳細については、System V Application Binary Interface をご覧ください。
  • C++ の名前マングルの方法。詳細については、Generic / Itanium C++ ABI をご覧ください。

このページでは、NDK がサポートしている ABI と、各 ABI の機能について説明します。

ABI は、プラットフォームでサポートされているネイティブ API も参照できます。32 ビットシステムに影響を及ぼす、その種の ABI の問題の一覧については、32 ビット ABI のバグをご覧ください。

サポート対象 ABI

表 1. ABI とサポート対象命令セット

ABI サポート対象命令セット
armeabi-v7a
  • armeabi
  • Thumb-2
  • ネオン
  • ARMv5 / ARMv6 デバイスと互換性なし。
    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
  • CMPXCHG16B
  • 完全な x86-64-v1、ただし x86-64-v2 の一部のみ(LAHF-SAHF なし)。

    注: これまで NDK は ARMv5(armeabi)、32 ビットおよび 64 ビットの MIPS をサポートしていましたが、こうした ABI のサポートは NDK r17 で削除されました。

    armeabi-v7a

    この ABI は 32 ビットの ARM CPU 向けです。Thumb-2 と Neon が含まれています。

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

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

    Neon の歴史の詳細については、NEON サポートをご覧ください。

    歴史的な理由から、この ABI では -mfloat-abi=softfp を使用するため、関数呼び出し時にすべての float 値が整数レジスタで渡され、すべての double 値が整数レジスタペアで渡されます。その名前に反して、これは浮動小数点の呼び出し規則にのみ影響します。コンパイラは、演算にはハードウェア浮動小数点命令を使用します。

    この ABI は 64 ビット long doubledouble と同じ IEEE binary64)を使用します。

    arm64-v8a

    この ABI は 64 ビットの ARM CPU 向けです。

    Android 固有ではない ABI の部分について詳しくは、Arm 社の Learn the Architecture をご覧ください。また Arm 社は、移植に関するアドバイスを 64-bit Android Development で提供しています。

    C および C++ コードで NEON 組み込みを使用して、Advanced SIMD 拡張機能を利用できます。NEON 組み込みと NEON プログラミングの全般について詳しくは、Neon Programmer's Guide for Armv8-A をご覧ください。

    Android では、プラットフォーム固有の x18 レジスタは ShadowCallStack 用に予約されているため、コードでこのレジスタを使用しないでください。現在のバージョンの Clang は Android においてデフォルトで -ffixed-x18 オプションを使用するため、手書きのアセンブラ(または極めて古いコンパイラ)を使用する場合を除き、この点を心配する必要はありません。

    この ABI は 128 ビット long doubleIEEE binary128)を使用します。

    x86

    この ABI は、一般に「x86」、「i386」、「IA-32」と呼ばれる命令セットをサポートしている CPU 向けです。

    Android の ABI には、基本命令セットと MMXSSESSE2SSE3SSSE3 の拡張機能が含まれます。

    ABI には、MOVBE や SSE4 のバリアントなど、オプションの IA-32 命令セット拡張機能は含まれていません。このような拡張機能を利用するには、ランタイム機能プロービングを使用して対象の拡張機能を有効にし、サポートしていないデバイス向けにフォールバックを提供する必要があります。

    NDK ツールチェーンは、関数を呼び出す際、16 バイトのスタック アライメントを想定しています。デフォルトのツールと設定の場合、このルールが自動的に適用されます。アセンブリ コードを記述している場合、スタック アライメントを維持する必要があります。また、他のコンパイラにもこのルールを遵守させる必要があります。

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

    この ABI は 64 ビット long double(一般的な 80 ビット Intel 専用の long double ではなく、double と同じ IEEE binary64)を使用します。

    x86_64

    この ABI は、一般的に「x86-64」と呼ばれる命令セットをサポートしている CPU 向けです。

    Android の ABI は、ベースの命令セットのほか、MMXSSESSE2SSE3SSSE3SSE4.1SSE4.2、POPCNT 命令を含みます。

    ABI には、MOVBE、SHA、または AVX のバリアントなど、その他のオプションの x86-64 命令セット拡張機能は含まれていません。このような拡張機能を利用するには、ランタイム機能プロービングを使用して対象の拡張機能を有効にし、サポートしていないデバイス向けにフォールバックを提供する必要があります。

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

    この ABI は 128 ビット long doubleIEEE binary128)を使用します。

    特定の 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 のバイナリが 1 つの APK に組み込まれます。このような APK をファット APK と呼びます。ファット APK は、単一の ABI のバイナリだけを含む APK と比べて、サイズがかなり大きくなります。互換性が拡大するというメリットがありますが、サイズが大きくなるのは大きなデメリットとなります。したがって、App Bundle または APK 分割を利用して、APK のサイズを縮小しつつ、デバイス互換性を最大限維持していく方法を強くおすすめします。

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

    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
    

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

    アプリのインストール時、パッケージ管理システム サービスは 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 は、関数のプロローグでリターン アドレスを暗号で署名し、リターンアドレスがエピローグでも正しく署名されていることを確認することで、リターン アドレスを保護します。BTI は、各分岐ターゲットを、到達していれば問題ないことをプロセッサに伝える以外は何も行わない特別な命令とすることを要求して、コード内の任意の位置へのジャンプを防ぎます。

    Android は、新しい命令に対応していない以前のプロセッサでは何も行わない PAC / BTI 命令を使用します。PAC / BTI による保護が実装されるのは ARMC9 デバイスのみですが、同じコードは ARMv8 デバイスでも実行できます。ライブラリの複数のバリアントを用意する必要はありません。ARMv9 デバイスでも、PAC / BTI は 64 ビット コードにのみ適用されます。

    PAC / BTI を有効にすると、コードサイズが若干(通常は 1%)増えます

    攻撃ベクトルの PAC / BTI ターゲットと、保護の仕組みについて詳しくは、arm の Learn the architecture - Providing protection for complex softwarePDF)をご覧ください。

    ビルドの変更

    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 note があるかどうかを確認できます。
    $ 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
    [...]
    $
    
    • 以前のバージョン(1.1.1i より前)の OpenSSL には、手書きのアセンブラに PAC 障害の原因となるバグがあります。最新の OpenSSL にアップグレードしてください。

    • 一部のアプリの DRM システムの古いバージョンで生成されるコードは、PAC / BTI の要件に反しています。アプリの DRM を使用していて、PAC / BTI を有効にすると問題が発生する場合は、修正済みのバージョンについて DRM の提供元にお問い合わせください。