ABI de Android

Los diferentes dispositivos Android usan distintas CPU que, a su vez, admiten varios conjuntos de instrucciones. Cada combinación de CPU y conjunto de instrucciones tiene su propia interfaz binaria de aplicación (ABI). Una ABI incluye la siguiente información:

  • El conjunto de instrucciones de CPU (y las extensiones) que se pueden usar
  • El formato endian de las cargas y los almacenamientos en memoria durante el tiempo de ejecución (Android siempre es little endian)
  • Convenciones para pasar datos entre aplicaciones y el sistema, que incluyen restricciones de alineación y la manera en que el sistema usa la pila y los registros al llamar a funciones
  • El formato de objetos binarios ejecutables, como programas y bibliotecas compartidas, y los tipos de contenido que admiten (Android siempre usa ELF; para obtener más información, consulta Interfaz binaria de aplicación System V de ELF)
  • Cómo se alteran nombres de C++ (para obtener más información, consulta ABI Itanium C++ y genérica)

En esta página, se enumeran las ABI que admite el NDK y se proporciona información sobre cómo funciona cada una de ellas.

Las ABI también pueden hacer referencia a la API nativa compatible con la plataforma. Para obtener una lista de esos tipos de problemas de ABI que afectan a los sistemas de 32 bits, consulta errores de ABI de 32 bits .

ABI admitidas

Tabla 1: ABI y conjuntos de instrucciones admitidos

