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 Observações
armeabi-v7a
  • armeabi
  • Thumb-2
  • Neon
  • Incompatível com dispositivos ARMv5/v6.
    arm64-v8a
  • AArch64
  • Somente ARMv8.0.
    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
  • CMPXCHG16B
  • x86-64-v1 completo, mas x86-64-v2 parcial (sem LAHF-SAHF).

    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 ARM de 32 bits. Ele inclui Thumb-2 e Neon.

    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.

    Para mais informações sobre a história do Neon, consulte Suporte ao Neon.

    Por motivos históricos, essa ABI usa -mfloat-abi=softfp, fazendo com que todos os valores float sejam transmitidos em registros inteiros e todos os valores double sejam transmitidos em pares de registro inteiros ao fazer chamadas de função. Apesar do nome, isso afeta apenas a convenção de chamada de ponto flutuante: o compilador ainda vai usar instruções de ponto flutuante de hardware para aritmética.

    Essa ABI usa um long double de 64 bits (IEEE binary64, igual a double).

    arm64-v8a

    Esta ABI é para CPUs ARM de 64 bits.

    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.

    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 traz mais informações específicas sobre os intrínsecos e a programação do Neon, em geral.

    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.

    Essa ABI usa um long double de 128 bits (IEEE binary128).

    x86

    Essa ABI é voltada a CPUs com suporte ao conjunto de instruções conhecido como "x86", "i386" ou "IA-32".

    A ABI do Android inclui o conjunto de instruções básico mais MMX, SSE, SSE2, SSE3 e SSSE3 (links em inglês).

    A ABI não inclui nenhuma outra extensão de conjunto de instruções IA-32 opcional, como MOVBE ou 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 sem suporte a 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:

    Essa ABI usa um long double de 64 bits (IEEE binary64, igual a double, e não o long double de 80 bits mais comum, apenas para Intel.

    x86_64

    Essa ABI é direcionada a CPUs que oferecem suporte ao conjunto de instruções conhecido como “x86-64”.

    A ABI do Android inclui o conjunto de instruções básico, além de MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2 (links em inglês) e a instrução POPCNT.

    A ABI não inclui nenhuma outra extensão de conjunto de instruções x86-64 opcional, como MOVBE, SHA ou qualquer variante do AVX. 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 se suporte a elas.

    Consulte os documentos a seguir para saber mais:

    Essa ABI usa um long double de 128 bits (IEEE binary128).

    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 pelo Android Studio ou na 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 na Plataforma Android

    O sistema Android descobre no tempo de 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 a melhor performance, compile 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 do 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 vai falhar no momento da execução.

    ARMv9: como ativar PAC e BTI para C/C++

    A ativação das instruções PAC/BTI vai fornecer proteção contra alguns vetores de ataque. Para proteger endereços de retorno, a PAC os assina criptograficamente no prólogo de uma função e confere se eles estão assinados de forma correta no epílogo. A BTI impede pulos para locais arbitrários no seu código exigindo que cada destino de ramificação seja uma instrução especial que não faz nada além de dizer ao processador que não há problema em acessar esse local.

    O Android usa instruções PAC/BTI que não fazem nada em processadores mais antigos sem suporte às novas instruções. Apenas dispositivos ARMv9 vão ter a proteção PAC/BTI, mas também é possível executar o mesmo código em dispositivos ARMv8. Não é necessário ter diversas variantes da biblioteca. Mesmo em dispositivos ARMv9, a PAC/BTI se aplica apenas a códigos de 64 bits.

    A ativação da PAC/BTI causa um pequeno aumento no tamanho do código, normalmente de 1%.

    Consulte o artigo Conheça a arquitetura: como fornecer proteção para softwares complexos (link em inglês) do ARM (PDF) para ver uma explicação detalhada do destino PAC/BTI de vetores de ataque e como a proteção funciona.

    Mudanças no build

    ndk-build

    Defina LOCAL_BRANCH_PROTECTION := standard em cada módulo do Android.mk.

    CMake

    Use target_compile_options($TARGET PRIVATE -mbranch-protection=standard) para cada destino no CMakeLists.txt.

    Outros sistemas de build

    Compile seu código usando -mbranch-protection=standard. Essa sinalização só funciona ao compilar para a ABI arm64-v8a. Ela não precisa ser usada durante a vinculação.

    Solução de problemas

    Não temos conhecimento de problemas com o suporte ao compilador para PAC/BTI, mas:

    • Tenha cuidado para não misturar códigos BTI e não BTI ao vincular, porque isso resulta em uma biblioteca que não tem a proteção BTI ativada. Você pode usar llvm-readelf para verificar se a biblioteca resultante tem a anotação BTI ou não.
    $ 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
    [...]
    $
    
    • As versões antigas do OpenSSL (anteriores à 1.1.1i) têm um bug no assembler escrito à mão que causa falhas na PAC. Faça upgrade para o OpenSSL atual.

    • As versões antigas de alguns sistemas DRM de app geram um código que viola os requisitos da PAC/BTI. Se você estiver usando o DRM de app e encontrar problemas ao ativar a PAC/BTI, entre em contato com seu fornecedor para instalar uma versão corrigida.