ABI 穩定性

應用程式二進位介面 (ABI) 穩定性是僅框架更新的先決條件,因為供應商模組可能依賴駐留在系統分區的供應商本機開發工具包 (VNDK) 共用程式庫。在 Android 版本中,新建的 VNDK 共享庫必須與先前發布的 VNDK 共享庫 ABI 相容,以便供應商模組可以使用這些庫,而無需重新編譯,也不會出現運行時錯誤。在 Android 版本之間,VNDK 庫可以更改,並且沒有 ABI 保證。

為了幫助確保 ABI 相容性,Android 9 包含標頭 ABI 檢查器,如以下部分所述。

關於 VNDK 和 ABI 合規性

VNDK 是供應商模組可以連結到的一組限制性庫,並且支援僅框架更新。 ABI 合規性是指較新版本的共享庫能夠按預期與動態連結到它的模組一起工作(即像舊版本的庫一樣工作)。

關於導出的符號

導出符號(也稱為全域符號)是指滿足以下所有條件的符號:

  • 由共享庫的公共標頭導出。
  • 出現在共享庫對應的.so檔案的.dynsym表中。
  • 具有弱或全域綁定。
  • 可見性為預設或受保護。
  • 節索引不是 UNDEFINED。
  • 類型為 FUNC 或 OBJECT。

共享庫的公共標頭透過與共享庫對應的模組的Android.bp定義中的export_include_dirsexport_header_lib_headersexport_static_lib_headersexport_shared_lib_headersexport_generated_headers屬性定義為其他庫/二進位檔案可用的標頭。

關於可達類型

可到達類型是任何 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 *bar_t透過foo_exported.h導出。

bar_t包含一個foo_t類型的成員mfoo ,該成員透過foo_exported.h導出,這會導致導出更多類型:
  • int :m1的型別。
  • int * :m2的型別。
  • foo_private_t * :mPfoo的型別。

但是, foo_private_t無法訪問,因為它不是透過foo_exported.h導出的。 ( foo_private_t *是不透明的,因此允許對foo_private_t進行更改。)

對於透過基底類別說明符和模板參數可到達的類型也可以給出類似的解釋。

確保 ABI 合規性

對於對應Android.bp檔案中標記為vendor_available: true 」和vndk.enabled: true的函式庫,必須確保 ABI 合規性。例如:

cc_library {
    name: "libvndk_example",
    vendor_available: true,
    vndk: {
        enabled: true,
    }
}

對於匯出函數可直接或間接存取的資料類型,對庫的以下變更被歸類為破壞 ABI:

資料類型描述
結構和類別
  • 變更類別類型或結構類型的大小。
  • 基底類
    • 新增或刪除基底類別。
    • 新增或刪除虛擬繼承的基底類別。
    • 更改基類的順序。
  • 會員功能
    • 刪除成員函數*。
    • 從成員函數中新增或刪除參數。
    • 更改成員函數*的參數類型或傳回類型。
    • 更改虛擬表佈局。
  • 數據成員
    • 刪除靜態資料成員。
    • 新增或刪除非靜態資料成員。
    • 更改資料成員的類型。
    • 更改非靜態資料成員的偏移量**。
    • 更改資料成員的constvolatile和/或restricted限定符***。
    • 降級資料成員的存取說明符***。
  • 更改模板參數。
工會
  • 新增或刪除資料成員。
  • 更改聯合類型的大小。
  • 更改資料成員的類型。
列舉
  • 更改基礎類型。
  • 更改枚舉器的名稱。
  • 更改枚舉數的值。
全球符號
  • 刪除公共標頭導出的符號。
  • 對於 FUNC 類型的全域符號
    • 新增或刪除參數。
    • 更改參數類型。
    • 更改返回類型。
    • 降級存取說明符***。
  • 對於 OBJECT 類型的全域符號
    • 更改相應的C/C++類型。
    • 降級存取說明符***。

*公有和私有成員函數都不能更改或刪除,因為公用內聯函數可以引用私有成員函數。對私有成員函數的符號參考可以保存在呼叫者二進位檔案中。從共用庫中變更或刪除私有成員函數可能會導致向後不相容的二進位檔案。

**不得更改公有或私有資料成員的偏移量,因為內聯函數可以在其函數體中引用這些資料成員。更改資料成員偏移量可能會導致向後不相容的二進位檔案。

***雖然這些不會改變類型的記憶體佈局,但存在語義差異,可能導致庫無法如預期運作。

使用 ABI 合規性工具

建置 VNDK 程式庫時,會將程式庫的 ABI 與正在建置的 VNDK 版本的相應 ABI 參考進行比較。參考 ABI 轉儲位於:

