ABI Android

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ợ

Bảng 1. ABI và tập lệnh được hỗ trợ.

ABI Tập lệnh được hỗ trợ Lưu ý
armeabi-v7a
  • armeabi
  • Thumb-2
  • VFPv3-D16
  • Không tương thích với các thiết bị ARMv5/v6.
    arm64-v8a
  • AArch64
  • Chỉ Armv8.0.
    x86
  • x86 (IA-32)
  • MMX
  • SSE/2/3
  • SSSE3
  • Không hỗ trợ MOVBE hoặc SSE4.
    x86_64
  • x86-64
  • MMX
  • SSE/2/3
  • SSSE3
  • SSE4.1, 4.2
  • POPCNT
  • Chỉ x86-64-v1.

    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. Giao diện này bao gồm các lệnh với dấu phẩy động phần cứng (VFP) Neon và Thumb-2, cụ thể là VFPv3-D16 với 16 thanh ghi dấu phẩy động 64 bit chuyên dụng.

    Để 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.

    Các phần mở rộng khác, trong đó có Advanced SIMD (Neon) và VFPv3-D32, là không bắt buộc. Để biết thêm thông tin, hãy xem phần Hỗ trợ Neon.

    ABI này sử dụng -mfloat-abi=softfp để thực thi quy tắc mà trình biên dịch phải truyền tất cả giá trị float trong thanh ghi số nguyên và tất cả giá trị double trong cặp thanh ghi số nguyên khi thực hiện lệnh gọi hàm. Điều này chỉ ảnh hưởng đến quy ước gọi. Trình biên dịch sẽ vẫn sử dụng các lệnh với dấu phẩy động phần cứng.

    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, SSE3SSSE3.

    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:

    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:

    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-v7aarmeabi. Đố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.