ABI 稳定性

应用二进制接口 (ABI) 稳定性是进行仅针对框架的更新的前提条件,因为供应商模块可能依赖于系统分区中的供应商原生开发套件 (VNDK) 共享库。新编译的 VNDK 共享库必须与之前发布的 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 : [
    "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(通过 foo_exported.h 导出)的一个成员 mfoo,这会导致导出更多类型:
  • 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 合规性:

数据类型 说明
结构和类
  • 移除非静态数据成员。
  • 会导致类/结构体大小发生变化的更改。
  • 会导致虚表布局发生变化的更改。
  • 添加/移除基类。
  • 更改基类的顺序。
  • 更改模板参数。
  • 会导致数据成员的内存偏移发生变化的更改**
  • 更改成员的 const-volatile-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

例如,在为 VNDK 的 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
    图 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,同时还会建立一个库,其中包含来自传递依赖项的源文件。

目前,.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 并发出 ABI 信息(显示在 exported 目录内的公开标头中)。下面是 header-abi-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 字段)。对于限定类型、内置 C/C++ 类型、数组类型以及左值和右值参考类型(有关类型的此类记录信息允许递归差异),系统会在 .sdump 文件中记录类似信息。
  • functions:表示通过公开标头提供的函数。它们还包含函数的重整名称、返回值类型、参数类型、访问权限指定符等相关信息。

header-abi-linker

header-abi-linker 工具会将 header-abi-dumper 生成的中间文件作为输入,然后关联以下文件:

输入
  • header-abi-dumper 生成的中间文件
  • 版本脚本/映射文件(可选)
  • 共享库的 .so 文件
输出 用于记录共享库 ABI 的文件(例如,libfoo.so.lsdump 表示 libfoo 的 ABI)。

该工具会将收到的所有中间文件中的类型图合并在一起,并会将不同转换单元之间的单一定义(完全限定名称相同的不同转换单元中由用户定义的类型可能在语义上有所不同)差异考虑在内。然后,该工具会解析版本脚本或解析共享库的 .dynsym 表(.so 文件),以创建导出符号列表。

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

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

header-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 的类型从 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 和 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