במכשירי Android שונים יש מעבדים שונים, שתומכים בקבוצות הוראות שונות. לכל שילוב של מעבד וקבוצת הוראות יש ממשק בינארי של אפליקציה (ABI) משלו. ABI כולל את הפרטים הבאים:
- קבוצת ההוראות (והתוספים) של המעבד שאפשר להשתמש בהן.
- endianness של אחסון זיכרון וטעינה בזמן ריצה. 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 נתמכים
טבלה 1. ממשקי ABI וסט פקודות נתמכים.
ABI | סט הפקודות הנתמכים | הערות |
---|---|---|
armeabi-v7a |
|
לא תואם למכשירי ARMv5/v6. |
arm64-v8a |
Armv8.0 בלבד. | |
x86 |
אין תמיכה ב-MOVBE או ב-SSE4. | |
x86_64 |
|
x86-64-v2 מלא. |
הערה: בעבר, NDK תמך ב-ARMv5 (armeabi) וב-MIPS של 32 ביט ו-64 ביט, אבל התמיכה ב-ABIs האלה הוסרה ב-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 ביט.
במאמר בנושא לימוד הארכיטקטורה של Arm מפורטים פרטים מלאים על החלקים ב-ABI שאינם ספציפיים ל-Android. ב-Arm יש גם כמה טיפים להעברה במאמר פיתוח ל-Android ב-64 ביט.
כדי לנצל את התוסף Advanced SIMD, אפשר להשתמש בפונקציות פנימיות של Neon בקוד C ו-C++. מדריך למתכנתים של 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 Manual, Volume 3: System Programming Guide
- 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
- מדריך למפתחי תוכנה של Intel64 ו-IA-32 Intel Architecture, כרך 3: תכנות מערכת
ב-ABI הזה נעשה שימוש ב-long double
של 128 ביט (IEEE binary128).
יצירת קוד ל-ABI ספציפי
Gradle
כברירת מחדל, Gradle (בין אם משתמשים בו דרך Android Studio ובין אם דרך שורת הפקודה) יוצר גרסאות build לכל ממשקי ה-ABI שעדיין לא הוצאו משימוש. כדי להגביל את קבוצת ה-ABIs שהאפליקציה תומכת בהם, משתמשים ב-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 שמכילה קבצים בינאריים של ABI אחד היא גדולה בהרבה מחבילת 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 יכולים גם להריץ קובצי NDK בינאריים של armeabi-v7a
ו-armeabi
. במכשירים כאלה, 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 כדי לקבל גרסה מתוקנת.