ABI Conjuntos de instrucciones admitidos Notas
armeabi-v7a
  • armeabi
  • Thumb-2
  • VFPv3-D16
  • No es compatible con dispositivos ARMv5 y v6.
    arm64-v8a
  • AArch64
  • x86
  • x86 (IA-32)
  • MMX
  • SSE/2/3
  • SSSE3
  • No admite MOVBE ni SSE4.
    x86_64
  • x86-64
  • MMX
  • SSE/2/3
  • SSSE3
  • SSE4.1, 4.2
  • POPCNT
  • Nota: Históricamente, el NDK admitía ARMv5 (armeabi) y MIPS de 32 bits y 64 bits, pero la compatibilidad con esas ABI se quitó en el NDK r17.

    armeabi-v7a

    Esta ABI es para CPU basadas en ARM de 32 bits. La variante de Android incluye Thumb-2 y las instrucciones del punto flotante de hardware VFP, específicamente VFPv3-D16, que incluye 16 registros de punto flotante de 64 bits dedicados.

    Para obtener información sobre las partes de la ABI que no son específicas de Android, consulta Interfaz binaria de la aplicación (ABI) para la arquitectura ARM.

    Los sistemas de compilación del NDK generan código Thumb-2 de manera predeterminada a menos que uses LOCAL_ARM_MODE en tu Android.mk para ndk-build o ANDROID_ARM_MODE cuando configuras CMake.

    Otras extensiones que incluyen Advanced SIMD (Neon) y VFPv3-D32 son opcionales. Para obtener más información, consulta Compatibilidad con NEON.

    La ABI armeabi-v7a usa -mfloat-abi=softfp para forzar la regla que, aunque el sistema pueda ejecutar código de punto flotante, el compilador debe pasar todos los valores float en registros para enteros y todos los valores double en pares de registros para enteros cuando realiza llamadas a funciones.

    arm64-v8a

    Esta ABI es para CPU basadas en ARMv8-A, que admiten la arquitectura AArch64 de 64 bits. Incluye las extensiones de arquitectura Advanced SIMD (Neon).

    Puedes usar los objetos intrínsecos de Neon en el código C y C++ para aprovechar la extensión Advanced SIMD. La Guía del programador de Neon para Armv8-A proporciona más información sobre los objetos intrínsecos de Neon y la programación de Neon en general.

    Consulta Aprende la arquitectura de Arm para obtener detalles completos de las partes de la ABI que no son específicas de Android. Arm también ofrece algunos consejos de portabilidad en Desarrollo de Android de 64 bits.

    En Android, el registro x18 específico de la plataforma está reservado para ShadowCallStack y tu código no debe tocarlo. Las versiones actuales de Clang usan, de forma predeterminada, la opción -ffixed-x18 en Android, así que, a menos que tengas un ensamblador manual (o un compilador muy antiguo), no deberías preocuparte.

    x86

    Esta ABI es para CPU que admiten el conjunto de instrucciones comúnmente conocido como "x86", "i386" o "IA-32". Entre las características de esta ABI, se incluyen las siguientes:

    • Instrucciones normalmente generadas por GCC con indicadores de compilador, como las siguientes:
          -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32
          

      Estas marcas se orientan al conjunto de instrucciones Pentium Pro, junto con las extensiones de conjuntos de instrucciones MMX, SSE, SSE2, SSE3 y SSSE3. El código que se genera es una optimización equilibrada entre las principales CPU Intel de 32 bits.

      Para obtener más información sobre los indicadores del compilador (especialmente relacionados con la optimización del rendimiento), consulta las sugerencias de rendimiento de x86 de GCC.

    • Uso de la convención de llamada estándar de 32 bits de x86 de Linux, en lugar de la de SVR. Para obtener más información, consulta la sección 6, "Uso de registros", del documento Convenciones de llamada para diferentes compiladores C++ y sistemas operativos.

    La ABI no incluye ninguna otra extensión opcional del conjunto de instrucciones IA-32, como las siguientes:

    • MOVBE
    • Cualquier variante de SSE4

    De todas maneras, podrás usar estas extensiones, siempre y cuando utilices sondeo de funciones en tiempo de ejecución para habilitarlas y proporciones reservas para los dispositivos que no las admitan.

    El conjunto de herramientas del NDK supone una alineación de pila de 16 bytes antes de una llamada a una función. Las herramientas y las opciones predeterminadas aplican esta regla. Si escribes código de ensamblado, debes asegurarte de mantener la alineación de la pila y de que los demás compiladores también cumplan esta regla.

    Para obtener más información, consulta los siguientes documentos:

    x86_64

    Esta ABI es para CPU que admiten el conjunto de instrucciones conocido como "x86-64". Admite instrucciones que GCC suele generar con los siguientes indicadores del compilador:

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

    Estas marcas se orientan al conjunto de instrucciones x86-64, de acuerdo con la documentación de GCC, junto con las extensiones de conjuntos de instrucciones MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2 y POPCNT. El código generado es una optimización equilibrada entre las principales CPU Intel de 64 bits.

    Para obtener más información sobre los indicadores del compilador (especialmente relacionados con la optimización del rendimiento), consulta Rendimiento de x86 de GCC.

    Esta ABI no incluye ninguna otra extensión opcional del conjunto de instrucciones x86-64, como las siguientes:

    • MOVBE
    • SHA
    • AVX
    • AVX2

    De todas maneras, podrás usar estas extensiones, siempre y cuando utilices sondeo de funciones en tiempo de ejecución para habilitarlas y proporciones reservas para los dispositivos que no las admitan.

    Para obtener más información, consulta los siguientes documentos:

    Generación de código para ABI específicas

    Gradle

    Gradle (ya sea que se use a través de Android Studio o desde la línea de comandos) se compila para todas las ABI no obsoletas de forma predeterminada. Para restringir el conjunto de ABI que admite tu aplicación, usa abiFilters. Por ejemplo, para compilar solo ABI de 64 bits, establece la siguiente configuración en tu build.gradle:

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

    ndk-build

    ndk-build compila todas las ABI no obsoletas de forma predeterminada. Para apuntar a una ABI específica, puedes configurar APP_ABI en tu archivo Application.mk. En el siguiente fragmento, se muestran algunos ejemplos del uso de 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.
        

    Para obtener más información sobre los valores que puedes especificar para APP_ABI, consulta Application.mk.

    CMake

    Con CMake, compilas una sola ABI a la vez y debes especificarla explícitamente. Lo haces con la variable ANDROID_ABI, que debe especificarse en la línea de comando (no se puede establecer en tu CMakeLists.txt). Por ejemplo:

    $ cmake -DANDROID_ABI=arm64-v8a ...
        $ cmake -DANDROID_ABI=armeabi-v7a ...
        $ cmake -DANDROID_ABI=x86 ...
        $ cmake -DANDROID_ABI=x86_64 ...
        

    Para conocer las otras marcas que se deben pasar a CMake para compilar con el NDK, consulta la Guía de CMake.

    De manera predeterminada, el sistema de compilación incluye los objetos binarios para cada ABI en un solo APK, también conocido como APK multiarquitectura. Un APK multiarquitectura es considerablemente más grande que el que solo contiene objetos binarios para una sola ABI; de este modo, se obtiene mayor compatibilidad, pero a costa de un APK más grande. Te recomendamos que aproveches los paquetes de aplicaciones o divisiones de APK para reducir el tamaño de tus archivos y mantener la máxima compatibilidad del dispositivo.

    Durante la instalación, el administrador de paquetes solo debe desempaquetar el código máquina más apropiado para el dispositivo de destino. Para obtener más detalles, consulta Extracción automática de código nativo al momento de la instalación.

    Administración de ABI en la plataforma de Android

    En esta sección, se proporciona información sobre cómo la plataforma de Android administra el código nativo en los APK.

    Código nativo en paquetes de apps

    Tanto Play Store como el administrador de paquetes esperan encontrar bibliotecas generadas con NDK en las rutas de acceso a archivos dentro del APK que coincidan con el siguiente patrón:

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

    En este caso, <abi> es uno de los nombres de ABI que aparecen en ABI admitidas y <name> es el nombre de la biblioteca tal como la definiste para la variable LOCAL_MODULE en el archivo Android.mk. Como los archivos APK son solo archivos ZIP, es muy fácil abrirlos para confirmar que las bibliotecas nativas compartidas están en el lugar correspondiente.

    Si el sistema no encuentra las bibliotecas nativas compartidas donde espera que estén, entonces no podrá usarlas. En ese caso, es la misma app la que tiene que copiar las bibliotecas y, luego, ejecutar dlopen().

    En el APK multiarquitectura, cada biblioteca se aloja en un directorio cuyo nombre coincide con una ABI correspondiente. Por ejemplo, un APK multiarquitectura puede contener lo siguiente:

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

    Nota: Los dispositivos Android basados en ARMv7 que ejecutan versiones 4.0.3 o anteriores instalan bibliotecas nativas desde el directorio armeabi en lugar de hacerlo desde armeabi-v7a, si existen ambos directorios. Esto se debe a que /lib/armeabi/ viene después de /lib/armeabi-v7a/ en el APK. Este problema se corrigió a partir de la versión 4.0.4.

    Compatibilidad de ABI en la plataforma de Android

    En el momento de la ejecución, el sistema Android sabe cuáles ABI admite, porque las propiedades del sistema específicas de la compilación indican lo siguiente:

    • La ABI principal del dispositivo, que corresponde al código máquina que se usa en la imagen del sistema.
    • De manera opcional, ABI secundarias, que corresponden a otra ABI que la imagen del sistema también admite.

    Este mecanismo garantiza que el sistema extraiga el mejor código máquina del paquete durante la instalación.

    Para obtener un mejor rendimiento, debes compilar directamente para la ABI principal. Por ejemplo, un dispositivo típico basado en ARMv5TE solo definiría la ABI principal: armeabi. Por el contrario, un dispositivo típico basado en ARMv7 definiría la ABI principal como armeabi-v7a y la secundaria como armeabi, ya que puede ejecutar objetos binarios nativos de la aplicación generados para cada una de ellas.

    Los dispositivos de 64 bits también admiten variantes de 32 bits. Si tomamos dispositivos arm64-v8a como ejemplo, el dispositivo también puede ejecutar código armeabi y armeabi-v7a. Sin embargo, ten en cuenta que tu app tendrá un mejor rendimiento en dispositivos de 64 bits si se orienta a arm64-v8a que si depende de un dispositivo que ejecuta la versión armeabi-v7a de tu app.

    Muchos dispositivos basados en x86 también pueden ejecutar objetos binarios armeabi-v7a y armeabi del NDK. Para esos dispositivos, la ABI principal sería x86, y la secundaria, armeabi-v7a.

    Puedes forzar la instalación de un APK para una ABI específica. Esto es útil para pruebas. Usa el siguiente comando:

        adb install --abi abi-identifier path_to_apk
        

    Extracción automática de código nativo al momento de la instalación

    Cuando instales una app, el servicio de administrador de paquetes escanea el APK y busca bibliotecas compartidas con el siguiente formato:

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

    Si no encuentra ninguna, y definiste una ABI secundaria, el servicio busca bibliotecas compartidas con el siguiente formato:

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

    Cuando encuentra las bibliotecas que está buscando, el administrador de paquetes las copia a /lib/lib<name>.so, en el directorio de bibliotecas nativas de la aplicación (<nativeLibraryDir>/). Los siguientes fragmentos recuperan el 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 );
        

    Si no se encuentra ningún archivo de objetos compartidos, la app se compila y se instala, pero falla en el tiempo de ejecución.