Các thiết bị Android khác nhau sử dụng CPU khác nhau, do đó sẽ hỗ trợ tập lệnh khác nhau. Mỗi sự kết hợp của CPU và tập lệnh sẽ có Giao diện nhị phân của ứng dụng (ABI) riêng. ABI bao gồm các thông tin sau:
- Tập lệnh CPU (và phần mở rộng) có thể sử dụng được.
- Thứ tự byte (endianness) của bộ nhớ lưu trữ và tải trong thời gian chạy. Android luôn theo thứ tự little-endian.
- Các quy ước về việc truyền dữ liệu giữa các ứng dụng và hệ thống, bao gồm cả giới hạn căn chỉnh và cách hệ thống sử dụng ngăn xếp và đăng ký khi gọi hàm.
- Định dạng của các tệp nhị phân có thể thực thi, chẳng hạn như các chương trình và thư viện dùng chung, cùng các loại nội dung mà các tệp đó hỗ trợ. Android luôn sử dụng ELF. Để biết thêm thông tin, hãy xem phần Giao diện nhị phân của ứng dụng ELF System V.
- Cách các tên C++ được xác minh trong lệnh. Để biết thêm thông tin, hãy xemGeneric/Itanium C++ ABI.
Trang này liệt kê các ABI mà NDK hỗ trợ đồng thời cung cấp thông tin về cách hoạt động của mỗi ABI.
ABI cũng có thể tham chiếu đến API gốc mà nền tảng hỗ trợ. Để biết danh sách các loại vấn đề về ABI ảnh hưởng đến hệ thống 32 bit, hãy xem các lỗi ABI 32 bit.
ABI được hỗ trợ
ABI | Tập lệnh được hỗ trợ | Lưu ý |
---|---|---|
armeabi-v7a |
|
Không tương thích với các thiết bị ARMv5/v6. |
arm64-v8a |
Chỉ Armv8.0. | |
x86 |
Không hỗ trợ MOVBE hoặc SSE4. | |
x86_64 |
|
x86-64-v1 đầy đủ, nhưng chỉ một phần x86-64-v2 (không có LAHF-SAHF). |
Lưu ý: Trước đây, NDK hỗ trợ ARMv5 (armeabi) cũng như MIPS 32 bit và 64 bit. Tuy nhiên, chúng tôi không còn hỗ trợ các Giao diện nhị phân ứng dụng (ABI) này trong NDK r17.
armeabi-v7a
ABI này dành cho CPU dựa trên ARM 32 bit. ABI này bao gồm Thumb-2 và Neon.
Để biết thông tin về các phần của ABI không dành riêng cho Android, hãy xem Giao diện nhị phân của ứng dụng (ABI) dành cho Kiến trúc ARM
Theo mặc định, hệ thống xây dựng của NDK tạo mã Thumb-2 trừ khi bạn sử dụng LOCAL_ARM_MODE
trong Android.mk
cho ndk-build hoặc ANDROID_ARM_MODE
khi định cấu hình CMake.
Để biết thêm thông tin về quá trình phát triển của Neon, hãy xem phần Hỗ trợ Neon.
Vì lý do lịch sử, ABI này sử dụng -mfloat-abi=softfp
khiến tất cả giá trị float
được truyền trong thanh ghi số nguyên và tất cả giá trị double
được truyền trong cặp thanh ghi số nguyên khi thực hiện lệnh gọi hàm. Mặc dù có tên như vậy, nhưng điều này chỉ ảnh hưởng đến quy ước gọi dấu phẩy động: trình biên dịch sẽ vẫn sử dụng các lệnh dấu phẩy động phần cứng cho phép tính.
ABI này sử dụng long double
64 bit (IEEE binary64 tương tự như double
).
arm64-v8a
ABI này dành cho CPU dựa trên ARM 64 bit.
Hãy xem phần Tìm hiểu kiến trúc của Arm để biết toàn bộ thông tin chi tiết về những phần của ABI không dành riêng cho Android. Arm cũng đưa ra một số lời khuyên về quy trình chuyển đổi trong phần Phát triển Android 64 bit.
Bạn có thể sử dụng hàm nội tại Neon trong mã C và C++ để tận dụng phần mở rộng Advanced SIMD. Hướng dẫn lập trình Neon cho kiến trúc Armv8-A cung cấp thêm thông tin về các hàm nội tại Neon và cách lập trình Neon nói chung.
Trên Android, thanh ghi x18 dành riêng cho nền tảng là dành riêng cho ShadowCallStack và mã của bạn không được chạm tới. Các phiên bản Clang hiện tại mặc định sử dụng tuỳ chọn -ffixed-x18
trên Android, nên trừ khi bạn sở hữu trình kết hợp được viết thủ công (hoặc một trình biên dịch rất cũ), bạn không cần lo lắng về điều này.
ABI này sử dụng long double
128 bit (IEEE binary128).
x86
ABI này dành cho các CPU hỗ trợ tập lệnh thường gọi là "x86", "i386" hoặc "IA-32".
ABI của Android chứa tập lệnh cơ sở cùng với các phần mở rộng tập lệnh MMX, SSE, SSE2, SSE3 và SSSE3.
ABI không chứa bất cứ phần mở rộng tập lệnh IA-32 tuỳ chọn nào khác, chẳng hạn như MOVBE hoặc biến thể bất kỳ của SSE4. Bạn vẫn có thể sử dụng những phần mở rộng này, miễn là bạn sử dụng kỹ thuật thăm dò tính năng thời gian chạy để bật phần mở rộng và cung cấp tính năng dự phòng cho các thiết bị không hỗ trợ các phần mở rộng này.
Chuỗi công cụ NDK giả định cách căn chỉnh ngăn xếp 16 byte trước lệnh gọi hàm. Các công cụ và tuỳ chọn mặc định sẽ thực thi quy tắc này. Nếu đang viết mã tập hợp, bạn phải đảm bảo duy trì cách căn chỉnh ngăn xếp, và đảm bảo rằng các trình biên dịch khác cũng tuân thủ quy tắc này.
Hãy tham khảo các tài liệu sau để biết thêm chi tiết:
- Quy ước gọi cho các trình biên dịch và hệ điều hành C++ khác nhau
- Hướng dẫn của Nhà phát triển phần mềm kiến trúc Intel IA-32, Tập 2: Tài liệu tham khảo về tập lệnh
- Hướng dẫn của Nhà phát triển phần mềm kiến trúc Intel IA-32, Tập 3: Hướng dẫn lập trình
- Giao diện nhị phân của ứng dụng System V: Bổ sung kiến trúc bộ xử lý Intel386
ABI này sử dụng long double
64 bit (IEEE binary64 giống như double
và không phải là long double
chỉ dành cho Intel 80 bit phổ biến hơn).
x86_64
ABI này dành cho các CPU hỗ trợ tập lệnh thường gọi là "x86-64".
ABI của Android chứa tập lệnh cơ sở cùng với các phần mở rộng tập lệnh MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2 và lệnh POPCNT.
ABI không chứa bất cứ phần mở rộng tập lệnh x86-64 tuỳ chọn nào khác, chẳng hạn như MOVBE, SHA hoặc biến thể bất kỳ của AVX. Bạn vẫn có thể sử dụng những phần mở rộng này, miễn là bạn sử dụng kỹ thuật thăm dò tính năng thời gian chạy để bật phần mở rộng và cung cấp tính năng dự phòng cho các thiết bị không hỗ trợ các phần mở rộng này.
Hãy tham khảo các tài liệu sau để biết thêm chi tiết:
- Quy ước gọi cho các trình biên dịch và hệ điều hành C++ khác nhau
- Hướng dẫn của Nhà phát triển phần mềm kiến trúc Intel64 và IA-32, Tập 2: Tài liệu tham khảo về tập lệnh
- Hướng dẫn của Nhà phát triển phần mềm kiến trúc Intel64 và IA-32, Tập 3: Lập trình hệ thống
ABI này sử dụng long double
128 bit (IEEE binary128).
Tạo mã cho một ABI cụ thể
Gradle
Theo mặc định, Gradle (bất kể được sử dụng qua Android Studio hay từ dòng lệnh) xây dựng cho tất cả ABI không còn được dùng nữa. Để hạn chế tập hợp ABI mà ứng dụng hỗ trợ, hãy sử dụng abiFilters
. Ví dụ: Để chỉ xây dựng cho ABI 64 bit, hãy đặt cấu hình sau trong build.gradle
:
android {
defaultConfig {
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
}
ndk-build
Theo mặc định, ndk-build xây dựng cho tất cả các ABI không được dùng nữa. Bạn có thể nhắm mục tiêu đến ABI cụ thể bằng cách đặt APP_ABI
trong tệp Application.mk. Đoạn mã sau đây cho thấy một vài ví dụ về cách sử dụng 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.
Để biết thêm thông tin về các giá trị mà bạn có thể chỉ định cho APP_ABI
, hãy xem Application.mk.
CMake
Với CMake, bạn tạo mỗi lần một ABI và phải chỉ định ABI một cách rõ ràng. Bạn thực hiện việc này bằng biến ANDROID_ABI
. Bạn phải chỉ định biến này trên dòng lệnh (không thể đặt trong CMakeLists.txt). Ví dụ:
$ cmake -DANDROID_ABI=arm64-v8a ...
$ cmake -DANDROID_ABI=armeabi-v7a ...
$ cmake -DANDROID_ABI=x86 ...
$ cmake -DANDROID_ABI=x86_64 ...
Đối với các cờ khác phải được truyền đến CMake để xây dựng bằng NDK, hãy xem Hướng dẫn về CMake.
Hành vi mặc định của hệ thống xây dựng là đưa tệp nhị phân cho mỗi ABI vào một APK duy nhất, còn gọi là APK lớn (fat APK). APK lớn sẽ lớn hơn đáng kể so với một APK chỉ chứa các tệp nhị phân của một ABI; bạn sẽ có được khả năng tương thích rộng hơn, nhưng đổi lại là APK lớn hơn. Bạn nên tận dụng Gói ứng dụng hoặc Phần phân tách APK để giảm kích thước tệp APK mà vẫn duy trì khả năng tương thích tối đa của thiết bị.
Tại thời điểm cài đặt, trình quản lý gói chỉ giải nén mã máy thích hợp nhất cho thiết bị mục tiêu. Để biết thông tin chi tiết, hãy xem phần Tự động trích xuất mã gốc tại thời điểm cài đặt.
Quản lý ABI trên nền tảng Android
Phần này cung cấp thông tin chi tiết về cách nền tảng Android quản lý mã gốc trong APK.
Mã gốc trong gói ứng dụng
Cả Cửa hàng Play và Trình quản lý gói đều muốn tìm thư viện do NDK tạo trên các đường dẫn tệp trong APK khớp với mẫu sau:
/lib/<abi>/lib<name>.so
Ở đây, <abi>
là một trong những tên ABI được liệt kê trong phần ABI được hỗ trợ, và<name>
là tên của thư viện như bạn đã xác định cho biến LOCAL_MODULE
trong tệp Android.mk
. Vì tệp APK chỉ là tệp zip, nên việc mở các tệp này và xác nhận rằng thư viện gốc dùng chung là nơi chứa các APK này không quan trọng.
Nếu không tìm thấy các thư viện dùng chung gốc tại vị trí dự kiến, thì hệ thống sẽ không thể sử dụng các thư viện đó. Trong trường hợp như vậy, ứng dụng phải tự sao chép các thư viện sang rồi thực hiện dlopen()
.
Trong một APK lớn, mỗi thư viện nằm trong một thư mục có tên khớp với ABI tương ứng. Ví dụ: Một APK lớn có thể chứa:
/lib/armeabi/libfoo.so /lib/armeabi-v7a/libfoo.so /lib/arm64-v8a/libfoo.so /lib/x86/libfoo.so /lib/x86_64/libfoo.so
Lưu ý: Các thiết bị Android dựa trên ARMv7 chạy phiên bản 4.0.3 trở xuống cài đặt thư viện gốc từ thư mục armeabi
thay vì thư mục armeabi-v7a
nếu có cả hai thư mục. Lý do là /lib/armeabi/
xuất hiện sau /lib/armeabi-v7a/
trong APK. Sự cố này được khắc phục từ bản phát hành 4.0.4.
Hỗ trợ ABI trên nền tảng Android
Trong thời gian chạy, hệ thống Android biết được ABI nào hệ thống hỗ trợ vì thuộc tính hệ thống dành riêng cho bản dựng cho biết:
- ABI chính của thiết bị tương ứng với mã máy được sử dụng trong chính ảnh hệ thống.
- Không bắt buộc, ABI phụ, tương ứng với ABI khác mà ảnh hệ thống cũng hỗ trợ.
Cơ chế này đảm bảo rằng hệ thống trích xuất mã máy tốt nhất từ gói tại thời điểm cài đặt.
Để có hiệu suất tốt nhất, bạn nên biên dịch trực tiếp cho ABI chính. Ví dụ: Một thiết bị dựa trên ARMv5TE thông thường sẽ chỉ xác định ABI chính: armeabi
. Ngược lại, một thiết bị dựa trên ARMv7 điển hình sẽ xác định ABI chính là armeabi-v7a
và ABI phụ làarmeabi
, vì thiết bị có thể chạy các tệp nhị phân gốc của ứng dụng được tạo cho từng API đó.
Thiết bị 64 bit cũng hỗ trợ biến thể 32 bit. Lấy thiết bị arm64-v8a làm ví dụ, thiết bị cũng có thể chạy mã armeabi và armeabi-v7a. Tuy nhiên, hãy lưu ý rằng ứng dụng sẽ hoạt động hiệu quả hơn nhiều trên thiết bị 64 bit nếu ứng dụng đó nhắm mục tiêu đến arm64-v8a thay vì dựa vào thiết bị chạy phiên bản armeabi-v7a của ứng dụng.
Nhiều thiết bị dựa trên x86 cũng có thể chạy tệp nhị phân NDK armeabi-v7a
và armeabi
. Đối với các thiết bị nêu trên, ABI chính là x86
và ABI phụ là armeabi-v7a
.
Bạn có thể buộc cài đặt một tệp apk cho một ABI cụ thể. Điều này rất hữu ích khi kiểm thử. Hãy sử dụng lệnh sau:
adb install --abi abi-identifier path_to_apk
Trích xuất tự động mã gốc tại thời điểm cài đặt
Khi cài đặt một ứng dụng, dịch vụ trình quản lý gói sẽ quét APK và tìm bất kỳ thư viện dùng chung nào có dạng:
lib/<primary-abi>/lib<name>.so
Nếu không tìm thấy thư viện nào như vậy và bạn đã xác định ABI phụ, thì dịch vụ sẽ quét các thư viện dùng chung có dạng:
lib/<secondary-abi>/lib<name>.so
Khi tìm thấy các thư viện cần tìm, trình quản lý gói sẽ sao chép các thư viện đó vào /lib/lib<name>.so
, trong thư mục thư viện gốc của ứng dụng (<nativeLibraryDir>/
). Các đoạn mã sau sẽ truy xuất 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 );
Nếu hoàn toàn không có tệp đối tượng dùng chung, thì ứng dụng sẽ tạo và cài đặt nhưng sẽ gặp sự cố vào thời gian chạy.
ARMv9: Bật PAC và BTI cho C/C++
Việc bật PAC/BTI sẽ cung cấp khả năng bảo vệ chống lại một số vectơ tấn công. PAC bảo vệ các địa chỉ trả về bằng cách ký mã hóa các địa chỉ này trong prolog của một hàm và kiểm tra để đảm bảo địa chỉ trả về vẫn được ký chính xác trong epilog. BTI ngăn chuyển đến các vị trí tùy ý trong mã bằng cách yêu cầu mỗi mục tiêu nhánh là một hướng dẫn đặc biệt không làm gì ngoài việc báo cho bộ xử lý biết là bạn có thể truy cập vào đó.
Android sử dụng hướng dẫn PAC/BTI mà không thực hiện được gì trên các bộ xử lý cũ hơn không hỗ trợ hướng dẫn mới. Chỉ các thiết bị ARMv9 mới có biện pháp bảo vệ PAC/BTI, nhưng bạn cũng có thể chạy cùng một mã trên các thiết bị ARMv8: không cần nhiều biến thể của thư viện. Ngay cả trên các thiết bị ARMv9, PAC/BTI chỉ áp dụng cho mã 64 bit.
Việc bật PAC/BTI sẽ làm tăng kích thước mã một chút, thường là 1%.
Hãy xem hướng dẫn của Arm trong phần Tìm hiểu kiến trúc - Cung cấp biện pháp bảo vệ cho phần mềm phức tạp (PDF) để biết nội dung giải thích chi tiết về mục tiêu PAC/BTI của các vectơ tấn công, cũng như cách hoạt động của các biện pháp bảo vệ.
Xây dựng các thay đổi
ndk-build
Đặt LOCAL_BRANCH_PROTECTION := standard
trong mỗi mô-đun của Android.mk.
CMake
Sử dụng target_compile_options($TARGET PRIVATE -mbranch-protection=standard)
cho từng mục tiêu trong CMakeLists.txt.
Các hệ thống xây dựng khác
Biên dịch mã bằng -mbranch-protection=standard
. Cờ này chỉ hoạt động khi biên dịch cho arm64-v8a ABI. Bạn không cần phải sử dụng cờ này khi liên kết.
Khắc phục sự cố
Chúng tôi không thấy có vấn đề nào liên quan đến việc hỗ trợ Trình biên dịch cho PAC/BTI, nhưng:
- Lưu ý không kết hợp mã BTI và mã không phải BTI khi liên kết, vì điều đó dẫn đến việc thư viện không được bật tính năng bảo vệ BTI. Bạn có thể sử dụng llvm-readelf để kiểm tra xem thư viện kết quả có ghi chú BTI hay không.
$ 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 [...] $
Các phiên bản cũ của OpenSSL (trước 1.1.1i) có một lỗi trong trình kết hợp được viết thủ công gây ra lỗi PAC. Nâng cấp lên phiên bản OpenSSL hiện tại.
Phiên bản cũ của một số hệ thống DRM ứng dụng tạo ra mã vi phạm các yêu cầu của PAC/BTI. Nếu bạn đang sử dụng DRM của ứng dụng và gặp sự cố khi bật PAC/BTI, vui lòng liên hệ với nhà cung cấp DRM của bạn để biết phiên bản sửa lỗi.