יציבות של Application Binary Interface (ABI) היא תנאי מוקדם לעדכונים של framework בלבד, כי מודולים של ספקים עשויים להיות תלויים בספריות משותפות של ערכת פיתוח מקומית לספק (VNDK) שנמצאות במחיצת המערכת. בגרסת Android, ספריות משותפות של VNDK שנבנו לאחרונה צריכות להיות תואמות ל-ABI של ספריות משותפות של VNDK שפורסמו בעבר, כדי שמודולים של ספקים יוכלו לפעול עם הספריות האלה בלי קומפילציה מחדש ובלי שגיאות בזמן הריצה. בין גרסאות Android, אפשר לשנות את ספריות ה-VNDK ואין ערבויות ל-ABI.
כדי לוודא שיש תאימות ל-ABI, Android 9 כולל בודק ABI של כותרות, כמו שמתואר בקטעים הבאים.
מידע על VNDK ותאימות ל-ABI
VNDK הוא קבוצה מגבילה של ספריות שמודולים של ספקים יכולים לקשר אליהן, והוא מאפשר עדכונים רק של המסגרת. תאימות ל-ABI מתייחסת ליכולת של גרסה חדשה יותר של ספרייה משותפת לפעול כמצופה עם מודול שמקושר אליה באופן דינמי (כלומר, לפעול כמו גרסה ישנה יותר של הספרייה).
מידע על סמלים שיוצאו
סמל מיוצא (שנקרא גם סמל גלובלי) הוא סמל שעומד בכל התנאים הבאים:
- מיוצאים על ידי הכותרות שגלויות לכולם של ספרייה משותפת.
- מופיע בטבלה
.dynsymבקובץ.soשמתאים לספרייה המשותפת. - יש לו קשירה מסוג WEAK או GLOBAL.
- החשיפה היא DEFAULT או PROTECTED.
- אינדקס הקטע לא מוגדר.
- הסוג הוא FUNC או OBJECT.
הכותרות הציבוריות של ספרייה משותפת מוגדרות ככותרות שזמינות לספריות או לקבצים בינאריים אחרים דרך המאפיינים export_include_dirs, export_header_lib_headers, export_static_lib_headers, export_shared_lib_headers ו-export_generated_headers בהגדרות Android.bp של המודול שמתאים לספרייה המשותפת.
מידע על סוגים של כתובות שאפשר להגיע אליהן
סוג שאפשר להגיע אליו הוא כל סוג מובנה או מוגדר על ידי המשתמש ב-C/C++ שאפשר להגיע אליו באופן ישיר או עקיף דרך סמל מיוצא, ושהוא מיוצא דרך כותרות ציבוריות. לדוגמה, libfoo.so כולל את הפונקציה Foo, שהיא סמל מיוצא שנמצא בטבלה .dynsym. ספריית libfoo.so כוללת את הפריטים הבאים:
| foo_exported.h | foo.private.h |
|---|---|
typedef struct foo_private foo_private_t; typedef struct foo { int m1; int *m2; foo_private_t *mPfoo; } foo_t; typedef struct bar { foo_t mfoo; } bar_t; bool Foo(int id, bar_t *bar_ptr); |
typedef struct foo_private { int m1; float mbar; } foo_private_t; |
| Android.bp |
|---|
cc_library { name : libfoo, vendor_available: true, vndk { enabled : true, } srcs : ["src/*.cpp"], export_include_dirs : [ "exported" ], } |
| טבלת .dynsym | |||||||
|---|---|---|---|---|---|---|---|
Num
|
Value
|
Size
|
Type
|
Bind
|
Vis
|
Ndx
|
Name
|
1
|
0
|
0
|
FUNC
|
GLOB
|
DEF
|
UND
|
dlerror@libc
|
2
|
1ce0
|
20
|
FUNC
|
GLOB
|
DEF
|
12
|
Foo
|
ב-Foo, הסוגים שאפשר להגיע אליהם ישירות או בעקיפין כוללים:
| סוג | תיאור |
|---|---|
bool
|
סוג הערך שמוחזר של Foo.
|
int
|
סוג הפרמטר הראשון Foo.
|
bar_t *
|
הסוג של הפרמטר השני של Foo. הייצוא של bar_t מתבצע דרך foo_exported.h, באמצעות bar_t *.
bar_t מכיל חבר mfoo, מסוג
foo_t, שמיוצא דרך foo_exported.h,
מה שמוביל לייצוא של סוגים נוספים:
עם זאת, אי אפשר להגיע אל foo_private_t כי הוא לא
יוצא דרך foo_exported.h. (foo_private_t *
הוא אטום, ולכן מותר לבצע שינויים ב-foo_private_t).
|
הסבר דומה יכול להינתן גם לסוגים שאפשר להגיע אליהם באמצעות מצייני מחלקת בסיס ופרמטרים של תבניות.
בדיקת תאימות של ABI
צריך לוודא שהספריות שמסומנות ב-vendor_available: true וב-vndk.enabled: true בקבצים המתאימים של Android.bp תואמות ל-ABI. לדוגמה:
cc_library { name: "libvndk_example", vendor_available: true, vndk: { enabled: true, } }
בספריות שבהן פונקציה מיוצאת יכולה לגשת לסוגי נתונים באופן ישיר או עקיף, השינויים הבאים נחשבים לשינויים ששוברים את תאימות ה-ABI:
| סוג נתונים | תיאור |
|---|---|
| מבנים ושיעורים |
|
| איגודים |
|
| ערכי ספירה |
|
| סמלים גלובליים |
|
* אסור לשנות או להסיר פונקציות חברות ציבוריות ופרטיות, כי פונקציות מוטבעות ציבוריות יכולות להתייחס לפונקציות חברות פרטיות. אפשר לשמור הפניות לסמלים של פונקציות חבר פרטיות בקבצים בינאריים של המתקשר. שינוי או הסרה של פונקציות פרטיות של חברים בספריות משותפות עלולים לגרום לקבצים בינאריים שלא תהיה להם תאימות לאחור.
** אסור לשנות את ההיסטים של חברי נתונים ציבוריים או פרטיים, כי פונקציות מוטבעות יכולות להתייחס לחברי הנתונים האלה בגוף הפונקציה שלהן. שינוי ההיסטים של חברי נתונים עלול לגרום לקבצים בינאריים שלא תהיה להם תאימות לאחור.
*** למרות שהשינויים האלה לא משפיעים על פריסת הזיכרון של הסוג, יש הבדלים סמנטיים שיכולים לגרום לכך שספריות לא יפעלו כצפוי.
שימוש בכלים לתאימות ABI
כשספריית VNDK נוצרת, ה-ABI של הספרייה מושווה להפניה המתאימה ל-ABI של גרסת ה-VNDK שנוצרת. הפניה קובצי ה-ABI נמצאים במיקום:
${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/vndk/<PLATFORM_VNDK_VERSION>/<BINDER_BITNESS>/<ARCH>/source-based
לדוגמה, בבנייה של libfoo עבור x86 ברמת API 27,
libfooה-ABI המשוער של libfoo מושווה להפניה שלו בכתובת:
${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/vndk/27/64/x86/source-based/libfoo.so.lsdump
שגיאה של פגיעה בתאימות ל-ABI
במקרה של שבירות ABI, ביומן הבנייה מוצגות אזהרות עם סוג האזהרה ונתיב לדוח abi-diff. לדוגמה, אם לממשק ABI של libbinder יש שינוי לא תואם, מערכת ה-build תציג שגיאה עם הודעה שדומה להודעה הבאה:
***************************************************** error: VNDK library: libbinder.so's ABI has INCOMPATIBLE CHANGES Please check compatibility report at: out/soong/.intermediates/frameworks/native/libs/binder/libbinder/android_arm64_armv8-a_cortex-a73_vendor_shared/libbinder.so.abidiff ****************************************************** ---- Please update abi references by running platform/development/vndk/tools/header-checker/utils/create_reference_dumps.py -l libbinder ----
יצירת בדיקות ABI של ספריית VNDK
כשספריית VNDK נוצרת:
-
header-abi-dumperמעבד את קובצי המקור שעברו קומפילציה כדי ליצור את ספריית ה-VNDK (קובצי המקור של הספרייה וגם קובצי מקור שעברו בירושה דרך יחסי תלות סטטיים טרנזיטיביים), כדי ליצור קובצי.sdumpשתואמים לכל מקור.
איור 1. יצירת הקבצים .sdump -
header-abi-linkerמעבד את קובצי.sdump(באמצעות סקריפט גרסה שסופק לו או קובץ.soשמתאים לספרייה המשותפת) כדי ליצור קובץ.lsdumpשמתעד את כל פרטי ממשק ה-ABI שמתאימים לספרייה המשותפת.
איור 2. יצירת הקובץ .lsdump -
header-abi-diffמשווה את קובץ.lsdumpלקובץ.lsdumpכדי ליצור דוח השוואה שמציג את ההבדלים בממשקי ה-ABI של שתי הספריות.
איור 3. יצירת דוח ההבדלים
header-abi-dumper
הכלי header-abi-dumper מנתח קובץ מקור של C/C++ ומייצא את ה-ABI שהוסק מקובץ המקור הזה לקובץ ביניים. מערכת ה-build מריצה header-abi-dumper על כל קובצי המקור שעברו קומפילציה, וגם יוצרת ספרייה שכוללת את קובצי המקור מיחסי תלות טרנזיטיביים.
| כניסות קלט |
|
|---|---|
| פלט | קובץ שמתאר את ה-ABI של קובץ המקור (לדוגמה,
foo.sdump represents foo.cpp's ABI).
|
נכון לעכשיו, קובצי .sdump הם בפורמט JSON, שלא מובטח שיהיה יציב בגרסאות עתידיות. לכן, הפורמט של קובץ .sdump
צריך להיחשב כפרט הטמעה של מערכת build.
לדוגמה, libfoo.so כולל את קובץ המקור הבא
foo.cpp:
#include <stdio.h> #include <foo_exported.h> bool Foo(int id, bar_t *bar_ptr) { if (id > 0 && bar_ptr->mfoo.m1 > 0) { return true; } return false; }
אפשר להשתמש ב-header-abi-dumper כדי ליצור קובץ .sdump ביניים שמייצג את ממשק ה-ABI שמוצג על ידי קובץ המקור באמצעות:
$ header-abi-dumper foo.cpp -I exported -o foo.sdump -- -I exported -x c++
הפקודה הזו אומרת ל-header-abi-dumper לנתח את foo.cpp עם דגלי הקומפיילר אחרי --, ולפלוט את פרטי ממשק ה-ABI שמיוצאים על ידי הכותרות הציבוריות בספרייה exported. התוכן הבא
foo.sdump נוצר על ידי
header-abi-dumper:
{ "array_types" : [], "builtin_types" : [ { "alignment" : 4, "is_integral" : true, "linker_set_key" : "_ZTIi", "name" : "int", "referenced_type" : "_ZTIi", "self_type" : "_ZTIi", "size" : 4 } ], "elf_functions" : [], "elf_objects" : [], "enum_types" : [], "function_types" : [], "functions" : [ { "function_name" : "FooBad", "linker_set_key" : "_Z6FooBadiP3foo", "parameters" : [ { "referenced_type" : "_ZTIi" }, { "referenced_type" : "_ZTIP3foo" } ], "return_type" : "_ZTI3bar", "source_file" : "exported/foo_exported.h" } ], "global_vars" : [], "lvalue_reference_types" : [], "pointer_types" : [ { "alignment" : 8, "linker_set_key" : "_ZTIP11foo_private", "name" : "foo_private *", "referenced_type" : "_ZTI11foo_private", "self_type" : "_ZTIP11foo_private", "size" : 8, "source_file" : "exported/foo_exported.h" }, { "alignment" : 8, "linker_set_key" : "_ZTIP3foo", "name" : "foo *", "referenced_type" : "_ZTI3foo", "self_type" : "_ZTIP3foo", "size" : 8, "source_file" : "exported/foo_exported.h" }, { "alignment" : 8, "linker_set_key" : "_ZTIPi", "name" : "int *", "referenced_type" : "_ZTIi", "self_type" : "_ZTIPi", "size" : 8, "source_file" : "exported/foo_exported.h" } ], "qualified_types" : [], "record_types" : [ { "alignment" : 8, "fields" : [ { "field_name" : "mfoo", "referenced_type" : "_ZTI3foo" } ], "linker_set_key" : "_ZTI3bar", "name" : "bar", "referenced_type" : "_ZTI3bar", "self_type" : "_ZTI3bar", "size" : 24, "source_file" : "exported/foo_exported.h" }, { "alignment" : 8, "fields" : [ { "field_name" : "m1", "referenced_type" : "_ZTIi" }, { "field_name" : "m2", "field_offset" : 64, "referenced_type" : "_ZTIPi" }, { "field_name" : "mPfoo", "field_offset" : 128, "referenced_type" : "_ZTIP11foo_private" } ], "linker_set_key" : "_ZTI3foo", "name" : "foo", "referenced_type" : "_ZTI3foo", "self_type" : "_ZTI3foo", "size" : 24, "source_file" : "exported/foo_exported.h" } ], "rvalue_reference_types" : [] }
foo.sdump מכיל מידע על ABI שיוצא מקובץ המקור foo.cpp והכותרות הציבוריות, לדוגמה:
record_types. אפשר לעיין במבנים, באיחודים או במחלקות שמוגדרים בכותרות הציבוריות. לכל סוג רשומה יש מידע על השדות שלה, הגודל שלה, מפרט הגישה, קובץ הכותרת שבו היא מוגדרת ומאפיינים אחרים.pointer_types. הפניה ישירה או עקיפה לסוגי מצביעים שמוזכרים ברשומות או בפונקציות המיוצאות בכותרות הציבוריות, יחד עם הסוג שאליו המצביע מצביע (דרך השדהreferenced_typeב-type_info). מידע דומה נרשם בקובץ.sdumpעבור סוגים מתאימים, סוגים מובנים של C/C++, סוגי מערכים וסוגי הפניות של lvalue ו-rvalue. המידע הזה מאפשר לבצע השוואה רקורסיבית.-
functions. מייצג פונקציות שמיוצאות על ידי כותרות ציבוריות. הם גם כוללים מידע על השם המעורבב של הפונקציה, סוג ההחזרה, סוגי הפרמטרים, מציין הגישה ומאפיינים אחרים.
header-abi-linker
הכלי header-abi-linker מקבל כקלט את הקבצים הזמניים שנוצרו על ידי header-abi-dumper, ואז מקשר בין הקבצים האלה:
| כניסות קלט |
|
|---|---|
| פלט | קובץ שמתאר את ממשק ה-ABI של ספרייה משותפת (לדוגמה,
libfoo.so.lsdump מייצג את ממשק ה-ABI של libfoo).
|
הכלי ממזג את הגרפים של הטיפוסים בכל הקבצים הזמניים שמועברים אליו, תוך התחשבות בהבדלים של הגדרה אחת (יכול להיות שטיפוסים שהוגדרו על ידי המשתמש ביחידות תרגום שונות עם אותו שם מוגדר במלואו יהיו שונים מבחינה סמנטית) בין יחידות התרגום. הכלי מנתח סקריפט גרסה או את טבלת .dynsym של הספרייה המשותפת (קובץ .so) כדי ליצור רשימה של הסמלים המיוצאים.
לדוגמה, libfoo מורכב מ-foo.cpp ומ-bar.cpp. אפשר להפעיל את header-abi-linker כדי ליצור את ה-ABI המקושר המלא של libfoo באופן הבא:
header-abi-linker -I exported foo.sdump bar.sdump \ -o libfoo.so.lsdump \ -so libfoo.so \ -arch arm64 -api current
פלט לדוגמה של פקודה ב-libfoo.so.lsdump:
{ "array_types" : [], "builtin_types" : [ { "alignment" : 1, "is_integral" : true, "is_unsigned" : true, "linker_set_key" : "_ZTIb", "name" : "bool", "referenced_type" : "_ZTIb", "self_type" : "_ZTIb", "size" : 1 }, { "alignment" : 4, "is_integral" : true, "linker_set_key" : "_ZTIi", "name" : "int", "referenced_type" : "_ZTIi", "self_type" : "_ZTIi", "size" : 4 } ], "elf_functions" : [ { "name" : "_Z3FooiP3bar" }, { "name" : "_Z6FooBadiP3foo" } ], "elf_objects" : [], "enum_types" : [], "function_types" : [], "functions" : [ { "function_name" : "Foo", "linker_set_key" : "_Z3FooiP3bar", "parameters" : [ { "referenced_type" : "_ZTIi" }, { "referenced_type" : "_ZTIP3bar" } ], "return_type" : "_ZTIb", "source_file" : "exported/foo_exported.h" }, { "function_name" : "FooBad", "linker_set_key" : "_Z6FooBadiP3foo", "parameters" : [ { "referenced_type" : "_ZTIi" }, { "referenced_type" : "_ZTIP3foo" } ], "return_type" : "_ZTI3bar", "source_file" : "exported/foo_exported.h" } ], "global_vars" : [], "lvalue_reference_types" : [], "pointer_types" : [ { "alignment" : 8, "linker_set_key" : "_ZTIP11foo_private", "name" : "foo_private *", "referenced_type" : "_ZTI11foo_private", "self_type" : "_ZTIP11foo_private", "size" : 8, "source_file" : "exported/foo_exported.h" }, { "alignment" : 8, "linker_set_key" : "_ZTIP3bar", "name" : "bar *", "referenced_type" : "_ZTI3bar", "self_type" : "_ZTIP3bar", "size" : 8, "source_file" : "exported/foo_exported.h" }, { "alignment" : 8, "linker_set_key" : "_ZTIP3foo", "name" : "foo *", "referenced_type" : "_ZTI3foo", "self_type" : "_ZTIP3foo", "size" : 8, "source_file" : "exported/foo_exported.h" }, { "alignment" : 8, "linker_set_key" : "_ZTIPi", "name" : "int *", "referenced_type" : "_ZTIi", "self_type" : "_ZTIPi", "size" : 8, "source_file" : "exported/foo_exported.h" } ], "qualified_types" : [], "record_types" : [ { "alignment" : 8, "fields" : [ { "field_name" : "mfoo", "referenced_type" : "_ZTI3foo" } ], "linker_set_key" : "_ZTI3bar", "name" : "bar", "referenced_type" : "_ZTI3bar", "self_type" : "_ZTI3bar", "size" : 24, "source_file" : "exported/foo_exported.h" }, { "alignment" : 8, "fields" : [ { "field_name" : "m1", "referenced_type" : "_ZTIi" }, { "field_name" : "m2", "field_offset" : 64, "referenced_type" : "_ZTIPi" }, { "field_name" : "mPfoo", "field_offset" : 128, "referenced_type" : "_ZTIP11foo_private" } ], "linker_set_key" : "_ZTI3foo", "name" : "foo", "referenced_type" : "_ZTI3foo", "self_type" : "_ZTI3foo", "size" : 24, "source_file" : "exported/foo_exported.h" } ], "rvalue_reference_types" : [] }
הכלי header-abi-linker:
- מקשר בין הקבצים
.sdumpשסופקו לו (foo.sdumpו-bar.sdump), ומסנן את פרטי ה-ABI שלא מופיעים בכותרות שנמצאות בספרייה:exported. - מנתח את
libfoo.soואוסף מידע על הסמלים שמיוצאים מהספרייה דרך טבלת.dynsym. - כולל
_Z3FooiP3barו_Z6FooBadiP3foo.
libfoo.so.lsdump הוא ה-ABI dump הסופי שנוצר מ-libfoo.so.
header-abi-diff
הכלי header-abi-diff משווה בין שני קבצים .lsdump שמייצגים את ממשק ה-ABI של שתי ספריות, ומפיק דוח השוואה שמציין את ההבדלים בין שני ממשקי ה-ABI.
| כניסות קלט |
|
|---|---|
| פלט | דוח השוואה שמציין את ההבדלים בממשקי ה-ABI שמוצעים על ידי שתי הספריות המשותפות שהושוו. |
קובץ ה-diff של ה-ABI הוא בפורמט טקסט של protobuf . הפורמט עשוי להשתנות בגרסאות עתידיות.
לדוגמה, יש לכם שתי גרסאות של libfoo: libfoo_old.so ו-libfoo_new.so. ב-libfoo_new.so, ב-bar_t, שינית את הסוג של mfoo מ-foo_t ל-foo_t *. מכיוון ש-bar_t הוא סוג שאפשר להגיע אליו, צריך לסמן את זה כשינוי שובר תאימות של ABI על ידי header-abi-diff.
כדי להריץ את header-abi-diff:
header-abi-diff -old libfoo_old.so.lsdump \
-new libfoo_new.so.lsdump \
-arch arm64 \
-o libfoo.so.abidiff \
-lib libfoo
פלט לדוגמה של פקודה ב-libfoo.so.abidiff:
lib_name: "libfoo" arch: "arm64" record_type_diffs { name: "bar" type_stack: "Foo-> bar *->bar " type_info_diff { old_type_info { size: 24 alignment: 8 } new_type_info { size: 8 alignment: 8 } } fields_diff { old_field { referenced_type: "foo" field_offset: 0 field_name: "mfoo" access: public_access } new_field { referenced_type: "foo *" field_offset: 0 field_name: "mfoo" access: public_access } } }
libfoo.so.abidiff מכיל דוח של כל השינויים שגורמים לשבירת תאימות של ABI ב-libfoo. ההודעה record_type_diffs מציינת שרשומה השתנתה ומפרטת את השינויים שלא תואמים, כולל:
- גודל הרשומה השתנה מ-
24בייטים ל-8בייטים. - סוג השדה של
mfooהשתנה מ-fooל-foo *(כל הגדרות הטיפוס הוסרו).
השדה type_stack מציין איך header-abi-diff הגיע לסוג ששונה (bar). אפשר לפרש את השדה הזה כך: Foo היא פונקציה מיוצאת שמקבלת את bar * כפרמטר, שמצביע על bar, שיוצא ושונה.
אכיפה של ABI ו-API
כדי לאכוף את ה-ABI ואת ה-API של ספריות משותפות של VNDK, צריך להכניס הפניות ל-ABI ל-${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/vndk/.
כדי ליצור את ההפניות האלה, מריצים את הפקודה הבאה:
${ANDROID_BUILD_TOP}/development/vndk/tools/header-checker/utils/create_reference_dumps.py
אחרי שיוצרים את ההפניות, כל שינוי בקוד המקור שגורם לשינוי לא תואם בממשק ABI/API בספריית ערכת פיתוח מקומית לספק (VNDK) גורם לשגיאת build.
כדי לעדכן הפניות לממשק ABI בספריות ספציפיות, מריצים את הפקודה הבאה:
${ANDROID_BUILD_TOP}/development/vndk/tools/header-checker/utils/create_reference_dumps.py -l <lib1> -l <lib2>
לדוגמה, כדי לעדכן הפניות ל-ABI של libbinder, מריצים את הפקודה:
${ANDROID_BUILD_TOP}/development/vndk/tools/header-checker/utils/create_reference_dumps.py -l libbinder