DWARF 模块版本控制

简介

当启用 CONFIG_MODVERSIONS 时,模块的符号版本通常使用 genksyms 工具从预处理的源代码计算得出。然而,这与诸如 Rust 之类的语言不兼容,在这些语言中,源代码没有关于结果 ABI 的足够信息。选择 CONFIG_GENDWARFKSYMS (和 CONFIG_DEBUG_INFO) 时,将使用 gendwarfksyms 从 DWARF 调试信息计算符号版本,其中包含关于最终模块 ABI 的必要细节。

用法

gendwarfksyms 在命令行上接受对象文件列表,并在标准输入中接受符号名称列表(每行一个)

Usage: gendwarfksyms [options] elf-object-file ... < symbol-list

Options:
  -d, --debug          Print debugging information
      --dump-dies      Dump DWARF DIE contents
      --dump-die-map   Print debugging information about die_map changes
      --dump-types     Dump type strings
      --dump-versions  Dump expanded type strings used for symbol versions
  -s, --stable         Support kABI stability features
  -T, --symtypes file  Write a symtypes file
  -h, --help           Print this message

类型信息可用性

虽然符号通常在定义它们的同一个翻译单元 (TU) 中导出,但一个 TU 导出外部符号也完全可以。例如,在计算独立汇编代码中的导出符号的版本时,就是这样做的。

为了确保编译器在实际导出符号的 TU 中发出必要的 DWARF 类型信息,gendwarfksyms 使用以下宏在 EXPORT_SYMBOL() 宏中添加一个指向导出符号的指针

#define __GENDWARFKSYMS_EXPORT(sym)                             \
        static typeof(sym) *__gendwarfksyms_ptr_##sym __used    \
                __section(".discard.gendwarfksyms") = &sym;

当在 DWARF 中找到符号指针时,即使符号在其他地方定义,gendwarfksyms 也可以使用其类型来计算符号版本。符号指针的名称预计以 __gendwarfksyms_ptr_ 开头,后跟导出符号的名称。

Symtypes 输出格式

与 genksyms 类似,gendwarfksyms 支持为每个处理过的对象编写一个 symtypes 文件,其中包含导出符号的类型以及用于计算符号版本的每个引用的类型。当试图确定到底是什么导致了构建之间的符号版本发生变化时,这些文件可能很有用。要在内核构建期间生成 symtypes 文件,请设置 KBUILD_SYMTYPES=1

为了匹配现有格式,每行的第一列包含类型引用或符号名称。类型引用有一个字母前缀,后跟 “#” 和类型的名称。支持四种引用类型

e#<type> = enum
s#<type> = struct
t#<type> = typedef
u#<type> = union

名称中带有空格的类型用单引号括起来,例如

s#'core::result::Result<u8, core::num::error::ParseIntError>'

行的其余部分包含一个类型字符串。与生成 C 风格类型字符串的 genksyms 不同,gendwarfksyms 使用与 --dump-dies 生成的相同的简单解析的 DWARF 格式,但使用类型引用而不是完全展开的字符串。

维护稳定的 kABI

由于 LTS 更新或反向移植,发行版维护者通常需要能够对内核数据结构进行 ABI 兼容的更改。使用传统的 #ifndef __GENKSYMS__ 从符号版本控制中隐藏这些更改在处理对象文件时不起作用。为了支持这种用例,gendwarfksyms 提供了 kABI 稳定性功能,旨在隐藏在计算版本时不会影响 ABI 的更改。这些功能都受到 --stable 命令行标志的限制,并且未在主线内核中使用。要在内核构建期间使用稳定功能,请设置 KBUILD_GENDWARFKSYMS_STABLE=1

scripts/gendwarfksyms/examples 目录中提供了使用这些功能的示例,包括用于源代码注释的辅助宏。请注意,由于这些功能仅用于转换符号版本控制的输入,因此用户有责任确保他们的更改实际上不会破坏 ABI。

kABI 规则

kABI 规则允许发行版微调 gendwarfksyms 输出的某些部分,从而控制符号版本的计算方式。这些规则在对象文件的 .discard.gendwarfksyms.kabi_rules 部分中定义,由具有以下结构的简单空终止字符串组成

