应用二进制接口 (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_dirs
、export_header_lib_headers
、export_static_lib_headers
、export_shared_lib_headers
和 export_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 : [ "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 包含一个 mfoo 成员,其类型为 foo_t ,该类型会通过 foo_exported.h 导出,这会导致导出更多类型:
不过, foo_private_t 不是可到达类型,因为它不是通过 foo_exported.h 导出的。(foot_private_t * 不透明,因此允许对 foo_private_t 进行更改。)
|
对于可通过基类指定符和模板参数到达的类型,也可给出类似解释。
确保 ABI 合规性
对于在对应的 Android.bp
文件中标有 vendor_available: true
和 vndk.enabled: true
的库,必须确保其 ABI 合规性。例如:
cc_library { name: "libvndk_example", vendor_available: true, vndk: { enabled: true, } }
对于可通过导出的函数直接或间接到达的数据类型,对库进行以下更改会破坏 ABI 合规性:
数据类型 | 说明 |
---|---|
结构和类 |
|
联合 |
|
枚举 |
|
全局符号 |
|
* 不得更改或移除公共和私有成员函数,因为公共内联函数可以引用私有成员函数。私有成员函数的符号引用可保存在调用程序二进制文件中。从共享库更改或移除私有成员函数会导致二进制文件向后不兼容。
** 不得更改公共或私有数据成员的偏移量,因为内联函数可以在其函数主体中引用这些数据成员。更改数据成员偏移量会导致二进制文件向后不兼容。
*** 虽然这些操作不会更改类型的内存布局,但它们之间存在语义差异,可能导致库无法按预期正常运行。
使用 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 库时:
header-abi-dumper
会处理为了构建 VNDK 库而编译的源文件(库本身的源文件以及通过静态传递依赖项沿用的源文件),以生成与各个源文件对应的.sdump
文件。
图 1. 创建 .sdump
文件- 然后,
header-abi-linker
会处理.sdump
文件(使用提供给它的版本脚本或与共享库对应的.so
文件),以生成.lsdump
文件,该文件用于记录与共享库对应的所有 ABI 信息。
图 2. 创建 .lsdump
文件 header-abi-diff
会将.lsdump
文件与参考.lsdump
文件进行比较,以生成差异报告,该报告中会简要说明两个库的 ABI 之间存在的差异。
图 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
,并发出位于 exported
目录的公开头文件中提供的 ABI 信息。下面是 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
生成的中间文件作为输入,然后关联以下文件:
输入 |
|
---|---|
输出 | 用于记录共享库的 ABI 的文件(例如,libfoo.so.lsdump 代表 libfoo 的 ABI)。
|
该工具会将收到的所有中间文件中的类型图合并在一起,并会将不同转换单元之间的单一定义(完全限定名称相同的不同转换单元中由用户定义的类型可能在语义上有所不同)差异考虑在内。然后,该工具会解析版本脚本或共享库(.so
文件)的 .dynsym
表,以创建导出符号的列表。
例如,当 libfoo
将 bar.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.sdump
和bar.sdump
),滤除位于exported
目录的头文件中不存在的 ABI 信息。 - 解析
libfoo.so
,然后通过其.dynsym
表格收集由库导出的符号的相关信息。 - 添加
_Z3FooiP3bar
和Bar
。
libfoo.so.lsdump
是最终生成的 libfoo.so
ABI 转储。
header-abi-diff
header-abi-diff
工具会将代表两个库的 ABI 的两个 .lsdump
文件进行比较,并生成差异报告,其中会说明这两个 ABI 之间存在的差异。
输入 |
|
---|---|
输出 | 差异报告,其中会说明在比较两个共享库提供的 ABI 之后发现的差异。 |
ABI 差异文件会尽可能详细且便于读懂。格式在未来版本中可能会发生变化。例如,假设您有两个版本的 libfoo
:libfoo_old.so
和 libfoo_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