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
Interfejs ABI | Obsługiwane zestawy instrukcji | Uwagi |
---|---|---|
armeabi-v7a |
|
Brak zgodności z urządzeniami z architekturą ARM w wersji 5 lub 6. |
arm64-v8a |
Tylko Armv8.0. | |
x86 |
Brak obsługi MOVBE i SSE4. | |
x86_64 |
|
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_MODE
w Android.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, SSE3 i SSSE3.
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:
- Konwencje wywoływania dla różnych kompilatorów C++ i systemów operacyjnych
- Intel IA-32 Intel Architecture Software Developer's Manual, Tom 2: Instruction Set Reference
- Intel IA-32 Intel Architecture Software Developer's Manual, Volume 3: System Programming Guide
- System V Application Binary Interface: dodatek dla procesora Intel386
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:
- Konwencje wywoływania w różnych kompilatorach C++ i systemach 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 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-v7a
i armeabi
. 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.