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). ABI zawiera te informacje:

  • Zestaw instrukcji procesora (i rozszerzenia), których można używać.
  • Kolejność bajtów w pamięci podczas zapisywania i wczytywania w czasie działania programu. Android zawsze używa formatu 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 plików wykonywalnych, takich jak programy i biblioteki współużytkowane, oraz typy treści, które obsługują. Android zawsze używa formatu ELF. Więcej informacji znajdziesz w specyfikacji interfejsu binarnego aplikacji ELF System V.
  • Sposób przekształcania nazw w C++. Więcej informacji znajdziesz w artykule Generic/Itanium C++ ABI.

Na tej stronie znajdziesz listę interfejsów ABI obsługiwanych przez NDK oraz informacje o działaniu każdego z nich.

ABI może też odnosić się do natywnego interfejsu API obsługiwanego przez platformę. Listę rodzajów problemów z interfejsem ABI, które mają wpływ na systemy 32-bitowe, znajdziesz w sekcji Błędy interfejsu ABI w systemach 32-bitowych.

Obsługiwane interfejsy ABI

Tabela 1. ABI i obsługiwane zestawy instrukcji.

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

    Uwaga: w przeszłości NDK obsługiwał ARMv5 (armeabi) oraz 32-bitowe i 64-bitowe 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 artykule Application Binary Interface (ABI) for the ARM Architecture (w języku angielskim).

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

    Więcej informacji o historii Neon znajdziesz na stronie pomocy Neon.

    Z przyczyn historycznych ten interfejs ABI używa -mfloat-abi=softfp, co powoduje, że wszystkie wartości float są przekazywane w rejestrach liczb całkowitych, a wszystkie wartości double są przekazywane w parach rejestrów liczb całkowitych podczas wywoływania funkcji. Wbrew nazwie ta opcja wpływa tylko na konwencję wywoływania liczb zmiennoprzecinkowych: kompilator nadal będzie używać sprzętowych instrukcji zmiennoprzecinkowych do obliczeń 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 elementach interfejsu ABI, które nie są specyficzne dla Androida, znajdziesz w sekcji Learn the Architecture (Poznaj architekturę) na stronie Arm. Firma Arm oferuje też porady dotyczące przenoszenia w 64-bitowym środowisku programistycznym Androida.

    W kodzie C i C++ możesz używać funkcji wewnętrznych Neon, aby korzystać z rozszerzenia Advanced SIMD. Więcej informacji o funkcjach wewnętrznych Neon i programowaniu w Neon znajdziesz w przewodniku programisty Neon dla architektury Armv8-A.

    Na 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 na Androidzie, więc jeśli nie masz ręcznie napisanego asemblera (lub bardzo starego kompilatora), nie musisz się tym przejmować.

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

    x86

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

    Interfejs ABI Androida obejmuje podstawową listę instrukcji procesora oraz rozszerzenia MMX, SSE, SSE2, SSE3SSSE3.

    Interfejs ABI nie obejmuje żadnych innych opcjonalnych rozszerzeń listy instrukcji procesora IA-32, takich jak MOVBE czy żadna odmiana SSE4. Możesz nadal korzystać z tych rozszerzeń, o ile używasz sondowania funkcji w czasie działania, aby je włączyć, i zapewniasz alternatywne rozwiązania dla urządzeń, które ich nie obsługują.

    Zestaw 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 wyrównanie stosu i upewnić się, że inne kompilatory również 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, który jest dostępny tylko na platformie Intel).

    x86_64

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

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

    Interfejs ABI nie obejmuje żadnych innych opcjonalnych rozszerzeń listy instrukcji procesora x86-64, takich jak MOVBE, SHA czy żadna odmiana AVX. Możesz nadal korzystać z tych rozszerzeń, o ile używasz sondowania funkcji w czasie działania, aby je włączyć, i zapewniasz alternatywne 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 formatu long double (IEEE binary128).

    Generowanie kodu dla konkretnego interfejsu ABI

    Gradle

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

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

    ndk-build

    Polecenie ndk-build domyślnie tworzy kompilacje dla wszystkich interfejsów ABI, które nie są wycofane. Możesz kierować reklamy na określone interfejsy ABI, ustawiając APP_ABI w pliku Application.mk. Poniższy fragment kodu zawiera kilka przykładów użycia tagu 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 sekcji Application.mk.

    CMake

    W przypadku CMake kompilacja odbywa się dla jednego interfejsu ABI naraz i musisz go określić wprost. Możesz to zrobić za pomocą zmiennej ANDROID_ABI, którą musisz podać 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 ...
    

    Informacje o innych flagach, które należy przekazać do CMake, aby skompilować projekt za pomocą NDK, znajdziesz w przewodniku po CMake.

    Domyślnie system kompilacji umieszcza pliki binarne dla każdego interfejsu ABI w jednym pliku APK, zwanym też wieloplatformowym 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 szerszą kompatybilność. Zdecydowanie zalecamy korzystanie z pakietów aplikacji lub podziału plików APK, aby zmniejszyć rozmiar plików APK przy zachowaniu maksymalnej zgodnoś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 interfejsem 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 ścieżkach plików w pakiecie APK, które pasują 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 udostępnione biblioteki natywne znajdują się we właściwym miejscu.

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

    W przypadku pliku APK typu fat każda biblioteka znajduje się w katalogu, którego nazwa odpowiada odpowiedniemu interfejsowi ABI. Na przykład plik APK może zawierać:

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

    Obsługa interfejsów ABI platformy 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.

    Możesz wymusić instalację pliku APK dla określonego ABI. Może to być przydatne podczas testowania na urządzeniach obsługujących więcej niż jeden interfejs ABI. 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 wyszukuje biblioteki współdzielone w formacie:

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

    Jeśli nie zostanie znaleziony żaden plik, a zdefiniowano pomocniczy interfejs ABI, usługa wyszuka biblioteki współdzielone w formacie:

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

    Gdy menedżer pakietów znajdzie szukane biblioteki, skopiuje je do katalogu /lib/lib<name>.so w katalogu bibliotek natywnych aplikacji (<nativeLibraryDir>/). Poniższe fragmenty kodu pobierają katalog 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 przypadku języków C/C++

    Włączenie PAC/BTI zapewni ochronę przed niektórymi wektorami ataku. PAC chroni adresy zwrotne, podpisując je kryptograficznie w prologu funkcji i sprawdzając, czy adres zwrotny jest nadal prawidłowo podpisany w epilogu. BTI zapobiega przechodzeniu do dowolnych lokalizacji w kodzie, wymagając, aby każdy cel rozgałęzienia był specjalną instrukcją, która nie robi nic poza informowaniem procesora, ż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. Tylko urządzenia ARMv9 będą miały ochronę PAC/BTI, ale ten sam kod możesz uruchamiać też na urządzeniach ARMv8: nie musisz tworzyć wielu wariantów biblioteki. Nawet na urządzeniach z architekturą ARMv9 PAC/BTI dotyczy tylko kodu 64-bitowego.

    Włączenie PAC/BTI spowoduje niewielkie zwiększenie rozmiaru kodu, zwykle o 1%.

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

    Wprowadzanie zmian w kompilacji

    ndk-build

    Ustaw wartość LOCAL_BRANCH_PROTECTION := standard w każdym module pliku Android.mk.

    CMake

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

    Inne systemy kompilacji

    Skompiluj kod za pomocą -mbranch-protection=standard. Ta flaga działa tylko podczas kompilacji dla interfejsu ABI arm64-v8a. Podczas łączenia nie musisz używać tego parametru.

    Rozwiązywanie problemów

    Nie mamy informacji o problemach z obsługą kompilatora w przypadku PAC/BTI, ale:

    • Podczas łączenia nie mieszaj kodu BTI i innego kodu, ponieważ spowoduje to utworzenie biblioteki bez włączonej ochrony BTI. Za pomocą narzędzia llvm-readelf możesz sprawdzić, czy w bibliotece wynikowej znajduje się notatka 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
    [...]
    $
    
    • Starsze wersje OpenSSL (starsze niż 1.1.1i) mają błąd w ręcznie napisanym asemblerze, który powoduje błędy PAC. Uaktualnij OpenSSL do bieżącej wersji.

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