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 |
|
Niezgodny z urządzeniami z architekturą ARMv5/v6. |
arm64-v8a |
Tylko Armv8.0. | |
x86 |
Brak obsługi MOVBE i SSE4. | |
x86_64 |
|
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:
- Calling conventions for different C++ compilers and operating systems
- Intel IA-32 Intel Architecture Software Developer's Manual, Volume 2: Instruction Set Reference
- Intel IA-32 Intel Architecture Software Developer's Manual, Volume 3: System Programming Guide
- System V Application Binary Interface: Intel386 Processor Architecture Supplement
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:
- Calling conventions for different C++ compilers and operating systems
- Intel64 and IA-32 Architectures Software Developer's Manual, Volume 2: Instruction Set Reference
- Intel64 and IA-32 Intel Architecture Software Developer's Manual Volume 3: System Programming
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ę.