ABI Android

I diversi dispositivi Android utilizzano CPU diverse, che a loro volta supportano set di istruzioni diversi. Ogni combinazione di CPU e set di istruzioni ha la propria Application Binary Interface (ABI). Un'ABI include le seguenti informazioni:

  • Il set di istruzioni della CPU (ed estensioni) che può essere utilizzato.
  • L'endianness degli archivi e dei caricamenti di memoria in fase di runtime. Android è sempre little-endian.
  • Le convenzioni per il passaggio dei dati tra le applicazioni e il sistema, inclusi i vincoli di allineamento e il modo in cui il sistema utilizza lo stack e i registri quando chiama le funzioni.
  • Il formato dei file binari eseguibili, come programmi e librerie condivise, e i tipi di contenuti che supportano. Android utilizza sempre ELF. Per saperne di più, consulta ELF System V Application Binary Interface.
  • Il modo in cui vengono modificati i nomi C++. Per saperne di più, consulta Generic/Itanium C++ ABI.

Questa pagina elenca le ABI supportate dall'NDK e fornisce informazioni su come funziona ogni ABI.

ABI può anche fare riferimento all'API nativa supportata dalla piattaforma. Per un elenco di questi tipi di problemi ABI che interessano i sistemi a 32 bit, consulta Bug ABI a 32 bit.

ABI supportate

Tabella 1. ABI e set di istruzioni supportati.

