יציבות ABI

יציבות ממשק בינארי של יישומים (ABI) היא תנאי מוקדם לעדכוני מסגרת בלבד מכיוון שמודולי ספק עשויים להיות תלויים בספריות המשותפות של ערכת הפיתוח המקורית של הספק (VNDK) השוכנות במחיצת המערכת. בתוך מהדורת אנדרואיד, ספריות משותפות VNDK שנבנו לאחרונה חייבות להיות תואמות ABI לספריות משותפות VNDK שפורסמו בעבר, כך שמודולי הספק יוכלו לעבוד עם ספריות אלה ללא הידור מחדש וללא שגיאות זמן ריצה. בין מהדורות אנדרואיד, ניתן לשנות ספריות VNDK ואין ערבויות של ABI.

כדי לסייע בהבטחת תאימות ABI, אנדרואיד 9 כולל בודק כותרת ABI, כמתואר בסעיפים הבאים.

על תאימות VNDK ו-ABI

ה-VNDK הוא קבוצה מגבילה של ספריות שמודולי הספק עשויים לקשר אליהן ומאפשרות עדכונים למסגרת בלבד. תאימות ABI מתייחסת ליכולת של גרסה חדשה יותר של ספרייה משותפת לעבוד כצפוי עם מודול שמקושר אליה באופן דינמי (כלומר עובד כמו שגרסה ישנה יותר של הספרייה תעשה).

על סמלים מיוצאים

סמל מיוצא (ידוע גם כסמל גלובלי ) מתייחס לסמל המקיים את כל התנאים הבאים:

  • מיוצא על ידי הכותרות הציבוריות של ספרייה משותפת.
  • מופיע בטבלת .dynsym של קובץ .so התואם לספרייה המשותפת.
  • בעל כריכה חלשה או גלובלית.
  • הראות היא 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 : [
    "include"
  ],
}
טבלת .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 * , bar_t מיוצא דרך foo_exported.h .

bar_t מכיל חבר mfoo , מסוג foo_t , המיוצא דרך foo_exported.h , מה שמביא לייצוא של סוגים נוספים:
  • int : הוא הסוג של m1 .
  • int * : הוא הסוג של m2 .
  • foo_private_t * : הוא הסוג של mPfoo .

