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ể chỉ 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
  • 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
  • Lưu ý: Trước đây, NDK hỗ trợ ARMv5 (armeabi) và MIPS 32 bit và 64 bit. Tuy nhiên, chúng tôi không còn hỗ trợ các ABI này trong NDK r17.

    armeabi-v7a

    ABI này dành cho CPU dựa trên ARM 32 bit. Biến thể Android bao gồm Thumb-2 và hướng dẫn dấu phẩy động phần cứng VFP, cụ thể là VFPv3-D16, bao gồm 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 armeabi-v7a sử dụng -mfloat-abi=softfp để thực thi quy tắc mà dù hệ thống có thể thực thi mã dấu phẩy động, trình biên dịch vẫn 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 số thanh ghi số nguyên khi thực hiện lệnh gọi hàm.

    arm64-v8a

    ABI này dành cho các CPU dựa trên ARMv8-A, hỗ trợ kiến trúc AArch64 64 bit. ABI này có cả phần mở rộng kiến trúc Advanced SIMD (Neon).

    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.

    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át triển Android 64 bit.

    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 có trình kết hợp được viết thủ công (hand-written) (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.

    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". Các đặc điểm của ABI này bao gồm:

    • Hướng dẫn thường do GCC tạo với cờ của trình biên dịch, chẳng hạn như sau:
      -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32
      

      Những cờ này nhắm mục tiêu đến tập lệnh Pentium Pro, cùng với phần mở rộng tập lệnh MMX, SSE, SSE2, SSE3SSSE3. Mã được tạo là sự tối ưu hoá cân bằng giữa các CPU Intel 32 bit hàng đầu.

      Để biết thêm thông tin về cờ của trình biên dịch, đặc biệt là liên quan đến việc tối ưu hoá hiệu suất, hãy tham khảo Gợi ý về hiệu suất GCC x86.

    • Sử dụng quy ước gọi Linux x86 32 bit tiêu chuẩn, trái ngược với quy ước gọi SVR. Để biết thêm thông tin, hãy xem phần 6, "Sử dụng thanh ghi", của Quy ước gọi cho các trình biên dịch và hệ điều hành C++ khác nhau.

    ABI không bao gồm bất kỳ phần mở rộng tập lệnh IA-32 tuỳ chọn nào khác, chẳng hạn như:

    • MOVBE
    • Bất kỳ biến thể nào 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:

    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 này hỗ trợ hướng dẫn mà GCC thường tạo với các cờ trình biên dịch sau đây:

    -march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel
    

    Các cờ này nhắm mục tiêu đến tập lệnh x86-64, theo tài liệu về GCC, cùng với phần mở rộng tập lệnh MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2, và POPCNT. Mã được tạo là sự tối ưu hoá cân bằng giữa các CPU Intel 64 bit hàng đầu.

    Để biết thêm thông tin về cờ của trình biên dịch, đặc biệt là liên quan đến việc tối ưu hoá hiệu suất, hãy tham khảo Gợi ý về hiệu suất GCC x86.

    ABI này không bao gồm bất kỳ 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
    • AVX
    • AVX2

    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:

    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 gặp sự cố vào thời gian chạy.