Interfejsy ABI Androida

Różne urządzenia z Androidem korzystają z różnych procesorów, które z kolei obsługują różne zestawy instrukcji. Każda kombinacja procesora i zestawu instrukcji ma własny interfejs binarny aplikacji (ABI). Zawiera on te informacje:

  • Zestaw instrukcji dotyczących procesora (i rozszerzenia), których można używać.
  • Wydłużenie czasu przechowywania i wczytywania pamięci w czasie działania. Android jest zawsze mały.
  • konwencje dotyczące przekazywania danych między aplikacjami a systemem, w tym ograniczenia wyrównania, oraz sposób korzystania ze stosu i rejestrowania się przez system podczas wywoływania funkcji.
  • Format wykonywalnych plików binarnych (np. programy i biblioteki udostępnione) oraz typy obsługiwanych treści. Android zawsze używa ELF. Więcej informacji znajdziesz w artykule o interfejsie binarnym aplikacji systemu ELF V.
  • Jak zniekształcone są nazwy C++ Więcej informacji znajdziesz w artykule o interfejsie ABI Ogólne/Itanium C++ ABI.

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

Interfejs ABI może też odnosić się do natywnego interfejsu API obsługiwanego przez platformę. Listę tego typu problemów z interfejsem ABI w systemach 32-bitowych znajdziesz w sekcji Błędy 32-bitowego interfejsu ABI.

Obsługiwane interfejsy ABI

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