עם זאת, foo_private_t אינו נגיש מכיוון שהוא אינו מיוצא דרך foo_exported.h . ( foot_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:

סוג מידע תיאור
מבנים וכיתות
  • שנה את הגודל של סוג המחלקה או סוג ה-struct.
  • שיעורי בסיס
    • הוסף או הסר מחלקות בסיס.
    • הוסף או הסר מחלקות בסיס כמעט בירושה.
    • שנה את סדר מחלקות הבסיס.
  • פונקציות חבר
    • הסר פונקציות חבר*.
    • הוסף או הסר ארגומנטים מפונקציות חבר.
    • שנה את סוגי הארגומנטים או סוגי ההחזרה של פונקציות איברים*.
    • שנה את פריסת הטבלה הוירטואלית.
  • חברי נתונים
    • הסר חברי נתונים סטטיים.
    • הוסף או הסר חברי נתונים שאינם סטטיים.
    • שנה את סוגי חברי הנתונים.
    • שנה את ההסטות לאברי נתונים לא סטטיים**.
    • שנה את ה- const , volatile ו/או מוקדמות restricted של חברי נתונים***.
    • שדרג לאחור את מפרטי הגישה של חברי נתונים***.
  • שנה את הארגומנטים של התבנית.
איגודים
  • הוסף או הסר חברי נתונים.
  • שנה את גודל סוג האיחוד.
  • שנה את סוגי חברי הנתונים.
  • שנה את סדר חברי הנתונים.
ספירות
  • שנה את הסוג הבסיסי.
  • שנה את השמות של המונים.
  • שנה את הערכים של המונים.
סמלים גלובליים
  • הסר את הסמלים שיוצאו על ידי כותרות ציבוריות.
  • עבור סמלים גלובליים מסוג FUNC
    • הוסף או הסר ארגומנטים.
    • שנה את סוגי הארגומנטים.
    • שנה את סוג ההחזרה.
    • שדרג לאחור את מפרט הגישה***.
  • עבור סמלים גלובליים מסוג OBJECT
    • שנה את סוג C/C++ המתאים.
    • שדרג לאחור את מפרט הגישה***.

* אין לשנות או להסיר את פונקציות החברים הציבוריות והפרטיות, מכיוון שפונקציות מוטבעות ציבוריות יכולות להתייחס לפונקציות חבר פרטיות. הפניות לסמלים לפונקציות חבר פרטיות יכולות להישמר בקבצים בינאריים של מתקשר. שינוי או הסרה של פונקציות חבר פרטיות מספריות משותפות עלול לגרום לקבצים בינאריים שאינם תואמים לאחור.

** אין לשנות את הקיזוזים לחברי נתונים ציבוריים או פרטיים מכיוון שפונקציות מוטבעות יכולות להתייחס לאברי נתונים אלה בגוף הפונקציה שלהם. שינוי קיזוז של חברי נתונים יכול לגרום לקבצים בינאריים שאינם תואמים לאחור.

*** למרות שאלו אינם משנים את פריסת הזיכרון של הסוג, ישנם הבדלים סמנטיים שעלולים להוביל לכך שספריות לא יפעלו כמצופה.

שימוש בכלי תאימות ABI

כאשר נבנית ספריית VNDK, ה-ABI של הספרייה מושווה עם התייחסות ה-ABI המקבילה עבור גרסת ה-VNDK הנבנית. מזבלות ABI ממוקמות ב:

${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/(v)ndk/<${PLATFORM_VNDK_VERSION}>/<BINDER_BITNESS>/<ARCH_ARCH-VARIANT>/source-based

לדוגמה, בבניית libfoo עבור רמת API 27 של libfoo , ה-ABI המוסק של libfoo מושווה להתייחסות שלו ב:

${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/(v)ndk/27/64/<ARCH_ARCH-VARIANT>/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:

  1. header-abi-dumper מעבד את קובצי המקור שהידור לבניית ספריית VNDK (קבצי המקור של הספרייה עצמה וכן קבצי מקור שהועברו בירושה באמצעות תלות טרנזיטיבית סטטית), כדי לייצר קבצי .sdump המתאימים לכל מקור.
    sdump creation
    איור 1. יצירת קבצי .sdump
  2. header-abi-linker לאחר מכן מעבד את קבצי ה-. .sdump (באמצעות סקריפט גרסה שסופק לו או קובץ ה- .so המתאים לספרייה המשותפת) כדי לייצר קובץ .lsdump את כל מידע ה-ABI המתאים לספרייה המשותפת.
    lsdump creation
    איור 2. יצירת קובץ .lsdump
  3. header-abi-diff משווה את קובץ .lsdump עם קובץ .lsdump כדי לייצר דוח diff שמתאר את ההבדלים ב-ABIs של שתי הספריות.
    abi diff creation
    איור 3. יצירת דוח ההבדל

header-abi-dumper

הכלי header-abi-dumper מנתח קובץ מקור של C/C++ ומשליך את ה-ABI המסיק מקובץ המקור הזה לקובץ ביניים. מערכת ה-build מריצה header-abi-dumper על כל קבצי המקור המהופפים תוך בניית ספרייה הכוללת את קבצי המקור מתלות מעבר.

נכון לעכשיו, קובצי .sdump מעוצבים כ- Protobuf TextFormatted , שלא מובטח שיהיו יציבים במהדורות עתידיות. ככזה, עיצוב קובץ .sdump צריך להיחשב כפרט הטמעת מערכת לבנות.

לדוגמה, ל- 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 -- -x c++

פקודה זו אומרת header-abi-dumper dumper לנתח את foo.cpp ולפלוט את מידע ה-ABI שנחשף בכותרות הציבוריות בספרייה exported . זהו קטע (לא ייצוג מלא) מתוך foo.sdump שנוצר על ידי header-abi-dumper :

record_types {
  type_info {
    name: "foo"
    size: 12
    alignment: 4
    referenced_type: "type-1"
    source_file: "foo/include/foo_exported.h"
    linker_set_key: "foo"
    self_type: "type-1"
  }
  fields {
    referenced_type: "type-2"
    field_offset: 0
    field_name: "m1"
    access: public_access
  }
  fields {
    referenced_type: "type-3"
    field_offset: 32
    field_name: "m2"
    access: public_access
  }
  fields {
    referenced_type: "type-5"
    field_offset: 64
    field_name: "mPfoo"
    access: public_access
  }
  access: public_access
  record_kind: struct_kind
  tag_info {
    unique_id: "_ZTS3foo"
  }
}
record_types {
  type_info {
    name: "bar"
    size: 12
    alignment: 4
    referenced_type: "type-6"
…
pointer_types {
  type_info {
    name: "bar *"
    size: 4
    alignment: 4
    referenced_type: "type-6"
    source_file: "foo/include/foo_exported.h"
    linker_set_key: "bar *"
    self_type: "type-8"
  }
}
builtin_types {
  type_info {
    name: "int"
    size: 4
    alignment: 4
    referenced_type: "type-2"
    source_file: ""
    linker_set_key: "int"
    self_type: "type-2"
  }
  is_unsigned: false
  is_integral: true
}
functions {
  return_type: "type-7"
  function_name: "Foo"
  source_file: "foo/include/foo_exported.h"
  parameters {
    referenced_type: "type-2"
    default_arg: false
  }
  parameters {
    referenced_type: "type-8"
    default_arg: false
  }
  linker_set_key: "_Z3FooiP3bar"
  access: public_access
}

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 כקלט ואז מקשר את הקבצים האלה:

תשומות
  • קבצי ביניים מיוצרים על ידי header-abi-dumper
  • גרסה script/קובץ מפה (אופציונלי)
  • קובץ .so של הספרייה המשותפת
תְפוּקָה קובץ שמתעד את ה-ABI של ספרייה משותפת (למשל libfoo.so.lsdump מייצג את ה-ABI של libfoo ).

הכלי ממזג את גרפי הסוגים בכל קבצי הביניים שניתנו לו, תוך התחשבות בהבדלים בהגדרה אחת (סוגים המוגדרים על ידי משתמש ביחידות תרגום שונות עם אותו שם מלא, עשויים להיות שונים מבחינה סמנטית) בין יחידות תרגום. לאחר מכן הכלי מנתח סקריפט גרסה או טבלת .dynsym של הספרייה המשותפת (קובץ .so ) כדי ליצור רשימה של הסמלים המיוצאים.

לדוגמה, כאשר libfoo מוסיף את קובץ bar.cpp (אשר חושף bar פונקציות C) להידור שלו, ניתן להפעיל את 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 :

record_types {
  type_info {
    name: "foo"
    size: 24
    alignment: 8
    referenced_type: "type-1"
    source_file: "foo/include/foo_exported.h"
    linker_set_key: "foo"
    self_type: "type-1"
  }
  fields {
    referenced_type: "type-2"
    field_offset: 0
    field_name: "m1"
    access: public_access
  }
  fields {
    referenced_type: "type-3"
    field_offset: 64
    field_name: "m2"
    access: public_access
  }
  fields {
    referenced_type: "type-4"
    field_offset: 128
    field_name: "mPfoo"
    access: public_access
  }
  access: public_access
  record_kind: struct_kind
  tag_info {
    unique_id: "_ZTS3foo"
  }
}
record_types {
  type_info {
    name: "bar"
    size: 24
    alignment: 8
...
builtin_types {
  type_info {
    name: "void"
    size: 0
    alignment: 0
    referenced_type: "type-6"
    source_file: ""
    linker_set_key: "void"
    self_type: "type-6"
  }
  is_unsigned: false
  is_integral: false
}
functions {
  return_type: "type-19"
  function_name: "Foo"
  source_file: "foo/include/foo_exported.h"
  parameters {
    referenced_type: "type-2"
    default_arg: false
  }
  parameters {
    referenced_type: "type-20"
    default_arg: false
  }
  linker_set_key: "_Z3FooiP3bar"
  access: public_access
}
functions {
  return_type: "type-6"
  function_name: "FooBad"
  source_file: "foo/include/foo_exported_bad.h"
  parameters {
    referenced_type: "type-2"
    default_arg: false
  }
parameters {
    referenced_type: "type-7"
    default_arg: false
  }
  linker_set_key: "_Z6FooBadiP3foo"
  access: public_access
}
elf_functions {
  name: "_Z3FooiP3bar"
}
elf_functions {
  name: "_Z6FooBadiP3foo"
}

הכלי header-abi-linker :

  • מקשר את קבצי ה-. .sdump שסופקו לו ( foo.sdump ו- bar.sdump ), מסנן את מידע ה-ABI שאינו קיים בכותרות השוכנות בספרייה: exported .
  • מנתח libfoo.so , ואוסף מידע על הסמלים המיוצאים על ידי הספרייה דרך טבלת ה-. .dynsym שלה.
  • מוסיף _Z3FooiP3bar ו- Bar .

libfoo.so.lsdump הוא ה-ABI ה-dump הסופי שנוצר של libfoo.so .

header-abi-diff

הכלי header-abi-diff משווה שני קבצי .lsdump המייצגים את ה-ABI של שתי ספריות ומפיק דוח diff המציין את ההבדלים בין שני ה-ABIs.

תשומות
  • קובץ .lsdump המייצג את ה-ABI של ספרייה משותפת ישנה.
  • קובץ .lsdump המייצג את ה-ABI של ספרייה משותפת חדשה.
תְפוּקָה דוח הבדל המציין את ההבדלים ב-ABIs המוצעים על ידי שתי הספריות המשותפות בהשוואה.

קובץ ABI diff מתוכנן להיות כמה שיותר מילולי וקריא. הפורמט נתון לשינויים במהדורות עתידיות. לדוגמה, יש לך שתי גרסאות של 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 ו-LLNDK, יש לבדוק הפניות ABI ב- ${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/(v)ndk/ . כדי ליצור הפניות אלה, הפעל את הפקודה הבאה:

${ANDROID_BUILD_TOP}/development/vndk/tools/header-checker/utils/create_reference_dumps.py

לאחר יצירת ההפניות, כל שינוי שנעשה בקוד המקור שגורם לשינוי ABI/API לא תואם בספריית VNDK או LLNDK גורם כעת לשגיאת בנייה.

כדי לעדכן הפניות של ABI עבור ספריות ליבה ספציפיות של VNDK, הפעל את הפקודה הבאה:

${ANDROID_BUILD_TOP}/development/vndk/tools/header-checker/utils/create_reference_dumps.py -l <lib1> -l <lib2>

לדוגמה, כדי לעדכן הפניות ל- libbinder ABI, הרץ:

${ANDROID_BUILD_TOP}/development/vndk/tools/header-checker/utils/create_reference_dumps.py -l libbinder

כדי לעדכן הפניות של ABI עבור ספריות LLNDK ספציפיות, הפעל את הפקודה הבאה:

${ANDROID_BUILD_TOP}/development/vndk/tools/header-checker/utils/create_reference_dumps.py -l <lib1> -l <lib2> --llndk

לדוגמה, כדי לעדכן הפניות ל- libm ABI, הרץ:

${ANDROID_BUILD_TOP}/development/vndk/tools/header-checker/utils/create_reference_dumps.py -l libm --llndk