Android 内核 ABI 监控

您可以使用 Android 11 及更高版本中提供的应用二进制接口 (ABI) 监控工具来稳定 Android 内核的内核内 ABI。该工具会收集和比较现有内核二进制文件(vmlinux+ GKI 模块)中的 ABI 表示形式。这些 ABI 表示形式是 .stg 文件和符号列表。表示形式为其提供视图的接口称为内核模块接口 (KMI)。您可以使用该工具跟踪和缓解 KMI 的变更。

ABI 监控工具在 AOSP 中开发,并使用 STG(或 Android 13 及更低版本中的 libabigail)来生成和比较表示形式。

本页面介绍了该工具、收集和分析 ABI 表示形式的过程,以及如何使用此类表示形式来提供内核内 ABI 的稳定性。本页面还提供了有关为 Android 内核贡献变更的信息。

流程

分析内核的 ABI 需要多个步骤,其中大多数步骤可以自动化

  1. 构建内核及其 ABI 表示形式.
  2. 分析构建版本和参考版本之间的 ABI 差异.
  3. 更新 ABI 表示形式(如果需要).
  4. 使用符号列表.

以下说明适用于您可以使用受支持工具链(例如预构建的 Clang 工具链)构建的任何内核repo 清单适用于所有 Android 通用内核分支和多个设备专用内核,它们可确保在您构建内核分发以进行分析时使用正确的工具链。

符号列表

KMI 不包含内核中的所有符号,甚至不包含 30,000 多个导出的符号中的所有符号。相反,供应商模块可以使用的符号显式列在一组符号列表文件中,这些文件公开维护在内核树的根目录中。所有符号列表文件中的所有符号的并集定义了作为稳定版维护的 KMI 符号集。符号列表文件示例为 abi_gki_aarch64_db845c,它声明了 DragonBoard 845c 所需的符号。

只有符号列表中列出的符号及其相关结构和定义才被视为 KMI 的一部分。如果所需的符号不存在,您可以发布对符号列表的更改。在新接口添加到符号列表并成为 KMI 描述的一部分后,它们将作为稳定版进行维护,并且在分支冻结后不得从符号列表中移除或修改。

每个 Android 通用内核 (ACK) KMI 内核分支都有自己的一组符号列表。没有尝试在不同的 KMI 内核分支之间提供 ABI 稳定性。例如,android12-5.10 的 KMI 完全独立于 android13-5.10 的 KMI。

ABI 工具使用 KMI 符号列表来限制必须监控哪些接口以确保稳定性。主符号列表包含 GKI 内核模块所需的符号。供应商应提交和更新其他符号列表,以确保他们依赖的接口保持 ABI 兼容性。例如,要查看 android13-5.15 的符号列表,请参阅 https://android.googlesource.com/kernel/common/+/refs/heads/android13-5.15/android

符号列表包含据报告特定供应商或设备所需的符号。工具使用的完整列表是所有 KMI 符号列表文件的并集。ABI 工具确定每个符号的详细信息,包括函数签名和嵌套数据结构。

当 KMI 冻结后,不允许对现有 KMI 接口进行任何更改;它们是稳定的。但是,只要添加的符号不影响现有 ABI 的稳定性,供应商可以随时向 KMI 添加符号。新添加的符号一旦被 KMI 符号列表引用,就会保持稳定。除非可以确认没有任何设备曾经依赖于某个符号,否则不应从内核列表中删除符号。

您可以使用如何使用符号列表中的说明,为设备生成 KMI 符号列表。许多合作伙伴为每个 ACK 提交一个符号列表,但这不是硬性要求。如果这有助于维护,您可以提交多个符号列表。

扩展 KMI

虽然 KMI 符号和相关结构保持稳定(意味着在 KMI 冻结的内核中,不能接受破坏稳定接口的更改),但 GKI 内核仍然可以扩展,以便今年晚些时候出货的设备无需在 KMI 冻结之前定义所有依赖项。要扩展 KMI,您可以为新的或现有的导出内核函数向 KMI 添加新符号,即使 KMI 已冻结。如果新的内核补丁不破坏 KMI,也可能会被接受。