Interfejs ABI Obsługiwane zestawy instrukcji Uwagi
armeabi-v7a
  • Armeabi
  • Kciuk-2
  • Neon
  • Brak zgodności z urządzeniami z architekturą ARMv5 lub 6.
    arm64-v8a
  • AArch64
  • Tylko Armv8.0.
    x86
  • x86 (IA-32)
  • kod MMX
  • SSE/2/3
  • SSSE3
  • Brak obsługi MOVBE i SSE4.
    x86_64
  • x86–64
  • kod MMX
  • SSE/2/3
  • SSSE3
  • SSE4.1, 4.2
  • protokół POPCN
  • Tylko x86-64-v1.

    Uwaga: wcześniej NDK obsługiwała ARMv5 (armeabi) oraz 32-bitowe i 64-bitowe MIPS, ale te interfejsy ABI zostały wycofane w NDK r17.

    Armeabi-V7a

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

    Informacje o częściach interfejsu ABI, które nie są związane z Androidem, znajdziesz w artykule o interfejsie binarnym aplikacji (ABI) dla architektury ARM.

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

    Więcej informacji o historii Neon znajdziesz w artykule pomocy dotyczącej neonów.

    Ze względów historycznych ten interfejs ABI używa -mfloat-abi=softfp, przez co wszystkie wartości float są przekazywane w rejestrach liczb całkowitych, a wszystkie wartości double są przekazywane w parach rejestrów całkowitych podczas wykonywania wywołań funkcji. Niezależnie od nazwy ma to wpływ tylko na konwencję wywołań liczb zmiennoprzecinkowych: kompilator nadal będzie używać sprzętowych instrukcji dotyczących liczby zmiennoprzecinkowej do arytmetyki.

    Ten interfejs ABI używa 64-bitowej dyrektywy long double (IEEE Binary64, tak samo 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ą związane z Androidem, znajdziesz w artykule Learn the Architecture. Argument udziela też porad dotyczących przenoszenia numerów w sekcji Tworzenie 64-bitowych wersji Androida.

    Aby skorzystać z rozszerzenia Advanced SIMD, możesz użyć kodu Neon intrinsics w językach C i C++. Artykuł Neon Programmer's Guide for Armv8-A zawiera więcej informacji o wbudowanym neonach i oprogramowaniu w Neon.

    W Androidzie rejestr x18 odpowiedni dla danej platformy jest zarezerwowany dla ShadowCallStack i kod nie powinien go dotyczyć. W bieżących wersjach Clang na Androidzie domyślnie używana jest opcja -ffixed-x18, więc jeśli nie masz ręcznego asemratora (lub bardzo starego kompilatora), nie musisz się tym przejmować.

    Ten interfejs ABI wykorzystuje 128-bitowy kod long double (IEEE plik binarny128).

    x86

    Ten interfejs ABI jest przeznaczony dla procesorów obsługujących zestaw instrukcji nazywany potocznie „x86”, „i386” lub „IA-32”.

    Interfejs ABI Androida zawiera podstawowy zestaw instrukcji oraz rozszerzenia MMX, SSE, SSE2, SSE3 i SSSE3.

    Interfejs ABI nie zawiera żadnych innych opcjonalnych rozszerzeń zestawu instrukcji IA-32, takich jak MOVBE ani żadnych wariantów SSE4. Możesz ich nadal używać, o ile je włączysz, za pomocą sondowania funkcji w czasie działania. Możesz też udostępnić reklamy zastępcze w przypadku 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 egzekwują tę regułę. Jeśli piszesz kod asemblera, pamiętaj o utrzymaniu wyrównania stosu i upewnij się, że inne kompilatory również przestrzegają tej reguły.

    Więcej informacji znajdziesz w tych dokumentach:

    Ten interfejs ABI wykorzystuje 64-bitowy interfejs long double (IEEE bin64 taki sam jak double, a nie bardziej typowy 80-bitowy long double przeznaczony tylko dla Intela).

    x86_64

    Ten interfejs ABI jest przeznaczony dla procesorów obsługujących zestaw instrukcji zwany potocznie „x86-64”.

    Interfejs ABI Androida obejmuje podstawowy zestaw instrukcji oraz instrukcje MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2 oraz instrukcję POPCNT.

    Interfejs ABI nie zawiera żadnych innych opcjonalnych rozszerzeń zestawu instrukcji x86-64, takich jak MOVBE, SHA ani żadnych wariantów AVX. Nadal możesz używać tych rozszerzeń, o ile je włączysz, pod warunkiem, że użyjesz sondowania funkcji w czasie działania aplikacji, i zastosuj wartości zastępcze dla urządzeń, które ich nie obsługują.

    Więcej informacji znajdziesz w tych dokumentach:

    Ten interfejs ABI wykorzystuje 128-bitowy kod long double (IEEE plik binarny128).

    Generowanie kodu dla określonego interfejsu ABI

    Gradle

    Gradle (używana w Android Studio lub za pomocą wiersza poleceń) domyślnie kompilowa wszystkie niewycofane interfejsy ABI. Aby ograniczyć zestaw interfejsów ABI obsługiwanych przez Twoją aplikację, użyj abiFilters. Aby na przykład kompilować tylko 64-bitowe interfejsy ABI, ustaw w build.gradle tę konfigurację:

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

    NK Build

    Domyślnie kompilacje ndk-build dla wszystkich niewycofanych interfejsów ABI. Możesz ustawić kierowanie na konkretne interfejsy ABI, ustawiając APP_ABI w pliku Application.mk. Ten fragment kodu zawiera kilka przykładów użycia właściwości 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żna określić dla elementu APP_ABI, znajdziesz w sekcji Application.mk.

    CMake

    Dzięki CMake możesz utworzyć jedną kompilację dla jednego interfejsu ABI naraz i dokładnie określić swój interfejs ABI. Służy do tego zmienna ANDROID_ABI, która musi być określona 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 pozostałych flagach, które trzeba przekazać do CMake, aby utworzyć kompilację z użyciem pakietu NDK, znajdziesz w przewodniku po CMake.

    Domyślnym zachowaniem systemu kompilacji jest umieszczanie plików binarnych dla każdego interfejsu ABI w jednym pliku APK, nazywanym też dużym plikiem APK. Duża liczba plików APK jest znacznie większa niż pakiet zawierający tylko pliki binarne dla pojedynczego interfejsu ABI. Ten kompromis zyskuje większą zgodność, ale kosztem większego pliku APK. Zdecydowanie zalecamy skorzystanie z pakietów aplikacji lub podziałów plików APK w celu zmniejszenia rozmiaru plików APK przy zachowaniu maksymalnej zgodności z urządzeniami.

    Podczas instalacji menedżer pakietów rozpakowuje tylko kod komputera najbardziej odpowiedni dla urządzenia docelowego. Więcej informacji znajdziesz w sekcji Automatyczne wyodrębnianie kodu natywnego w momencie instalacji.

    Zarządzanie interfejsem ABI na platformie Android

    Ta sekcja zawiera szczegółowe informacje o tym, jak platforma Androida zarządza kodem natywnym w plikach APK.

    Kod natywny w pakietach aplikacji

    Sklep Play i Menedżer pakietów oczekują, że biblioteki wygenerowane w ramach pakietu NDK będą znajdować się w ścieżkach plików pakietu APK zgodnych z tym wzorcem:

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

    W tym miejscu <abi> to jedna z nazw interfejsów ABI wymienionych w sekcji Obsługiwane interfejsy ABI, a <name> to nazwa biblioteki zdefiniowana przez Ciebie dla zmiennej LOCAL_MODULE w pliku Android.mk. Ponieważ pliki APK to tylko pliki ZIP, można je łatwo otworzyć i sprawdzić, czy udostępnione biblioteki natywne znajdują się tam, gdzie się znajdują.

    Jeśli system nie znajdzie natywnych bibliotek udostępnionych w oczekiwanych miejscach, nie będzie mógł ich użyć. W takim przypadku sama aplikacja musi skopiować biblioteki, a potem wykonać polecenie dlopen().

    W twardych plikach APK każda biblioteka znajduje się w katalogu, którego nazwa odpowiada odpowiedniemu interfejsowi ABI. Tłuszczowy pakiet 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: na urządzeniach z Androidem z procesorami ARMv7 i z Androidem 4.0.3 lub starszym instalujesz biblioteki natywne z katalogu armeabi, a nie z katalogu armeabi-v7a, jeśli istnieją oba katalogi. Dzieje się tak, ponieważ /lib/armeabi/ w pliku APK jest po /lib/armeabi-v7a/. Ten problem został rozwiązany od wersji 4.0.4.

    Obsługa interfejsu ABI platformy Androida

    System Android wie w czasie działania, które interfejsy ABI obsługuje, ponieważ właściwości systemowe związane z konkretną kompilacją wskazują:

    • Podstawowy interfejs ABI urządzenia odpowiadający kodowi maszyny używanemu w obrazie systemu.
    • Opcjonalnie dodatkowe interfejsy ABI odpowiadające innym interfejsom ABI obsługiwanym przez obraz systemu.

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

    Aby uzyskać najlepszą wydajność, skompiluj bezpośrednio dla głównego interfejsu ABI. Na przykład typowe urządzenie oparte na ARMv5TE określa tylko podstawowy interfejs ABI: armeabi. Z kolei typowe urządzenie z architekturą ARMv7 definiowałoby podstawowy interfejs ABI jako armeabi-v7a, a dodatkowe jako armeabi, ponieważ może uruchamiać natywne pliki binarne aplikacji wygenerowane dla każdego z nich.

    Urządzenia 64-bitowe obsługują również wersje 32-bitowe. Korzystając z na przykład urządzeń Arm64-v8a, może ono uruchomić kod armeabi i armeabi-v7a. Pamiętaj jednak, że aplikacja będzie działać o wiele lepiej na urządzeniach 64-bitowych, jeśli będzie kierowana na architekturę Arm64-v8a, a nie na urządzeniu, na którym działa wersja armeabi-v7a.

    Na wielu urządzeniach z architekturą x86 można też uruchamiać pliki binarne armeabi-v7a i armeabi NDK. W przypadku takich urządzeń podstawowy interfejs ABI to x86, a drugi – armeabi-v7a.

    Możesz wymusić instalację pliku APK dla określonego interfejsu ABI. Jest to przydatne podczas 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 bibliotek udostępnionych w tym formularzu:

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

    Jeśli nie zostanie znaleziony i zdefiniowany jest dodatkowy interfejs ABI, usługa przeskanuje biblioteki udostępnione w tym formularzu:

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

    Po znalezieniu bibliotek, których poszukuje, menedżer pakietów kopiuje je do katalogu biblioteki /lib/lib<name>.so aplikacji (<nativeLibraryDir>/). Ten fragment kodu pobiera 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 w ogóle nie ma pliku współdzielonego obiektu, aplikacja kompilowa się i instaluje, ale ulega awarii w czasie działania.

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

    Włączenie PAC/BTI zapewni ochronę przed niektórymi wektorami ataku. Kod PAC chroni adresy zwrotne przez podpisywanie ich kryptograficznie w prologu funkcji i sprawdzanie, czy adres zwrotny jest nadal prawidłowo podpisany w epilogu. BTI zapobiega przeskakiwaniu do dowolnych lokalizacji w kodzie, wymagając, aby każde miejsce docelowe gałęzi było specjalną instrukcją, która nie robi nic poza informacją dla procesora, że można tam trafić.

    Android używa instrukcji PAC/BTI, które nie mają żadnego wpływu na starsze procesory, które nie obsługują nowych instrukcji. Zabezpieczenie PAC/BTI działa tylko w przypadku urządzeń z architekturą ARMv9, ale ten sam kod możesz też uruchamiać na urządzeniach z architekturą ARMv8: nie trzeba tworzyć wielu wariantów biblioteki. Nawet na urządzeniach z architekturą ARMv9 kod PAC/BTI ma zastosowanie tylko do kodu 64-bitowego.

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

    Zapoznaj się z dokumentem Armii Armii: Nauczanie architektury – zapewnianie ochrony złożonego oprogramowania (PDF), aby dowiedzieć się więcej o celu wektorów PAC/BTI oraz o tym, jak działa ochrona.

    Utwórz zmiany

    NK Build

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

    CMake

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

    Inne systemy kompilacji

    Skompiluj kod za pomocą narzędzia -mbranch-protection=standard. Ta flaga działa tylko podczas kompilowania do interfejsu ABI Arm64-v8a. Nie trzeba używać tej flagi do tworzenia linków.

    Rozwiązywanie problemów

    Nie wiemy o żadnych problemach z obsługą kompilatora w przypadku PAC/BTI, ale:

    • Uważaj, aby podczas łączenia nie mieszać kodu BTI i innego niż BTI, ponieważ w efekcie biblioteka nie będzie miała włączonej ochrony 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
    [...]
    $
    
    • W starszych wersjach OpenSSL (sprzed 1.1.1i) występuje błąd w ręcznie napisanym asystowaniu, który powoduje błędy PAC. Uaktualnij OpenSSL do bieżącej wersji.

    • Stare wersje niektórych systemów DRM aplikacji generują kod naruszający wymagania PAC/BTI. Jeśli korzystasz z DRM aplikacji i masz problemy z włączaniem plików PAC/BTI, skontaktuj się z dostawcą zabezpieczeń DRM, aby uzyskać poprawioną wersję.