${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/vndk/<PLATFORM_VNDK_VERSION>/<BINDER_BITNESS>/<ARCH>/source-based

例如,在 API 等級 27 為 x86 建構libfoo時, libfoo的推論 ABI 與其參考進行比較:

${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/vndk/27/64/x86/source-based/libfoo.so.lsdump

ABI 損壞錯誤

對於 ABI 損壞,建置日誌會顯示警告以及警告類型和 abi-diff 報告的路徑。例如,如果libbinder的 ABI 有不相容的更改,建置系統會拋出錯誤,並顯示類似以下內容的訊息:

*****************************************************
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 ----

建置 VNDK 庫 ABI 檢查

建置 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 報告,概述兩個函式庫的 ABI 差異。
    abi diff creation
    圖 3.建立差異報告

標頭 abi 轉儲器

header-abi-dumper工具解析 C/C++ 原始檔並將從該原始檔推斷的 ABI 轉儲到中間檔。建置系統在所有已編譯的來源檔案上執行header-abi-dumper ,同時也建置一個包含來自傳遞依賴項的原始檔案的函式庫。

輸入
  • AC/C++ 原始檔
  • 導出的包含目錄
  • 編譯器標誌
輸出描述原始檔 ABI 的檔案(例如foo.sdump代表foo.cpp的 ABI)。

目前.sdump檔案採用 JSON 格式,不保證在未來版本中保持穩定。因此, .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 -- -I exported -x c++

此指令告訴header-abi-dumper使用--後面的編譯器標誌解析foo.cpp ,並發出由exported目錄中的公共標頭導出的 ABI 資訊。以下是header-abi-dumper產生的foo.sdump

{
 "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包含來源檔案foo.cpp匯出的 ABI 資訊和公共標頭,例如:

  • record_types 。引用公共標頭中定義的結構、聯合或類別。每個記錄類型都包含有關其欄位、大小、存取說明符、定義它的頭文件以及其他屬性的資訊。
  • pointer_types 。引用在公共標頭中導出的記錄/函數直接/間接引用的指標類型,以及指標指向的類型(透過type_info中的referenced_type欄位)。限定類型、內建 C/C++ 類型、陣列類型以及左值和右值參考類型的類似資訊記錄在.sdump檔案中。此類資訊允許遞歸比較。
  • functions 。表示由公共標頭導出的函數。它們還具有有關函數的修飾名稱、傳回類型、參數類型、存取說明符和其他屬性的資訊。

頭 abi 連結器

header-abi-linker工具將header-abi-dumper產生的中間檔案作為輸入,然後連結這些檔案:

輸入
  • header-abi-dumper產生的中間文件
  • 版本腳本/映射檔案(可選)
  • 共享庫的.so文件
  • 導出的包含目錄
輸出描述共用程式庫 ABI 的檔案(例如, libfoo.so.lsdump代表libfoo的 ABI)。

此工具合併提供給它的所有中間文件中的類型圖,同時考慮到翻譯單元之間的單一定義(具有相同完全限定名稱的不同翻譯單元中的使用者定義類型,可能在語義上不同)差異。然後,該工具會解析版本腳本或共享庫( .so檔案)的.dynsym表,以產生導出符號的清單。

例如, libfoofoo.cppbar.cpp組成。可以呼叫header-abi-linker來建立libfoo的完整連結 ABI 轉儲,如下所示:

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.sdumpbar.sdump ),過濾掉以下目錄中的標頭中不存在的 ABI 資訊: exported
  • 解析libfoo.so ,並透過其.dynsym表收集有關庫導出的符號的資訊。
  • 新增_Z3FooiP3bar_Z6FooBadiP3foo

libfoo.so.lsdumplibfoo.so最終產生的 ABI 轉儲。

標頭 abi-diff

header-abi-diff工具比較代表兩個函式庫的 ABI 的兩個.lsdump文件,並產生一份 diff 報告,說明兩個 ABI 之間的差異。

輸入
  • .lsdump文件,表示舊共享庫的 ABI。
  • .lsdump文件,表示新共享庫的 ABI。
輸出一份 diff 報告,說明所比較的兩個共享庫提供的 ABI 的差異。

ABI diff 檔案採用protobuf 文字格式。未來版本中的格式可能會變更。

例如,您有兩個版本的libfoolibfoo_old.solibfoo_new.so 。在libfoo_new.sobar_t中,將mfoo的類型從foo_t改為foo_t * 。由於bar_t是可存取類型,因此應透過header-abi-diff將其標記為 ABI 重大變更。

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包含libfoo中所有 ABI 重大變更的報表。 record_type_diffs訊息指示記錄已更改並列出不相容的更改,其中包括:

  • 記錄的大小從24位元組更改為8位元組。
  • mfoo的欄位類型從foo改為foo * (所有 typedef 都被剝離)。

type_stack欄位指示header-abi-diff如何到達更改的類型 ( bar )。該字段可以解釋為Foo是一個導出函數,它接受bar *作為參數,指向bar ,該函數已導出並更改。

執行 ABI/API

若要強制執行 VNDK 共享庫的 ABI/API,必須將 ABI 引用簽入${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/vndk/ 。若要建立這些引用,請執行以下命令:

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

建立引用後,對原始程式碼所做的任何更改如果導致 VNDK 庫中出現不相容的 ABI/API 更改,現在都會導致生成錯誤。

若要更新特定庫的 ABI 引用,請執行下列命令:

${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