יציבות של ממשק בינארי של אפליקציה (ABI) היא תנאי מוקדם לעדכונים של מסגרת בלבד, כי מודולים של ספקים עשויים להיות תלויים בספריות המשותפות של Vendor Native Development Kit (VNDK) שנמצאות במחיצה של המערכת. בגרסה של Android, ספריות משותפות של VNDK שפותחו לאחרונה צריכות להיות תואמות ל-ABI לספריות משותפות של VNDK שהושקו בעבר, כדי שהמודולים של ספקים יוכלו לעבוד עם הספריות האלה ללא הידור מחדש וללא שגיאות בזמן הריצה. בין גרסאות Android, יכול להיות שיחולו שינויים בספריות VNDK, ואין garanties של ABI.
כדי להבטיח תאימות ל-ABI, גרסת Android 9 כוללת בדיקת ABI של כותרות, כפי שמתואר בקטעים הבאים.
מידע על תאימות ל-VNDK ול-ABI
VNDK היא קבוצה מוגבלת של ספריות שמודולים של ספקים יכולים לקשר אליהן, ומאפשרת עדכונים של מסגרת בלבד. תאימות ABI מתייחסת ליכולת של גרסה חדשה יותר של ספרייה משותפת לפעול כצפוי עם מודול שמקושר אליה באופן דינמי (כלומר, פועלת כמו גרסה ישנה יותר של הספרייה).
מידע על סמלים שיוצאו
סמל שיוצא (נקרא גם סמל גלובלי) הוא סמל שעומד בכל הדרישות הבאות:
- מיוצאים על ידי כותרות הציבוריות של ספרייה משותפת.
- מופיע בטבלה
.dynsym
בקובץ.so
שמתאים לספרייה המשותפת. - יש לו קישור WEAK או GLOBAL.
- החשיפה היא 'ברירת מחדל' או 'מוגן'.
- אינדקס הקטע הוא לא UNDEFINED.
- הערך של Type הוא 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
צריך לוודא תאימות ABI בספריות שמסומנות ב-vendor_available: true
וב-vndk.enabled: true
בקבצים המתאימים של Android.bp
. לדוגמה:
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, מתבצעת השוואה בין ה-ABI המשוער של libfoo
לבין ההפניה שלו ב:
${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/vndk/27/64/x86/source-based/libfoo.so.lsdump
שגיאה ב-ABI
במקרה של שגיאות ABI, ביומן ה-build יוצגו אזהרות עם סוג האזהרה ונתיב לדוח 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
כדי ליצור דוח diff שמפרט את ההבדלים ב-ABI של שתי הספריות.
איור 3. יצירת דוח ההבדלים
header-abi-dumper
הכלי header-abi-dumper
מנתח קובץ מקור C/C++ ושולח את ה-ABI שהוסקו מקובץ המקור הזה לקובץ ביניים. מערכת ה-build מפעילה את header-abi-dumper
בכל קובצי המקור שעברו הידור, תוך כדי בניית ספרייה שכוללת את קובצי המקור מיחסי תלות טרנזיים.
כניסות קלט |
|
---|---|
פלט | קובץ שמתאר את ה-ABI של קובץ המקור (לדוגמה, foo.sdump מייצג את ה-ABI של foo.cpp ).
|
נכון לעכשיו, קבצים מסוג .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. המידע הזה מאפשר לבצע השוואה חזרה (recursive diffing).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
הוא קובץ ה-dump הסופי של ABI שנוצר מ-libfoo.so
.
header-abi-diff
הכלי header-abi-diff
משווה בין שני קובצי .lsdump
שמייצגים את ה-ABI של שתי ספריות, ומפיק דוח הבדלים בין שני ממשקי ה-ABI.
כניסות קלט |
|
---|---|
פלט | דוח diff שמציג את ההבדלים ב-ABIs שמוצעים בשתי הספריות המשותפות שנערכת ביניהם השוואה. |
קובץ ההבדלים של 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 *
(כל ה-typedefs מוסרים).
השדה 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