关于 KMI 破坏

内核有代码,二进制文件从这些源代码构建而来。ABI 监控的内核分支包含当前 GKI ABI 的 ABI 表示形式(以 .stg 文件的形式)。在构建二进制文件(vmlinuxImage 和任何 GKI 模块)后,可以从二进制文件中提取 ABI 表示形式。对内核源文件所做的任何更改都可能影响二进制文件,进而也影响提取的 .stgAbiAnalyzer 分析器将提交的 .stg 文件与从构建工件中提取的文件进行比较,如果发现语义差异,则在 Gerrit 更改中设置 Lint-1 标签。

处理 ABI 破坏

例如,以下补丁引入了一个非常明显的 ABI 破坏

diff --git a/include/linux/mm_types.h b/include/linux/mm_types.h
index 42786e6364ef..e15f1d0f137b 100644
--- a/include/linux/mm_types.h
+++ b/include/linux/mm_types.h
@@ -657,6 +657,7 @@ struct mm_struct {
                ANDROID_KABI_RESERVE(1);
        } __randomize_layout;

+       int tickle_count;
        /*
         * The mm_cpumask needs to be at the end of mm_struct, because it
         * is dynamically sized based on nr_cpu_ids.

当您应用此补丁运行 build ABI 时,工具会退出并返回非零错误代码,并报告类似于以下的 ABI 差异

function symbol 'struct block_device* I_BDEV(struct inode*)' changed
  CRC changed from 0x8d400dbd to 0xabfc92ad

function symbol 'void* PDE_DATA(const struct inode*)' changed
  CRC changed from 0xc3c38b5c to 0x7ad96c0d

function symbol 'void __ClearPageMovable(struct page*)' changed
  CRC changed from 0xf489e5e8 to 0x92bd005e

... 4492 omitted; 4495 symbols have only CRC changes

type 'struct mm_struct' changed
  byte size changed from 992 to 1000
  member 'int tickle_count' was added
  member 'unsigned long cpu_bitmap[0]' changed
    offset changed by 64

在构建时检测到 ABI 差异

错误最常见的原因是驱动程序使用了内核中的新符号,但该符号不在任何符号列表中。

如果符号未包含在符号列表 (android/abi_gki_aarch64) 中,那么您需要首先验证它是否使用 EXPORT_SYMBOL_GPL(symbol_name) 导出,然后更新 ABI XML 表示形式和符号列表。例如,以下更改将新的增量 FS 功能添加到 android-12-5.10 分支,其中包括更新符号列表和 ABI XML 表示形式。

如果符号已导出(无论是您导出的还是以前导出的),但没有其他驱动程序正在使用它,您可能会收到类似于以下的构建错误。

Comparing the KMI and the symbol lists:
+ build/abi/compare_to_symbol_list out/$BRANCH/common/Module.symvers out/$BRANCH/common/abi_symbollist.raw
ERROR: Differences between ksymtab and symbol list detected!
Symbols missing from ksymtab:
Symbols missing from symbol list:
 - simple_strtoull

要解决此问题,请更新您的内核和 ACK 中的 KMI 符号列表(请参阅更新 ABI 表示形式)。有关在 ACK 中更新 ABI XML 和符号列表的示例,请参阅aosp/1367601

解决内核 ABI 破坏

您可以通过重构代码以不更改 ABI更新 ABI 表示形式来处理内核 ABI 破坏。使用下表确定最适合您情况的方法。

ABI Breakage Flow Chart

图 1. ABI 破坏解决方案

重构代码以避免 ABI 更改

尽一切努力避免修改现有 ABI。在许多情况下,您可以重构代码以删除影响 ABI 的更改。

  • 重构结构字段更改。如果更改修改了调试功能的 ABI,请在字段(在结构和源引用中)周围添加 #ifdef,并确保用于 #ifdefCONFIG 对于生产 defconfig 和 gki_defconfig 处于禁用状态。有关如何在不破坏 ABI 的情况下将调试配置添加到结构的示例,请参阅 此补丁集

  • 重构功能以不更改核心内核。如果需要将新功能添加到 ACK 以支持合作伙伴模块,请尝试重构更改的 ABI 部分,以避免修改内核 ABI。有关使用现有内核 ABI 添加其他功能而不更改内核 ABI 的示例,请参阅 aosp/1312213

在 Android Gerrit 上修复损坏的 ABI

如果您并非有意破坏内核 ABI,则需要使用 ABI 监控工具提供的指导进行调查。破坏的最常见原因是数据结构更改和相关的符号 CRC 更改,或者由于配置选项更改而导致上述任何情况。首先解决工具发现的问题。

您可以在本地重现 ABI 结果,请参阅构建内核及其 ABI 表示形式

关于 Lint-1 标签

如果您将更改上传到包含冻结或最终确定的 KMI 的分支,则更改必须通过 AbiAnalyzer,以确保更改不会以不兼容的方式影响稳定的 ABI。在此过程中,AbiAnalyzer 会查找在构建期间创建的 ABI 报告(执行正常构建,然后执行一些 ABI 提取和比较步骤的扩展构建)。

如果 AbiAnalyzer 找到非空报告,它将设置 Lint-1 标签,并且更改将被阻止提交,直到解决为止;直到补丁集收到 Lint+1 标签。

更新内核 ABI

如果修改 ABI 是不可避免的,那么您必须将代码更改、ABI 表示形式和符号列表应用于 ACK。要使 Lint 删除 -1 并且不破坏 GKI 兼容性,请按照以下步骤操作

  1. 将代码更改上传到 ACK.

  2. 等待接收补丁集的 Code-Review +2。

  3. 更新参考 ABI 表示形式.

  4. 合并您的代码更改和 ABI 更新更改。

将 ABI 代码更改上传到 ACK

更新 ACK ABI 取决于所做的更改类型。

  • 如果 ABI 更改与影响 CTS 或 VTS 测试的功能相关,则通常可以将更改按原样 cherry-pick 到 ACK。例如

  • 如果 ABI 更改是针对可以与 ACK 共享的功能,则可以将该更改按原样 cherry-pick 到 ACK。例如,以下更改不是 CTS 或 VTS 测试所必需的,但可以与 ACK 共享

  • 如果 ABI 更改引入了 ACK 中不需要包含的新功能,则可以使用桩程序将符号引入 ACK,如下节所述。

将桩程序用于 ACK

桩程序必须仅对于不 Benefit ACK 的核心内核更改是必要的,例如性能和功耗更改。以下列表详细介绍了 ACK 中用于 GKI 的桩程序和部分 cherry-pick 的示例。

  • 核心隔离功能桩程序 (aosp/1284493)。ACK 中的功能不是必需的,但符号需要存在于 ACK 中,以便您的模块可以使用这些符号。

  • 供应商模块的占位符符号 (aosp/1288860)。

  • 仅 ABI 的每个进程 mm 事件跟踪功能的 cherry-pick (aosp/1288454)。原始补丁被 cherry-pick 到 ACK,然后被修剪为仅包含解决 task_structmm_event_count 的 ABI 差异所需的更改。此补丁还更新了 mm_event_type 枚举以包含最终成员。

  • 需要的不仅仅是添加新的 ABI 字段的热结构 ABI 更改的部分 cherry-pick。

    • 补丁 aosp/1255544 解决了合作伙伴内核和 ACK 之间的 ABI 差异。

    • 补丁 aosp/1291018 修复了在先前补丁的 GKI 测试期间发现的功能问题。此修复包括初始化传感器参数结构,以将多个热区域注册到单个传感器。

  • CONFIG_NL80211_TESTMODE ABI 更改 (aosp/1344321)。此补丁添加了 ABI 所需的结构更改,并确保附加字段不会导致功能差异,从而使合作伙伴能够在他们的生产内核中包含 CONFIG_NL80211_TESTMODE 并仍然保持 GKI 兼容性。

在运行时强制执行 KMI

GKI 内核使用 TRIM_UNUSED_KSYMS=yUNUSED_KSYMS_WHITELIST=<所有符号列表的并集> 配置选项,这些选项将导出的符号(例如使用 EXPORT_SYMBOL_GPL() 导出的符号)限制为符号列表中列出的那些。所有其他符号都未导出,并且加载需要未导出符号的模块将被拒绝。此限制在构建时强制执行,并且缺少条目会被标记。

出于开发目的,您可以使用不包含符号修剪的 GKI 内核构建(意味着可以使用所有通常导出的符号)。要查找这些构建,请在 ci.android.com 上查找 kernel_debug_aarch64 构建。

使用模块版本控制强制执行 KMI

通用内核映像 (GKI) 内核使用模块版本控制 (CONFIG_MODVERSIONS) 作为在运行时强制执行 KMI 兼容性的附加措施。如果模块的预期 KMI 与 vmlinux KMI 不匹配,模块版本控制可能会在模块加载时导致循环冗余校验 (CRC) 不匹配失败。例如,以下是在模块加载时由于符号 module_layout() 的 CRC 不匹配而发生的典型故障

init: Loading module /lib/modules/kernel/.../XXX.ko with args ""
XXX: disagrees about version of symbol module_layout
init: Failed to insmod '/lib/modules/kernel/.../XXX.ko' with args ''

模块版本控制的用途

模块版本控制对于以下原因很有用

  • 模块版本控制可以捕获数据结构可见性中的更改。如果模块更改了不透明的数据结构,即不属于 KMI 的数据结构,则它们会在将来对结构进行更改后中断。

    例如,考虑 struct device 中的 fwnode 字段。此字段对于模块必须是不透明的,以便它们无法更改 device->fw_node 的字段或对其大小做出假设。

    但是,如果模块包含 <linux/fwnode.h>(直接或间接),则 struct device 中的 fwnode 字段对其不再是不透明的。然后,模块可以更改 device->fwnode->devdevice->fwnode->ops。由于以下几个原因,这种情况是有问题的,如下所述

    • 它可能会破坏核心内核代码对其内部数据结构所做的假设。

    • 如果将来的内核更新更改了 struct fwnode_handlefwnode 的数据类型),则模块将不再与新内核一起工作。此外,stgdiff 不会显示任何差异,因为模块正在通过直接操作内部数据结构(仅通过检查二进制表示形式无法捕获的方式)来破坏 KMI。

  • 当当前模块在稍后日期被不兼容的新内核加载时,该模块被视为 KMI 不兼容。模块版本控制添加了运行时检查,以避免意外加载与内核不 KMI 兼容的模块。此检查可防止难以调试的运行时问题和内核崩溃,这些问题和崩溃可能是由 KMI 中未检测到的不兼容性引起的。

启用模块版本控制可以防止所有这些问题。

在不启动设备的情况下检查 CRC 不匹配

stgdiff 比较并报告内核之间的 CRC 不匹配以及其他 ABI 差异。

此外,启用 CONFIG_MODVERSIONS 的完整内核构建会在正常构建过程中生成 Module.symvers 文件。此文件对于内核 (vmlinux) 和模块导出的每个符号都有一行。每行都包含 CRC 值、符号名称、符号命名空间、导出符号的 vmlinux 或模块名称以及导出类型(例如,EXPORT_SYMBOLEXPORT_SYMBOL_GPL)。

您可以比较 GKI 构建和您的构建之间的 Module.symvers 文件,以检查 vmlinux 导出的符号中是否存在任何 CRC 差异。如果 vmlinux 导出的任何符号中存在 CRC 值差异,并且该符号被您在设备中加载的模块之一使用,则模块不会加载。

如果您没有所有构建工件,但确实有 GKI 内核和您的内核的 vmlinux 文件,则可以通过在两个内核上运行以下命令并比较输出来比较特定符号的 CRC 值

nm <path to vmlinux>/vmlinux | grep __crc_<symbol name>

例如,以下命令检查 module_layout 符号的 CRC 值

nm vmlinux | grep __crc_module_layout
0000000008663742 A __crc_module_layout

解决 CRC 不匹配

使用以下步骤解决加载模块时出现的 CRC 不匹配

  1. 使用 --kbuild_symtypes 选项构建 GKI 内核和您的设备内核,如下命令所示

    tools/bazel run --kbuild_symtypes //common:kernel_aarch64_dist

    此命令为每个 .o 文件生成一个 .symtypes 文件。有关详细信息,请参阅 Kleaf 中的 KBUILD_SYMTYPES

    对于 Android 13 及更低版本,通过将 KBUILD_SYMTYPES=1 前置到您用于构建内核的命令来构建 GKI 内核和您的设备内核,如下命令所示

    KBUILD_SYMTYPES=1 BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh

    当使用 build_abi.sh 时,KBUILD_SYMTYPES=1 标志已隐式设置。

  2. 使用以下命令查找导出具有 CRC 不匹配的符号的 .c 文件

    cd common && git grep EXPORT_SYMBOL.*module_layout
    kernel/module.c:EXPORT_SYMBOL(module_layout);
  3. .c 文件在 GKI 和您的设备内核构建工件中都有对应的 .symtypes 文件。使用以下命令找到 .c 文件

    cd out/$BRANCH/common && ls -1 kernel/module.*
    kernel/module.o
    kernel/module.o.symversions
    kernel/module.symtypes

    以下是 .c 文件的特征

    • .c 文件的格式是每个符号一行(可能很长)。

    • 行首的 [s|u|e|etc]# 表示符号的数据类型为 [struct|union|enum|etc]。例如

      t#bool typedef _Bool bool
      
    • 行首缺少 # 前缀表示该符号是一个函数。例如

      find_module s#module * find_module ( const char * )
      
  4. 比较这两个文件并修复所有差异。

情况 1:数据类型可见性导致的差异

如果一个内核使符号或数据类型对模块不透明,而另一个内核不这样做,则两个内核的 .symtypes 文件之间会出现这种差异。其中一个内核的 .symtypes 文件对于某个符号具有 UNKNOWN,而另一个内核的 .symtypes 文件具有符号或数据类型的展开视图。

例如,在您的内核中将以下行添加到 include/linux/device.h 文件会导致 CRC 不匹配,其中一个用于 module_layout()

 #include <linux/fwnode.h>

比较该符号的 module.symtypes,会显示以下差异

 $ diff -u <GKI>/kernel/module.symtypes <your kernel>/kernel/module.symtypes
  --- <GKI>/kernel/module.symtypes
  +++ <your kernel>/kernel/module.symtypes
  @@ -334,12 +334,15 @@
  ...
  -s#fwnode_handle struct fwnode_handle { UNKNOWN }
  +s#fwnode_reference_args struct fwnode_reference_args { s#fwnode_handle * fwnode ; unsigned int nargs ; t#u64 args [ 8 ] ; }
  ...

如果您的内核的值为 UNKNOWN,并且 GKI 内核具有符号的展开视图(非常不可能),则将最新的 Android Common Kernel 合并到您的内核中,以便您使用最新的 GKI 内核基础。

在大多数情况下,GKI 内核的值为 UNKNOWN,但由于对您的内核所做的更改,您的内核具有符号的内部详细信息。这是因为您内核中的某个文件添加了 GKI 内核中不存在的 #include

通常,解决方法只是从 genksyms 中隐藏新的 #include

#ifndef __GENKSYMS__
#include <linux/fwnode.h>
#endif

否则,要识别导致差异的 #include,请按照以下步骤操作

  1. 打开定义具有此差异的符号或数据类型的头文件。例如,编辑 include/linux/fwnode.h 以获取 struct fwnode_handle

  2. 在头文件的顶部添加以下代码

    #ifdef CRC_CATCH
    #error "Included from here"
    #endif
    
  3. 在具有 CRC 不匹配的模块的 .c 文件中,在任何 #include 行之前添加以下内容作为第一行。

    #define CRC_CATCH 1
    
  4. 编译您的模块。生成的构建时错误显示了导致此 CRC 不匹配的头文件 #include 链。例如

    In file included from .../drivers/clk/XXX.c:16:`
    In file included from .../include/linux/of_device.h:5:
    In file included from .../include/linux/cpu.h:17:
    In file included from .../include/linux/node.h:18:
    .../include/linux/device.h:16:2: error: "Included from here"
    #error "Included from here"
    

    #include 链中的一个链接是由于在您的内核中所做的更改,该更改在 GKI 内核中缺失。

  5. 识别更改,在您的内核中还原它,或将其上传到 ACK 并将其合并

情况 2:数据类型更改导致的差异

如果符号或数据类型的 CRC 不匹配不是由于可见性差异引起的,那么它是由数据类型本身的实际更改(添加、删除或更改)引起的。

例如,在您的内核中进行以下更改会导致多个 CRC 不匹配,因为许多符号会间接受到此类更改的影响

diff --git a/include/linux/iommu.h b/include/linux/iommu.h
  --- a/include/linux/iommu.h
  +++ b/include/linux/iommu.h
  @@ -259,7 +259,7 @@ struct iommu_ops {
     void (*iotlb_sync)(struct iommu_domain *domain);
     phys_addr_t (*iova_to_phys)(struct iommu_domain *domain, dma_addr_t iova);
     phys_addr_t (*iova_to_phys_hard)(struct iommu_domain *domain,
  -        dma_addr_t iova);
  +        dma_addr_t iova, unsigned long trans_flag);
     int (*add_device)(struct device *dev);
     void (*remove_device)(struct device *dev);
     struct iommu_group *(*device_group)(struct device *dev);

一个 CRC 不匹配是针对 devm_of_platform_populate()

如果您比较该符号的 .symtypes 文件,它可能看起来像这样

 $ diff -u <GKI>/drivers/of/platform.symtypes <your kernel>/drivers/of/platform.symtypes
  --- <GKI>/drivers/of/platform.symtypes
  +++ <your kernel>/drivers/of/platform.symtypes
  @@ -399,7 +399,7 @@
  ...
  -s#iommu_ops struct iommu_ops { ... ; t#phy
  s_addr_t ( * iova_to_phys_hard ) ( s#iommu_domain * , t#dma_addr_t ) ; int
    ( * add_device ) ( s#device * ) ; ...
  +s#iommu_ops struct iommu_ops { ... ; t#phy
  s_addr_t ( * iova_to_phys_hard ) ( s#iommu_domain * , t#dma_addr_t , unsigned long ) ; int ( * add_device ) ( s#device * ) ; ...

要识别更改的类型,请按照以下步骤操作

  1. 在源代码中(通常在 .h 文件中)找到符号的定义。

    • 对于您的内核和 GKI 内核之间的符号差异,通过运行以下命令找到提交
    git blame
    • 对于已删除的符号(其中一个符号在一个树中被删除,并且您也想在另一个树中删除它),您需要找到删除该行的更改。在删除了该行的树上使用以下命令
    git log -S "copy paste of deleted line/word" -- <file where it was deleted>
  2. 查看返回的提交列表以找到更改或删除。第一个提交可能是您正在搜索的提交。如果不是,请浏览列表直到找到提交。

  3. 在您确定更改后,要么在您的内核中还原它,要么将其上传到 ACK 并将其合并