ABI для Android

На разных устройствах Android используются разные ЦП, которые поддерживают разные наборы команд. Каждое сочетание ЦП и набора команд имеет собственный двоичный интерфейс приложения (ABI). В ABI содержится следующая информация:

  • Набор команд ЦП (и расширений), которые можно использовать.
  • Порядок байтов при сохранении и загрузке памяти во время выполнения. Android всегда использует прямой порядок байтов.
  • Соглашения о передаче данных между приложениями и системой, включая ограничения выравнивания, а также о том, как система использует стек и регистры при вызове функций.
  • Формат исполняемых двоичных файлов, таких как программы и общие библиотеки, и типы поддерживаемого ими контента. Android всегда использует ELF. Подробную информацию можно найти в статье ELF System V двоичный интерфейс приложения.
  • Как декорируются имена C++. Дополнительную информацию можно найти в статье Generic/Itanium C++ ABI.

На этой странице перечислены ABI, которые поддерживает NDK, и приведена информация о том, как работает каждый ABI.

ABI также может относиться к нативному API, поддерживаемому платформой. Список таких проблем с ABI, затрагивающих 32-разрядные системы, приведен в разделе Ошибки ABI в 32-разрядных системах.

Поддерживаемые 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
  • CMPXCHG16B;
  • LAHF-SAHF
  • Полная поддержка x86-64-v2.

    Примечание. Ранее NDK поддерживал ARMv5 (armeabi), а также 32- и 64-разрядные MIPS, но в NDK r17 поддержка этих ABI была удалена.

    armeabi-v7a

    Этот ABI предназначен для 32-разрядных процессоров ARM. Включает Thumb-2 и Neon.

    Информацию о частях ABI, не относящихся к Android, можно найти в статье Двоичный интерфейс приложений (ABI) для архитектуры ARM.

    Системы сборки NDK по умолчанию генерируют код Thumb-2, если вы не используете LOCAL_ARM_MODE в файле Android.mk для ndk-build или ANDROID_ARM_MODE при настройке CMake.

    Дополнительную информацию об истории Neon можно найти в справочном центре Neon.

    По историческим причинам этот ABI использует -mfloat-abi=softfp, из-за чего при вызове функций все значения float передаются в целочисленных регистрах, а все значения double – в парах целочисленных регистров. Несмотря на название, это влияет только на соглашение о вызовах с плавающей запятой: компилятор по-прежнему будет использовать аппаратные инструкции с плавающей запятой для арифметики.

    Этот ABI использует 64-битный long double (IEEE binary64, как и double).

    arm64-v8a

    Этот ABI предназначен для 64-разрядных процессоров ARM.

    Полную информацию о частях ABI, не относящихся к Android, можно найти в разделе Learn the Architecture (Изучение архитектуры) на сайте Arm. Компания Arm также предлагает рекомендации по переносу в статье 64-bit Android Development (Разработка 64-разрядных приложений для Android).

    Чтобы использовать расширение Advanced SIMD, вы можете добавить в код на C и C++ встроенные функции Neon. Дополнительную информацию о встроенных функциях Neon и программировании Neon в целом можно найти в руководстве по программированию Neon для Armv8-A.

    На Android регистр x18 зарезервирован для ShadowCallStack, и ваш код не должен его затрагивать. В текущих версиях Clang по умолчанию используется параметр -ffixed-x18 для Android, поэтому, если у вас нет написанного вручную ассемблера (или очень старого компилятора), вам не нужно об этом беспокоиться.

    Этот ABI использует 128-битный тип long double (IEEE binary128).

    x86

    Этот ABI предназначен для процессоров, поддерживающих набор команд, известный как x86, i386 или IA-32.

    ABI Android включает базовый набор команд и расширения MMX, SSE, SSE2, SSE3 и SSSE3.

    ABI не включает другие необязательные расширения набора команд IA-32, такие как MOVBE или любой вариант SSE4. Вы можете использовать эти расширения, если включите их с помощью проверки функций во время выполнения и предоставите резервные варианты для устройств, которые их не поддерживают.

    Тулчейн NDK предполагает 16-байтовое выравнивание стека перед вызовом функции. Стандартные инструменты и параметры обеспечивают соблюдение этого правила. Если вы пишете код на ассемблере, убедитесь, что выравнивание стека сохраняется и другие компиляторы также соблюдают это правило.

    Подробную информацию можно найти в следующих документах:

    Этот ABI использует 64-битный long double (IEEE binary64, как и double, а не более распространенный 80-битный long double, который используется только в Intel).

    x86_64

    Этот ABI предназначен для процессоров, поддерживающих набор команд, который обычно называют x86-64-v2.

    ABI Android включает базовый набор команд, а также MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.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, поддержка которых не прекращена. Вы можете настроить таргетинг на определенные ABI, задав значение APP_ABI в файле Application.mk. Ниже показано несколько примеров использования 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 за раз и должны явно указать его. Для этого используется переменная 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, также известный как fat APK. Такой файл значительно больше, чем файл, содержащий двоичные файлы только для одного ABI. Таким образом, вы получаете более широкую совместимость, но за счет большего размера APK. Чтобы уменьшить размер APK-файлов, сохранив при этом максимальную совместимость с устройствами, настоятельно рекомендуем использовать наборы App Bundle или раздельные APK-файлы.

    При установке менеджер пакетов распаковывает только машинный код, наиболее подходящий для целевого устройства. Подробнее об автоматическом извлечении нативного кода при установке…

    Управление ABI на платформе Android

    В этом разделе рассказывается, как платформа Android управляет нативным кодом в APK-файлах.

    Нативный код в пакетах приложений

    Google Play и Package Manager ожидают найти библиотеки, созданные NDK, по путям к файлам в APK, соответствующим следующему шаблону:

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

    Здесь <abi> – одно из названий ABI, перечисленных в разделе Поддерживаемые ABI, а <name> – название библиотеки, которое вы указали для переменной LOCAL_MODULE в файле Android.mk. Поскольку APK-файлы – это ZIP-архивы, их можно легко открыть и проверить, где находятся общие нативные библиотеки.

    Если система не найдет общие библиотеки в ожидаемом месте, она не сможет их использовать. В этом случае приложение должно само скопировать библиотеки, а затем выполнить dlopen().

    В таком файле APK каждая библиотека находится в каталоге, название которого совпадает с соответствующим ABI. Например, в состав такого файла APK могут входить:

    /lib/armeabi-v7a/libfoo.so
    /lib/arm64-v8a/libfoo.so
    /lib/x86/libfoo.so
    /lib/x86_64/libfoo.so
    

    Поддержка ABI платформы Android

    Система Android определяет, какие ABI она поддерживает, во время выполнения, поскольку системные свойства, относящиеся к сборке, указывают:

    • Основной ABI для устройства, соответствующий машинному коду, который используется в самом образе системы.
    • Дополнительные ABI, соответствующие другим ABI, которые также поддерживаются образом системы (необязательно).

    Этот механизм гарантирует, что при установке система извлечет из пакета лучший машинный код.

    Вы можете принудительно установить APK для определенного ABI. Это может быть полезно для тестирования на устройствах, поддерживающих несколько ABI. Используйте следующую команду:

    adb install --abi abi-identifier path_to_apk
    

    Автоматическое извлечение нативного кода при установке

    При установке приложения сервис Package Manager сканирует 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: включение PAC и BTI для C/C++

    Включение PAC/BTI обеспечит защиту от некоторых векторов атак. PAC защищает адреса возврата, подписывая их криптографически в прологе функции и проверяя, что адрес возврата по-прежнему правильно подписан в эпилоге. BTI предотвращает переход к произвольным местам в коде, требуя, чтобы каждая цель ветвления была специальной инструкцией, которая только сообщает процессору, что переход в это место безопасен.

    Android использует инструкции PAC/BTI, которые не выполняются на старых процессорах, не поддерживающих новые инструкции. Защита PAC/BTI будет работать только на устройствах ARMv9, но вы можете запускать тот же код и на устройствах ARMv8. Создавать несколько вариантов библиотеки не нужно. Даже на устройствах ARMv9 PAC и BTI применяются только к 64-битному коду.

    Включение PAC/BTI приведет к небольшому увеличению размера кода, обычно на 1%.

    Подробное описание векторов атак, на которые нацелены PAC и BTI, и принципов работы защиты можно найти в документе Learn the architecture – Providing protection for complex software (Изучение архитектуры – обеспечение защиты для сложного программного обеспечения) (PDF) от Arm.

    Изменения в сборке

    ndk-build

    Задайте LOCAL_BRANCH_PROTECTION := standard в каждом модуле файла Android.mk.

    CMake

    Используйте target_compile_options($TARGET PRIVATE -mbranch-protection=standard) для каждой цели в файле CMakeLists.txt.

    Другие системы сборки

    Скомпилируйте код с помощью -mbranch-protection=standard. Этот флаг работает только при компиляции для ABI arm64-v8a. При связывании этот флаг использовать не нужно.

    Устранение неполадок

    Нам неизвестно о каких-либо проблемах с поддержкой PAC/BTI в компиляторе, но:

    • При связывании не смешивайте код с BTI и без него, иначе защита BTI не будет включена в библиотеке. Чтобы проверить, есть ли в библиотеке запись BTI, можно использовать llvm-readelf.
    $ 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 за исправленной версией.