應用程序二進制接口 (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_dirs
、 export_header_lib_headers
、 export_static_lib_headers
、 export_shared_lib_headers
和export_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 導出,從而導致更多類型被導出:
但是, 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
例如,在為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 庫時:
-
header-abi-dumper
處理編譯的源文件以構建 VNDK 庫(庫自己的源文件以及通過靜態傳遞依賴項繼承的源文件),以生成與每個源對應的.sdump
文件。圖 1.創建 .sdump
文件 - 然後
header-abi-linker
處理.sdump
文件(使用提供給它的版本腳本或與共享庫對應的.so
文件)以生成記錄與共享庫對應的所有 ABI 信息的.lsdump
文件。圖 2.創建 .lsdump
文件 header-abi-diff
將.lsdump
文件與參考.lsdump
文件進行比較,以生成概述兩個庫的 ABI 差異的差異報告。圖 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
生成的中間文件作為輸入,然後鏈接這些文件:
輸入 |
|
---|---|
輸出 | 記錄共享庫的 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
),過濾掉駐留在目錄中的標頭中不存在的 ABI 信息:exported
。 - 解析
libfoo.so
,並通過其.dynsym
表收集有關庫導出的符號的信息。 - 添加
_Z3FooiP3bar
和Bar
。
libfoo.so.lsdump
是最終生成的libfoo.so
的 ABI 轉儲。
標頭-abi-diff
header-abi-diff
工具比較代表兩個庫的 ABI 的兩個.lsdump
文件,並生成一個差異報告,說明兩個 ABI 之間的差異。
輸入 |
|
---|---|
輸出 | 一份差異報告,說明比較的兩個共享庫提供的 ABI 的差異。 |
ABI 差異文件旨在盡可能詳細和可讀。格式可能會在未來版本中更改。例如,您有兩個版本的libfoo
: libfoo_old.so
和libfoo_new.so
。在libfoo_new.so
中,在bar_t
中,您將 mfoo 的類型從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 和 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