ABI Set di istruzioni supportati Note
armeabi-v7a
  • armeabi
  • Thumb-2
  • Neon
  • Incompatibile con i dispositivi ARMv5/v6.
    arm64-v8a
  • AArch64
  • Solo Armv8.0.
    x86
  • x86 (IA-32)
  • MMX
  • SSE/2/3
  • SSSE3
  • Nessun supporto per MOVBE o SSE4.
    x86_64
  • x86-64
  • MMX
  • SSE/2/3
  • SSSE3
  • SSE4.1, 4.2
  • POPCNT
  • CMPXCHG16B
  • LAHF-SAHF
  • x86-64-v2 completo .

    Nota: in passato, l'NDK supportava ARMv5 (armeabi) e MIPS a 32 e 64 bit, ma il supporto per queste ABI è stato rimosso nell'NDK r17.

    armeabi-v7a

    Questa ABI è per le CPU ARM a 32 bit. Include Thumb-2 e Neon.

    Per informazioni sulle parti dell'ABI non specifiche di Android, consulta Application Binary Interface (ABI) for the ARM Architecture

    I sistemi di build dell'NDK generano codice Thumb-2 per impostazione predefinita, a meno che tu non utilizzi LOCAL_ARM_MODE in your Android.mk for ndk-build or ANDROID_ARM_MODE when configuring CMake.

    Per saperne di più sulla storia di Neon, consulta Supporto di Neon.

    Per motivi storici, questa ABI utilizza -mfloat-abi=softfp, il che fa sì che tutti i valori float vengano passati nei registri interi e tutti i valori double vengano passati in coppie di registri interi quando si effettuano chiamate di funzioni. Nonostante il nome, questo influisce solo sulla convenzione di chiamata in virgola mobile: il compilatore utilizzerà comunque le istruzioni in virgola mobile hardware per l'aritmetica.

    Questa ABI utilizza un long double a 64 bit (IEEE binary64, lo stesso di double).

    arm64-v8a

    Questa ABI è per le CPU ARM a 64 bit.

    Per tutti i dettagli sulle parti dell'ABI non specifiche di Android, consulta Learn the Architecture di Arm. Arm offre anche alcuni consigli per il porting in 64-bit Android Development.

    Puoi utilizzare gli intrinsechi Neon nel codice C e C++ per sfruttare l'estensione Advanced SIMD. La guida per programmatori Neon per Armv8-A fornisce ulteriori informazioni sulle funzioni intrinseche Neon e sulla programmazione Neon in generale.

    Su Android, il registro x18 specifico della piattaforma è riservato a ShadowCallStack e non deve essere toccato dal tuo codice. Le versioni attuali di Clang utilizzano per impostazione predefinita l'opzione -ffixed-x18 su Android, quindi, a meno che tu non abbia scritto assembler a mano (o un compilatore molto vecchio), non dovresti preoccuparti di questo.

    Questa ABI utilizza un 128-bit long double (IEEE binary128).

    x86

    Questa ABI è per le CPU che supportano il set di istruzioni comunemente noto come "x86", "i386" o "IA-32".

    L'ABI di Android include il set di istruzioni di base più le estensioni MMX, SSE, SSE2, SSE3 e SSSE3.

    L'ABI non include altre estensioni del set di istruzioni IA-32 facoltative, come MOVBE o qualsiasi variante di SSE4. Puoi comunque utilizzare queste estensioni, a condizione che tu utilizzi il rilevamento delle funzionalità in fase di runtime per abilitarle e fornisca fallback per i dispositivi che non le supportano.

    La toolchain NDK presuppone un allineamento dello stack di 16 byte prima di una chiamata di funzione. Gli strumenti e le opzioni predefiniti applicano questa regola. Se scrivi codice assembly, devi assicurarti di mantenere l'allineamento dello stack e che anche gli altri compilatori rispettino questa regola.

    Per maggiori dettagli, consulta i seguenti documenti:

    Questa ABI utilizza un long double a 64 bit (IEEE binary64 lo stesso di double, e non il long double a 80 bit solo Intel più comune).

    x86_64

    Questa ABI è per le CPU che supportano il set di istruzioni comunemente noto come "x86-64".

    L'ABI di Android include il set di istruzioni di base più MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2 e l'istruzione POPCNT.

    L'ABI non include altre estensioni del set di istruzioni x86-64 facoltative, come MOVBE, SHA o qualsiasi variante di AVX. Puoi comunque utilizzare queste estensioni, a condizione che tu utilizzi il rilevamento delle funzionalità in fase di runtime per abilitarle e fornisca fallback per i dispositivi che non le supportano.

    Per maggiori dettagli, consulta i seguenti documenti:

    Questa ABI utilizza un 128-bit long double (IEEE binary128).

    Generare codice per un'ABI specifica

    Gradle

    Gradle (utilizzato tramite Android Studio o dalla riga di comando) esegue la build per tutte le ABI non obsolete per impostazione predefinita. Per limitare l'insieme di ABI supportate dall'applicazione, utilizza abiFilters. Ad esempio, per eseguire la build solo per le ABI a 64 bit, imposta la seguente configurazione in build.gradle:

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

    ndk-build

    ndk-build esegue la build per tutte le ABI non obsolete per impostazione predefinita. Puoi scegliere come target ABI specifiche impostando APP_ABI nel file Application.mk. Il seguente snippet mostra alcuni esempi di utilizzo di 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.
    

    Per saperne di più sui valori che puoi specificare per APP_ABI, consulta Application.mk.

    CMake

    Con CMake, esegui la build per una singola ABI alla volta e devi specificare l'ABI in modo esplicito. Per farlo, utilizza la variabile ANDROID_ABI, che deve essere specificata nella riga di comando (non può essere impostata in CMakeLists.txt). Ad esempio:

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

    Per gli altri flag che devono essere passati a CMake per eseguire la build con l'NDK, consulta la guida di CMake.

    Il comportamento predefinito del sistema di compilazione è includere i file binari per ogni ABI in un singolo APK, noto anche come APK fat. Un APK fat è notevolmente più grande di uno che contiene solo i file binari per una singola ABI; il compromesso è ottenere una compatibilità più ampia, ma a scapito di un APK più grande. Ti consigliamo vivamente di sfruttare gli app bundle o le suddivisioni APK per ridurre le dimensioni degli APK mantenendo al contempo la massima compatibilità con i dispositivi.

    Al momento dell'installazione, il gestore dei pacchetti decomprime solo il codice macchina più appropriato per il dispositivo di destinazione. Per maggiori dettagli, consulta Estrazione automatica del codice nativo al momento dell'installazione.

    Gestione ABI sulla piattaforma Android

    Questa sezione fornisce dettagli su come la piattaforma Android gestisce il codice nativo negli APK.

    Codice nativo nei pacchetti di app

    Sia il Play Store sia il gestore dei pacchetti si aspettano di trovare le librerie generate dall'NDK nei percorsi dei file all'interno dell'APK che corrispondono al seguente pattern:

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

    Qui, <abi> è uno dei nomi ABI elencati in ABI supportate, e <name> è il nome della libreria come l'hai definita per la LOCAL_MODULE variabile nel file Android.mk. Poiché i file APK sono solo file zip, è facile aprirli e verificare che le librerie native condivise si trovino nella posizione corretta.

    Se il sistema non trova le librerie condivise native nella posizione prevista, non può utilizzarle. In questo caso, l'app stessa deve copiare le librerie ed eseguire dlopen().

    In un APK fat, ogni libreria si trova in una directory il cui nome corrisponde a un'ABI corrispondente. Ad esempio, un APK fat può contenere:

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

    Nota: i dispositivi Android basati su ARMv7 che eseguono la versione 4.0.3 o precedenti installano le librerie native dalla directory armeabi anziché dalla directory armeabi-v7a se esistono entrambe le directory. Questo perché /lib/armeabi/ viene dopo /lib/armeabi-v7a/ nell'APK. Questo problema è stato risolto a partire dalla versione 4.0.4.

    Supporto ABI della piattaforma Android

    Il sistema Android sa in fase di runtime quali ABI supporta, perché le proprietà di sistema specifiche della build indicano:

    • L'ABI principale per il dispositivo, corrispondente al codice macchina utilizzato nell'immagine di sistema stessa.
    • Facoltativamente, le ABI secondarie, corrispondenti ad altre ABI supportate anche dall'immagine di sistema.

    Questo meccanismo garantisce che il sistema estragga il codice macchina migliore dal pacchetto al momento dell'installazione.

    Per ottenere prestazioni ottimali, devi compilare direttamente per l'ABI principale. Ad esempio, un dispositivo tipico basato su ARMv5TE definirebbe solo l'ABI principale: armeabi. Al contrario, un dispositivo tipico basato su ARMv7 definirebbe l'ABI principale come armeabi-v7a e quella secondaria come armeabi, poiché può eseguire i file binari nativi dell'applicazione generati per ciascuna di esse.

    I dispositivi a 64 bit supportano anche le varianti a 32 bit. Utilizzando come esempio i dispositivi arm64-v8a, il dispositivo può eseguire anche il codice armeabi e armeabi-v7a. Tieni presente, tuttavia, che la tua applicazione avrà prestazioni molto migliori sui dispositivi a 64 bit se scegli come target arm64-v8a anziché fare affidamento sul dispositivo che esegue la versione armeabi-v7a dell'applicazione.

    Molti dispositivi basati su x86 possono eseguire anche i file binari NDK armeabi-v7a e armeabi. Per questi dispositivi, l'ABI principale sarebbe x86 e la seconda armeabi-v7a.

    Puoi forzare l'installazione di un APK per un'ABI specifica. Questa opzione è utile per i test. Utilizza il seguente comando:

    adb install --abi abi-identifier path_to_apk
    

    Estrazione automatica del codice nativo al momento dell'installazione

    Quando si installa un'applicazione, il servizio di gestione dei pacchetti esegue la scansione dell'APK e cerca le librerie condivise nel formato:

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

    Se non ne viene trovata nessuna e hai definito un'ABI secondaria, il servizio esegue la scansione delle librerie condivise nel formato:

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

    Quando trova le librerie che sta cercando, il gestore dei pacchetti copia le in /lib/lib<name>.so, nella directory delle librerie native dell'applicazione (<nativeLibraryDir>/). I seguenti snippet recuperano il 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 non è presente alcun file di oggetti condivisi, l'applicazione viene compilata e installata, ma si arresta in modo anomalo in fase di runtime.

    ARMv9: attivare PAC e BTI per C/C++

    L'attivazione di PAC/BTI fornisce protezione contro alcuni vettori di attacco. PAC protegge gli indirizzi di ritorno firmandoli crittograficamente nel prologo di una funzione e verificando che l'indirizzo di ritorno sia ancora firmato correttamente nell'epilogo. BTI impedisce di passare a posizioni arbitrarie nel codice richiedendo che ogni target di ramo sia un'istruzione speciale che non fa altro che comunicare al processore che è consentito atterrare lì.

    Android utilizza istruzioni PAC/BTI che non fanno nulla sui processori meno recenti che non supportano le nuove istruzioni. Solo i dispositivi ARMv9 avranno la protezione PAC/BTI, ma puoi eseguire lo stesso codice anche sui dispositivi ARMv8: non sono necessarie più varianti della libreria. Anche sui dispositivi ARMv9, PAC/BTI si applica solo al codice a 64 bit.

    L'attivazione di PAC/BTI comporta un leggero aumento delle dimensioni del codice, in genere dell'1%.

    Per una spiegazione dettagliata dei vettori di attacco di PAC/BTI e di come funziona la protezione, consulta Learn the architecture - Providing protection for complex software (PDF) di Arm.

    Modifiche alla build

    ndk-build

    Imposta LOCAL_BRANCH_PROTECTION := standard in ogni modulo di Android.mk.

    CMake

    Utilizza target_compile_options($TARGET PRIVATE -mbranch-protection=standard) per ogni target in CMakeLists.txt.

    Altri sistemi di build

    Compila il codice utilizzando -mbranch-protection=standard. Questo flag funziona solo quando la compilazione viene eseguita per l'ABI arm64-v8a. Non è necessario utilizzare questo flag durante il collegamento.

    Risoluzione dei problemi

    Non siamo a conoscenza di problemi con il supporto del compilatore per PAC/BTI, ma:

    • Fai attenzione a non combinare codice BTI e non BTI durante il collegamento, perché il risultato è una libreria senza protezione BTI abilitata. Puoi utilizzare llvm-readelf per verificare se la libreria risultante ha la nota BTI o meno.
    $ 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
    [...]
    $
    
    • Le versioni precedenti di OpenSSL (prima della 1.1.1i) hanno un bug nell'assembler scritto a mano che causa errori PAC. Esegui l'upgrade alla versione attuale di OpenSSL.

    • Le versioni precedenti di alcuni sistemi DRM per app generano codice che viola i requisiti PAC/BTI. Se utilizzi DRM per app e riscontri problemi durante l'attivazione di PAC/BTI, contatta il fornitore di DRM per una versione corretta.