במכשירי Android שונים יש מעבדים שונים, שבתורם תומכים בקבוצות שונות של פקודות. לכל שילוב של מעבד (CPU) וסט פקודות יש Application Binary Interface משלו (ABI). ממשק ABI כולל את הפרטים הבאים:
- סט הפקודות של המעבד (והתוספים) שאפשר להשתמש בהן.
- סדר הבתים של הזיכרון נשמר ונטען בזמן הריצה. Android תמיד משתמשת בשיטת הסדר הקטן.
- מוסכמות להעברת נתונים בין אפליקציות לבין המערכת, כולל אילוצים של יישור, ואיך המערכת משתמשת במחסנית ובאוגרים כשהיא קוראת לפונקציות.
- הפורמט של קבצים בינאריים הפעלה, כמו תוכניות וספריות משותפות, וסוגי התוכן שהם תומכים בהם. ב-Android תמיד נעשה שימוש ב-ELF. מידע נוסף זמין במאמר בנושא ממשק בינארי של אפליקציות ELF System V.
- איך שמות ב-C++ עוברים שינוי. מידע נוסף זמין במאמר בנושא Generic/Itanium C++ ABI.
בדף הזה מפורטים ממשקי ה-ABI שנתמכים על ידי NDK, ומוסבר איך כל אחד מהם פועל.
ABI יכול להתייחס גם ל-API המקורי שנתמך על ידי הפלטפורמה. רשימה של בעיות ABI שמשפיעות על מערכות 32 ביט זמינה במאמר בנושא באגים ב-ABI של 32 ביט.
ממשקי ABI נתמכים
טבלה 1. ABIs וסטים של פקודות נתמכות.
| ABI | סטים נתמכים של פקודות | הערות |
|---|---|---|
armeabi-v7a |
|
לא תואם למכשירי ARMv5/v6. |
arm64-v8a |
Armv8.0 בלבד. | |
x86 |
אין תמיכה ב-MOVBE או ב-SSE4. | |
x86_64 |
|
x86-64-v2 מלא. |
הערה: בעבר, NDK תמך ב-ARMv5 (armeabi) וב-MIPS 32 ביט ו-64 ביט, אבל התמיכה בממשקי ה-ABI האלה הוסרה ב-NDK r17.
armeabi-v7a
ממשק ה-ABI הזה מיועד למעבדי ARM 32 ביט. היא כוללת את Thumb-2 ו-Neon.
למידע על החלקים ב-ABI שלא ספציפיים ל-Android, ראו Application Binary Interface (ABI) for the ARM Architecture
מערכות ה-build של NDK יוצרות קוד Thumb-2 כברירת מחדל, אלא אם משתמשים ב-LOCAL_ARM_MODE ב-Android.mk עבור ndk-build או ב-ANDROID_ARM_MODE כשמגדירים CMake.
מידע נוסף על ההיסטוריה של Neon זמין במאמר בנושא תמיכה ב-Neon.
מסיבות היסטוריות, ממשק ה-ABI הזה משתמש ב--mfloat-abi=softfp, ולכן כל הערכים של float מועברים ברגיסטרים של מספרים שלמים, וכל הערכים של double מועברים בזוגות של רגיסטרים של מספרים שלמים כשמבצעים קריאות לפונקציות. למרות השם, ההגדרה הזו משפיעה רק על מוסכמת הקריאה של נקודה צפה: הקומפיילר עדיין ישתמש בהוראות נקודה צפה של החומרה לחישובים אריתמטיים.
ממשק ה-ABI הזה משתמש ב-long double של 64 ביט (IEEE binary64, כמו double).
arm64-v8a
ממשק ה-ABI הזה מיועד למעבדי ARM של 64 ביט.
פרטים מלאים על החלקים של ממשק ABI שלא ספציפיים ל-Android מופיעים במאמר Learn the Architecture של Arm. חברת Arm מספקת גם עצות בנושא העברה בפיתוח Android 64 ביט.
אפשר להשתמש בפונקציות פנימיות של Neon בקוד C ו-C++ כדי לנצל את היתרונות של תוסף Advanced SIMD. במדריך לתכנות Neon עבור Armv8-A יש מידע נוסף על פונקציות פנימיות (intrinsics) של Neon ועל תכנות Neon באופן כללי.
ב-Android, הרגיסטר x18 הספציפי לפלטפורמה שמור ל-ShadowCallStack ואסור לקוד שלכם לגעת בו. בגרסאות הנוכחיות של Clang, ברירת המחדל היא שימוש באפשרות -ffixed-x18 ב-Android, כך שאם לא כתבתם קוד אסמבלר באופן ידני (או אם אתם משתמשים בקומפיילר ישן מאוד), לא תצטרכו לדאוג לגבי זה.
ממשק ה-ABI הזה משתמש ב-long double של 128 ביט (IEEE binary128).
x86
ממשק ה-ABI הזה מיועד למעבדים שתומכים בסט הפקודות שנקרא בדרך כלל x86, i386 או IA-32.
ממשק ה-ABI של Android כולל את סט הפקודות הבסיסי בתוספת ההרחבות MMX, SSE, SSE2, SSE3 ו-SSSE3.
ממשק ה-ABI לא כולל הרחבות אחרות אופציונליות של סט פקודות IA-32, כמו MOVBE או כל וריאציה של SSE4. עדיין אפשר להשתמש בתוספים האלה, כל עוד משתמשים בבדיקת תכונות בזמן ריצה כדי להפעיל אותם, ומספקים חלופות למכשירים שלא תומכים בהם.
ערכת הכלים של NDK מניחה יישור של 16 בייט של הסטאק לפני בקשה להפעלת פונקציה. כלי ברירת המחדל והאפשרויות אוכפים את הכלל הזה. אם אתם כותבים קוד Assembly, אתם צריכים לוודא ששומרים על יישור המחסנית, ולוודא שגם קומפיילרים אחרים פועלים לפי הכלל הזה.
מידע נוסף זמין במסמכים הבאים:
- מוסכמות קריאה (calling) עבור קומפיילרים שונים של C++ ומערכות הפעלה שונות
- Intel IA-32 Intel Architecture Software Developer's Manual, Volume 2: Instruction Set Reference
- Intel IA-32 Intel Architecture Software Developer's Manual, Volume 3: System Programming Guide
- ממשק בינארי של אפליקציות System V: תוסף לארכיטקטורת מעבד Intel386
ה-ABI הזה משתמש ב-long double של 64 ביט (IEEE binary64, כמו 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. עדיין אפשר להשתמש בתוספים האלה, כל עוד משתמשים בבדיקת תכונות בזמן ריצה כדי להפעיל אותם, ומספקים חלופות למכשירים שלא תומכים בהם.
מידע נוסף זמין במסמכים הבאים:
- מוסכמות קריאה עבור מהדרים שונים של C++ ומערכות הפעלה שונות
- Intel64 and IA-32 Architectures Software Developer's Manual, Volume 2: Instruction Set Reference
- Intel64 and IA-32 Intel Architecture Software Developer's Manual Volume 3: System Programming
ממשק ה-ABI הזה משתמש ב-long double של 128 ביט (IEEE binary128).
יצירת קוד עבור ABI ספציפי
Gradle
Gradle (בין אם משתמשים בו דרך Android Studio או משורת הפקודה) מבצע build לכל ממשקי ה-ABI שלא הוצאו משימוש כברירת מחדל. כדי להגביל את קבוצת ה-ABI שהאפליקציה תומכת בה, משתמשים ב-abiFilters. לדוגמה, כדי לבצע build רק לממשקי ABI של 64 ביט, מגדירים את ההגדרה הבאה בקובץ build.gradle:
android {
defaultConfig {
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
}
ndk-build
כברירת מחדל, הפקודה ndk-build יוצרת קובצי 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 כדי לבצע build באמצעות NDK, אפשר לעיין במדריך ל-CMake.
התנהגות ברירת המחדל של מערכת ה-build היא לכלול את הקבצים הבינאריים של כל ABI בחבילת APK אחת, שנקראת גם חבילת APK גדולה. חבילת APK גדולה יותר באופן משמעותי מחבילת APK שמכילה רק את הקבצים הבינאריים של ABI יחיד. היתרון הוא תאימות רחבה יותר, אבל החיסרון הוא חבילת APK גדולה יותר. מומלץ מאוד להשתמש בחבילות App Bundle או בפיצול של קובצי APK כדי להקטין את הגודל של קובצי ה-APK, תוך שמירה על תאימות מקסימלית למכשירים.
בזמן ההתקנה, מנהל החבילות פורק רק את קוד המכונה המתאים ביותר למכשיר היעד. פרטים נוספים זמינים במאמר בנושא חילוץ אוטומטי של קוד Native בזמן ההתקנה.
ניהול ABI בפלטפורמת Android
בקטע הזה מוסבר איך פלטפורמת Android מנהלת קוד מקורי בקובצי APK.
קוד Native בחבילות APK
גם חנות Play וגם מנהל החבילות מצפים למצוא ספריות שנוצרו על ידי NDK בנתיבי קבצים בתוך ה-APK שתואמים לתבנית הבאה:
/lib/<abi>/lib<name>.so
כאן, <abi> הוא אחד משמות ה-ABI שמופיעים בקטע Supported ABIs, ו-<name> הוא שם הספרייה כפי שהגדרתם אותו למשתנה LOCAL_MODULE בקובץ Android.mk. מכיוון שקובצי APK הם רק קובצי ZIP, קל לפתוח אותם ולוודא שהספריות המשותפות נמצאות במקום הנכון.
אם המערכת לא מוצאת את ספריות Native משותפות במקום שבו היא מצפה למצוא אותן, היא לא יכולה להשתמש בהן. במקרה כזה, האפליקציה עצמה צריכה להעתיק את הספריות ואז לבצע dlopen().
ב-APK גדול, כל ספרייה נמצאת בספרייה ששמה תואם ל-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 היא תומכת, כי מאפייני המערכת הספציפיים לבנייה מציינים:
- ממשק ה-ABI הראשי של המכשיר, שמתאים לשפת המכונה שמשמשת בקובץ אימג' של המערכת עצמו.
- אופציונלי: קובצי ABI משניים, שמתאימים לקובצי ABI אחרים שקובץ אימג' של המערכת תומך בהם.
המנגנון הזה מבטיח שהמערכת תחלץ את קוד המכונה הטוב ביותר מהחבילה בזמן ההתקנה.
כדי להשיג את הביצועים הטובים ביותר, צריך לבצע קומפילציה ישירות עבור ה-ABI הראשי. לדוגמה, במכשיר טיפוסי שמבוסס על ARMv5TE מוגדר רק ממשק ה-ABI הראשי: armeabi. לעומת זאת, במכשיר טיפוסי שמבוסס על ARMv7, ממשק ה-ABI הראשי יוגדר כ-armeabi-v7a וממשק ה-ABI המשני יוגדר כ-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
חילוץ אוטומטי של קוד Native בזמן ההתקנה
כשמתקינים אפליקציה, שירות מנהל החבילות סורק את ה-APK ומחפש ספריות משותפות מהסוג הבא:
lib/<primary-abi>/lib<name>.so
אם לא נמצאה אף אחת, והגדרתם ABI משני, השירות יסרוק ספריות משותפות מהצורה:
lib/<secondary-abi>/lib<name>.so
כשהוא מוצא את ספריות ה-Native שהוא מחפש, כלי לניהול חבילות מעתיק אותן אל /lib/lib<name>.so, בספריית ה-Native של האפליקציה (<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%.
במאמר של Arm בנושא הכרת הארכיטקטורה – הגנה על תוכנות מורכבות (PDF) מוסבר בפירוט על וקטורי התקפה של PAC/BTI ועל אופן הפעולה של ההגנה.
שינויים בבנייה
ndk-build
מגדירים את LOCAL_BRANCH_PROTECTION := standard בכל מודול של Android.mk.
CMake
משתמשים ב-target_compile_options($TARGET PRIVATE -mbranch-protection=standard)
לכל יעד בקובץ CMakeLists.txt.
מערכות build אחרות
מהדרים את הקוד באמצעות -mbranch-protection=standard. הדגל הזה פועל רק כשמבצעים קומפילציה עבור arm64-v8a ABI. אין צורך להשתמש בדגל הזה כשמבצעים קישור.
פתרון בעיות
לא ידוע לנו על בעיות בתמיכה של מהדר ב-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) יש באג ב-assembler שנכתב ידנית שגורם לכשלים ב-PAC. שדרוג לגרסה הנוכחית של OpenSSL.
גרסאות ישנות של מערכות DRM מסוימות באפליקציות יוצרות קוד שמפר את הדרישות של PAC/BTI. אם אתם משתמשים ב-DRM באפליקציה ונתקלים בבעיות בהפעלת PAC/BTI, עליכם לפנות לספק ה-DRM כדי לקבל גרסה מתוקנת.