Interfejsy ABI Androida

Różne urządzenia z Androidem mają różne procesory, które z kolei obsługują różne zestawy instrukcji. Każda kombinacja procesora i listy instrukcji procesora ma własny interfejs binarny aplikacji (ABI). Interfejs ABI zawiera te informacje:

  • zestaw instrukcji procesora (i rozszerzenia), których można używać;
  • kolejność bajtów w pamięci podczas zapisywania i odczytywania w czasie działania; Android zawsze używa kolejności little-endian;
  • konwencje przekazywania danych między aplikacjami a systemem, w tym ograniczenia dotyczące wyrównania oraz sposób, w jaki system używa stosu i rejestrów podczas wywoływania funkcji;
  • format wykonywalnych plików binarnych, takich jak programy i biblioteki współdzielone, oraz typy treści, które obsługują; Android zawsze używa formatu ELF; więcej informacji znajdziesz w dokumencie ELF System V interfejs binarny aplikacji.
  • sposób przekształcania nazw w C++; więcej informacji znajdziesz w dokumencie Generic/Itanium C++ ABI.

Na tej stronie znajdziesz listę interfejsów ABI obsługiwanych przez NDK oraz informacje o tym, jak działa każdy z nich.

Interfejs ABI może też odnosić się do natywnego interfejsu API obsługiwanego przez platformę. Listę problemów z interfejsem ABI, które występują w systemach 32-bitowych, znajdziesz w artykule Błędy w 32-bitowym interfejsie ABI.

Obsługiwane interfejsy ABI

Tabela 1. Interfejsy ABI i obsługiwane zestawy instrukcji.

