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 宏來簡化程式碼。