ABIs de Android

Los diferentes dispositivos Android usan distintas CPUs 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 los 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
  • Solo Armv8.0.
    x86
  • x86 (IA-32)
  • MMX
  • SSE/2/3
  • SSSE3
  • No admite MOVBE o SSE4.
    x86_64
  • x86-64
  • MMX
  • SSE/2/3
  • SSSE3
  • SSE4.1, 4.2
  • POPCNT
  • Solo x86-64-v1.

    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 CPUs basadas en ARM de 32 bits. Incluye Thumb-2 y las instrucciones de punto flotante de Neon (VFP), específicamente VFPv3-D16 con 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.

    Esta ABI usa -mfloat-abi=softfp para aplicar la regla que indica que el compilador debe pasar todos los valores float en registros enteros y todos los valores double en pares de registros enteros cuando realiza llamadas a funciones. Esto solo afecta a la convención de llamada. El compilador seguirá usando las instrucciones del punto flotante de hardware.

    Esta ABI usa un long double de 64 bits (IEEE binary igual que double).

    arm64-v8a

    Esta ABI es para CPUs basadas en ARM de 64 bits.

    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.

    Puedes usar las funciones intrínsecas 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.

    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.

    Esta ABI usa un long double de 128 bits (IEEE binary128).

    x86

    Esta ABI es para CPUs que admiten el conjunto de instrucciones comúnmente conocido como "x86", "i386" o "IA-32".

    La ABI de Android incluye el conjunto de instrucciones base más las extensiones MMX, SSE, SSE2, SSE3 ySSSE3.

    La ABI no incluye ninguna otra extensión opcional del conjunto de instrucciones IA-32, como MOVBE o 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 resguardos 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:

    Esta ABI usa un long double de 64 bits (IEEE binary64 igual que double, y no el long double más común de 80 bits solo para Intel).

    x86_64

    Esta ABI es para CPUs que admiten el conjunto de instrucciones comúnmente denominado "x86-64".

    La ABI de Android incluye el conjunto de instrucciones base más MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2 y la instrucción POPCNT.

    La ABI no incluye ninguna otra extensión opcional del conjunto de instrucciones x86-64, como MOVBE, SHA o cualquier variante de AVX. De todas maneras, podrás usar estas extensiones, siempre y cuando utilices sondeo de funciones en tiempo de ejecución para habilitarlas y proporciones resguardos para los dispositivos que no las admitan.

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

    Esta ABI usa un long double de 128 bits (IEEE binary128).

    Generación de código para ABIs 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 reside 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 en el 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 aplicación se compila y se instala, pero falla en el tiempo de ejecución.

    ARMv9: Habilitación de PAC y BTI para C/C++

    La habilitación de PAC/BTI brindará protección contra algunos vectores de ataque. PAC protege las direcciones de devolución con firmas criptográficas en el prolog de una función y verifica que dicha dirección de devolución esté firmada correctamente. BTI impide que se omitan ubicaciones arbitrarias en tu código, ya que requiere que cada destino de la rama sea una instrucción especial que solo le indica al procesador que es correcta esa ubicación.

    Android usa instrucciones de PAC/BTI que no funcionan en procesadores más antiguos que no admiten nuevas instrucciones. Solo los dispositivos ARMv9 tendrán la protección PAC/BTI, pero también puedes ejecutar el mismo código en dispositivos ARMv8, ya que no hay necesidad de múltiples variantes de tu biblioteca. Incluso en dispositivos ARMv9, PAC/BTI solo se aplica al código de 64 bits.

    Si habilitas PAC/BTI, habrá un leve aumento en el tamaño del código, que suele ser del 1%.

    Consulta la documentación de Arm sobre la arquitectura para brindar protección a software complejos (PDF) y obtén una explicación detallada de los vectores de ataque cuyo objetivo son los PAC/BTI y cómo funciona la protección.

    Cambios en la compilación

    ndk-build

    Configura LOCAL_BRANCH_PROTECTION := standard en cada módulo de tu archivo Android.mk.

    CMake

    Usa target_compile_options($TARGET PRIVATE -mbranch-protection=standard) para cada destino de tu CMakeLists.txt.

    Otros sistemas de compilaciones

    Compila tu código con -mbranch-protection=standard. Esta marca solo funciona cuando se compila para la ABI de arm64-v8a. No es necesario usar esta marca durante la vinculación.

    Solución de problemas

    No estamos al tanto de ningún problema con la compatibilidad del compilador para PAC/BTI, pero ten en cuenta lo siguiente:

    • No mezcles código BTI y código que no sea BTI durante la vinculación, ya que eso da como resultado una biblioteca sin protección BTI. Puedes usar llvm-readelf para verificar si tu biblioteca resultante tiene o no la nota 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
    [...]
    $
    
    • Las versiones anteriores de OpenSSL (anteriores a la 1.1.1i) tienen un error en el ensamblador escrito a mano que causa fallas de PAC. Actualiza a la versión actual de OpenSSL.

    • Las versiones anteriores de algunos sistemas DRM de la app generan un código que infringe los requisitos de PAC/BTI. Si usas la DRM de la app y tienes problemas para habilitar PAC/BTI, comunícate con tu proveedor de DRM para obtener una versión fija.