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 |
|
Niezgodne z urządzeniami ARMv5/v6. |
arm64-v8a |
Tylko Armv8.0. | |
x86 |
Brak obsługi instrukcji MOVBE i SSE4. | |
x86_64 |
|
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, SSE3 i SSSE3.
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:
- Konwencje wywoływania w przypadku różnych kompilatorów C++ i systemów operacyjnych
- 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, 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:
- Konwencje wywoływania w przypadku różnych kompilatorów C++ i systemów operacyjnych
- 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 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ę.