ABI ของ Android

อุปกรณ์ Android ที่แตกต่างกันใช้ CPU ที่แตกต่างกัน ซึ่งรองรับชุดคำสั่งที่แตกต่างกัน การผสมผสานระหว่าง CPU และชุดคำสั่งแต่ละชุดจะมีอินเทอร์เฟซแบบไบนารีของแอปพลิเคชัน (ABI) ของตัวเอง ABI ประกอบด้วยข้อมูลต่อไปนี้

  • ชุดคำสั่ง CPU (และส่วนขยาย) ที่ใช้ได้
  • Endianness ของการจัดเก็บและการโหลดหน่วยความจำในระหว่างรันไทม์ Android จะเป็นแบบ Little-Endian เสมอ
  • ข้อกำหนดสำหรับการส่งข้อมูลระหว่างแอปพลิเคชันกับระบบ ซึ่งรวมถึงข้อจำกัดด้านการจัดแนว และวิธีที่ระบบใช้สแต็กและรีจิสเตอร์เมื่อเรียกใช้ฟังก์ชัน
  • รูปแบบของไบนารีที่เรียกใช้งานได้ เช่น โปรแกรมและไลบรารีที่แชร์ รวมถึงประเภทเนื้อหาที่รองรับ Android ใช้ ELF เสมอ ดูข้อมูลเพิ่มเติมได้ที่ ELF System V อินเทอร์เฟซแบบไบนารีของแอปพลิเคชัน
  • วิธีที่ชื่อ C++ ถูก Mangled ดูข้อมูลเพิ่มเติมได้ที่ Generic/Itanium C++ ABI

หน้านี้แสดงรายการ ABI ที่ NDK รองรับ และให้ข้อมูลเกี่ยวกับวิธีที่ ABI แต่ละรายการทำงาน

ABI ยังหมายถึง Native API ที่แพลตฟอร์มรองรับด้วย ดูรายการปัญหาเกี่ยวกับ ABI ประเภทดังกล่าวที่ส่งผลต่อระบบ 32 บิตได้ที่ ข้อบกพร่องของ ABI แบบ 32 บิต

ABI ที่รองรับ

ตารางที่ 1 ABI และชุดคำสั่งที่รองรับ

