ממשקי ABI של Android

במכשירי Android שונים יש מעבדים שונים, שבתורן תומכים בהבדלים של מערכי הוראות. לכל שילוב של מעבד (CPU) וערכת הוראות יש Application Binary Interface (ABI). ממשק ABI כולל את הפרטים הבאים:

  • קבוצת ההוראות של ה-CPU (והתוספים) שבהם אפשר להשתמש.
  • סוף הזמן של אחסון וטעינה של זיכרון בזמן ריצה. מערכת Android היא תמיד הקטנה אנד-אנדית.
  • מוסכמות להעברת נתונים בין אפליקציות למערכת, כולל מגבלות יישור והאופן שבו המערכת משתמשת בסטאק נרשם בזמן קריאה לפונקציות.
  • הפורמט של קבצים בינאריים להפעלה, כמו תוכנות וספריות משותפות, ואת סוגי התוכן שהם תומכים בהם. מערכת Android תמיד משתמשת ב-ELF. לקבלת מידע נוסף מידע נוסף, ראה ELF System V Application Binary Interface.
  • איך שמות C++ מעוותים. מידע נוסף זמין במאמר הבא: גנרי/Itanium C++ ABI.

בדף הזה מפורטים ממשקי ה-ABI שנתמכים ב-NDK, ומספק מידע האופן שבו פועל כל ממשק ABI.

ABI יכול גם להתייחס ל-API המקורי שנתמך על ידי הפלטפורמה. עבור רשימה של סוגי הבעיות האלה בממשק ABI שמשפיעות על מערכות 32 ביט: באג ABI ב-32 ביט.

ממשקי ABI נתמכים

טבלה 1. ממשקי ABI וערכות הוראות נתמכות.