Interfejs ABI Obsługiwane zestawy instrukcji Uwagi
armeabi-v7a
  • armeabi
  • Thumb-2
  • Neon
  • Niezgodny z urządzeniami z architekturą ARMv5/v6.
    arm64-v8a
  • AArch64
  • Tylko Armv8.0.
    x86
  • x86 (IA-32)
  • MMX
  • SSE/2/3
  • SSSE3
  • Brak obsługi MOVBE i SSE4.
    x86_64
  • x86-64
  • MMX
  • SSE/2/3
  • SSSE3
  • SSE4.1, 4.2
  • POPCNT
  • CMPXCHG16B
  • LAHF-SAHF
  • Pełna wersja x86-64-v2.

    Uwaga: historycznie NDK obsługiwał architekturę ARMv5 (armeabi) oraz 32-bitową i 64-bitową architekturę MIPS, ale obsługa tych interfejsów ABI została usunięta w NDK r17.

    armeabi-v7a

    Ten interfejs ABI jest przeznaczony dla 32-bitowych procesorów ARM. Obejmuje on Thumb-2 i Neon.

    Informacje o częściach interfejsu binarny aplikacji (ABI), które nie są specyficzne dla Androida, znajdziesz w dokumencie Application Binary Interface (ABI) for the ARM Architecture.

    Systemy kompilacji NDK domyślnie generują kod Thumb-2, chyba że używasz LOCAL_ARM_MODE w pliku Android.mk dla ndk-build lub ANDROID_ARM_MODE podczas konfigurowania CMake.

    Więcej informacji o historii Neon znajdziesz w artykule Neon Support.

    Z przyczyn historycznych ten interfejs ABI używa -mfloat-abi=softfp, co powoduje, że podczas wywoływania funkcji wszystkie float wartości są przekazywane w rejestrach liczb całkowitych, a wszystkie double wartości – w parach rejestrów liczb całkowitych. Pomimo nazwy ma to wpływ tylko na konwencję wywoływania liczb zmiennoprzecinkowych: kompilator nadal będzie używać sprzętowych instrukcji zmiennoprzecinkowych do wykonywania operacji arytmetycznych.

    Ten interfejs ABI używa 64-bitowego typu long double (IEEE binary64, czyli takiego samego jak double).

    arm64-v8a

    Ten interfejs ABI jest przeznaczony dla 64-bitowych procesorów ARM.

    Szczegółowe informacje o częściach interfejsu ABI, które nie są specyficzne dla Androida, znajdziesz w artykule Arm's Learn the Architecture. Arm oferuje też kilka porad dotyczących przenoszenia kodu w artykule 64-bit Android Development.

    Aby korzystać z rozszerzenia Advanced SIMD, możesz używać funkcji wewnętrznych Neon w kodzie C i C++. Więcej informacji o funkcjach wewnętrznych Neon i programowaniu w Neon znajdziesz w przewodniku Neon Programmer's Guide for Armv8-A.

    W Androidzie rejestr x18 specyficzny dla platformy jest zarezerwowany dla ShadowCallStack i nie powinien być używany w Twoim kodzie. Obecne wersje Clang domyślnie używają opcji -ffixed-x18 w Androidzie, więc nie musisz się tym przejmować, chyba że masz ręcznie napisany kod asemblera (lub bardzo stary kompilator).

    Ten interfejs ABI używa 128-bitowego typu long double (IEEE binary128).

    x86

    Ten interfejs ABI jest przeznaczony dla procesorów obsługujących listę instrukcji procesora powszechnie znaną jako „x86”, „i386” lub „IA-32”.

    Interfejs ABI Androida obejmuje podstawową lista instrukcji procesora oraz rozszerzenia MMX, SSE, SSE2, SSE3 i SSSE3.

    Interfejs ABI nie obejmuje żadnych innych opcjonalnych rozszerzeń listy instrukcji procesora IA-32, takich jak MOVBE czy jakikolwiek wariant SSE4. Możesz nadal używać tych rozszerzeń, o ile włączysz je za pomocą sondowania funkcji w czasie działania i udostępnisz rezerwowe rozwiązania dla urządzeń, które ich nie obsługują.

    Łańcuch narzędzi NDK zakłada 16-bajtowe wyrównanie stosu przed wywołaniem funkcji. Domyślne narzędzia i opcje wymuszają tę regułę. Jeśli piszesz kod asemblera, musisz zadbać o zachowanie wyrównania stosu i upewnić się, że inne kompilatory też przestrzegają tej reguły.

    Więcej informacji znajdziesz w tych dokumentach:

    Ten interfejs ABI używa 64-bitowego typu long double (IEEE binary64, czyli takiego samego jak double, a nie bardziej popularnego 80-bitowego typu long double dostępnego tylko na procesorach Intel).

    x86_64

    Ten interfejs ABI jest przeznaczony dla procesorów obsługujących listę instrukcji procesora powszechnie znaną jako „x86-64”.

    Interfejs ABI Androida obejmuje podstawową listę instrukcji procesora oraz MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2 i instrukcję POPCNT.

    Interfejs ABI nie obejmuje żadnych innych opcjonalnych rozszerzeń listy instrukcji procesora x86-64, takich jak MOVBE, SHA czy jakikolwiek wariant AVX. Możesz nadal używać tych rozszerzeń, o ile włączysz je za pomocą sondowania funkcji w czasie działania i udostępnisz rezerwowe rozwiązania dla urządzeń, które ich nie obsługują.

    Więcej informacji znajdziesz w tych dokumentach:

    Ten interfejs ABI używa 128-bitowego typu long double (IEEE binary128).

    Generowanie kodu dla konkretnego interfejsu ABI

    Gradle

    Gradle (używany w Android Studio lub w wierszu poleceń) domyślnie kompiluje kod dla wszystkich nieprzestarzałych interfejsów ABI. Aby ograniczyć zestaw interfejsów ABI obsługiwanych przez aplikację, użyj abiFilters. Aby na przykład kompilować kod tylko dla 64-bitowych interfejsów ABI, ustaw w pliku build.gradle tę konfigurację:

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

    ndk-build

    ndk-build domyślnie kompiluje kod dla wszystkich nieprzestarzałych interfejsów ABI. Możesz kierować kod na konkretne interfejsy ABI, ustawiając APP_ABI w pliku Application.mk. Ten fragment kodu pokazuje kilka przykładów użycia 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.
    

    Więcej informacji o wartościach, które możesz określić w przypadku APP_ABI, znajdziesz w artykule Application.mk.

    CMake

    W CMake kompilujesz kod dla jednego interfejsu ABI naraz i musisz go określić. Robisz to za pomocą zmiennej ANDROID_ABI, którą musisz określić w wierszu poleceń (nie można jej ustawić w pliku CMakeLists.txt). Przykład:

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

    Więcej informacji o innych flagach, które trzeba przekazać do CMake, aby kompilować kod za pomocą NDK, znajdziesz w przewodniku CMake.

    Domyślne działanie systemu kompilacji polega na umieszczeniu plików binarnych dla każdego interfejsu ABI w jednym pliku APK, zwanym też grubym plikiem APK. Gruby plik APK jest znacznie większy niż plik zawierający tylko pliki binarne dla jednego interfejsu ABI. W zamian za większy rozmiar pliku APK zyskujesz jednak szerszą kompatybilność. Zdecydowanie zalecamy korzystanie z pakietów aplikacji lub podziału plików APK, aby zmniejszyć rozmiar plików APK przy zachowaniu maksymalnej kompatyktbilności z urządzeniami.

    Podczas instalacji menedżer pakietów rozpakowuje tylko najbardziej odpowiedni kod maszynowy dla urządzenia docelowego. Szczegółowe informacje znajdziesz w sekcji Automatyczne wyodrębnianie kodu natywnego podczas instalacji.

    Zarządzanie interfejsami ABI na platformie Android

    W tej sekcji znajdziesz szczegółowe informacje o tym, jak platforma Android zarządza kodem natywnym w plikach APK.

    Kod natywny w pakietach aplikacji

    Zarówno Sklep Play, jak i Menedżer pakietów oczekują, że biblioteki wygenerowane przez NDK będą znajdować się w plikach APK w ścieżkach pasujących do tego wzorca:

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

    W tym przypadku <abi> to jedna z nazw interfejsów ABI wymienionych w sekcji Obsługiwane interfejsy ABI, a <name> to nazwa biblioteki zdefiniowana w zmiennej LOCAL_MODULE w pliku Android.mk. Pliki APK to po prostu pliki ZIP, więc można je łatwo otworzyć i sprawdzić, czy współdzielone biblioteki natywne znajdują się we właściwym miejscu.

    Jeśli system nie znajdzie współdzielonych bibliotek natywnych w oczekiwanym miejscu, nie będzie mógł ich użyć. W takim przypadku aplikacja musi skopiować biblioteki, a następnie wykonać dlopen().

    W grubym pliku APK każda biblioteka znajduje się w katalogu, którego nazwa odpowiada danemu interfejsowi ABI. Gruby plik APK może na przykład zawierać:

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

    Uwaga: jeśli istnieją oba katalogi, urządzenia z Androidem oparte na architekturze ARMv7 z Androidem w wersji 4.0.3 lub starszej instalują biblioteki natywne z katalogu armeabi, a nie z katalogu armeabi-v7a. Dzieje się tak, ponieważ w pliku APK katalog /lib/armeabi/ występuje po katalogu /lib/armeabi-v7a/. Ten problem został rozwiązany w wersji 4.0.4.

    Obsługa interfejsów ABI na platformie Android

    System Android wie w czasie działania, które interfejsy ABI obsługuje, ponieważ właściwości systemu specyficzne dla kompilacji wskazują:

    • podstawowy interfejs ABI urządzenia, odpowiadający kodowi maszynowemu używanemu w obrazie systemu;
    • opcjonalnie – dodatkowe interfejsy ABI, odpowiadające innym interfejsom ABI, które obsługuje też obraz systemu.

    Ten mechanizm zapewnia, że podczas instalacji system wyodrębni z pakietu najlepszy kod maszynowy.

    Aby uzyskać najlepszą wydajność, kompiluj kod bezpośrednio dla podstawowego interfejsu ABI. Na przykład typowe urządzenie oparte na architekturze ARMv5TE definiuje tylko podstawowy interfejs ABI: armeabi. Natomiast typowe urządzenie oparte na architekturze ARMv7 definiuje podstawowy interfejs ABI jako armeabi-v7a, a dodatkowy jako armeabi, ponieważ może uruchamiać natywne pliki binarne aplikacji wygenerowane dla każdego z nich.

    Urządzenia 64-bitowe obsługują też swoje 32-bitowe warianty. Na przykład urządzenia z architekturą arm64-v8a mogą też uruchamiać kod armeabi i armeabi-v7a. Pamiętaj jednak, że aplikacja będzie działać znacznie lepiej na urządzeniach 64-bitowych, jeśli będzie kierowana na architekturę arm64-v8a, a nie na wersję armeabi-v7a.

    Wiele urządzeń z architekturą x86 może też uruchamiać pliki binarne NDK armeabi-v7a i armeabi. W przypadku takich urządzeń podstawowym interfejsem ABI będzie x86, a dodatkowym – armeabi-v7a.

    Możesz wymusić instalację pliku APK dla konkretnego interfejsu ABI. Jest to przydatne do testowania. Użyj tego polecenia:

    adb install --abi abi-identifier path_to_apk
    

    Automatyczne wyodrębnianie kodu natywnego podczas instalacji

    Podczas instalowania aplikacji usługa menedżera pakietów skanuje plik APK i szuka współdzielonych bibliotek w tym formacie:

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

    Jeśli nie znajdzie żadnej biblioteki i zdefiniujesz dodatkowy interfejs ABI, usługa będzie skanować w poszukiwaniu współdzielonych bibliotek w tym formacie:

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

    Gdy menedżer pakietów znajdzie szukane biblioteki, skopiuje je do /lib/lib<name>.so, w katalogu bibliotek natywnych aplikacji (<nativeLibraryDir>/). Te fragmenty kodu pobierają 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 );

    Jeśli nie ma pliku obiektu współdzielonego, aplikacja zostanie skompilowana i zainstalowana, ale ulegnie awarii w czasie działania.

    ARMv9: włączanie PAC i BTI w C/C++

    Włączenie PAC/BTI zapewni ochronę przed niektórymi wektorami ataku. PAC chroni adresy powrotne, podpisując je kryptograficznie w prologu funkcji i sprawdzając, czy adres powrotny jest nadal prawidłowo podpisany w epilogu. BTI zapobiega skakaniu do dowolnych miejsc w kodzie, wymagając, aby każdy cel rozgałęzienia był specjalną instrukcją, która nic nie robi, tylko informuje procesor, że można tam przejść.

    Android używa instrukcji PAC/BTI, które nie działają na starszych procesorach, które nie obsługują nowych instrukcji. Ochrona PAC/BTI będzie dostępna tylko na urządzeniach z architekturą ARMv9, ale ten sam kod możesz uruchamiać też na urządzeniach z architekturą ARMv8. Nie musisz tworzyć wielu wariantów biblioteki. Nawet na urządzeniach z architekturą ARMv9 PAC/BTI działa tylko w przypadku kodu 64-bitowego.

    Włączenie PAC/BTI spowoduje niewielki wzrost rozmiaru kodu, zwykle o 1%.

    Szczegółowe wyjaśnienie wektorów ataku, na które kierowane są PAC/BTI, oraz sposobu działania ochrony znajdziesz w dokumencie Arm's Learn the architecture - Providing protection for complex software (PDF) .

    Zmiany w kompilacji

    ndk-build

    W każdym module pliku Android.mk ustaw LOCAL_BRANCH_PROTECTION := standard.

    CMake

    W przypadku każdego celu w pliku CMakeLists.txt użyj target_compile_options($TARGET PRIVATE -mbranch-protection=standard).

    Inne systemy kompilacji

    Kompiluj kod za pomocą -mbranch-protection=standard. Ta flaga działa tylko podczas kompilowania dla interfejsu ABI arm64-v8a. Podczas łączenia nie musisz używać tej flagi.

    Rozwiązywanie problemów

    Nie znamy żadnych problemów z obsługą PAC/BTI przez kompilator, ale:

    • Podczas łączenia uważaj, aby nie mieszać kodu BTI z kodem innym niż BTI, ponieważ spowoduje to utworzenie biblioteki, w której nie będzie włączona ochrona BTI. Aby sprawdzić, czy wynikowa biblioteka zawiera notatkę BTI, możesz użyć llvm-readelf.
    $ 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
    [...]
    $
    
    • Starsze wersje OpenSSL (sprzed 1.1.1i) mają błąd w ręcznie napisanym asemblerze, który powoduje awarie PAC. Zaktualizuj OpenSSL do najnowszej wersji.

    • Starsze wersje niektórych systemów DRM aplikacji generują kod, który narusza wymagania PAC/BTI. Jeśli używasz DRM aplikacji i widzisz problemy podczas włączania PAC/BTI, skontaktuj się z dostawcą DRM, aby uzyskać poprawioną wersję.