במכשירי Android שונים יש מעבדים שונים, שבתוכה תומכים בקבוצות הוראות שונות. לכל שילוב של מעבד (CPU) ומערך הוראות יש ממשק Application Binary Interface (ABI) משלו. ABI כולל את הפרטים הבאים:
- קבוצת ההוראות (והתוספים) של המעבד שאפשר להשתמש בהן.
- סוף הזמן של אחסון וטעינה של זיכרון בזמן ריצה. Android תמיד בפורמט little-endian.
- מוסכמות להעברת נתונים בין האפליקציות למערכת, כולל אילוצים של התאמה, והאופן שבו המערכת משתמשת בסטאק ובריגיסטרים כשהיא קוראת לפונקציות.
- הפורמט של קובצי בינארי שניתן להריץ, כמו תוכנות וספריות משותפות, וסוגי התוכן שהם תומכים בהם. ב-Android תמיד נעשה שימוש ב-ELF. למידע נוסף, ראו ELF System V Application Binary Interface.
- איך שמות ב-C++ עוברים עיבוד. מידע נוסף זמין במאמר Generic/Itanium C++ ABI.
בדף הזה מפורטים ממשקי ה-ABI שנתמכים ב-NDK, ומוסבר איך כל ממשק ABI פועל.
ABI יכול גם להתייחס ל-API המקורי שנתמך בפלטפורמה. בקישור הבא תוכלו למצוא רשימה של בעיות ABI כאלה שמשפיעות על מערכות 32 ביט: באגים ב-ABI של 32 ביט.
ממשקי ABI נתמכים
ABI | סט הפקודות הנתמכים | הערות |
---|---|---|
armeabi-v7a |
|
לא תואם למכשירי ARMv5/v6. |
arm64-v8a |
Armv8.0 בלבד. | |
x86 |
אין תמיכה ב-MOVBE או SSE4. | |
x86_64 |
|
תמיכה מלאה ב-x86-64-v1, אבל תמיכה חלקית בלבד ב-x86-64-v2 (ללא LAHF-SAHF). |
הערה: בעבר, NDK תמך ב-ARMv5 (armeabi) וב-MIPS של 32 ביט ו-64 ביט, אבל התמיכה ב-ABIs האלה הוסרה ב-NDK r17.
armeabi-v7a
ה-ABI הזה מיועד למעבדי ARM של 32 ביט. הוא כולל את אגודל 2 וניאון.
למידע על החלקים של ה-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 ביט.
במאמר בנושא לימוד הארכיטקטורה של Arm מפורטים פרטים מלאים על החלקים ב-ABI שאינם ספציפיים ל-Android. Arm מציע גם כמה עצות לגבי ניוד בפיתוח Android ב-64 ביט.
אפשר להשתמש בפונקציות פנימיות של Neon בקוד C ו-C++ כדי לנצל את התוסף Advanced SIMD. במדריך למתכנתים של Neon עבור Armv8-A מפורט מידע נוסף על פונקציות פנימיות של 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 בייטים לפני קריאה לפונקציה. הכלים והאפשרויות שמוגדרים כברירת מחדל אוכפים את הכלל הזה. אם כותבים קוד באסמבלי, צריך להקפיד לשמור על התאמה של הערכים ב-stack, ולוודא שגם קומפילרים אחרים פועלים לפי הכלל הזה.
פרטים נוספים זמינים במסמכים הבאים:
- כללי קריאה למהדרים ולמערכות הפעלה שונים של C++
- Intel IA-32 Intel Architecture Software Developer's Manual, Volume 2: Instruction Set Reference
- Intel IA-32 Intel Architecture Software Developer’s Guide, נפח 3: מדריך תכנות מערכת
- System V Application Binary Interface: Intel386 Processor Architecture Supplement
ה-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 ו-IA-32, כרך 2: Instruction Set Reference
- נפח 3 ידנית של מפתחי תוכנת הארכיטקטורה של Intel64 ו-IA-32 Intel: תכנות מערכת
ב-ABI הזה נעשה שימוש ב-long double
של 128 ביט (IEEE binary128).
יצירת קוד ל-ABI ספציפי
Gradle
כברירת מחדל, Gradle (בין אם משתמשים בו דרך Android Studio ובין אם דרך שורת הפקודה) יוצר גרסאות build לכל ממשקי ה-ABI שעדיין לא הוצאו משימוש. כדי להגביל את קבוצת ה-ABIs שהאפליקציה תומכת בהם, משתמשים ב-abiFilters
. לדוגמה, כדי ליצור ממשק 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 כדי לבצע פיתוח באמצעות NDK מפורטים במדריך ל-CMake.
התנהגות ברירת המחדל של מערכת ה-build היא לכלול את הקבצים הבינאריים של כל ABI ב-APK יחיד, שנקרא גם fat APK. חבילת APK גדולה גדולה בהרבה מחבילת APK שמכילה רק את הקבצים הבינאריים של ABI יחיד. בתמורה לגודל הגדול יותר, חבילת ה-APK הגדולה מאפשרת תאימות רחבה יותר. מומלץ מאוד להשתמש בחבילות App Bundle או בחלוקות APK כדי לצמצם את גודל חבילות ה-APK ועדיין לשמור על תאימות מקסימלית למכשירים.
בזמן ההתקנה, מנהל החבילות פותח רק את קוד המכונה המתאים ביותר למכשיר היעד. פרטים נוספים זמינים במאמר חילוץ אוטומטי של קוד מקורי בזמן ההתקנה.
ניהול ABI בפלטפורמת Android
בקטע הזה מוסבר איך פלטפורמת Android מנהלת קוד מקורי בקובצי APK.
קוד מקורי בחבילות אפליקציה
גם חנות Play וגם מנהל החבילות מצפים למצוא ספריות שנוצרו על ידי NDK בנתיבי קבצים בתוך קובץ ה-APK שתואמים לתבנית הבאה:
/lib/<abi>/lib<name>.so
כאן, <abi>
הוא אחד משמות ה-ABI שמפורטים בקטע ABI נתמכים, ו-<name>
הוא שם הספרייה כפי שהגדרתם אותו למשתנה LOCAL_MODULE
בקובץ Android.mk
. מכיוון שקובצי APK הם רק קובצי ZIP, לא פשוט לפתוח אותם ולאשר שהספריות המקוריות המשותפות הן לאן שהן שייכות.
אם המערכת לא מוצאת את הספריות המשותפות המקומיות במיקום שבו היא מצפה למצוא אותן, היא לא יכולה להשתמש בהן. במקרה כזה, האפליקציה עצמה צריכה להעתיק את הספריות ואז לבצע את הפקודה 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 עם גרסת Android 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
וה-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
חילוץ אוטומטי של קוד מקורי בזמן ההתקנה
כשמתקינים אפליקציה, שירות מנהל החבילות סורק את קובץ ה-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%.
במאמר Learn the architecture - Providing protection for complex software (PDF) של Arm מוסבר בפירוט על יעדים של התקפות 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
. הדגל הזה פועל רק במהלך הידור של ממשק ה-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) יש באג ב-assembler שנכתב ביד שגורם לכשלים ב-PAC. שדרוג ל-OpenSSL הנוכחי.
גרסאות ישנות של חלק ממערכות DRM לאפליקציות יוצרות קוד שמפר את הדרישות של PAC/BTI. אם אתם משתמשים ב-DRM של האפליקציה ונתקלתם בבעיות בהפעלת PAC/BTI, יש לפנות אל ספק ה-DRM לקבלת גרסה מתוקנת.