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。

共享库的公开头文件是指通过以下属性提供给其他库/二进制文件使用的头文件:export_include_dirsexport_header_lib_headersexport_static_lib_headersexport_shared_lib_headersexport_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 : [
    "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 diff
    图 3. 创建差异报告

header-abi-dumper

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 - 表示公开头文件导出的函数。它们还包含函数的重整名称、返回类型、参数类型、访问权限说明符和其他属性的相关信息。

header-abi-linker

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),滤除位于 exported 目录的头文件中不存在的 ABI 信息。
  • 解析 libfoo.so,然后通过其 .dynsym 表格收集由库导出的符号的相关信息。
  • 添加 _Z3FooiP3bar_Z6FooBadiP3foo

libfoo.so.lsdump 是最终生成的 libfoo.so ABI 转储。

header-abi-diff

header-abi-diff 工具会将代表两个库的 ABI 的两个 .lsdump 文件进行比较,并生成差异报告,其中会说明这两个 ABI 之间存在的差异。

输入
  • 代表旧共享库的 ABI 的 .lsdump 文件。
  • 代表新共享库的 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 *(去除了所有类型定义符)。

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