version\0type\0target\0value\0

这个字符串序列根据需要重复多次以表达所有规则。字段如下

  • version:确保结构未来更改的向后兼容性。目前预计为 “1”。

  • type:指示应用的规则类型。

  • target:指定规则的目标,通常是 DWARF 调试信息条目 (DIE) 的完全限定名称。

  • value:提供特定于规则的数据。

例如,以下辅助宏可用于在源代码中指定规则

#define ___KABI_RULE(hint, target, value)                           \
        static const char __PASTE(__gendwarfksyms_rule_,             \
                                  __COUNTER__)[] __used __aligned(1) \
                __section(".discard.gendwarfksyms.kabi_rules") =     \
                        "1\0" #hint "\0" target "\0" value

#define __KABI_RULE(hint, target, value) \
        ___KABI_RULE(hint, #target, #value)

目前,仅支持本节中讨论的规则,但该格式具有足够的可扩展性,允许根据需要添加更多规则。

管理定义可见性

当其他包含项被拉入翻译单元时,声明可以更改为完整定义。即使 ABI 保持不变,这也更改了引用该类型的任何符号的版本。由于可能无法在不破坏构建的情况下删除包含项,因此可以使用 declonly 规则将类型指定为仅声明,即使调试信息包含完整定义也是如此。

规则字段预计如下

  • type:“declonly”

  • target:目标数据结构的完全限定名称(如 --dump-dies 输出中所示)。

  • value:此字段将被忽略。

使用 __KABI_RULE 宏,此规则可以定义为

#define KABI_DECLONLY(fqn) __KABI_RULE(declonly, fqn, )

用法示例

struct s {
        /* definition */
};

KABI_DECLONLY(s);

添加枚举器

对于枚举,所有枚举器及其值都包含在计算符号版本中,如果以后我们需要添加更多枚举器而不更改符号版本,这将成为一个问题。enumerator_ignore 规则允许我们从输入中隐藏命名的枚举器。

规则字段预计如下

  • type:“enumerator_ignore”

  • target:目标枚举的完全限定名称(如 --dump-dies 输出中所示)和枚举器字段的名称,用空格分隔。

  • value:此字段将被忽略。

使用 __KABI_RULE 宏,此规则可以定义为

#define KABI_ENUMERATOR_IGNORE(fqn, field) \
        __KABI_RULE(enumerator_ignore, fqn field, )

用法示例

enum e {
        A, B, C, D,
};

KABI_ENUMERATOR_IGNORE(e, B);
KABI_ENUMERATOR_IGNORE(e, C);

如果枚举还包含一个结束标记,并且必须在中间添加新值,那么在计算版本时,我们可能需要对最后一个枚举器使用旧值。enumerator_value 规则允许我们覆盖用于版本计算的枚举器的值

  • type:“enumerator_value”

  • target:目标枚举的完全限定名称(如 --dump-dies 输出中所示)和枚举器字段的名称,用空格分隔。

  • value:用于该字段的整数值。

使用 __KABI_RULE 宏,此规则可以定义为

#define KABI_ENUMERATOR_VALUE(fqn, field, value) \
        __KABI_RULE(enumerator_value, fqn field, value)

用法示例

enum e {
        A, B, C, LAST,
};

KABI_ENUMERATOR_IGNORE(e, C);
KABI_ENUMERATOR_VALUE(e, LAST, 2);

管理结构大小更改

如果数据结构的分配由核心内核处理,并且模块只需要访问其某些成员,则该数据结构对于模块来说可以是部分不透明的。在这种情况下,只要原始成员的布局保持不变,就可以在不破坏 ABI 的情况下将新成员附加到结构中。

要附加新成员,我们可以按照 隐藏成员部分中的描述从符号版本控制中隐藏它们,但我们无法隐藏结构大小的增加。byte_size 规则允许我们覆盖用于符号版本控制的结构大小。

规则字段预计如下

  • type:“byte_size”

  • target:目标数据结构的完全限定名称(如 --dump-dies 输出中所示)。

  • value:一个正十进制数,表示结构大小(以字节为单位)。

使用 __KABI_RULE 宏,此规则可以定义为

#define KABI_BYTE_SIZE(fqn, value) \
        __KABI_RULE(byte_size, fqn, value)

用法示例

struct s {
        /* Unchanged original members */
        unsigned long a;
        void *p;

        /* Appended new members */
        KABI_IGNORE(0, unsigned long n);
};

KABI_BYTE_SIZE(s, 16);

覆盖类型字符串

在极少数情况下,发行版必须对无意中包含在已发布 ABI 中的其他不透明数据结构进行重大更改时,使用更有针对性的 kABI 规则保持符号版本稳定可能会变得乏味。type_string 规则允许我们覆盖类型或符号的完整类型字符串,甚至为不再存在于内核中的版本控制添加类型。

规则字段预计如下

  • type:“type_string”

  • target:目标数据结构(如 --dump-dies 输出中所示)或符号的完全限定名称。

  • value:要使用的有效类型字符串(如 --symtypes 输出中所示),而不是实际类型。

使用 __KABI_RULE 宏,此规则可以定义为

#define KABI_TYPE_STRING(type, str) \
        ___KABI_RULE("type_string", type, str)

用法示例

/* Override type for a structure */
KABI_TYPE_STRING("s#s",
        "structure_type s { "
                "member base_type int byte_size(4) "
                        "encoding(5) n "
                "data_member_location(0) "
        "} byte_size(8)");

/* Override type for a symbol */
KABI_TYPE_STRING("my_symbol", "variable s#s");

仅当使用其他方法无法合理地实现维护稳定的符号版本时,才应将 type_string 规则用作最后的手段。覆盖类型字符串会增加实际 ABI 中断未被注意到的风险,因为它会隐藏对该类型的所有更改。

添加结构成员

也许最常见的 ABI 兼容更改是将成员添加到内核数据结构。当预计对结构进行更改时,发行版维护者可以先发制人地在结构中保留空间,并在以后使用它而不会破坏 ABI。如果需要更改没有保留空间的数据结构,则可以改为使用现有的对齐孔。虽然可以为这些类型的更改添加 kABI 规则,但通常使用联合是一种更自然的方法。本节介绍 gendwarfksyms 对在数据结构中使用保留空间以及在计算符号版本时隐藏不更改 ABI 的成员的支持。

保留空间和替换成员

通常通过将整数类型或数组附加到数据结构的末尾来保留空间以供以后使用,但可以使用任何类型。每个保留成员都需要一个唯一的名称,但由于在保留空间时通常不知道实际用途,为方便起见,在计算符号版本时会省略以 __kabi_ 开头的名称

struct s {
        long a;
        long __kabi_reserved_0; /* reserved for future use */
};

可以通过将成员包装在联合中来使用保留空间,该联合包括原始类型和替换成员

struct s {
        long a;
        union {
                long __kabi_reserved_0; /* original type */
                struct b b; /* replaced field */
        };
};

如果在保留空间时使用了 __kabi_ 命名方案,则联合的第一个成员的名称必须以 __kabi_reserved 开头。这确保了在计算版本时使用原始类型,但再次省略了名称。联合的其余部分将被忽略。

如果我们替换的成员不遵循此命名约定,我们还需要保留原始名称以避免更改版本,我们可以通过将第一个联合成员的名称更改为以 __kabi_renamed 开头,后跟原始名称来完成。

这些示例包括 KABI_(RESERVE|USE|REPLACE)* 宏,这些宏有助于简化该过程,并确保替换成员正确对齐,并且其大小不会超过保留空间。

隐藏成员

预测哪些结构在支持时限内需要更改并非总是可能的,在这种情况下,可能不得不将新成员放入现有的对齐孔中

struct s {
        int a;
        /* a 4-byte alignment hole */
        unsigned long b;
};

虽然这不会更改数据结构的大小,但需要能够从符号版本控制中隐藏添加的成员。与保留字段类似,这可以通过将添加的成员包装到联合中来实现,其中一个字段的名称以 __kabi_ignored 开头

struct s {
        int a;
        union {
                char __kabi_ignored_0;
                int n;
        };
        unsigned long b;
};

使用 --stable 时,两个版本都会生成相同的符号版本。这些示例包括一个 KABI_IGNORE 宏来简化代码。