ABI ชุดคำสั่งที่รองรับ หมายเหตุ
armeabi-v7a
  • armeabi
  • Thumb-2
  • นีออน
  • ใช้ร่วมกับอุปกรณ์ ARMv5/v6 ไม่ได้
    arm64-v8a
  • AArch64
  • Armv8.0 เท่านั้น
    x86
  • x86 (IA-32)
  • MMX
  • SSE/2/3
  • SSSE3
  • ไม่รองรับ MOVBE หรือ SSE4
    x86_64
  • x86-64
  • MMX
  • SSE/2/3
  • SSSE3
  • SSE4.1, 4.2
  • POPCNT
  • CMPXCHG16B
  • LAHF-SAHF
  • x86-64-v2 แบบเต็ม

    หมายเหตุ: ในอดีต NDK รองรับ ARMv5 (armeabi) รวมถึง MIPS แบบ 32 บิตและ 64 บิต แต่การรองรับ ABI เหล่านี้ถูกนำออกใน NDK r17

    armeabi-v7a

    ABI นี้ใช้สำหรับ CPU ARM แบบ 32 บิต ซึ่งรวมถึง Thumb-2 และ Neon

    ดูข้อมูลเกี่ยวกับส่วนต่างๆ ของ ABI ที่ไม่ได้เฉพาะเจาะจงสำหรับ Android ได้ที่ Application Binary Interface (ABI) for the ARM Architecture

    ระบบบิลด์ของ NDK จะสร้างโค้ด Thumb-2 โดยค่าเริ่มต้น เว้นแต่คุณจะใช้ LOCAL_ARM_MODE ใน Android.mk สำหรับ ndk-build หรือ ANDROID_ARM_MODE เมื่อกำหนดค่า CMake

    ดูข้อมูลเพิ่มเติมเกี่ยวกับประวัติของ Neon ได้ที่ Neon Support

    ABI นี้ใช้ -mfloat-abi=softfp ด้วยเหตุผลทางประวัติศาสตร์ ซึ่งทำให้ค่า float ทั้งหมดถูกส่งผ่านในรีจิสเตอร์จำนวนเต็ม และค่า double ทั้งหมดถูกส่งผ่าน ในรีจิสเตอร์จำนวนเต็มแบบคู่เมื่อทำการเรียกใช้ฟังก์ชัน แม้ว่าจะมีชื่อเช่นนี้ แต่การดำเนินการนี้จะส่งผลต่อ ข้อกำหนดการเรียก จุดลอยตัวเท่านั้น โดยคอมไพเลอร์จะยังคง ใช้คำสั่งจุดลอยตัวของฮาร์ดแวร์สำหรับการคำนวณ

    ABI นี้ใช้ long double แบบ 64 บิต (IEEE binary64 ซึ่งเหมือนกับ double)

    arm64-v8a

    ABI นี้ใช้สำหรับ CPU ARM แบบ 64 บิต

    ดูรายละเอียดทั้งหมดเกี่ยวกับส่วนต่างๆ ของ ABI ที่ไม่ได้เฉพาะเจาะจงสำหรับ Android ได้ที่ Arm's Learn the Architecture นอกจากนี้ Arm ยังมีคำแนะนำในการพอร์ตบางอย่างใน 64-bit Android Development

    คุณสามารถใช้ Neon Intrinsics ในโค้ด C และ C++ เพื่อใช้ประโยชน์จากส่วนขยาย Advanced SIMD The Neon Programmer's Guide for Armv8-A มีข้อมูลเพิ่มเติมเกี่ยวกับ Neon Intrinsics และการเขียนโปรแกรม Neon โดยทั่วไป

    ใน Android รีจิสเตอร์ x18 ที่เฉพาะเจาะจงสำหรับแพลตฟอร์มจะสงวนไว้สำหรับ ShadowCallStack และโค้ดของคุณไม่ควรแตะต้องรีจิสเตอร์นี้ Clang เวอร์ชันปัจจุบันจะใช้ตัวเลือก -ffixed-x18 ใน Android โดยค่าเริ่มต้น ดังนั้นคุณจึงไม่จำเป็นต้องกังวลเกี่ยวกับเรื่องนี้ เว้นแต่คุณจะมีแอสเซมเบลอร์ที่เขียนด้วยมือ (หรือคอมไพเลอร์ที่เก่ามาก)

    ABI นี้ใช้ 128 บิต long double (IEEE binary128)

    x86

    ABI นี้ใช้สำหรับ CPU ที่รองรับชุดคำสั่งที่เรียกกันโดยทั่วไปว่า "x86", "i386" หรือ "IA-32"

    ABI ของ Android มีชุดคำสั่งพื้นฐาน รวมถึงส่วนขยาย MMX, SSE, SSE2, SSE3 และ SSSE3

    ABI ไม่รวมส่วนขยายชุดคำสั่ง IA-32 ที่เป็นตัวเลือกอื่นๆ เช่น MOVBE หรือ SSE4 เวอร์ชันต่างๆ คุณยังคงใช้ส่วนขยายเหล่านี้ได้ ตราบใดที่คุณใช้การตรวจสอบฟีเจอร์ในระหว่างรันไทม์เพื่อเปิดใช้ส่วนขยาย และมีฟอลแบ็กสำหรับอุปกรณ์ที่ไม่รองรับ

    เครื่องมือ Toolchain ของ NDK จะถือว่ามีการจัดแนวสแต็ก 16 ไบต์ก่อนการเรียกใช้ฟังก์ชัน เครื่องมือและตัวเลือกเริ่มต้นจะบังคับใช้กฎนี้ หากคุณเขียนโค้ดแอสเซมบลี คุณต้องตรวจสอบว่าได้รักษาการจัดแนวสแต็กไว้ และตรวจสอบว่าคอมไพเลอร์อื่นๆ ปฏิบัติตามกฎนี้ด้วย

    ดูรายละเอียดเพิ่มเติมได้ในเอกสารต่อไปนี้

    ABI นี้ใช้ long double แบบ 64 บิต (IEEE binary64 ซึ่งเหมือนกับ double และไม่ใช่ long double แบบ 80 บิตที่ใช้เฉพาะใน Intel ซึ่งพบได้ บ่อยกว่า)

    x86_64

    ABI นี้ใช้สำหรับ CPU ที่รองรับชุดคำสั่งที่เรียกกันโดยทั่วไปว่า "x86-64-v2"

    ABI ของ Android มีชุดคำสั่งพื้นฐาน รวมถึง MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2 และ คำสั่ง POPCNT

    ABI ไม่รวมส่วนขยายชุดคำสั่ง x86-64 ที่เป็นตัวเลือกอื่นๆ เช่น MOVBE, SHA หรือ AVX เวอร์ชันต่างๆ คุณยังคงใช้ส่วนขยายเหล่านี้ได้ ตราบใดที่คุณใช้การตรวจสอบฟีเจอร์ในระหว่างรันไทม์เพื่อเปิดใช้ส่วนขยาย และมีฟอลแบ็กสำหรับอุปกรณ์ที่ไม่รองรับ

    ดูรายละเอียดเพิ่มเติมได้ในเอกสารต่อไปนี้

    ABI นี้ใช้ 128 บิต long double (IEEE binary128)

    สร้างโค้ดสำหรับ ABI ที่เฉพาะเจาะจง

    Gradle

    Gradle (ไม่ว่าจะใช้ผ่าน Android Studio หรือจากบรรทัดคำสั่ง) จะสร้าง ABI ทั้งหมดที่ไม่เลิกใช้งานแล้วโดยค่าเริ่มต้น หากต้องการจำกัดชุด ABI ที่แอปพลิเคชันรองรับ ให้ใช้ abiFilters ตัวอย่างเช่น หากต้องการสร้างเฉพาะ ABI แบบ 64 บิต ให้ตั้งค่าการกำหนดค่าต่อไปนี้ใน build.gradle

    android {
        defaultConfig {
            ndk {
                abiFilters 'arm64-v8a', 'x86_64'
            }
        }
    }
    

    ndk-build

    ndk-build จะสร้าง ABI ทั้งหมดที่ไม่เลิกใช้งานแล้วโดยค่าเริ่มต้น คุณสามารถกำหนดเป้าหมาย ABI ที่เฉพาะเจาะจงได้โดยการตั้งค่า APP_ABI ในไฟล์ Application.mk ข้อมูลโค้ดต่อไปนี้แสดงตัวอย่างการใช้ 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.
    

    ดูข้อมูลเพิ่มเติมเกี่ยวกับค่าที่คุณระบุได้สำหรับ APP_ABI ได้ที่ Application.mk

    CMake

    เมื่อใช้ CMake คุณจะสร้าง ABI ได้ครั้งละ 1 รายการ และต้องระบุ ABI อย่างชัดเจน คุณทำได้โดยใช้ตัวแปร ANDROID_ABI ซึ่งต้องระบุในบรรทัดคำสั่ง (ตั้งค่าใน CMakeLists.txt ไม่ได้) เช่น

    $ cmake -DANDROID_ABI=arm64-v8a ...
    $ cmake -DANDROID_ABI=armeabi-v7a ...
    $ cmake -DANDROID_ABI=x86 ...
    $ cmake -DANDROID_ABI=x86_64 ...
    

    ดูแฟล็กอื่นๆ ที่ต้องส่งไปยัง CMake เพื่อสร้างด้วย NDK ได้ที่ คู่มือ CMake

    ลักษณะการทำงานเริ่มต้นของระบบบิลด์คือการรวมไบนารีสำหรับ ABI แต่ละรายการไว้ใน APK เดียว ซึ่งเรียกอีกอย่างว่า Fat APK Fat APK มีขนาดใหญ่กว่า APK ที่มีเฉพาะไบนารีสำหรับ ABI เดียวอย่างมาก ข้อดีคือมีความเข้ากันได้ที่กว้างขึ้น แต่ข้อเสียคือ APK มีขนาดใหญ่ขึ้น เราขอแนะนำอย่างยิ่ง ให้คุณใช้ประโยชน์จาก App Bundle หรือ APK Split เพื่อ ลดขนาด APK ในขณะที่ยังคงรักษาความเข้ากันได้กับอุปกรณ์ สูงสุด

    ในระหว่างการติดตั้ง ตัวจัดการแพ็กเกจจะคลายแพ็กเฉพาะรหัสเครื่องที่เหมาะสมที่สุดสำหรับอุปกรณ์เป้าหมาย ดูรายละเอียดได้ที่การแยกโค้ดแบบเนทีฟโดยอัตโนมัติ ในระหว่างการติดตั้ง

    การจัดการ ABI ในแพลตฟอร์ม Android

    ส่วนนี้ให้รายละเอียดเกี่ยวกับวิธีที่แพลตฟอร์ม Android จัดการโค้ดที่มาพร้อมเครื่องใน APK

    โค้ดแบบเนทีฟในแพ็กเกจแอป

    ทั้ง Play Store และตัวจัดการแพ็กเกจคาดว่าจะพบไลบรารีที่สร้างโดย NDK ในเส้นทางไฟล์ภายใน APK ที่ตรงกับรูปแบบต่อไปนี้

    /lib/<abi>/lib<name>.so
    

    โดยที่ <abi> คือชื่อ ABI รายการใดรายการหนึ่งที่แสดงใน ABI ที่รองรับ, และ <name> คือชื่อไลบรารีตามที่คุณกำหนดไว้สำหรับตัวแปร LOCAL_MODULE ในไฟล์ Android.mk เนื่องจากไฟล์ APK เป็นเพียงไฟล์ ZIP จึงเปิดไฟล์เหล่านี้และยืนยันว่าไลบรารีที่แชร์ที่มาพร้อมเครื่องอยู่ในตำแหน่งที่ถูกต้องได้ง่าย

    หากระบบไม่พบไลบรารีที่แชร์ที่มาพร้อมเครื่องในตำแหน่งที่คาดไว้ ระบบจะใช้ไลบรารีเหล่านั้นไม่ได้ ในกรณีดังกล่าว แอปเองจะต้องคัดลอกไลบรารี และจากนั้นจึงเรียกใช้ dlopen()

    ใน Fat APK ไลบรารีแต่ละรายการจะอยู่ในไดเรกทอรีที่มีชื่อตรงกับ ABI ที่เกี่ยวข้อง ตัวอย่างเช่น Fat APK อาจมีรายการต่อไปนี้

    /lib/armeabi-v7a/libfoo.so
    /lib/arm64-v8a/libfoo.so
    /lib/x86/libfoo.so
    /lib/x86_64/libfoo.so
    

    การรองรับ ABI ของแพลตฟอร์ม Android

    ระบบ Android จะทราบในระหว่างรันไทม์ว่าระบบรองรับ ABI ใดบ้าง เนื่องจากพร็อพเพอร์ตี้ระบบที่เฉพาะเจาะจงสำหรับบิลด์จะระบุข้อมูลต่อไปนี้

    • ABI หลักสำหรับอุปกรณ์ ซึ่งสอดคล้องกับรหัสเครื่องที่ใช้ในอิมเมจระบบเอง
    • ABI รอง (ไม่บังคับ) ซึ่งสอดคล้องกับ ABI อื่นๆ ที่อิมเมจระบบ รองรับด้วย

    กลไกนี้ช่วยให้มั่นใจได้ว่าระบบจะแยกโค้ดเครื่องที่ดีที่สุดออกจากแพ็กเกจในระหว่างการติดตั้ง

    คุณสามารถบังคับติดตั้ง APK สำหรับ ABI ที่เฉพาะเจาะจงได้ ซึ่งอาจมีประโยชน์สำหรับการทดสอบในอุปกรณ์ที่รองรับ ABI มากกว่า 1 รายการ ใช้คำสั่งต่อไปนี้

    adb install --abi abi-identifier path_to_apk
    

    การแยกโค้ดแบบเนทีฟโดยอัตโนมัติในระหว่างการติดตั้ง

    เมื่อติดตั้งแอปพลิเคชัน บริการตัวจัดการแพ็กเกจจะสแกน APK และมองหาไลบรารีที่แชร์ในรูปแบบต่อไปนี้

    lib/<primary-abi>/lib<name>.so
    

    หากไม่พบไลบรารีที่แชร์ในรูปแบบดังกล่าว และคุณได้กำหนด ABI รองไว้ บริการจะสแกนหาไลบรารีที่แชร์ในรูปแบบต่อไปนี้

    lib/<secondary-abi>/lib<name>.so
    

    เมื่อพบไลบรารีที่ต้องการ เครื่องมือจัดการแพ็กเกจจะคัดลอก ไลบรารีเหล่านั้นไปยัง /lib/lib<name>.so ในไดเรกทอรีไลบรารีแบบเนทีฟของแอปพลิเคชัน (<nativeLibraryDir>/) ข้อมูลโค้ดต่อไปนี้จะดึงข้อมูล 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 );

    หากไม่มีไฟล์ออบเจ็กต์ที่แชร์เลย แอปพลิเคชันจะสร้างและติดตั้งได้ แต่จะเกิดข้อขัดข้องในระหว่างรันไทม์

    ARMv9: การเปิดใช้ PAC และ BTI สำหรับ C/C++

    การเปิดใช้ PAC/BTI จะช่วยป้องกันเวกเตอร์การโจมตีบางอย่าง PAC จะปกป้องที่อยู่ส่งคืนโดยการลงชื่อแบบเข้ารหัสลับในบทนำของฟังก์ชัน และตรวจสอบว่าที่อยู่ส่งคืนยังคงลงชื่ออย่างถูกต้องในบทสรุป BTI จะป้องกันการข้ามไปยังตำแหน่งที่กำหนดเองในโค้ดโดยกำหนดให้เป้าหมายการแยกแต่ละรายการเป็นคำสั่งพิเศษที่ไม่ทำอะไรเลย แต่จะบอกโปรเซสเซอร์ว่าสามารถข้ามไปยังตำแหน่งนั้นได้

    Android ใช้คำสั่ง PAC/BTI ที่ไม่ทำอะไรเลยในโปรเซสเซอร์รุ่นเก่าที่ไม่รองรับคำสั่งใหม่ เฉพาะอุปกรณ์ ARMv9 เท่านั้นที่จะมีการป้องกัน PAC/BTI แต่คุณสามารถเรียกใช้โค้ดเดียวกันในอุปกรณ์ ARMv8 ได้ด้วย โดยไม่จำเป็นต้องมีไลบรารีหลายเวอร์ชัน แม้ในอุปกรณ์ ARMv9 PAC/BTI จะใช้ได้กับโค้ด 64 บิตเท่านั้น

    การเปิดใช้ PAC/BTI จะทำให้ขนาดโค้ดเพิ่มขึ้นเล็กน้อย โดยปกติจะอยู่ที่ 1%

    ดูคำอธิบายโดยละเอียดเกี่ยวกับเวกเตอร์การโจมตีที่ PAC/BTI กำหนดเป้าหมาย และวิธีที่การป้องกันทำงานได้ที่ Learn the architecture - Providing protection for complex software (PDF) ของ Arm

    การเปลี่ยนแปลงบิลด์

    ndk-build

    ตั้งค่า LOCAL_BRANCH_PROTECTION := standard ในแต่ละโมดูลของ Android.mk

    CMake

    ใช้ target_compile_options($TARGET PRIVATE -mbranch-protection=standard) สำหรับแต่ละเป้าหมายใน CMakeLists.txt

    ระบบบิลด์อื่นๆ

    คอมไพล์โค้ดโดยใช้ -mbranch-protection=standard แฟล็กนี้จะทำงานเมื่อคอมไพล์สำหรับ ABI arm64-v8a เท่านั้น คุณไม่จำเป็นต้องใช้แฟล็กนี้เมื่อลิงก์

    การแก้ปัญหา

    เราไม่พบปัญหาใดๆ เกี่ยวกับการรองรับ PAC/BTI ของคอมไพเลอร์ แต่มีข้อควรระวังดังนี้

    • ระมัดระวังไม่ให้รวมโค้ด BTI และโค้ดที่ไม่ใช่ BTI เข้าด้วยกันเมื่อลิงก์ เนื่องจากจะทำให้ไลบรารีไม่มีการป้องกัน BTI ที่เปิดใช้อยู่ คุณสามารถใช้ llvm-readelf เพื่อตรวจสอบว่าไลบรารีที่ได้มีโน้ต 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
    [...]
    $
    
    • OpenSSL เวอร์ชันเก่า (ก่อน 1.1.1i) มีข้อบกพร่องในแอสเซมเบลอร์ที่เขียนด้วยมือซึ่งทำให้ PAC ล้มเหลว โปรดอัปเกรดเป็น OpenSSL เวอร์ชันปัจจุบัน

    • ระบบ DRM ของแอปบางระบบในเวอร์ชันเก่าจะสร้างโค้ดที่ละเมิดข้อกำหนด PAC/BTI หากคุณใช้ DRM ของแอปและพบปัญหาเมื่อเปิดใช้ PAC/BTI โปรดติดต่อผู้ให้บริการ DRM เพื่อขอเวอร์ชันที่แก้ไขแล้ว