ABIs do Android

Dispositivos Android diferentes usam CPUs diferentes, que são compatíveis com conjuntos de instruções variados. Cada combinação de CPU e conjunto de instruções tem a própria Interface binária do aplicativo (ABI, na sigla em inglês). Uma ABI inclui as seguintes informações:

  • o conjunto de instruções de CPU (e extensões) que pode ser usado;
  • Ordenação (endianness) de armazenamentos e cargas de memória no momento da execução. O Android é sempre little-endian.
  • Convenções para a transmissão de dados entre aplicativos e o sistema, incluindo restrições de alinhamento, e como o sistema usa a pilha e se registra quando chama funções.
  • Formato de binários executáveis, como programas e bibliotecas compartilhadas, e os tipos de conteúdo compatíveis. O Android sempre usa ELF. Para saber mais, consulte Interface binária do aplicativo ELF System V (link em inglês).
  • Como os nomes C++ são danificados. Para saber mais, consulte ABI genérica/Itanium C++ (link em inglês).

Esta página enumera as ABIs compatíveis com o NDK e traz informações sobre como cada ABI funciona.

A ABI também pode se referir à API nativa compatível com a plataforma. Para ver uma lista desses tipos de problema de ABI que afetam sistemas de 32 bits, consulte Bugs da ABI de 32 bits (link em inglês).

ABIs compatíveis

Tabela 1. ABIs e conjuntos de instruções compatíveis.

