ABI 穩定性

應用程式二進位介面 (ABI) 穩定性是僅限架構更新的先決條件,因為供應商模組可能依附於系統分割區中的供應商原生開發套件 (VNDK) 共用程式庫。在 Android 版本中,新建立的 VNDK 共用程式庫必須與先前發布的 VNDK 共用程式庫 ABI 相容,這樣供應商模組才能使用這些程式庫,不必重新編譯,也不會發生執行階段錯誤。在 Android 版本之間,VNDK 程式庫可能會變更,且不保證 ABI。

為確保 ABI 相容性,Android 9 包含標頭 ABI 檢查工具,詳情請參閱下列章節。

關於 VNDK 和 ABI 相容性

VNDK 是一組受限的程式庫,供應商模組可連結至這些程式庫,並啟用僅限架構的更新。ABI 相容性是指新版共用程式庫是否能與動態連結至該程式庫的模組正常運作 (也就是說,是否能像舊版程式庫一樣運作)。

關於匯出的符號

匯出的符號 (也稱為全域符號) 是指符合下列所有條件的符號:

  • 由共用程式庫的公開標頭匯出。
  • 顯示在與共用程式庫對應的 .so 檔案 .dynsym 表格中。
  • 具有 WEAK 或 GLOBAL 繫結。
  • 可見度為「DEFAULT」或「PROTECTED」。
  • 區段索引不是 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 包含 mfoo 成員 (類型為 foo_t),透過 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: truevndk.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
    圖 1. 建立 .sdump 檔案
  2. header-abi-linker 接著會處理 .sdump 檔案 (使用提供給它的版本指令碼,或對應共用程式庫的 .so 檔案),產生 .lsdump 檔案,記錄對應共用程式庫的所有 ABI 資訊。
    建立 lsdump
    圖 2. 建立 .lsdump 檔案
  3. header-abi-diff 會比較 .lsdump 檔案與參照 .lsdump 檔案,產生差異報表,列出兩個程式庫 ABI 的差異。
    建立 ABI 差異
    圖 3. 建立差異報表

header-abi-dumper

header-abi-dumper 工具會剖析 C/C++ 來源檔案,並將從該來源檔案推斷出的 ABI 傾印至中繼檔案。建構系統會對所有已編譯的來源檔案執行 header-abi-dumper,同時建構程式庫,其中包含來自遞移依附元件的來源檔案。

輸入
  • C/C++ 來源檔案
  • 匯出的 include 目錄
  • 編譯器旗標
輸出 描述來源檔案 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 資訊。以下是 foo.sdumpheader-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 包含來源檔案 foo.cpp 匯出的 ABI 資訊和公開標頭,例如:

  • record_types。請參閱公用標頭中定義的結構體、聯集或類別。每種記錄類型都有欄位、大小、存取指定符、定義所在的標頭檔案,以及其他屬性的相關資訊。
  • pointer_types。請參閱公開標頭中匯出的記錄/函式直接/間接參照的指標型別,以及指標指向的型別 (透過 type_info 中的 referenced_type 欄位)。對於合格型別、內建 C/C++ 型別、陣列型別,以及左值和右值參照型別,系統會在 .sdump 檔案中記錄類似資訊。這類資訊可進行遞迴差異比較。
  • functions。代表公開標頭匯出的函式。 此外,這些資訊也包含函式的亂碼名稱、傳回型別、參數型別、存取指定符和其他屬性。

header-abi-linker

header-abi-linker 工具會將 header-abi-dumper 產生的中繼檔案做為輸入內容,然後連結這些檔案:

輸入
  • header-abi-dumper 產生的中繼檔案
  • 版本指令碼/對應檔案 (選用)
  • 共用程式庫的 .so 檔案
  • 匯出的 include 目錄
輸出 說明共用程式庫 ABI 的檔案 (例如,libfoo.so.lsdump 代表 libfoo 的 ABI)。

這項工具會合併提供給它的所有中繼檔案中的型別圖,並考量翻譯單元間的單一定義差異 (不同翻譯單元中具有相同完整名稱的使用者定義型別,在語意上可能有所不同)。然後,工具會剖析版本指令碼或共用程式庫的 .dynsym 表格 (.so 檔案),列出匯出的符號。

舉例來說,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),並篩除目錄 exported 中標頭未提供的 ABI 資訊。
  • 剖析 libfoo.so,並透過程式庫的 .dynsym 表格收集程式庫匯出符號的相關資訊。
  • 新增 _Z3FooiP3bar_Z6FooBadiP3foo

libfoo.so.lsdumplibfoo.so 的最終生成 ABI 傾印檔案。

header-abi-diff

header-abi-diff 工具會比較兩個 .lsdump 檔案,這兩個檔案代表兩個程式庫的 ABI,並產生差異報表,說明兩個 ABI 之間的差異。

輸入
  • .lsdump 檔案,代表舊共用程式庫的 ABI。
  • 代表新共用程式庫 ABI 的 .lsdump 檔案。
輸出 差異報表,說明所比較兩個共用程式庫提供的 ABI 差異。

ABI 差異檔案採用 protobuf 文字格式。格式可能在後續版本中有所變動。

舉例來說,您有兩個版本的 libfoolibfoo_old.solibfoo_new.so。在 libfoo_new.so 中,將 bar_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