ABI 穩定性

應用程式二進位介面 (ABI) 穩定性是僅限架構更新的必要條件,因為供應商模組可能會依附位於系統分區的供應商原生開發套件 (VNDK) 共用資料庫。在 Android 版本中,新建的 VNDK 共用程式庫必須與先前發布的 VNDK 共用程式庫相容,以便供應商模組可與這些程式庫搭配運作,且不會發生重新編譯和執行階段錯誤。在 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.soFoo 函式,這是 .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 包含 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: 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 檔案。
    建立轉儲
    圖 1. 建立 .sdump 檔案
  2. header-abi-linker 接著會處理 .sdump 檔案 (使用提供給它的版本指令碼,或與共用程式庫相對應的 .so 檔案),產生 .lsdump 檔案,記錄與共用程式庫相對應的所有 ABI 資訊。
    建立 lsdump
    圖 2. 建立 .lsdump 檔案
  3. header-abi-diff 會比較 .lsdump 檔案與參考 .lsdump 檔案,產生差異報告,概述兩個程式庫的 ABI 差異。
    建立 ABI 差異
    圖 3. 建立差異比較報表

標頭 -abi-dumper

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

輸入裝置
  • C/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 欄位)。類似資訊會記錄在 .sdump 檔案中,用於符合條件的類型、內建 C/C++ 類型、陣列類型,以及 lvalue 和 rvalue 參照類型。這類資訊可用於遞迴差異比較。
  • functions。代表由公開標頭匯出的函式。這些資訊還包含函式的經過竄改的名稱、傳回類型、參數類型、存取指定符和其他屬性。

header-abi-linker

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

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

這項工具會合併所有傳入的轉譯中間檔案中的類型圖表,並考量轉譯單元之間的差異,例如單一定義 (不同轉譯單元中具有相同完整名稱的使用者定義類型,可能在語意上有所不同)。接著,這項工具會剖析版本指令碼或共用程式庫的 .dynsym 表格 (.so 檔案),製作匯出符號的清單。

例如 libfoo 包含 foo.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 工具會比較兩個代表兩個程式庫 ABI 的 .lsdump 檔案,並產生差異報告,說明兩個 ABI 之間的差異。

輸入裝置
  • .lsdump 檔案,代表舊共用程式庫的 ABI。
  • .lsdump 檔案代表新共用程式庫的 ABI。
輸出 差異報告,指出兩個比較中的共用程式庫提供的 ABI 差異。

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

舉例來說,您有兩個 libfoo 版本:libfoo_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