ABI Conjuntos de instrução compatíveis Notas
armeabi-v7a
  • armeabi
  • Thumb-2
  • VFPv3-D16
  • Incompatível com dispositivos ARMv5/v6.
    arm64-v8a
  • AArch64
  • x86
  • x86 (IA-32)
  • MMX
  • SSE/2/3
  • SSSE3
  • Sem compatibilidade com MOVBE ou SSE4.
    x86_64
  • x86-64
  • MMX
  • SSE/2/3
  • SSSE3
  • SSE4.1, 4.2
  • POPCNT
  • Observação: anteriormente, o NDK era compatível com a ARMv5 (armeabi) e a MIPS de 32 e 64 bits, mas a compatibilidade com essas ABIs foi removida no NDK r17.

    armeabi-v7a

    Esta ABI é para CPUs de 32 bits baseadas em ARM. A variante do Android inclui as instruções de ponto flutuante de hardware Thumb-2 e VFP, especificamente VFPv3-D16, que tem 16 registros dedicados de ponto flutuante de 64 bits.

    Para ver informações sobre as partes da ABI que não são específicas do Android, consulte Interface binária do aplicativo (ABI) para a arquitetura ARM (link em inglês)

    Os sistemas de compilação do NDK geram o código Thumb-2 por padrão, a menos que você use LOCAL_ARM_MODE no Android.mk para ndk-build ou ANDROID_ARM_MODE ao configurar o CMake.

    Outras extensões, incluindo Advanced SIMD (Neon) e VFPv3-D32, são opcionais. Para saber mais, consulte Compatibilidade com NEON.

    A ABI armeabi-v7a usa -mfloat-abi=softfp para aplicar a regra que, embora o sistema possa executar código de ponto flutuante, o compilador precisa transmitir todos os valores float em registros inteiros e todos os valores double em pares de registros inteiros ao fazer chamadas de função.

    arm64-v8a

    Essa ABI é destinada a CPUs baseadas em ARMv8-A compatíveis com a arquitetura AArch64 de 64 bits. Ela inclui as extensões da arquitetura Advanced SIMD (Neon).

    Você pode usar os intrínsecos do Neon (link em inglês) no código C e C++ para aproveitar a extensão Advanced SIMD. O Guia do programador do Neon para Armv8-A (link em inglês) traz mais informações específicas sobre os intrínsecos e a programação do Neon, em geral.

    Consulte a página Conheça a arquitetura (link em inglês) do Arm para ver detalhes completos sobre as partes da ABI que não são específicas do Android. O Arm também oferece algumas recomendações de portabilidade em Desenvolvimento Android de 64 bits.

    No Android, o registro x18 específico da plataforma é reservado para ShadowCallStack e não deve ser modificado pelo seu código. As versões atuais do Clang usam como padrão a opção -ffixed-x18 no Android. Portanto, a menos que você tenha um assembler escrito à mão (ou um compilador muito antigo), não se preocupe com isso.

    x86

    Essa ABI está destinada a CPUs compatíveis com o conjunto de instruções conhecido como "x86", "i386" ou "IA-32". São características dessa ABI:

    • Instruções normalmente geradas por GCC com sinalizações de compilador, por exemplo:
      -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32
      

      Essas sinalizações são voltadas ao conjunto de instruções do Pentium Pro, com as extensões de conjuntos de instruções MMX, SSE, SSE2, SSE3 e SSSE3. O código gerado é uma otimização balanceada nas principais CPUs Intel de 32 bits.

      Para saber mais sobre sinalizações do compilador, principalmente as relacionadas à otimização de desempenho, consulte Dicas de desempenho do GCC x86 (link em inglês).

    • Use a convenção de chamada padrão do Linux x86 de 32 bits em vez daquela do SVR. Para saber mais, consulte a seção 6, "Uso de registros", de Convenções de chamadas de diferentes compiladores C++ e sistemas operacionais (link em inglês).

    A ABI não contém nenhuma outra extensão de conjunto de instruções IA-32 opcional, como:

    • MOVBE
    • Qualquer variante do SSE4

    Você ainda pode usar essas extensões, desde que use sondagem de recursos no ambiente de execução para ativá-las, além de fornecer substitutos para dispositivos incompatíveis com elas.

    O conjunto de ferramentas do NDK presume um alinhamento de pilha de 16 bytes antes de uma chamada de função. As ferramentas e opções padrão aplicam essa regra. Se você estiver criando um código Assembly, será preciso manter o alinhamento de pilhas, além de garantir que outros compiladores também obedeçam a essa regra.

    Consulte os documentos a seguir para saber mais:

    x86_64

    Esta ABI se destina a CPUs compatíveis com o conjunto de instruções conhecido como “x86-64”. Ela é compatível com instruções que o GCC costuma gerar com as seguintes sinalizações de compilador:

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

    Essas sinalizações são voltadas ao conjunto de instruções x86-64, de acordo com a documentação do GCC, com as extensões de conjuntos de instruções MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2 e POPCNT. O código gerado é uma otimização balanceada na parte superior das CPUs Intel de 64 bits.

    Para saber mais sobre sinalizações de compilador, especialmente aquelas relacionadas à otimização de desempenho, consulte Desempenho do GCC x86 (link em inglês).

    Essa ABI não inclui nenhuma outra extensão de conjunto de instruções x86-64 opcional, como:

    • MOVBE
    • SHA
    • AVX
    • AVX2

    Você ainda pode usar essas extensões, desde que use sondagem de recursos no ambiente de execução para ativá-las, além de fornecer substitutos para dispositivos incompatíveis com elas.

    Consulte os documentos a seguir para saber mais:

    Gerar código para uma ABI específica

    Gradle

    Por padrão, o Gradle cria para todas as ABIs não obsoletas, seja ele usado por meio do Android Studio ou a partir da linha de comando. Para restringir o conjunto de ABIs compatíveis com seu app, use abiFilters. Por exemplo, para criar somente para ABIs de 64 bits, defina a seguinte configuração no build.gradle:

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

    ndk-build

    O ndk-build cria para todas as ABIs não obsoletas por padrão. Você pode segmentar uma ABI específica configurando APP_ABI no arquivo Application.mk. O snippet a seguir mostra alguns exemplos de como usar 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 saber mais sobre os valores que você pode especificar para APP_ABI, consulte Application.mk.

    CMake

    Com o CMake, você cria uma ABI de cada vez e precisa especificar sua ABI explicitamente. Isso é feito com a variável ANDROID_ABI, que precisa ser especificada na linha de comando e não pode ser definida no CMakeLists.txt. Por exemplo:

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

    Para as outras sinalizações que precisam ser transmitidas ao CMake para criar com o NDK, consulte o Guia do CMake.

    O comportamento padrão do sistema de compilação é incluir os binários para cada ABI em um único APK, também conhecido como APK multiarquitetura. Um APK multiarquitetura é significativamente maior que um contendo apenas os binários para uma única ABI. A vantagem é ganhar maior compatibilidade, embora o APK fique maior. É altamente recomendável que você aproveite os App Bundles ou as divisões de APK para reduzir o tamanho dos seus APKs mantendo a máxima compatibilidade com dispositivos.

    No momento da instalação, o gerenciador de pacotes descompacta somente o código de máquina mais adequado para o dispositivo de destino. Para saber mais, consulte Extração automática de código nativo no momento da instalação.

    Gerenciamento de ABIs na Plataforma Android

    Esta seção traz detalhes sobre como a Plataforma Android gerencia código nativo nos APKs.

    Código nativo em pacotes de apps

    Tanto a Play Store quanto o Gerenciador de pacotes esperam encontrar bibliotecas geradas pelo NDK em caminhos de arquivo que estejam dentro do APK e correspondam ao seguinte padrão:

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

    Aqui, <abi> é um dos nomes da ABI listados em ABIs compatíveis, e <name> é o nome da biblioteca definida para a variável LOCAL_MODULE no arquivo Android.mk. Como os arquivos do APK são do tipo ZIP, é importante abri-los para confirmar se as bibliotecas nativas compartilhadas estão no lugar certo.

    Se o sistema não encontrar as bibliotecas nativas compartilhadas no local esperado, não será possível usá-las. Nesse caso, o próprio app precisa copiar as bibliotecas e depois executar dlopen().

    Em APKs multiarquitetura, cada biblioteca reside em um diretório cujo nome corresponde a uma ABI relevante. Por exemplo, um APK multiarquitetura pode conter:

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

    Observação: dispositivos Android baseados em ARMv7 que executam a versão 4.0.3 ou versões anteriores instalam bibliotecas nativas do diretório armeabi em vez do diretório armeabi-v7a, se os dois existirem. Isso ocorre porque /lib/armeabi/ vem depois de /lib/armeabi-v7a/ no APK. Esse problema foi corrigido na versão 4.0.4.

    Compatibilidade com ABIs da Plataforma Android

    O sistema Android descobre no momento da execução as ABIs compatíveis, porque as propriedades do sistema específicas do build indicam:

    • a ABI principal do dispositivo, correspondente ao código de máquina usado na imagem do sistema;
    • ABIs secundárias opcionais, correspondentes a outra ABI também compatível com a imagem do sistema.

    Esse mecanismo garante que o sistema extraia o melhor código de máquina do pacote no momento de instalação.

    Para ter o melhor desempenho, crie diretamente para a ABI principal. Por exemplo, um dispositivo baseado em ARMv5TE típico só definiria a ABI principal: armeabi. Por outro lado, um dispositivo baseado em ARMv7 definiria a ABI principal como armeabi-v7a e a secundária como armeabi, já que ele pode executar binários nativos do app gerados para cada uma delas.

    Dispositivos de 64 bits também são compatíveis com as variantes de 32 bits. Por exemplo, dispositivos arm64-v8a também podem executar código armeabi e armeabi-v7a. No entanto, o app terá um desempenho muito melhor em dispositivos de 64 bits se ele for destinado a arm64-v8a, em vez de depender de o dispositivo executar a versão armeabi-v7a do seu app.

    Muitos dispositivos baseados em x86 também podem executar binários NDK armeabi-v7a e armeabi. Para esses dispositivos, a ABI principal seria x86 e a secundária seria armeabi-v7a.

    Você pode forçar a instalação de um APK para uma ABI. Isso é útil para testes. Use o seguinte comando:

    adb install --abi abi-identifier path_to_apk
    

    Extração automática de código nativo no momento da instalação

    Ao instalar um app, o serviço do gerenciador de pacotes verifica o APK e busca bibliotecas compartilhadas com o seguinte formato:

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

    Se nenhuma biblioteca for encontrada e você tiver definido uma ABI secundária, o serviço procurará bibliotecas compartilhadas com o seguinte formato:

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

    Quando o gerenciador de pacotes encontra as bibliotecas que está procurando, ele as copia para /lib/lib<name>.so no diretório da biblioteca nativa do app (<nativeLibraryDir>/). Os seguintes snippets recuperam o 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 );
    

    Se não houver arquivos de objeto compartilhado, o app será criado e instalado, mas falhará no momento da execução.