ABI 穩定性

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

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

關於 VNDK 和 ABI 合規性

VNDK 是一組限制性的庫,供應商模塊可以鏈接到這些庫,並啟用僅框架更新。 ABI 合規性是指更新版本的共享庫與動態鏈接到它的模塊一起按預期工作的能力(即像舊版本的庫一樣工作)。

關於導出的符號

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

  • 由共享庫的公共標頭導出。
  • 出現在共享庫對應的.so文件的.dynsym表中。
  • 具有 WEAK 或 GLOBAL 綁定。
  • 可見性是默認或受保護的。
  • 部分索引未定義。
  • 類型是 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;
安卓.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包含一個類型為foo_t的成員mfoo ,它通過foo_exported.h導出,從而導致更多類型被導出:
  • int :m1的類型。
  • int * :m2的類型。
  • foo_private_t * :mPfoo的類型。

但是, foo_private_t不可訪問,因為它不是通過foo_exported.h導出的。 ( foot_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/(v)ndk/<${PLATFORM_VNDK_VERSION}>/<BINDER_BITNESS>/<ARCH_ARCH-VARIANT>/source-based

例如,在為libfoo的 API 級別 27 構建 libfoo 時,將libfoo的推斷 ABI 與其在以下位置的參考進行比較:

${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/(v)ndk/27/64/<ARCH_ARCH-VARIANT>/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文件)以生成記錄與共享庫對應的所有 ABI 信息的.lsdump文件。
    lsdump creation
    圖 2.創建.lsdump文件
  3. header-abi-diff.lsdump文件與參考.lsdump文件進行比較,以生成概述兩個庫的 ABI 差異的差異報告。
    abi diff creation
    圖 3.創建差異報告

標頭-abi-dumper

header-abi-dumper工具解析 C/C++ 源文件並將從該源文件推斷出的 ABI 轉儲到中間文件中。構建系統在所有編譯的源文件上運行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解析foo.cpp並發出在exported目錄的公共標頭中公開的 ABI 信息。這是header-abi-dumper dumper 生成的foo.sdump的摘錄(不是完整的表示):

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包含源文件foo.cpp公開的 ABI 信息,例如:

  • record_types 。引用公共標頭公開的結構、聯合或類。每個記錄類型都有關於其字段、大小、訪問說明符、它所暴露的頭文件等的信息。
  • pointer_types 。引用由公共標頭公開的記錄/函數直接/間接引用的指針類型,以及指針指向的類型(通過type_info中的referenced_type字段)。類似的信息記錄在.sdump文件中,用於限定類型、內置 C/C++ 類型、數組類型以及左值和右值引用類型(關於類型的此類記錄信息允許遞歸比較)。
  • functions 。表示由公共標頭公開的函數。它們還包含有關函數名稱、返回類型、參數類型、訪問說明符等的信息。

標頭-abi-鏈接器

header-abi-linker工具將 header- header-abi-dumper生成的中間文件作為輸入,然後鏈接這些文件:

輸入
  • header-abi-dumper生成的中間文件
  • 版本腳本/地圖文件(可選)
  • 共享庫的.so文件
輸出記錄共享庫的 ABI 的文件(例如libfoo.so.lsdump代表libfoo的 ABI)。

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

例如,當libfoobar.cpp文件(它公開了一個 C 函數bar )添加到其編譯中時,可以調用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中的示例命令輸出:

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.sdumpbar.sdump ),過濾掉駐留在目錄中的標頭中不存在的 ABI 信息: exported
  • 解析libfoo.so ,並通過其.dynsym表收集有關庫導出的符號的信息。
  • 添加_Z3FooiP3barBar

libfoo.so.lsdump是最終生成的libfoo.so的 ABI 轉儲。

標頭-abi-diff

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

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

ABI 差異文件旨在盡可能詳細和可讀。格式可能會在未來版本中更改。例如,您有兩個版本的libfoolibfoo_old.solibfoo_new.so 。在libfoo_new.so中,在bar_t中,您將 mfoo 的類型從mfoo foo_tfoo_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 和 LLNDK 共享庫的 ABI/API,必須將 ABI 引用簽入${ANDROID_BUILD_TOP}/prebuilts/abi-dumps/(v)ndk/ 。要創建這些引用,請運行以下命令:

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

創建引用後,對源代碼所做的任何更改都會導致 VNDK 或 LLNDK 庫中的 ABI/API 更改不兼容,現在會導致生成錯誤。

要更新特定 VNDK 核心庫的 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

要更新特定 LLNDK 庫的 ABI 引用,請運行以下命令:

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