Android 内核 ABI 监控

您可以使用 Android 11 及更高版本中提供的应用二进制接口 (ABI) 监控工具来稳定 Android 内核的内核内 ABI。该工具可从现有内核二进制文件(vmlinux+ 模块)收集 ABI 表示法并进行比较。这些 ABI 表示法是 .stg 文件和符号列表。表示法提供视图所在的接口称为“内核模块接口”(KMI)。您可以使用该工具来跟踪 KMI 的变化并弱化相关影响。

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

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

流程

内核的 ABI 分析需要执行多个步骤,其中大多数步骤都可以实现自动化:

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

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

符号列表

KMI 并非包含内核中的所有符号,甚至并非包含完整的 3 万多个导出符号。实际上,可被供应商模块使用的符号都明确列在一组符号列表文件中,这些文件在内核树的根目录中公开维护。所有符号列表文件中所有符号的并集定义了一组作为稳定版维护的 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 接口进行任何更改;它们是稳定的。不过,供应商可以随时向 KMI 中添加符号,只要添加符号不影响现有 ABI 的稳定性即可。被 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 表示法。

  • 如需查看功能更改示例,请参阅 aosp/1345659
  • 如需查看符号列表示例,请参阅 aosp/1346742
  • 如需查看 ABI XML 更改示例,请参阅 aosp/1349377

如果该符号已导出(由您导出或之前已导出),但没有其他驱动程序正在使用该符号,您可能会收到类似如下所示的构建错误。

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 合规性破坏问题流程图

图 1. 解决 ABI 合规性破坏问题

重构代码以避免更改 ABI

应尽可能避免修改现有 ABI。在许多情况下,您可以重构代码以移除会影响 ABI 的更改。

  • 重构结构体字段更改。如果变更会修改调试功能的 ABI,请在字段(在结构体和源代码引用中)周围添加一个 #ifdef;并确保已针对正式版 defconfig 和 gki_defconfig,停用了用于 #ifdefCONFIG。如需查看相关示例,了解如何在不破坏 ABI 的情况下将调试配置添加到结构体中,请参阅此补丁集

  • 重构功能以避免更改核心内核。如果需要向 ACK 中添加新功能以支持合作伙伴模块,请尝试重构更改的 ABI 部分,以避免修改内核 ABI。如需查看相关示例,了解如何使用现有内核 ABI 在不更改内核 ABI 的情况下添加其他功能,请参阅 aosp/1312213

修复 Android Gerrit 上的 ABI 合规性破坏问题

如果您没有故意破坏内核 ABI,则需要按照 ABI 监控工具提供的指南调查原因。最常见的合规性破坏原因包括:更改了数据结构和关联的符号 CRC,或者因更改了配置选项导致了前面提及的任意问题。首先要解决该工具发现的问题。

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

lint-1 标签简介

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

如果 AbiAnalyzer 发现非空报告,它会设置 lint-1 标签,并禁止提交该更改,直到问题解决;或直到补丁集收到 lint+1 标签为止。

更新内核 ABI

如果不可避免地需要修改 ABI,您必须将代码更改、ABI 表示和符号列表应用于 ACK。为了使 lint 去掉 -1 并且不会破坏 GKI 兼容性,请执行以下步骤:

  1. 将代码更改上传到 ACK

  2. 等待收到补丁集的代码审核 +2。

  3. 更新参考 ABI 表示

  4. 合并代码更改及 ABI 更新更改。

将 ABI 代码更改上传到 ACK

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

  • 如果 ABI 更改与影响 CTS 或 VTS 测试的功能相关,那么系统通常可以按原样择优挑选更改并应用于 ACK。例如:

  • 如果 ABI 更改是针对可与 ACK 共享的功能,则可以按原样择优挑选更改并应用于 ACK。例如,CTS 或 VTS 测试不需要进行以下更改,但这些更改可以与 ACK 共享:

  • 如果 ABI 更改引入了一项无需包含在 ACK 中的新功能,您可以使用桩将这些符号引入 ACK,如下一部分中所述。

针对 ACK 使用桩

仅当核心内核更改对 ACK 没有益处时(例如性能和电源更改),才必须要使用桩。以下列表详细介绍了针对 GKI 的 ACK 中的桩和部分择优挑选的示例。

  • 核心隔离功能桩 (aosp/1284493)。 ACK 中的功能不是必需的,但 ACK 中必须包含这些符号,您的模块才能使用这些符号。

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

  • 仅针对 ABI 择优挑选按进程 mm 事件跟踪功能补丁 (aosp/1288454)。系统会针对 ACK 择优挑选原始补丁,然后删减补丁,使其仅包含解决 task_structmm_event_count 的 ABI 差异所需的必要更改。此补丁还会更新 mm_event_type 枚举,以包含最终成员。

  • 部分择优挑选热结构体 ABI 更改(不止需要添加新的 ABI 字段)。

    • 补丁 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=<union of all symbol lists> 配置选项,这些选项可将导出的符号(例如使用 EXPORT_SYMBOL_GPL() 导出的符号)限制为符号列表中列出的符号。所有其他符号均不会被导出,如果加载的模块需要未导出的符号,该加载操作会被拒绝。系统会在构建时强制执行此限制,并标记缺失的条目。

出于开发目的,您可以使用未采取符号删减(即,所有通常会导出的符号都可使用)的 GKI 内核 build。如需找到这些 build,请在 ci.android.com 上查找 kernel_debug_aarch64 build。

使用模块版本控制强制执行 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 的完整内核 build 还会在正常的构建流程中生成 Module.symvers 文件。对于内核 (vmlinux) 和模块导出的每一个符号,此文件中都会有一行内容与之对应。每一行都包含以下内容:CRC 值、符号名称、符号命名空间、导出该符号的 vmlinux 或模块的名称以及导出类型(例如 EXPORT_SYMBOLEXPORT_SYMBOL_GPL)。

您可以比较 GKI build 和您的 build 所生成的 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 通用内核合并到您的内核中,以便使用最新的 GKI 内核基础。

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

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

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

此外,如要确定导致差异的 #include,请按以下步骤操作:

  1. 打开定义具有此差异的符号或数据类型的头文件。例如,修改 struct fwnode_handleinclude/linux/fwnode.h

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

    #ifdef CRC_CATCH
    #error "Included from here"
    #endif
    
  3. 在模块的 .c 文件(具有 CRC 不一致问题的文件)中,在任何 #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);

devm_of_platform_populate() 有一个 CRC 不一致问题。

如果您比较该符号的 .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 并合并