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 instrukcji ma własny interfejs binarny aplikacji (ABI). Interfejs ABI zawiera te informacje:

  • Zestaw instrukcji procesora (i rozszerzeń), których można używać.
  • Koniec pamięci zapisywanej i wczytywanej w czasie działania. Android jest zawsze mały.
  • Konwencje przekazywania danych między aplikacjami a systemem, w tym ograniczenia wyrównania oraz sposób używania przez system stosu i rejestrów podczas wywoływania funkcji.
  • Format plików binarnych, takich jak programy i biblioteki współużytkowane, oraz typy treści, które obsługują. Android zawsze używa ELF. Więcej informacji znajdziesz w interfejsie binarnym aplikacji ELF System V.
  • Jak są przetwarzane nazwy w C++. Więcej informacji znajdziesz w artykule Generic/Itanium C++ ABI.

Na tej stronie znajdziesz listę ABI-ów obsługiwanych przez NDK oraz informacje o ich działaniu.

Interfejs ABI może też odnosić się do natywnego interfejsu API obsługiwanego przez platformę. Listę takich problemów z interfejsem ABI, które występują w systemach 32-bitowych, 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
  • Brak zgodności z urządzeniami z architekturą ARM w wersji 5 lub 6.
    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
  • Pełny format x86-64-v1, ale tylko częściowe x86-64-v2 (bez LAHF-SAHF).

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

    armeabi-v7a

    Ten ABI jest przeznaczony dla 32-bitowych procesorów ARM. Dotyczy to modeli Thumb-2 i Neon.

    Informacje o częściach interfejsu 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 użyjesz opcji LOCAL_ARM_MODEAndroid.mk w ndk-build lub ANDROID_ARM_MODE podczas konfigurowania CMake.

    Więcej informacji o historii Neona znajdziesz w Centrum pomocy Neona.

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

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

    arm64-v8a

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

    Szczegółowe informacje o częściach ABI, które nie są specyficzne dla Androida, znajdziesz w dokumentacji Arm na temat architektury. Arm oferuje też porady dotyczące portowania w artykule 64-bitowy rozwój aplikacji na Androida.

    Aby korzystać z rozszerzenia Advanced SIMD, możesz używać wbudowanych instrukcji Neon w kodzie C i C++. Więcej informacji o funkcjach wewnętrznych Neon i programowaniu w Neon znajdziesz w przewodniku dla programistów Neon na temat Armv8-A.

    Na Androidzie rejestr x18 specyficzny dla platformy jest zarezerwowany dla ShadowCallStack i nie powinien być modyfikowany przez kod. Aktualne wersje Clang domyślnie używają opcji -ffixed-x18 na Androidzie, więc jeśli nie masz własnoręcznie napisanego asemblera (lub bardzo starego kompilatora), nie musisz się tym przejmować.

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

    x86

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

    ABI Androida obejmuje podstawową instrukcję oraz rozszerzenia MMX, SSE, SSE2, SSE3SSSE3.

    Interfejs ABI nie zawiera żadnych innych opcjonalnych rozszerzeń zestawów instrukcji IA-32, takich jak MOVBE czy żaden wariant SSE4. Możesz nadal używać tych rozszerzeń, o ile używasz funkcji sprawdzania funkcji w czasie wykonywania, aby je włączyć, oraz zapewniasz alternatywne rozwiązania na urządzeniach, które ich nie obsługują.

    Narzędzie 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 w języku asemblera, musisz zachować wyrównanie stosu i zadbać o to, aby inne kompilatory również przestrzegały tej reguły.

    Więcej informacji znajdziesz w tych dokumentach:

    Ten interfejs ABI używa 64-bitowego kodu long double (IEEE binarnemu64 to samo co double, a nie bardziej popularne 80-bitowe rozwiązanie long double tylko z procesorem Intel).

    x86_64

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

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

    ABI nie zawiera żadnych innych opcjonalnych rozszerzeń zestawu instrukcji x86-64, takich jak MOVBE, SHA czy dowolna wersja AVX. Możesz nadal używać tych rozszerzeń, o ile używasz funkcji sprawdzania funkcji w czasie wykonywania, aby je włączyć, oraz zapewniasz alternatywne rozwiązania na urządzeniach, które ich nie obsługują.

    Więcej informacji znajdziesz w tych dokumentach:

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

    Generowanie kodu dla określonego ABI

    Gradle

    Gradle (używany w Android Studio lub w wierszu poleceń) domyślnie kompiluje wszystkie interfejsy ABI, które nie zostały wycofane. Aby ograniczyć zestaw ABI obsługiwanych przez aplikację, użyj abiFilters. Aby na przykład kompilować tylko 64-bitowe interfejsy ABI, ustaw w pliku build.gradle tę konfigurację:

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

    ndk-build

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

    CMake

    W CMake kompilujesz dla jednego ABI naraz i musisz wyraźnie określić ABI. W tym celu użyj zmiennej ANDROID_ABI, którą należy 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 ...
    

    Inne flagi, które należy przekazać do CMake, aby skompilować kod za pomocą NDK, znajdziesz w przewodniku CMake.

    Domyślne zachowanie systemu kompilacji polega na uwzględnieniu plików binarnych dla każdego ABI w pojedynczym pliku APK, zwanym też grubym plikiem APK. Pełny plik APK jest znacznie większy niż ten, który zawiera tylko binarne pliki dla jednego ABI. W zamian uzyskuje się większą zgodność, ale kosztem większego pliku APK. Zdecydowanie zalecamy korzystanie z pakietów aplikacji lub podzielenia pliku 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 artykule Automatyczne wyodrębnianie kodu natywnego podczas instalacji.

    Zarządzanie 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

    Zarówno Sklep Play, jak i Menedżer pakietów oczekują, że w plikach ścieżek w pliku APK znajdą biblioteki wygenerowane przez NDK, które odpowiadają temu wzorcem:

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

    W tym przykładzie <abi> to jedna z nazwy ABI wymienionych w sekcji Obsługiwane ABI, a <name> to nazwa biblioteki zdefiniowana dla zmiennej LOCAL_MODULE w pliku Android.mk. Ponieważ pliki APK to po prostu pliki ZIP, 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 oczekiwanych miejscach, nie będzie mógł z nich korzystać. W takim przypadku aplikacja musi skopiować biblioteki, a następnie wykonać działanie dlopen().

    W pliku APK typu fat każda biblioteka znajduje się w katalogu o nazwie odpowiadającej odpowiedniemu ABI. Na przykład duży plik APK może 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: urządzenia z Androidem oparte na ARMv7 z wersją 4.0.3 lub starszą instalują natywne biblioteki z katalogu armeabi zamiast z katalogu armeabi-v7a, jeśli oba istnieją. Dzieje się tak, ponieważ w pliku APK /lib/armeabi/ występuje po /lib/armeabi-v7a/. Ten problem został rozwiązany w wersji 4.0.4.

    Obsługa interfejsu ABI platformy Androida

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

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

    Ten mechanizm zapewnia, że system wyodrębnia najlepszy kod maszynowy z pakietu w momencie instalacji.

    Aby uzyskać najlepszą wydajność, skompiluj bezpośrednio dla podstawowego ABI. Na przykład typowe urządzenie oparte na ARMv5TE definiuje tylko podstawowy ABI: armeabi. Natomiast typowe urządzenie z procesorem ARMv7 zdefiniuje podstawowy 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 wersje 32-bitowe. Na przykład urządzenia z interfejsem arm64-v8a mogą też obsługiwać kody armeabi i armeabi-v7a. Pamiętaj jednak, że aplikacja będzie działać znacznie lepiej na urządzeniach 64-bitowych, jeśli będzie przeznaczona na platformę arm64-v8a, a nie na wersję armeabi-v7a.

    Wiele urządzeń z procesorami x86 może też uruchamiać pliki binarne NDK armeabi-v7aarmeabi. W przypadku takich urządzeń główny ABI to x86, a drugi to armeabi-v7a.

    Możesz wymusić instalację pakietu 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 w momencie instalacji;

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

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

    Jeśli nie zostanie znaleziony żaden ABI, a użytkownik zdefiniuje dodatkowy ABI, usługa przeskanuje udostępnione biblioteki w formie:

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

    Po znalezieniu bibliotek, których szuka, menedżer pakietów kopiuje je do katalogu /lib/lib<name>.so w katalogu biblioteki natywnej 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 w cale pliku obiektu współdzielonego, aplikacja jest kompilowana i instalowana, ale ulega awarii w czasie działania.

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

    Włączenie plików 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 poprawnie podpisany w epilogu. BTI zapobiega przeskakiwaniu do dowolnych lokalizacji w kodzie, wymagając, aby każdy element docelowy gałęzi był specjalną instrukcją, która nie robi nic poza informowaniem procesora, że może się tam zatrzymać.

    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ą chronione PAC/BTI, ale ten sam kod możesz uruchomić również na urządzeniach ARMv8. Nie musisz tworzyć wielu wariantów swojej biblioteki. Nawet na urządzeniach z architekturą ARMv9 kod PAC/BTI dotyczy tylko kodu 64-bitowego.

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

    Szczegółowe informacje o wektorach ataków, na które narażone są PAC/BTI, oraz o działaniu ochrony znajdziesz w artykule Arm Learn the architecture - Providing protection for complex software (PDF).

    Zmiany w kompilacji

    ndk-build

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

    CMake

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

    Inne systemy kompilacji

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

    Rozwiązywanie problemów

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

    • Podczas łączenia nie należy mieszać kodu BTI z kodem bez BTI, ponieważ może to spowodować, że biblioteka nie będzie miała włączonej ochrony BTI. Możesz użyć narzędzia llvm-readelf, aby sprawdzić, czy utworzona biblioteka zawiera notatkę 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) zawierają błąd w ręcznie napisanym kodzie asemblera, który powoduje błędy w PAC. Uaktualnij OpenSSL do bieżącej wersji.

    • Stare 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łączaniem PAC/BTI, skontaktuj się z dostawcą DRM w sprawie wersji z poprawkami.