ABI ערכות הוראות נתמכות הערות
armeabi-v7a
  • ארמאבי
  • אגודל 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
  • מלא x86-64-v1, אלא רק x86-64-v2 חלקי (ללא CMPXCHG16B או LAHF-SAHF).

    הערה: בעבר, ה-NDK תמך ב-ARMv5 (armeabi) ו-MIPS של 32 ביט ו-64 ביט, אבל התמיכה בממשקי ה-ABI האלה הוסרה NDK 17.

    Armeabi-v7a

    ה-ABI הזה מיועד למעבדי ARM 32 ביט. הוא כולל את אגודל 2 וניאון.

    לקבלת מידע על חלקי ה-ABI שהם לא ספציפיים ל-Android, אפשר לעיין במאמר Application Binary Interface (ABI) לארכיטקטורת ARM

    מערכות ה-build של NDK יוצרות קוד Thumb-2 כברירת מחדל, אלא אם משתמשים LOCAL_ARM_MODE ב-Android.mk עבור ndk-build או ANDROID_ARM_MODE כשמגדירים את CMake.

    מידע נוסף על ההיסטוריה של ניאון זמין במאמר תמיכה בנאון.

    מסיבות היסטוריות, ה-ABI הזה משתמש ב--mfloat-abi=softfp שגורמת לכל float שיש להעביר במרשמים של מספרים שלמים ואת כל ערכי double שיש להעביר בזוגות של מספרים שלמים כשמבצעים הפעלות של פונקציות. למרות השם, משפיעה רק על מוסכמות הקריאות של הנקודה הצפה: המהדר עדיין להשתמש בהנחיות נקודה צפה (floating-point) של חומרה לצורכי חשבון.

    ה-ABI הזה משתמש ב-long double של 64 ביט (קובץ בינארי של IEEE, שזהה ל-double).

    Arm64-v8a

    ה-ABI הזה מיועד למעבדי ARM 64 ביט.

    לצפייה בזרועות למידת הארכיטקטורה לקבלת פרטים מלאים על החלקים ב-ABI שהם לא ספציפיים ל-Android. קבוצה מציע גם כמה עצות לניוד פיתוח Android ב-64 ביט.

    אפשר להשתמש ב-Neon intrinsics בקוד C ו-C++ כדי לנצל את תוסף ה-SIMD המתקדם. מדריך למתכנת ניאון עבור Armv8-A מספק מידע נוסף על אינטנסיביות של ניאון ועל תכנות ניאון באופן כללי.

    ב-Android, הרישום x18 הספציפי לפלטפורמה שמור ShadowCallStack והקוד שלכם לא צריך לגעת בהם. ברירת המחדל של הגרסאות הנוכחיות של Clang היא באמצעות האפשרות -ffixed-x18 ב-Android, כך אלא אם כתבתם מכלים (או מהדרים ישנים מאוד) שאין צורך לדאוג לכך.

    ה-ABI הזה משתמש ב-long double של 128 ביט (קובץ בינארי של IEEE 128).

    x86

    ה-ABI הזה מיועד למעבדים שתומכים בקבוצת ההוראות, שמכונה 'x86'. "i386" או "IA-32".

    ה-ABI של Android כולל את ערכת ההוראות הבסיסית וגם ה-MMX, SSE, SSE2, SSE3, וגם תוספי SSSE3.

    ה-ABI לא כולל אף קבוצת הוראות אופציונלית אחרת של IA-32 תוספים כמו MOVBE או כל וריאציה של SSE4. עדיין אפשר להשתמש בתוספים האלה, כל עוד להפעיל אותן ולספק חלופות למכשירים שלא תומכים בהן.

    צרור הכלים של NDK משתמש ביישור סטאק של 16 בייטים לפני קריאה לפונקציה. כלי ברירת המחדל אוכפות את הכלל הזה. אם אתם כותבים קוד הרכבה, צריך לוודא שהסטאק ולוודא שגם מהדרים אחרים מצייתים לכלל הזה.

    פרטים נוספים זמינים במסמכים הבאים:

    ה-ABI הזה משתמש ב-long double בגרסת 64 ביט (בינארי של IEEE, שזהה ל-double, אבל לא יותר מזה long double נפוץ בגרסת 80 ביט של Intel בלבד).

    x86_64

    ה-ABI הזה מיועד למעבדים שתומכים בקבוצת ההוראות, שנקראת בדרך כלל x86-64.

    ה-ABI של Android כולל את ערכת ההוראות הבסיסית וגם MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2, וגם את ההוראה של POPCNT.

    ה-ABI לא כולל קבוצות הוראות אופציונליות אחרות מסוג x86-64 תוספים כמו MOVBE , SHA או כל גרסה אחרת של AVX. עדיין אפשר להשתמש בתוספים האלה, כל עוד משתמשים בתכונה של סביבת זמן הריצה להפעיל אותן ולספק חלופות למכשירים שלא תומכים בהן.

    פרטים נוספים זמינים במסמכים הבאים:

    ה-ABI הזה משתמש ב-long double של 128 ביט (קובץ בינארי של IEEE 128).

    יצירת קוד לממשק ABI ספציפי

    גרדל

    Gradle (באמצעות Android Studio או משורת הפקודה) יוצרת עבור כברירת מחדל, כל ממשקי ה-ABI שלא יצאו משימוש. כדי להגביל את הקבוצה של ממשקי ה-ABI שהאפליקציה תומכת בהן, צריך להשתמש ב-abiFilters. לדוגמה, כדי ליצור עבור ממשקי ABI של 64 ביט, קובעים את ההגדרות הבאות ב-build.gradle:

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

    ndk-build

    כברירת מחדל, גרסאות 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 אחד בכל פעם ולציין את ה-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.

    התנהגות ברירת המחדל של מערכת ה-build היא לכלול את הקבצים הבינאריים של כל ABI ב-APK יחיד, שנקרא גם APK שמן. APK שומן גדול יותר באופן משמעותי יותר מקובץ ABI אחד שמכיל רק את הקבצים הבינאריים. את הפשרה הזו תאימות רחבה יותר, אבל על חשבון APK גדול יותר. חשוב מאוד מומלץ להשתמש בקובצי App Bundle או בפיצולי APK כדי להקטין את הגודל של חבילות ה-APK ועדיין לשמור על מספר המכשירים המקסימלי בתאימות מלאה.

    בזמן ההתקנה, מנהל החבילות פורק את הנתונים המתאימים ביותר את הקוד של מכשיר היעד. לפרטים נוספים, אפשר לעיין במאמר חילוץ אוטומטי של קוד מקורי בזמן ההתקנה.

    ניהול ממשק ABI בפלטפורמת Android

    בקטע הזה אנחנו מסבירים איך פלטפורמת Android מנהלת מודעות מותאמות ב-APKs.

    קוד מקורי בחבילות אפליקציה

    חנות Play ומנהל החבילות מצפים למצוא תוכן שנוצר על ידי NDK ספריות בנתיבי קבצים בתוך ה-APK שתואמות לדפוס הבא:

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

    כאן, <abi> הוא אחד משמות ה-ABI המפורטים בקטע ממשקי ABI נתמכים, ו-<name> הוא שם הספרייה כפי שהגדרתם אותה עבור LOCAL_MODULE בקובץ Android.mk. מאז קובצי APK הם רק קובצי ZIP, לא חשוב לפתוח אותם ולאשר שקובץ ה-Native המשותף ספריות הן המקום שבו הן שייכות.

    אם המערכת לא תמצא את הספריות המקוריות המשותפות במקום שהיא מצפה להן, היא לא תוכל להשתמש אותם. במקרה כזה, האפליקציה עצמה צריכה להעתיק את הספריות ואז מבצעים את dlopen().

    ב-APK מסוג fat, כל ספרייה נמצאת תחת ספרייה ששמה תואם ל-ABI תואם. לדוגמה, APK שמן עשוי להכיל:

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

    הערה: מכשירי Android מבוססי ARMv7 עם מערכת הפעלה 4.0.3 ומטה להתקין ספריות מקוריות מהספרייה armeabi במקום armeabi-v7a אם שתי הספריות קיימות. הסיבה לכך היא ש-/lib/armeabi/ מגיע אחרי /lib/armeabi-v7a/ ב-APK. הבעיה הזו תוקנה החל מ-4.0.4.

    תמיכה ב-ABI של פלטפורמת Android

    מערכת Android יודעת בזמן הריצה באילו ממשקי ABI היא תומכת, כי מערכת ספציפית של build מציינים:

    • ממשק ה-ABI הראשי של המכשיר, שתואם לקוד המכונה שנעשה בו שימוש את קובץ האימג' של המערכת עצמה.
    • אופציונלי: ממשקי ABI משניים, שתואמים לממשק ABI אחר שתמונת המערכת יש תמיכה גם ב- גם.

    המנגנון הזה מבטיח שהמערכת תחלץ את קוד המכונה הטוב ביותר מ- החבילה בזמן ההתקנה.

    כדי להשיג את הביצועים הכי טובים, כדאי לבצע הידור ישיר ל-ABI הראשי. לדוגמה, מכשיר טיפוסי מבוסס ARMv5TE יגדיר רק את ה-ABI הראשי: armeabi. לעומת זאת, מכשיר מבוסס ARMv7 טיפוסי יגדיר את ה-ABI הראשי כ-armeabi-v7a ואת אחד בתור armeabi, כי הוא יכול להריץ קבצים בינאריים מקוריים של אפליקציה שנוצרו בשביל כל אחד מהם.

    מכשירים עם 64 ביט תומכים גם בווריאציות של 32 ביט. נעשה שימוש במכשירי Arm64-v8a לדוגמה, המכשיר יכול להריץ גם קוד Armeabi ו- Armeabi-v7a. הערה: עם זאת, שביצועי האפליקציה שלך יהיו טובים בהרבה במכשירי 64 ביט מטרגטת Arm64-v8a במקום להסתמך על המכשיר שמריץ את Armeabi-v7a של האפליקציה.

    במכשירים רבים מבוססי x86 אפשר גם להריץ קבצים בינאריים של armeabi-v7a ו-armeabi NDK. עבור במכשירים כאלה, ה-ABI הראשי יהיה x86 והשני, armeabi-v7a.

    אפשר להתקין לפי הגדרת האדמין APK של ABI ספציפי. הכללת דף יכולה להיות שימושית לצורכי בדיקה. משתמשים בפקודה הבאה:

    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 מגן על כתובות להחזרת מוצרים על ידי חתימה עליהן באופן קריפטוגרפי ולבדוק שהכתובת להחזרת מוצרים עדיין חתומה כמו שצריך Epilog. טכנולוגיית BTI מונעת מעבר למיקומים שרירותיים בקוד שלך על-ידי דרישה שכל הסתעפות היא הוראה מיוחדת שלא עושה דבר מלבד את המעבד שיכולים להגיע לשם.

    ב-Android משתמשים בהוראות של PAC/BTI ולא מבצעים שום פעולה במעבדים ישנים יותר. לא תומכים בהוראות החדשות. PAC/BTI יהיה זמין רק במכשירים עם מעבדי ARMv9 אבל אפשר להריץ את אותו הקוד גם במכשירי ARMv8: אין צורך בכמה וריאציות של הספרייה. גם במכשירי ARMv9 חלים PAC/BTI בלבד לקוד של 64 ביט.

    הפעלת PAC/BTI תגרום לעלייה קלה בגודל הקוד, בדרך כלל 1%.

    ראו "ללמוד את הארכיטקטורה" - מתן הגנה עבור תוכנות מורכבות (PDF) לקבלת הסבר מפורט על הווקטורים של וקטורי ההתקפה ביעד PAC/BTI, ההגנה עובדת.

    יצירת שינויים

    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 הנוכחי.

    • גרסאות ישנות של חלק ממערכות ניהול זכויות היוצרים של האפליקציות מייצרות קוד שמפר את PAC/BTI בדרישות שלנו. אם אתם משתמשים ב-DRM של האפליקציה ונתקלתם בבעיות בהפעלת PAC/BTI, עליך לפנות לספק ה-DRM לקבלת גרסה מתוקנת.