Skip to content

[lld][DebugInfo/BTF] Add BTF section merging and deduplication#183915

Open
caniko wants to merge 1 commit into
llvm:mainfrom
caniko:btf-linker
Open

[lld][DebugInfo/BTF] Add BTF section merging and deduplication#183915
caniko wants to merge 1 commit into
llvm:mainfrom
caniko:btf-linker

Conversation

@caniko

@caniko caniko commented Feb 28, 2026

Copy link
Copy Markdown

Summary

This adds --btf-merge support to lld's ELF linker, allowing it to merge and deduplicate .BTF sections from multiple input object files into a single output section.

Companion to #183929 which adds -gbtf for target-independent BTF emission. Together they enable native BTF for Linux kernel builds with Clang, including LTO + Rust configurations.

Motivation

The Linux kernel requires BTF (DEBUG_INFO_BTF=y) for the BPF subsystem. Today this is generated by pahole, which converts DWARF to BTF as a post-link step. This breaks in two specific ways:

  1. LTO + Rust: With LTO, the linker merges compilation units. Pahole then encounters Rust DWARF constructs it doesn't understand (e.g. Rust enums, trait objects). The PAHOLE_HAS_LANG_EXCLUDE flag filters out Rust CUs, but with LTO the linker can interleave C and Rust code within the same CU, so filtering by language no longer works.

  2. Performance: Pahole's DWARF→BTF conversion is a sequential post-link pass over the entire binary's debug info. For large kernels this adds significant build time.

This produces the kernel's current Kconfig constraint:

config RUST
    depends on !DEBUG_INFO_BTF || (PAHOLE_HAS_LANG_EXCLUDE && !LTO)

The fix is to skip DWARF→BTF entirely: each compiler emits .BTF directly (GCC already does via -gbtf, Clang support in #183929), and the linker merges/deduplicates them — the same way it already handles .gdb_index. This lifts the Kconfig restriction.

What's here

New LLVM library code (llvm/lib/DebugInfo/BTF/):

  • BTFBuilder — mutable in-memory BTF container that can parse, merge, and serialize .BTF sections
  • BTF::dedup() — 5-pass deduplication algorithm ported from libbpf's btf_dedup():
    1. String dedup (hash-based)
    2. Primitive type dedup (INT, ENUM, FWD, FLOAT)
    3. Composite type dedup (STRUCT/UNION with DFS equivalence + cycle handling)
    4. Reference type dedup (PTR, TYPEDEF, CONST, etc.)
    5. Compaction + global ID remapping

lld integration (lld/ELF/):

  • BtfSection<ELFT> synthetic section that collects .BTF inputs, merges via BTFBuilder, deduplicates, and emits the result
  • --btf-merge / --no-btf-merge flags (off by default)
  • Disabled under -r (relocatable linking)

Tests (86 unit tests + lld LIT tests):

  • 21 BTFBuilder unit tests: construction, roundtrip via BTFParser, merge with ID/string remapping for all type kinds, error paths (bad magic/version/bounds/truncation with rollback), mutable byte access
  • 35 BTFDedup unit tests: all 19 BTF type kinds, duplicate/non-duplicate cases, self-referential structs, mutual recursion, diamond dependencies, stress test (100→1), DECL_TAG component_idx differentiation, struct mismatch cases (offsets/names/member count), full roundtrip after dedup
  • lld LIT tests: merge, dedup, empty/single input, -r, --gc-sections, COMDAT isLive(), big-endian

Algorithm notes

The dedup algorithm follows @anakryiko's design from libbpf (writeup). The composite type pass uses a hypothesis map for DFS graph equivalence, which correctly handles recursive types (linked lists, trees, etc.). I chose to put the library code in llvm/lib/DebugInfo/BTF/ rather than inline in lld so other tools (llvm-objcopy, llvm-readobj) can reuse it.

I'd appreciate review from @MaskRay on the lld integration side — I followed the DebugNamesSection pattern. And @anakryiko / @yonghong-song for the dedup algorithm correctness, since you wrote the original.

cc @4ast

@github-actions

Copy link
Copy Markdown

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot

llvmbot commented Feb 28, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-debuginfo

@llvm/pr-subscribers-lld

Author: Can H. Tartanoglu (caniko)

Changes

Summary

This adds --btf-merge support to lld's ELF linker, allowing it to merge and deduplicate .BTF sections from multiple input object files into a single output section. This is a prerequisite for enabling LTO + Rust + BTF in the Linux kernel — currently blocked because pahole's DWARF→BTF conversion breaks when LTO merges C and Rust compilation units.

The approach: each compiler emits .BTF directly (GCC already does via -gbtf, Clang support is in progress), and the linker merges them, the same way it already handles .eh_frame or .gdb_index.

What's here

New LLVM library code (llvm/lib/DebugInfo/BTF/):

  • BTFBuilder — mutable in-memory BTF container that can parse, merge, and serialize .BTF sections
  • btfDedup() — 5-pass deduplication algorithm ported from libbpf's btf_dedup():
    1. String dedup (hash-based)
    2. Primitive type dedup (INT, ENUM, FWD, FLOAT)
    3. Composite type dedup (STRUCT/UNION with DFS equivalence + cycle handling)
    4. Reference type dedup (PTR, TYPEDEF, CONST, etc.)
    5. Compaction + global ID remapping

lld integration (lld/ELF/):

  • BtfSection synthetic section that collects .BTF inputs, merges via BTFBuilder, deduplicates, and emits the result
  • --btf-merge / --no-btf-merge flags (off by default)

Tests:

  • Unit tests for BTFBuilder and btfDedup covering roundtrip, all type kinds, self-referential structs, forward declaration resolution
  • LIT tests for the lld flag and basic merge/dedup behavior

Motivation

The kernel's Kconfig currently has:

config RUST
    depends on !DEBUG_INFO_BTF || (PAHOLE_HAS_LANG_EXCLUDE &amp;&amp; !LTO)

With compiler-generated BTF + linker-side merging, this restriction goes away. GNU ld already has this via libctf; this brings lld to parity, which matters because lld is what the kernel uses for Clang LTO builds.

Algorithm notes

The dedup algorithm follows @anakryiko's design from libbpf (writeup). The composite type pass uses a hypothesis map for DFS graph equivalence, which correctly handles recursive types (linked lists, trees, etc.). I chose to put the library code in llvm/lib/DebugInfo/BTF/ rather than inline in lld so other tools (llvm-objcopy, llvm-readobj) can reuse it.

I'd appreciate review from @MaskRay on the lld integration side — I followed the GdbIndexSection pattern as closely as possible. And @anakryiko / @yonghong-song for the dedup algorithm correctness, since you wrote the original.

cc @4ast


Patch is 83.50 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/183915.diff

18 Files Affected:

  • (modified) lld/ELF/CMakeLists.txt (+1)
  • (modified) lld/ELF/Config.h (+3)
  • (modified) lld/ELF/Driver.cpp (+1)
  • (modified) lld/ELF/Options.td (+4)
  • (modified) lld/ELF/SyntheticSections.cpp (+54)
  • (modified) lld/ELF/SyntheticSections.h (+16)
  • (added) lld/test/ELF/Inputs/btf-merge-int.s (+26)
  • (added) lld/test/ELF/Inputs/btf-merge-long.s (+26)
  • (added) lld/test/ELF/btf-merge-basic.s (+55)
  • (added) lld/test/ELF/btf-merge-dedup.s (+45)
  • (added) llvm/include/llvm/DebugInfo/BTF/BTFBuilder.h (+96)
  • (added) llvm/include/llvm/DebugInfo/BTF/BTFDedup.h (+46)
  • (added) llvm/lib/DebugInfo/BTF/BTFBuilder.cpp (+416)
  • (added) llvm/lib/DebugInfo/BTF/BTFDedup.cpp (+777)
  • (modified) llvm/lib/DebugInfo/BTF/CMakeLists.txt (+2)
  • (added) llvm/unittests/DebugInfo/BTF/BTFBuilderTest.cpp (+426)
  • (added) llvm/unittests/DebugInfo/BTF/BTFDedupTest.cpp (+399)
  • (modified) llvm/unittests/DebugInfo/BTF/CMakeLists.txt (+2)
diff --git a/lld/ELF/CMakeLists.txt b/lld/ELF/CMakeLists.txt
index e22897c2789d8..c2a9265f5281b 100644
--- a/lld/ELF/CMakeLists.txt
+++ b/lld/ELF/CMakeLists.txt
@@ -66,6 +66,7 @@ add_lld_library(lldELF
   BinaryFormat
   BitWriter
   Core
+  DebugInfoBTF
   DebugInfoDWARF
   Demangle
   DTLTO
diff --git a/lld/ELF/Config.h b/lld/ELF/Config.h
index 237df52194210..d9e279a5dbe03 100644
--- a/lld/ELF/Config.h
+++ b/lld/ELF/Config.h
@@ -56,6 +56,7 @@ struct Partition;
 struct PhdrEntry;
 
 class BssSection;
+class BtfSection;
 class GdbIndexSection;
 class GotPltSection;
 class GotSection;
@@ -332,6 +333,7 @@ struct Config {
   bool fixCortexA8;
   bool formatBinary = false;
   bool fortranCommon;
+  bool btfMerge;
   bool gcSections;
   bool gdbIndex;
   bool gnuHash = false;
@@ -569,6 +571,7 @@ struct InStruct {
   std::unique_ptr<SyntheticSection> riscvAttributes;
   std::unique_ptr<BssSection> bss;
   std::unique_ptr<BssSection> bssRelRo;
+  std::unique_ptr<BtfSection> btfSection;
   std::unique_ptr<SyntheticSection> gnuProperty;
   std::unique_ptr<SyntheticSection> gnuStack;
   std::unique_ptr<GotSection> got;
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index d7bfa7357d4ed..6fa4cae5156e0 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -1436,6 +1436,7 @@ static void readConfigs(Ctx &ctx, opt::InputArgList &args) {
       args.hasArg(OPT_fix_cortex_a8) && !args.hasArg(OPT_relocatable);
   ctx.arg.fortranCommon =
       args.hasFlag(OPT_fortran_common, OPT_no_fortran_common, false);
+  ctx.arg.btfMerge = args.hasFlag(OPT_btf_merge, OPT_no_btf_merge, false);
   ctx.arg.gcSections = args.hasFlag(OPT_gc_sections, OPT_no_gc_sections, false);
   ctx.arg.gnuUnique = args.hasFlag(OPT_gnu_unique, OPT_no_gnu_unique, true);
   ctx.arg.gdbIndex = args.hasFlag(OPT_gdb_index, OPT_no_gdb_index, false);
diff --git a/lld/ELF/Options.td b/lld/ELF/Options.td
index c2111e58c12b9..55a5a00e3d8c9 100644
--- a/lld/ELF/Options.td
+++ b/lld/ELF/Options.td
@@ -281,6 +281,10 @@ defm gc_sections: B<"gc-sections",
     "Enable garbage collection of unused sections",
     "Disable garbage collection of unused sections (default)">;
 
+defm btf_merge: BB<"btf-merge",
+    "Merge and deduplicate .BTF sections",
+    "Do not merge .BTF sections (default)">;
+
 defm gdb_index: BB<"gdb-index",
     "Generate .gdb_index section",
     "Do not generate .gdb_index section (default)">;
diff --git a/lld/ELF/SyntheticSections.cpp b/lld/ELF/SyntheticSections.cpp
index 2cfc88d8389b0..863aefc229cc0 100644
--- a/lld/ELF/SyntheticSections.cpp
+++ b/lld/ELF/SyntheticSections.cpp
@@ -32,6 +32,8 @@
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/BinaryFormat/Dwarf.h"
 #include "llvm/BinaryFormat/ELF.h"
+#include "llvm/DebugInfo/BTF/BTFBuilder.h"
+#include "llvm/DebugInfo/BTF/BTFDedup.h"
 #include "llvm/DebugInfo/DWARF/DWARFAcceleratorTable.h"
 #include "llvm/DebugInfo/DWARF/DWARFDebugPubTable.h"
 #include "llvm/Support/DJB.h"
@@ -3676,6 +3678,53 @@ void GdbIndexSection::writeTo(uint8_t *buf) {
 
 bool GdbIndexSection::isNeeded() const { return !chunks.empty(); }
 
+BtfSection::BtfSection(Ctx &ctx)
+    : SyntheticSection(ctx, ".BTF", SHT_PROGBITS, 0, 1) {}
+
+template <class ELFT>
+std::unique_ptr<BtfSection> BtfSection::create(Ctx &ctx) {
+  llvm::TimeTraceScope timeScope("Merge BTF");
+
+  const bool isLE = ELFT::Endianness == llvm::endianness::little;
+  llvm::BTFBuilder builder;
+
+  // Collect all .BTF input sections and merge them.
+  // Mark originals dead so they don't appear in the output.
+  for (InputSectionBase *s : ctx.inputSections) {
+    if (s->name != ".BTF" || !s->isLive())
+      continue;
+    s->markDead();
+    ArrayRef<uint8_t> data = s->content();
+    StringRef raw(reinterpret_cast<const char *>(data.data()), data.size());
+    Expected<uint32_t> idBase = builder.merge(raw, isLE);
+    if (!idBase) {
+      Warn(ctx) << s << ": failed to parse .BTF section: "
+                << toString(idBase.takeError());
+      continue;
+    }
+  }
+
+  auto ret = std::make_unique<BtfSection>(ctx);
+
+  if (builder.typesCount() == 0)
+    return ret;
+
+  // Deduplicate merged types.
+  if (Error e = llvm::btfDedup(builder)) {
+    Warn(ctx) << "BTF deduplication failed: " << toString(std::move(e));
+    // Fall through and emit un-deduped BTF.
+    builder.write(ret->outputData, isLE);
+    return ret;
+  }
+
+  builder.write(ret->outputData, isLE);
+  return ret;
+}
+
+void BtfSection::writeTo(uint8_t *buf) {
+  memcpy(buf, outputData.data(), outputData.size());
+}
+
 VersionDefinitionSection::VersionDefinitionSection(Ctx &ctx)
     : SyntheticSection(ctx, ".gnu.version_d", SHT_GNU_verdef, SHF_ALLOC,
                        sizeof(uint32_t)) {}
@@ -4907,6 +4956,11 @@ template <class ELFT> void elf::createSyntheticSections(Ctx &ctx) {
     add(*ctx.in.debugNames);
   }
 
+  if (ctx.arg.btfMerge) {
+    ctx.in.btfSection = BtfSection::create<ELFT>(ctx);
+    add(*ctx.in.btfSection);
+  }
+
   if (ctx.arg.gdbIndex) {
     ctx.in.gdbIndex = GdbIndexSection::create<ELFT>(ctx);
     add(*ctx.in.gdbIndex);
diff --git a/lld/ELF/SyntheticSections.h b/lld/ELF/SyntheticSections.h
index 1ae03dc24a2f2..61e438155fb82 100644
--- a/lld/ELF/SyntheticSections.h
+++ b/lld/ELF/SyntheticSections.h
@@ -1013,6 +1013,22 @@ class GdbIndexSection final : public SyntheticSection {
   size_t size;
 };
 
+// Merges and deduplicates .BTF (BPF Type Format) sections from multiple input
+// object files into a single output .BTF section. Uses the BTFBuilder/BTFDedup
+// library to parse, merge, and deduplicate BTF type information.
+class BtfSection final : public SyntheticSection {
+public:
+  BtfSection(Ctx &);
+  template <typename ELFT>
+  static std::unique_ptr<BtfSection> create(Ctx &);
+  void writeTo(uint8_t *buf) override;
+  size_t getSize() const override { return outputData.size(); }
+  bool isNeeded() const override { return !outputData.empty(); }
+
+private:
+  SmallVector<uint8_t, 0> outputData;
+};
+
 // For more information about .gnu.version and .gnu.version_r see:
 // https://www.akkadia.org/drepper/symbol-versioning
 
diff --git a/lld/test/ELF/Inputs/btf-merge-int.s b/lld/test/ELF/Inputs/btf-merge-int.s
new file mode 100644
index 0000000000000..10b45a1de01cb
--- /dev/null
+++ b/lld/test/ELF/Inputs/btf-merge-int.s
@@ -0,0 +1,26 @@
+# Input file with a BTF section containing a single INT type "int" (4 bytes).
+.text
+.globl foo
+.type foo, @function
+foo:
+  ret
+
+.section .BTF,"",@progbits
+# BTF Header (24 bytes)
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len (1 INT type = 12 + 4 bytes)
+.long 16                # str_off
+.long 5                 # str_len ("\0int\0")
+# Type 1: INT "int" size=4
+.long 1                 # name_off = 1 ("int")
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 4                 # size = 4
+.long 0x00000020        # INT encoding: bits=32, offset=0, encoding=0
+# String table
+.byte 0                 # ""
+.ascii "int"
+.byte 0
diff --git a/lld/test/ELF/Inputs/btf-merge-long.s b/lld/test/ELF/Inputs/btf-merge-long.s
new file mode 100644
index 0000000000000..5cc930408c543
--- /dev/null
+++ b/lld/test/ELF/Inputs/btf-merge-long.s
@@ -0,0 +1,26 @@
+# Input file with a BTF section containing a single INT type "long" (8 bytes).
+.text
+.globl bar
+.type bar, @function
+bar:
+  ret
+
+.section .BTF,"",@progbits
+# BTF Header (24 bytes)
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len (1 INT type = 12 + 4 bytes)
+.long 16                # str_off
+.long 6                 # str_len ("\0long\0")
+# Type 1: INT "long" size=8
+.long 1                 # name_off = 1 ("long")
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 8                 # size = 8
+.long 0x00000040        # INT encoding: bits=64, offset=0, encoding=0
+# String table
+.byte 0                 # ""
+.ascii "long"
+.byte 0
diff --git a/lld/test/ELF/btf-merge-basic.s b/lld/test/ELF/btf-merge-basic.s
new file mode 100644
index 0000000000000..fac348ad8aae2
--- /dev/null
+++ b/lld/test/ELF/btf-merge-basic.s
@@ -0,0 +1,55 @@
+# REQUIRES: x86
+## Test basic --btf-merge functionality: merging .BTF sections from two input
+## files into a single output .BTF section.
+
+# RUN: llvm-mc -filetype=obj -triple=x86_64-unknown-linux %s -o %t1.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-unknown-linux %p/Inputs/btf-merge-long.s -o %t2.o
+
+## Verify that without --btf-merge, input .BTF sections pass through as-is.
+# RUN: ld.lld %t1.o %t2.o -o %t.no-merge
+# RUN: llvm-readelf -S %t.no-merge | FileCheck %s --check-prefix=NO-MERGE
+
+## Verify that --btf-merge produces a single merged .BTF section.
+# RUN: ld.lld --btf-merge %t1.o %t2.o -o %t.merged
+# RUN: llvm-readelf -S %t.merged | FileCheck %s --check-prefix=MERGED
+# RUN: llvm-readelf -x .BTF %t.merged | FileCheck %s --check-prefix=BTF-HEX
+
+## Verify --no-btf-merge disables merging.
+# RUN: ld.lld --btf-merge --no-btf-merge %t1.o %t2.o -o %t.disabled
+# RUN: llvm-readelf -S %t.disabled | FileCheck %s --check-prefix=NO-MERGE
+
+# NO-MERGE: .BTF
+
+## When merged, there should be exactly one .BTF section.
+# MERGED:     .BTF PROGBITS
+# MERGED-NOT: .BTF PROGBITS
+
+## Check BTF header magic and that it's valid.
+## The hex dump should start with the BTF magic 9feb (LE for 0xeb9f).
+# BTF-HEX: Hex dump of section '.BTF':
+# BTF-HEX: 0x{{[0-9a-f]+}} 9feb0100
+
+.text
+.globl _start
+_start:
+  ret
+
+.section .BTF,"",@progbits
+# BTF Header (24 bytes)
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len (1 INT type = 12 + 4 bytes)
+.long 16                # str_off
+.long 5                 # str_len ("\0int\0")
+# Type 1: INT "int" size=4
+.long 1                 # name_off = 1 ("int")
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 4                 # size = 4
+.long 0x00000020        # INT encoding: bits=32, offset=0, encoding=0
+# String table
+.byte 0                 # ""
+.ascii "int"
+.byte 0
diff --git a/lld/test/ELF/btf-merge-dedup.s b/lld/test/ELF/btf-merge-dedup.s
new file mode 100644
index 0000000000000..d8940b4226be4
--- /dev/null
+++ b/lld/test/ELF/btf-merge-dedup.s
@@ -0,0 +1,45 @@
+# REQUIRES: x86
+## Test --btf-merge deduplication: two input files with identical BTF types
+## should produce a single merged .BTF section with duplicates removed.
+
+# RUN: llvm-mc -filetype=obj -triple=x86_64-unknown-linux %s -o %t1.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-unknown-linux %p/Inputs/btf-merge-int.s -o %t2.o
+
+## Both files have identical INT "int" types. After merge+dedup, only one
+## type should remain, making the output smaller than the sum of inputs.
+# RUN: ld.lld --btf-merge %t1.o %t2.o -o %t.merged
+# RUN: llvm-readelf -S %t.merged | FileCheck %s --check-prefix=MERGED
+
+## Verify the merged section exists and has valid BTF.
+# RUN: llvm-readelf -x .BTF %t.merged | FileCheck %s --check-prefix=BTF-HEX
+
+# MERGED: .BTF PROGBITS
+
+## Verify BTF magic is present.
+# BTF-HEX: Hex dump of section '.BTF':
+# BTF-HEX: 0x{{[0-9a-f]+}} 9feb0100
+
+.text
+.globl _start
+_start:
+  ret
+
+.section .BTF,"",@progbits
+# BTF Header (24 bytes)
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len (1 INT type = 12 + 4 bytes)
+.long 16                # str_off
+.long 5                 # str_len ("\0int\0")
+# Type 1: INT "int" size=4 (identical to Inputs/btf-merge-int.s)
+.long 1                 # name_off = 1 ("int")
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 4                 # size = 4
+.long 0x00000020        # INT encoding: bits=32, offset=0, encoding=0
+# String table
+.byte 0                 # ""
+.ascii "int"
+.byte 0
diff --git a/llvm/include/llvm/DebugInfo/BTF/BTFBuilder.h b/llvm/include/llvm/DebugInfo/BTF/BTFBuilder.h
new file mode 100644
index 0000000000000..a401788ec88f0
--- /dev/null
+++ b/llvm/include/llvm/DebugInfo/BTF/BTFBuilder.h
@@ -0,0 +1,96 @@
+//===- BTFBuilder.h - BTF builder/writer -----------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Mutable in-memory representation of BTF type information.
+///
+/// BTFBuilder provides an interface for constructing and merging .BTF
+/// sections. Types and strings can be added individually or merged from
+/// raw .BTF section data parsed from ELF object files. The result can
+/// be serialized back to binary .BTF format.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_DEBUGINFO_BTF_BTFBUILDER_H
+#define LLVM_DEBUGINFO_BTF_BTFBUILDER_H
+
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/DebugInfo/BTF/BTF.h"
+#include "llvm/Support/Compiler.h"
+#include "llvm/Support/Error.h"
+
+namespace llvm {
+
+/// A mutable container for BTF type information that supports construction,
+/// merging, and serialization.
+///
+/// Types are stored as contiguous raw bytes in native byte order.
+/// Type IDs are 1-based (ID 0 = void, never stored).
+class BTFBuilder {
+  // String table: concatenated NUL-terminated strings.
+  // Offset 0 is always the empty string (single NUL byte).
+  SmallVector<char, 0> Strings;
+
+  // Raw type data in native byte order. Types are stored sequentially,
+  // each as CommonType header followed by kind-specific tail data.
+  SmallVector<uint8_t, 0> TypeData;
+
+  // TypeOffsets[i] is the byte offset in TypeData for type ID (i+1).
+  SmallVector<uint32_t, 0> TypeOffsets;
+
+public:
+  LLVM_ABI BTFBuilder();
+
+  /// Add a string, returning its offset in the string table.
+  LLVM_ABI uint32_t addString(StringRef S);
+
+  /// Add a type header, returning the new 1-based type ID.
+  /// Append kind-specific tail data with addTail() immediately after.
+  LLVM_ABI uint32_t addType(const BTF::CommonType &Header);
+
+  /// Append kind-specific tail data for the most recently added type.
+  template <typename T> void addTail(const T &Data) {
+    const auto *Ptr = reinterpret_cast<const uint8_t *>(&Data);
+    TypeData.append(Ptr, Ptr + sizeof(Data));
+  }
+
+  /// Merge all types and strings from a raw .BTF section, remapping
+  /// type IDs and string offsets. Returns the first new type ID.
+  LLVM_ABI Expected<uint32_t> merge(StringRef RawBTFSection,
+                                    bool IsLittleEndian);
+
+  /// Look up a type by 1-based ID. Returns nullptr for invalid IDs.
+  LLVM_ABI const BTF::CommonType *findType(uint32_t Id) const;
+
+  /// Get raw bytes for a type entry (CommonType + tail data).
+  LLVM_ABI ArrayRef<uint8_t> getTypeBytes(uint32_t Id) const;
+
+  /// Get mutable raw bytes for a type entry.
+  LLVM_ABI MutableArrayRef<uint8_t> getMutableTypeBytes(uint32_t Id);
+
+  /// Look up a string by offset in the string table.
+  LLVM_ABI StringRef findString(uint32_t Offset) const;
+
+  /// Number of types, excluding void (type 0).
+  uint32_t typesCount() const { return TypeOffsets.size(); }
+
+  /// Compute the byte size of a type entry from its CommonType header.
+  LLVM_ABI static size_t typeByteSize(const BTF::CommonType *T);
+
+  /// Returns true if CommonType.Type is a type reference for this kind.
+  LLVM_ABI static bool hasTypeRef(uint32_t Kind);
+
+  /// Serialize to binary .BTF format, appending to Out.
+  LLVM_ABI void write(SmallVectorImpl<uint8_t> &Out,
+                      bool IsLittleEndian) const;
+};
+
+} // namespace llvm
+
+#endif // LLVM_DEBUGINFO_BTF_BTFBUILDER_H
diff --git a/llvm/include/llvm/DebugInfo/BTF/BTFDedup.h b/llvm/include/llvm/DebugInfo/BTF/BTFDedup.h
new file mode 100644
index 0000000000000..302c1b13c0716
--- /dev/null
+++ b/llvm/include/llvm/DebugInfo/BTF/BTFDedup.h
@@ -0,0 +1,46 @@
+//===- BTFDedup.h - BTF type deduplication ---------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// BTF type deduplication algorithm.
+///
+/// Implements the 5-pass deduplication algorithm from libbpf:
+///   1. String deduplication
+///   2. Primitive/struct/enum type dedup with DFS equivalence checking
+///   3. Reference type dedup (PTR, TYPEDEF, etc.)
+///   4. Type compaction (remove duplicates, assign new IDs)
+///   5. Type ID remapping
+///
+/// The algorithm achieves ~137x type reduction on a full Linux kernel
+/// (3.6M types -> 26K types).
+///
+/// Reference: https://nakryiko.com/posts/btf-dedup/
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_DEBUGINFO_BTF_BTFDEDUP_H
+#define LLVM_DEBUGINFO_BTF_BTFDEDUP_H
+
+#include "llvm/Support/Compiler.h"
+#include "llvm/Support/Error.h"
+
+namespace llvm {
+
+class BTFBuilder;
+
+/// Deduplicate types in a BTFBuilder in-place.
+///
+/// After deduplication, structurally equivalent types are merged and
+/// all type IDs are remapped to point to canonical representatives.
+/// The resulting BTFBuilder can be serialized to produce a compact
+/// .BTF section.
+LLVM_ABI Error btfDedup(BTFBuilder &Builder);
+
+} // namespace llvm
+
+#endif // LLVM_DEBUGINFO_BTF_BTFDEDUP_H
diff --git a/llvm/lib/DebugInfo/BTF/BTFBuilder.cpp b/llvm/lib/DebugInfo/BTF/BTFBuilder.cpp
new file mode 100644
index 0000000000000..d9875133b4dfb
--- /dev/null
+++ b/llvm/lib/DebugInfo/BTF/BTFBuilder.cpp
@@ -0,0 +1,416 @@
+//===- BTFBuilder.cpp - BTF builder/writer implementation -----------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/DebugInfo/BTF/BTFBuilder.h"
+#include "llvm/Support/Endian.h"
+#include "llvm/Support/SwapByteOrder.h"
+
+using namespace llvm;
+
+BTFBuilder::BTFBuilder() {
+  // String table starts with the empty string at offset 0.
+  Strings.push_back('\0');
+}
+
+uint32_t BTFBuilder::addString(StringRef S) {
+  uint32_t Offset = Strings.size();
+  Strings.append(S.begin(), S.end());
+  Strings.push_back('\0');
+  return Offset;
+}
+
+uint32_t BTFBuilder::addType(const BTF::CommonType &Header) {
+  TypeOffsets.push_back(TypeData.size());
+  const auto *Ptr = reinterpret_cast<const uint8_t *>(&Header);
+  TypeData.append(Ptr, Ptr + sizeof(Header));
+  return TypeOffsets.size(); // 1-based ID
+}
+
+const BTF::CommonType *BTFBuilder::findType(uint32_t Id) const {
+  if (Id == 0 || Id > TypeOffsets.size())
+    return nullptr;
+  return reinterpret_cast<const BTF::CommonType *>(
+      &TypeData[TypeOffsets[Id - 1]]);
+}
+
+// Returns {Start, Size} for a type's byte range, or {0, 0} for invalid IDs.
+static std::pair<uint32_t, uint32_t>
+typeBounds(uint32_t Id, const SmallVectorImpl<uint32_t> &TypeOffsets,
+           size_t TypeDataSize) {
+  if (Id == 0 || Id > TypeOffsets.size())
+    return {0, 0};
+  uint32_t Start = TypeOffsets[Id - 1];
+  uint32_t End =
+      (Id < TypeOffsets.size()) ? TypeOffsets[Id] : TypeDataSize;
+  return {Start, End - Start};
+}
+
+ArrayRef<uint8_t> BTFBuilder::getTypeBytes(uint32_t Id) const {
+  auto [Start, Size] = typeBounds(Id, TypeOffsets, TypeData.size());
+  if (Size == 0)
+    return {};
+  return ArrayRef<uint8_t>(&TypeData[Start], Size);
+}
+
+MutableArrayRef<uint8_t> BTFBuilder::getMutableTypeBytes(uint32_t Id) {
+  auto [Start, Size] = typeBounds(Id, TypeOffsets, TypeData.size());
+  if (Size == 0)
+    return {};
+  return MutableArrayRef<ui...
[truncated]

@llvmbot

llvmbot commented Feb 28, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-lld-elf

Author: Can H. Tartanoglu (caniko)

Changes

Summary

This adds --btf-merge support to lld's ELF linker, allowing it to merge and deduplicate .BTF sections from multiple input object files into a single output section. This is a prerequisite for enabling LTO + Rust + BTF in the Linux kernel — currently blocked because pahole's DWARF→BTF conversion breaks when LTO merges C and Rust compilation units.

The approach: each compiler emits .BTF directly (GCC already does via -gbtf, Clang support is in progress), and the linker merges them, the same way it already handles .eh_frame or .gdb_index.

What's here

New LLVM library code (llvm/lib/DebugInfo/BTF/):

  • BTFBuilder — mutable in-memory BTF container that can parse, merge, and serialize .BTF sections
  • btfDedup() — 5-pass deduplication algorithm ported from libbpf's btf_dedup():
    1. String dedup (hash-based)
    2. Primitive type dedup (INT, ENUM, FWD, FLOAT)
    3. Composite type dedup (STRUCT/UNION with DFS equivalence + cycle handling)
    4. Reference type dedup (PTR, TYPEDEF, CONST, etc.)
    5. Compaction + global ID remapping

lld integration (lld/ELF/):

  • BtfSection synthetic section that collects .BTF inputs, merges via BTFBuilder, deduplicates, and emits the result
  • --btf-merge / --no-btf-merge flags (off by default)

Tests:

  • Unit tests for BTFBuilder and btfDedup covering roundtrip, all type kinds, self-referential structs, forward declaration resolution
  • LIT tests for the lld flag and basic merge/dedup behavior

Motivation

The kernel's Kconfig currently has:

config RUST
    depends on !DEBUG_INFO_BTF || (PAHOLE_HAS_LANG_EXCLUDE &amp;&amp; !LTO)

With compiler-generated BTF + linker-side merging, this restriction goes away. GNU ld already has this via libctf; this brings lld to parity, which matters because lld is what the kernel uses for Clang LTO builds.

Algorithm notes

The dedup algorithm follows @anakryiko's design from libbpf (writeup). The composite type pass uses a hypothesis map for DFS graph equivalence, which correctly handles recursive types (linked lists, trees, etc.). I chose to put the library code in llvm/lib/DebugInfo/BTF/ rather than inline in lld so other tools (llvm-objcopy, llvm-readobj) can reuse it.

I'd appreciate review from @MaskRay on the lld integration side — I followed the GdbIndexSection pattern as closely as possible. And @anakryiko / @yonghong-song for the dedup algorithm correctness, since you wrote the original.

cc @4ast


Patch is 83.50 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/183915.diff

18 Files Affected:

  • (modified) lld/ELF/CMakeLists.txt (+1)
  • (modified) lld/ELF/Config.h (+3)
  • (modified) lld/ELF/Driver.cpp (+1)
  • (modified) lld/ELF/Options.td (+4)
  • (modified) lld/ELF/SyntheticSections.cpp (+54)
  • (modified) lld/ELF/SyntheticSections.h (+16)
  • (added) lld/test/ELF/Inputs/btf-merge-int.s (+26)
  • (added) lld/test/ELF/Inputs/btf-merge-long.s (+26)
  • (added) lld/test/ELF/btf-merge-basic.s (+55)
  • (added) lld/test/ELF/btf-merge-dedup.s (+45)
  • (added) llvm/include/llvm/DebugInfo/BTF/BTFBuilder.h (+96)
  • (added) llvm/include/llvm/DebugInfo/BTF/BTFDedup.h (+46)
  • (added) llvm/lib/DebugInfo/BTF/BTFBuilder.cpp (+416)
  • (added) llvm/lib/DebugInfo/BTF/BTFDedup.cpp (+777)
  • (modified) llvm/lib/DebugInfo/BTF/CMakeLists.txt (+2)
  • (added) llvm/unittests/DebugInfo/BTF/BTFBuilderTest.cpp (+426)
  • (added) llvm/unittests/DebugInfo/BTF/BTFDedupTest.cpp (+399)
  • (modified) llvm/unittests/DebugInfo/BTF/CMakeLists.txt (+2)
diff --git a/lld/ELF/CMakeLists.txt b/lld/ELF/CMakeLists.txt
index e22897c2789d8..c2a9265f5281b 100644
--- a/lld/ELF/CMakeLists.txt
+++ b/lld/ELF/CMakeLists.txt
@@ -66,6 +66,7 @@ add_lld_library(lldELF
   BinaryFormat
   BitWriter
   Core
+  DebugInfoBTF
   DebugInfoDWARF
   Demangle
   DTLTO
diff --git a/lld/ELF/Config.h b/lld/ELF/Config.h
index 237df52194210..d9e279a5dbe03 100644
--- a/lld/ELF/Config.h
+++ b/lld/ELF/Config.h
@@ -56,6 +56,7 @@ struct Partition;
 struct PhdrEntry;
 
 class BssSection;
+class BtfSection;
 class GdbIndexSection;
 class GotPltSection;
 class GotSection;
@@ -332,6 +333,7 @@ struct Config {
   bool fixCortexA8;
   bool formatBinary = false;
   bool fortranCommon;
+  bool btfMerge;
   bool gcSections;
   bool gdbIndex;
   bool gnuHash = false;
@@ -569,6 +571,7 @@ struct InStruct {
   std::unique_ptr<SyntheticSection> riscvAttributes;
   std::unique_ptr<BssSection> bss;
   std::unique_ptr<BssSection> bssRelRo;
+  std::unique_ptr<BtfSection> btfSection;
   std::unique_ptr<SyntheticSection> gnuProperty;
   std::unique_ptr<SyntheticSection> gnuStack;
   std::unique_ptr<GotSection> got;
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index d7bfa7357d4ed..6fa4cae5156e0 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -1436,6 +1436,7 @@ static void readConfigs(Ctx &ctx, opt::InputArgList &args) {
       args.hasArg(OPT_fix_cortex_a8) && !args.hasArg(OPT_relocatable);
   ctx.arg.fortranCommon =
       args.hasFlag(OPT_fortran_common, OPT_no_fortran_common, false);
+  ctx.arg.btfMerge = args.hasFlag(OPT_btf_merge, OPT_no_btf_merge, false);
   ctx.arg.gcSections = args.hasFlag(OPT_gc_sections, OPT_no_gc_sections, false);
   ctx.arg.gnuUnique = args.hasFlag(OPT_gnu_unique, OPT_no_gnu_unique, true);
   ctx.arg.gdbIndex = args.hasFlag(OPT_gdb_index, OPT_no_gdb_index, false);
diff --git a/lld/ELF/Options.td b/lld/ELF/Options.td
index c2111e58c12b9..55a5a00e3d8c9 100644
--- a/lld/ELF/Options.td
+++ b/lld/ELF/Options.td
@@ -281,6 +281,10 @@ defm gc_sections: B<"gc-sections",
     "Enable garbage collection of unused sections",
     "Disable garbage collection of unused sections (default)">;
 
+defm btf_merge: BB<"btf-merge",
+    "Merge and deduplicate .BTF sections",
+    "Do not merge .BTF sections (default)">;
+
 defm gdb_index: BB<"gdb-index",
     "Generate .gdb_index section",
     "Do not generate .gdb_index section (default)">;
diff --git a/lld/ELF/SyntheticSections.cpp b/lld/ELF/SyntheticSections.cpp
index 2cfc88d8389b0..863aefc229cc0 100644
--- a/lld/ELF/SyntheticSections.cpp
+++ b/lld/ELF/SyntheticSections.cpp
@@ -32,6 +32,8 @@
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/BinaryFormat/Dwarf.h"
 #include "llvm/BinaryFormat/ELF.h"
+#include "llvm/DebugInfo/BTF/BTFBuilder.h"
+#include "llvm/DebugInfo/BTF/BTFDedup.h"
 #include "llvm/DebugInfo/DWARF/DWARFAcceleratorTable.h"
 #include "llvm/DebugInfo/DWARF/DWARFDebugPubTable.h"
 #include "llvm/Support/DJB.h"
@@ -3676,6 +3678,53 @@ void GdbIndexSection::writeTo(uint8_t *buf) {
 
 bool GdbIndexSection::isNeeded() const { return !chunks.empty(); }
 
+BtfSection::BtfSection(Ctx &ctx)
+    : SyntheticSection(ctx, ".BTF", SHT_PROGBITS, 0, 1) {}
+
+template <class ELFT>
+std::unique_ptr<BtfSection> BtfSection::create(Ctx &ctx) {
+  llvm::TimeTraceScope timeScope("Merge BTF");
+
+  const bool isLE = ELFT::Endianness == llvm::endianness::little;
+  llvm::BTFBuilder builder;
+
+  // Collect all .BTF input sections and merge them.
+  // Mark originals dead so they don't appear in the output.
+  for (InputSectionBase *s : ctx.inputSections) {
+    if (s->name != ".BTF" || !s->isLive())
+      continue;
+    s->markDead();
+    ArrayRef<uint8_t> data = s->content();
+    StringRef raw(reinterpret_cast<const char *>(data.data()), data.size());
+    Expected<uint32_t> idBase = builder.merge(raw, isLE);
+    if (!idBase) {
+      Warn(ctx) << s << ": failed to parse .BTF section: "
+                << toString(idBase.takeError());
+      continue;
+    }
+  }
+
+  auto ret = std::make_unique<BtfSection>(ctx);
+
+  if (builder.typesCount() == 0)
+    return ret;
+
+  // Deduplicate merged types.
+  if (Error e = llvm::btfDedup(builder)) {
+    Warn(ctx) << "BTF deduplication failed: " << toString(std::move(e));
+    // Fall through and emit un-deduped BTF.
+    builder.write(ret->outputData, isLE);
+    return ret;
+  }
+
+  builder.write(ret->outputData, isLE);
+  return ret;
+}
+
+void BtfSection::writeTo(uint8_t *buf) {
+  memcpy(buf, outputData.data(), outputData.size());
+}
+
 VersionDefinitionSection::VersionDefinitionSection(Ctx &ctx)
     : SyntheticSection(ctx, ".gnu.version_d", SHT_GNU_verdef, SHF_ALLOC,
                        sizeof(uint32_t)) {}
@@ -4907,6 +4956,11 @@ template <class ELFT> void elf::createSyntheticSections(Ctx &ctx) {
     add(*ctx.in.debugNames);
   }
 
+  if (ctx.arg.btfMerge) {
+    ctx.in.btfSection = BtfSection::create<ELFT>(ctx);
+    add(*ctx.in.btfSection);
+  }
+
   if (ctx.arg.gdbIndex) {
     ctx.in.gdbIndex = GdbIndexSection::create<ELFT>(ctx);
     add(*ctx.in.gdbIndex);
diff --git a/lld/ELF/SyntheticSections.h b/lld/ELF/SyntheticSections.h
index 1ae03dc24a2f2..61e438155fb82 100644
--- a/lld/ELF/SyntheticSections.h
+++ b/lld/ELF/SyntheticSections.h
@@ -1013,6 +1013,22 @@ class GdbIndexSection final : public SyntheticSection {
   size_t size;
 };
 
+// Merges and deduplicates .BTF (BPF Type Format) sections from multiple input
+// object files into a single output .BTF section. Uses the BTFBuilder/BTFDedup
+// library to parse, merge, and deduplicate BTF type information.
+class BtfSection final : public SyntheticSection {
+public:
+  BtfSection(Ctx &);
+  template <typename ELFT>
+  static std::unique_ptr<BtfSection> create(Ctx &);
+  void writeTo(uint8_t *buf) override;
+  size_t getSize() const override { return outputData.size(); }
+  bool isNeeded() const override { return !outputData.empty(); }
+
+private:
+  SmallVector<uint8_t, 0> outputData;
+};
+
 // For more information about .gnu.version and .gnu.version_r see:
 // https://www.akkadia.org/drepper/symbol-versioning
 
diff --git a/lld/test/ELF/Inputs/btf-merge-int.s b/lld/test/ELF/Inputs/btf-merge-int.s
new file mode 100644
index 0000000000000..10b45a1de01cb
--- /dev/null
+++ b/lld/test/ELF/Inputs/btf-merge-int.s
@@ -0,0 +1,26 @@
+# Input file with a BTF section containing a single INT type "int" (4 bytes).
+.text
+.globl foo
+.type foo, @function
+foo:
+  ret
+
+.section .BTF,"",@progbits
+# BTF Header (24 bytes)
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len (1 INT type = 12 + 4 bytes)
+.long 16                # str_off
+.long 5                 # str_len ("\0int\0")
+# Type 1: INT "int" size=4
+.long 1                 # name_off = 1 ("int")
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 4                 # size = 4
+.long 0x00000020        # INT encoding: bits=32, offset=0, encoding=0
+# String table
+.byte 0                 # ""
+.ascii "int"
+.byte 0
diff --git a/lld/test/ELF/Inputs/btf-merge-long.s b/lld/test/ELF/Inputs/btf-merge-long.s
new file mode 100644
index 0000000000000..5cc930408c543
--- /dev/null
+++ b/lld/test/ELF/Inputs/btf-merge-long.s
@@ -0,0 +1,26 @@
+# Input file with a BTF section containing a single INT type "long" (8 bytes).
+.text
+.globl bar
+.type bar, @function
+bar:
+  ret
+
+.section .BTF,"",@progbits
+# BTF Header (24 bytes)
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len (1 INT type = 12 + 4 bytes)
+.long 16                # str_off
+.long 6                 # str_len ("\0long\0")
+# Type 1: INT "long" size=8
+.long 1                 # name_off = 1 ("long")
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 8                 # size = 8
+.long 0x00000040        # INT encoding: bits=64, offset=0, encoding=0
+# String table
+.byte 0                 # ""
+.ascii "long"
+.byte 0
diff --git a/lld/test/ELF/btf-merge-basic.s b/lld/test/ELF/btf-merge-basic.s
new file mode 100644
index 0000000000000..fac348ad8aae2
--- /dev/null
+++ b/lld/test/ELF/btf-merge-basic.s
@@ -0,0 +1,55 @@
+# REQUIRES: x86
+## Test basic --btf-merge functionality: merging .BTF sections from two input
+## files into a single output .BTF section.
+
+# RUN: llvm-mc -filetype=obj -triple=x86_64-unknown-linux %s -o %t1.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-unknown-linux %p/Inputs/btf-merge-long.s -o %t2.o
+
+## Verify that without --btf-merge, input .BTF sections pass through as-is.
+# RUN: ld.lld %t1.o %t2.o -o %t.no-merge
+# RUN: llvm-readelf -S %t.no-merge | FileCheck %s --check-prefix=NO-MERGE
+
+## Verify that --btf-merge produces a single merged .BTF section.
+# RUN: ld.lld --btf-merge %t1.o %t2.o -o %t.merged
+# RUN: llvm-readelf -S %t.merged | FileCheck %s --check-prefix=MERGED
+# RUN: llvm-readelf -x .BTF %t.merged | FileCheck %s --check-prefix=BTF-HEX
+
+## Verify --no-btf-merge disables merging.
+# RUN: ld.lld --btf-merge --no-btf-merge %t1.o %t2.o -o %t.disabled
+# RUN: llvm-readelf -S %t.disabled | FileCheck %s --check-prefix=NO-MERGE
+
+# NO-MERGE: .BTF
+
+## When merged, there should be exactly one .BTF section.
+# MERGED:     .BTF PROGBITS
+# MERGED-NOT: .BTF PROGBITS
+
+## Check BTF header magic and that it's valid.
+## The hex dump should start with the BTF magic 9feb (LE for 0xeb9f).
+# BTF-HEX: Hex dump of section '.BTF':
+# BTF-HEX: 0x{{[0-9a-f]+}} 9feb0100
+
+.text
+.globl _start
+_start:
+  ret
+
+.section .BTF,"",@progbits
+# BTF Header (24 bytes)
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len (1 INT type = 12 + 4 bytes)
+.long 16                # str_off
+.long 5                 # str_len ("\0int\0")
+# Type 1: INT "int" size=4
+.long 1                 # name_off = 1 ("int")
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 4                 # size = 4
+.long 0x00000020        # INT encoding: bits=32, offset=0, encoding=0
+# String table
+.byte 0                 # ""
+.ascii "int"
+.byte 0
diff --git a/lld/test/ELF/btf-merge-dedup.s b/lld/test/ELF/btf-merge-dedup.s
new file mode 100644
index 0000000000000..d8940b4226be4
--- /dev/null
+++ b/lld/test/ELF/btf-merge-dedup.s
@@ -0,0 +1,45 @@
+# REQUIRES: x86
+## Test --btf-merge deduplication: two input files with identical BTF types
+## should produce a single merged .BTF section with duplicates removed.
+
+# RUN: llvm-mc -filetype=obj -triple=x86_64-unknown-linux %s -o %t1.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-unknown-linux %p/Inputs/btf-merge-int.s -o %t2.o
+
+## Both files have identical INT "int" types. After merge+dedup, only one
+## type should remain, making the output smaller than the sum of inputs.
+# RUN: ld.lld --btf-merge %t1.o %t2.o -o %t.merged
+# RUN: llvm-readelf -S %t.merged | FileCheck %s --check-prefix=MERGED
+
+## Verify the merged section exists and has valid BTF.
+# RUN: llvm-readelf -x .BTF %t.merged | FileCheck %s --check-prefix=BTF-HEX
+
+# MERGED: .BTF PROGBITS
+
+## Verify BTF magic is present.
+# BTF-HEX: Hex dump of section '.BTF':
+# BTF-HEX: 0x{{[0-9a-f]+}} 9feb0100
+
+.text
+.globl _start
+_start:
+  ret
+
+.section .BTF,"",@progbits
+# BTF Header (24 bytes)
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len (1 INT type = 12 + 4 bytes)
+.long 16                # str_off
+.long 5                 # str_len ("\0int\0")
+# Type 1: INT "int" size=4 (identical to Inputs/btf-merge-int.s)
+.long 1                 # name_off = 1 ("int")
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 4                 # size = 4
+.long 0x00000020        # INT encoding: bits=32, offset=0, encoding=0
+# String table
+.byte 0                 # ""
+.ascii "int"
+.byte 0
diff --git a/llvm/include/llvm/DebugInfo/BTF/BTFBuilder.h b/llvm/include/llvm/DebugInfo/BTF/BTFBuilder.h
new file mode 100644
index 0000000000000..a401788ec88f0
--- /dev/null
+++ b/llvm/include/llvm/DebugInfo/BTF/BTFBuilder.h
@@ -0,0 +1,96 @@
+//===- BTFBuilder.h - BTF builder/writer -----------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Mutable in-memory representation of BTF type information.
+///
+/// BTFBuilder provides an interface for constructing and merging .BTF
+/// sections. Types and strings can be added individually or merged from
+/// raw .BTF section data parsed from ELF object files. The result can
+/// be serialized back to binary .BTF format.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_DEBUGINFO_BTF_BTFBUILDER_H
+#define LLVM_DEBUGINFO_BTF_BTFBUILDER_H
+
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/DebugInfo/BTF/BTF.h"
+#include "llvm/Support/Compiler.h"
+#include "llvm/Support/Error.h"
+
+namespace llvm {
+
+/// A mutable container for BTF type information that supports construction,
+/// merging, and serialization.
+///
+/// Types are stored as contiguous raw bytes in native byte order.
+/// Type IDs are 1-based (ID 0 = void, never stored).
+class BTFBuilder {
+  // String table: concatenated NUL-terminated strings.
+  // Offset 0 is always the empty string (single NUL byte).
+  SmallVector<char, 0> Strings;
+
+  // Raw type data in native byte order. Types are stored sequentially,
+  // each as CommonType header followed by kind-specific tail data.
+  SmallVector<uint8_t, 0> TypeData;
+
+  // TypeOffsets[i] is the byte offset in TypeData for type ID (i+1).
+  SmallVector<uint32_t, 0> TypeOffsets;
+
+public:
+  LLVM_ABI BTFBuilder();
+
+  /// Add a string, returning its offset in the string table.
+  LLVM_ABI uint32_t addString(StringRef S);
+
+  /// Add a type header, returning the new 1-based type ID.
+  /// Append kind-specific tail data with addTail() immediately after.
+  LLVM_ABI uint32_t addType(const BTF::CommonType &Header);
+
+  /// Append kind-specific tail data for the most recently added type.
+  template <typename T> void addTail(const T &Data) {
+    const auto *Ptr = reinterpret_cast<const uint8_t *>(&Data);
+    TypeData.append(Ptr, Ptr + sizeof(Data));
+  }
+
+  /// Merge all types and strings from a raw .BTF section, remapping
+  /// type IDs and string offsets. Returns the first new type ID.
+  LLVM_ABI Expected<uint32_t> merge(StringRef RawBTFSection,
+                                    bool IsLittleEndian);
+
+  /// Look up a type by 1-based ID. Returns nullptr for invalid IDs.
+  LLVM_ABI const BTF::CommonType *findType(uint32_t Id) const;
+
+  /// Get raw bytes for a type entry (CommonType + tail data).
+  LLVM_ABI ArrayRef<uint8_t> getTypeBytes(uint32_t Id) const;
+
+  /// Get mutable raw bytes for a type entry.
+  LLVM_ABI MutableArrayRef<uint8_t> getMutableTypeBytes(uint32_t Id);
+
+  /// Look up a string by offset in the string table.
+  LLVM_ABI StringRef findString(uint32_t Offset) const;
+
+  /// Number of types, excluding void (type 0).
+  uint32_t typesCount() const { return TypeOffsets.size(); }
+
+  /// Compute the byte size of a type entry from its CommonType header.
+  LLVM_ABI static size_t typeByteSize(const BTF::CommonType *T);
+
+  /// Returns true if CommonType.Type is a type reference for this kind.
+  LLVM_ABI static bool hasTypeRef(uint32_t Kind);
+
+  /// Serialize to binary .BTF format, appending to Out.
+  LLVM_ABI void write(SmallVectorImpl<uint8_t> &Out,
+                      bool IsLittleEndian) const;
+};
+
+} // namespace llvm
+
+#endif // LLVM_DEBUGINFO_BTF_BTFBUILDER_H
diff --git a/llvm/include/llvm/DebugInfo/BTF/BTFDedup.h b/llvm/include/llvm/DebugInfo/BTF/BTFDedup.h
new file mode 100644
index 0000000000000..302c1b13c0716
--- /dev/null
+++ b/llvm/include/llvm/DebugInfo/BTF/BTFDedup.h
@@ -0,0 +1,46 @@
+//===- BTFDedup.h - BTF type deduplication ---------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// BTF type deduplication algorithm.
+///
+/// Implements the 5-pass deduplication algorithm from libbpf:
+///   1. String deduplication
+///   2. Primitive/struct/enum type dedup with DFS equivalence checking
+///   3. Reference type dedup (PTR, TYPEDEF, etc.)
+///   4. Type compaction (remove duplicates, assign new IDs)
+///   5. Type ID remapping
+///
+/// The algorithm achieves ~137x type reduction on a full Linux kernel
+/// (3.6M types -> 26K types).
+///
+/// Reference: https://nakryiko.com/posts/btf-dedup/
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_DEBUGINFO_BTF_BTFDEDUP_H
+#define LLVM_DEBUGINFO_BTF_BTFDEDUP_H
+
+#include "llvm/Support/Compiler.h"
+#include "llvm/Support/Error.h"
+
+namespace llvm {
+
+class BTFBuilder;
+
+/// Deduplicate types in a BTFBuilder in-place.
+///
+/// After deduplication, structurally equivalent types are merged and
+/// all type IDs are remapped to point to canonical representatives.
+/// The resulting BTFBuilder can be serialized to produce a compact
+/// .BTF section.
+LLVM_ABI Error btfDedup(BTFBuilder &Builder);
+
+} // namespace llvm
+
+#endif // LLVM_DEBUGINFO_BTF_BTFDEDUP_H
diff --git a/llvm/lib/DebugInfo/BTF/BTFBuilder.cpp b/llvm/lib/DebugInfo/BTF/BTFBuilder.cpp
new file mode 100644
index 0000000000000..d9875133b4dfb
--- /dev/null
+++ b/llvm/lib/DebugInfo/BTF/BTFBuilder.cpp
@@ -0,0 +1,416 @@
+//===- BTFBuilder.cpp - BTF builder/writer implementation -----------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/DebugInfo/BTF/BTFBuilder.h"
+#include "llvm/Support/Endian.h"
+#include "llvm/Support/SwapByteOrder.h"
+
+using namespace llvm;
+
+BTFBuilder::BTFBuilder() {
+  // String table starts with the empty string at offset 0.
+  Strings.push_back('\0');
+}
+
+uint32_t BTFBuilder::addString(StringRef S) {
+  uint32_t Offset = Strings.size();
+  Strings.append(S.begin(), S.end());
+  Strings.push_back('\0');
+  return Offset;
+}
+
+uint32_t BTFBuilder::addType(const BTF::CommonType &Header) {
+  TypeOffsets.push_back(TypeData.size());
+  const auto *Ptr = reinterpret_cast<const uint8_t *>(&Header);
+  TypeData.append(Ptr, Ptr + sizeof(Header));
+  return TypeOffsets.size(); // 1-based ID
+}
+
+const BTF::CommonType *BTFBuilder::findType(uint32_t Id) const {
+  if (Id == 0 || Id > TypeOffsets.size())
+    return nullptr;
+  return reinterpret_cast<const BTF::CommonType *>(
+      &TypeData[TypeOffsets[Id - 1]]);
+}
+
+// Returns {Start, Size} for a type's byte range, or {0, 0} for invalid IDs.
+static std::pair<uint32_t, uint32_t>
+typeBounds(uint32_t Id, const SmallVectorImpl<uint32_t> &TypeOffsets,
+           size_t TypeDataSize) {
+  if (Id == 0 || Id > TypeOffsets.size())
+    return {0, 0};
+  uint32_t Start = TypeOffsets[Id - 1];
+  uint32_t End =
+      (Id < TypeOffsets.size()) ? TypeOffsets[Id] : TypeDataSize;
+  return {Start, End - Start};
+}
+
+ArrayRef<uint8_t> BTFBuilder::getTypeBytes(uint32_t Id) const {
+  auto [Start, Size] = typeBounds(Id, TypeOffsets, TypeData.size());
+  if (Size == 0)
+    return {};
+  return ArrayRef<uint8_t>(&TypeData[Start], Size);
+}
+
+MutableArrayRef<uint8_t> BTFBuilder::getMutableTypeBytes(uint32_t Id) {
+  auto [Start, Size] = typeBounds(Id, TypeOffsets, TypeData.size());
+  if (Size == 0)
+    return {};
+  return MutableArrayRef<ui...
[truncated]

@MaskRay MaskRay left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the motivation is thin: "enabling the LTO + Rust + BTF pipeline in the Linux kernel" needs more context — what specifically was broken or missing?

If the lld implementation is a thin wrapper of DebugInfoBTF, it doesn't need so many tests.

The five test .s files (btf-merge-basic.s, btf-merge-dedup.s, btf-merge-single.s, btf-merge-struct.s) embed near-identical BTF bytes inline. Can they be restructured and grouped to a small set of tests? If you read test history in this directory, I have many refactoring patches that group related tests with split-file. Newer tests tend to not use Inputs/ at all. The lack of a dump tool is also a problem. Dumping the section content as hex in lld/test/ELF is not ideal. Most tests should be on the LLVM BTF side.

In addition we should have -r and --gc-sections tests. - should not merge the section contents.

Comment thread lld/ELF/Options.td Outdated
@@ -281,6 +281,10 @@ defm gc_sections: B<"gc-sections",
"Enable garbage collection of unused sections",
"Disable garbage collection of unused sections (default)">;

defm btf_merge: BB<"btf-merge",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be alphabetically placed near other b-options

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Moved after --branch-to-branch.

Comment thread lld/ELF/SyntheticSections.cpp Outdated
// Collect all .BTF input sections and merge them.
// Mark originals dead so they don't appear in the output.
for (InputSectionBase *s : ctx.inputSections) {
if (s->name != ".BTF" || !s->isLive())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need a test to cover the s->isLive condition.

Comment thread lld/ELF/SyntheticSections.cpp Outdated
}

auto ret = std::make_unique<BtfSection>(ctx);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete blank line

Comment thread lld/ELF/SyntheticSections.cpp Outdated
@@ -4907,6 +4956,11 @@ template <class ELFT> void elf::createSyntheticSections(Ctx &ctx) {
add(*ctx.in.debugNames);
}

if (ctx.arg.btfMerge) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use this order: btf, debugNames, gdbIndex. Should use std::make_unique<BtfSection<ELFT>>(ctx);

@MaskRay MaskRay left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header validation and byte-swapping logic in BTFBuilder::merge() (lines ~963–1000) duplicates BTFParser::parseBTF(). The cleanest fix would probably to refactor BTFParser::parseBTF to take a StringRef. I'll defer that to BTF reviewers.

Comment thread lld/ELF/SyntheticSections.cpp Outdated
return ret;

// Deduplicate merged types.
if (Error e = llvm::btfDedup(builder)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btfDedup can be placed inside llvm::btf

std::unique_ptr<BtfSection> BtfSection::create(Ctx &ctx) {
llvm::TimeTraceScope timeScope("Merge BTF");

const bool isLE = ELFT::Endianness == llvm::endianness::little;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete unneeded blank line

We need a big-endian test. Try aarch64be or ppc64

@caniko

caniko commented Feb 28, 2026

Copy link
Copy Markdown
Author

the motivation is thin: "enabling the LTO + Rust + BTF pipeline in the Linux kernel" needs more context — what specifically was broken or missing?

The kernel generates BTF via pahole's DWARF→BTF conversion. With LTO, pahole breaks on the merged DWARF because Rust compilation units produce DWARF constructs pahole doesn't understand. Even with PAHOLE_HAS_LANG_EXCLUDE (which filters out Rust CUs), LTO can interleave C and Rust code in the same CU, so the kernel currently requires !LTO when DEBUG_INFO_BTF is enabled with Rust — hence the Kconfig constraint depends on !DEBUG_INFO_BTF || (PAHOLE_HAS_LANG_EXCLUDE && !LTO). The fix is to skip the DWARF→BTF conversion entirely: compilers emit .BTF directly (GCC already does via -gbtf), and the linker merges them. This is the linker-side piece of that pipeline.

If the lld implementation is a thin wrapper of DebugInfoBTF, it doesn't need so many tests. [...] Most tests should be on the LLVM BTF side.

Agreed, the detailed type-level verification belongs in the LLVM unit tests (86 tests across BTFBuilder and BTFDedup already cover all 19 type kinds, recursive types, error paths, etc.). I've consolidated the lld tests to a single btf-merge.s that only tests linker-level concerns: flag plumbing (--btf-merge/--no-btf-merge), -r behavior, --gc-sections interaction, COMDAT isLive(), and big-endian support.

The lack of a dump tool is also a problem. Dumping the section content as hex in lld/test/ELF is not ideal.

Agreed, I've reduced the hex checks to just verifying BTF magic (confirming a valid merged section was produced). A proper llvm-btfdump or extending llvm-readelf with --btf would make lld-side assertions much cleaner — happy to look into that as a follow-up if you think it's worth having.

The header validation and byte-swapping logic in BTFBuilder::merge() (lines ~963–1000) duplicates BTFParser::parseBTF(). The cleanest fix would probably to refactor BTFParser::parseBTF to take a StringRef.

The duplication exists because BTFParser produces read-only parsed output (for tools like llvm-readelf/objdump), while BTFBuilder::merge() needs to ingest the raw bytes into a mutable container with ID/string remapping. The header validation is similar but the downstream actions diverge. That said, factoring out at least the header validation + sanity checks into a shared helper (or having BTFParser::parseBTF accept a StringRef as you suggest) would eliminate the duplication at the validation layer. I'll look into this as a follow-up — the refactor touches the existing BTFParser contract so it probably deserves its own patch.

@smithp35 smithp35 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the motivation is thin: "enabling the LTO + Rust + BTF pipeline in the Linux kernel" needs more context — what specifically was broken or missing?

Closest external link I can find about this is https://lwn.net/Articles/991719/ "BTF, Rust, and the kernel toolchain"

Apologies in advance for naive questions about btf.

While I can get that an external tool to translate DWARF isn't the ideal architecture, has there been any consideration been given to updating pahole to filter out the Rust DWARF information that cannot be represented?

Are there any plans to implement this option in ld.bfd? Sure it won't support Rust/LTO (at least not yet), but parity between the toolchains to avoid pahole seems like a good idea.

If the lld implementation is a thin wrapper of DebugInfoBTF, it doesn't need so many tests.

The five test .s files (btf-merge-basic.s, btf-merge-dedup.s, btf-merge-single.s, btf-merge-struct.s) embed near-identical BTF bytes inline. Can they be restructured and grouped to a small set of tests? If you read test history in this directory, I have many refactoring patches that group related tests with split-file. Newer tests tend to not use Inputs/ at all. The lack of a dump tool is also a problem. Dumping the section content as hex in lld/test/ELF is not ideal. Most tests should be on the LLVM BTF side.

In addition we should have -r and --gc-sections tests. - should not merge the section contents.

I agree with the sentiment that human readable dump output would help a lot. That may even enable bits of this patch to be split up into smaller parts that can be reviewed independently.

I note that GNU objdump https://sourceware.org/binutils/docs/binutils/objdump.html#index-CTF has some support for ctf output . Could that be used/extended to btf and implemented?

Comment thread lld/ELF/Options.td
@@ -63,6 +63,10 @@ defm branch_to_branch: BB<"branch-to-branch",
"Enable branch-to-branch optimization (default at -O2)",
"Disable branch-to-branch optimization (default at -O0 and -O1)">;

defm btf_merge: BB<"btf-merge",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an easy to miss man page in lld/docs/ld.lld.1 that could do with an entry for btf-merge.

Comment thread lld/ELF/Driver.cpp Outdated
@@ -1436,6 +1436,9 @@ static void readConfigs(Ctx &ctx, opt::InputArgList &args) {
args.hasArg(OPT_fix_cortex_a8) && !args.hasArg(OPT_relocatable);
ctx.arg.fortranCommon =
args.hasFlag(OPT_fortran_common, OPT_no_fortran_common, false);
ctx.arg.btfMerge =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason not to enable by default? If the option is not implemented on all linkers then you'll need a lld specific command line option in a build system that needs to support both GNU and llvm toolchains.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. For the initial landing I'd prefer to keep it off by default — it's a large new feature and having an explicit opt-in lets us shake out any issues before it's on everywhere. Once it's proven in production I plan to flip the default in a follow-up. I'll also add the man page entry you noted.

@@ -0,0 +1,96 @@
//===- BTFBuilder.h - BTF builder/writer -----------------------*- C++ -*-===//

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be worth adding reviewers from llvm/DebugInfo. While putting the code here is likely the best place, it may be out of the scope of a purely lld review for such a large amount of code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @caniko, I'm not suitable for reviewing new debug information formats, as I don't have the time to look into them, given the rest of my work and review load. I'm commenting here though because I briefly noticed that you're using the legacy style for header comments with the C++ annotation. Please take a look at the LLVM coding standards, which show the correct style to use.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood on your review bandwidth. Thanks for catching the header comment style despite your load, I will update those to current LLVM standards.

@caniko

caniko commented Mar 2, 2026

Copy link
Copy Markdown
Author

Per @smithp35's suggestion, adding reviewers from llvm/DebugInfo for the new library code in llvm/lib/DebugInfo/BTF/:

@dwblaikie @jh7370 — would appreciate your review on the BTFBuilder / BTF dedup library portions of this PR. The lld integration is relatively thin, but the bulk of the new code lives under llvm/include/llvm/DebugInfo/BTF/ and llvm/lib/DebugInfo/BTF/.

Also added a man page entry for --btf-merge in lld/docs/ld.lld.1 per review feedback.


Addressing @smithp35's review questions:

Why not fix pahole instead?

Pahole already has PAHOLE_HAS_LANG_EXCLUDE to filter out Rust CUs, and that works — without LTO. The fundamental problem is that with LTO the linker merges compilation units, so C and Rust code get interleaved within the same CU. At that point there's no CU-level language tag to filter on; pahole would need to understand individual Rust DWARF constructs (Rust enums, trait objects, etc.), which is a moving target. This is what the LWN article you linked covers in detail. The kernel's current Kconfig constraint is:

config RUST
    depends on !DEBUG_INFO_BTF || (PAHOLE_HAS_LANG_EXCLUDE && !LTO)

Having the compiler emit BTF directly and the linker merge it sidesteps the problem entirely — no DWARF→BTF conversion, no pahole dependency, and LTO + Rust + BTF all work together.

Plans for ld.bfd?

I'm not aware of plans on the GNU side, but GCC already emits BTF via -gbtf, so the compiler half is there. Linker-side BTF merging in ld.bfd would make sense for parity. That said, it's outside our scope here — we can only control what lld does.

Human-readable BTF dump / splitting the patch

Agreed this would help. llvm-objdump already uses BTFParser for inline BPF relocation annotation, but there's no standalone --dump-btf flag today. Adding a human-readable BTF dump to llvm-readelf (or llvm-objdump) as a prerequisite patch would let the lld LIT tests use llvm-readelf --dump-btf instead of raw hex, and would also make the patch series easier to review incrementally. I'll look into splitting this out.

CTF support in GNU objdump → BTF?

CTF (Compact C Type Format) and BTF (BPF Type Format) are structurally different formats despite some conceptual overlap — CTF comes from Solaris/DTrace, BTF from the Linux BPF subsystem. GNU objdump's --ctf support wouldn't directly extend to BTF. The right path is a native BTF dumper in LLVM's own tools, which can then be used in tests.

@github-actions

github-actions Bot commented Mar 3, 2026

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

@github-actions

github-actions Bot commented Mar 3, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 192296 tests passed
  • 4926 tests skipped

✅ The build succeeded and all tests passed.

Comment thread lld/ELF/Config.h Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

place among b options

Comment thread lld/ELF/Driver.cpp Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert

Comment thread lld/ELF/Driver.cpp Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert unneeded change

Comment thread lld/ELF/SyntheticSections.cpp Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please only format the modified lines, not the whole file.

@yonghong-song

Copy link
Copy Markdown
Contributor

@caniko I have a few questions below:

  1. Could you give an end-to-end example (from source to the final binary with BTF)? It looked at the patch and having the test BTFBuilderTest.cpp and BTFDedupTest.cpp, but I didn't find the llvm code which actually generates BTF. Is is done by rust compiler part? How about C parts?
  2. You mentioned it is a moving part for rust to generate some debuginfo. I am not sure how much rust may change their debuginfo for the same construct. But I think hopefully pahole can learn it. I am not familiar with rust. but for each file, the dwarf will have its own cu. So I do not understand how one cu can have both rust and C. Could you give an example for this?
  3. Please take a look at pahole. pahole generates BTF based on dwarf types but also based on some other vmlinux.o sections (e.g., .BTF_ids, .data..percpu, etc.) and also did some analysis. Also, in pahole, there are some efforts to recover the true signature for some static functions based on locations, where those functions have been optimized. But these are kernel/pahole specific stuff, llvm probably do not like it. Although later kernel/bpf can correct it but it may need to adjust the compiler generated btf. We will need to check how it works and how complex it is.

@caniko

caniko commented Mar 15, 2026

Copy link
Copy Markdown
Author
  1. Could you give an end-to-end example (from source to the final binary with BTF)? It looked at the patch and having the test BTFBuilderTest.cpp and BTFDedupTest.cpp, but I didn't find the llvm code which actually generates BTF. Is is done by rust compiler part? How about C parts?

The BTF generation from DWARF happens in a companion PR, #183929 (-gbtf). That PR adds a target-independent pass in Clang/LLVM that converts DWARF debug info to .BTF sections at compile time, per object file. So the flow is: clang -gbtf -c foo.c produces foo.o with a .BTF section, then ld.lld --btf-merge foo.o bar.o -o out merges and deduplicates those sections. GCC already has the equivalent with its own -gbtf flag. This PR only covers the linker side (merge + dedup).

  1. You mentioned it is a moving part for rust to generate some debuginfo. I am not sure how much rust may change their debuginfo for the same construct. But I think hopefully pahole can learn it. I am not familiar with rust. but for each file, the dwarf will have its own cu. So I do not understand how one cu can have both rust and C. Could you give an example for this?

With LTO, the linker performs cross-module inlining and merging before emitting DWARF. When you have a mixed C + Rust kernel build with LTO enabled, the linker can inline a Rust function into a C function (or vice versa), and the resulting DWARF ends up in a single CU that contains types from both languages. Pahole's PAHOLE_HAS_LANG_EXCLUDE filters entire CUs by DW_AT_language, so it can't selectively remove just the Rust types from a mixed CU. That's the fundamental problem. With the approach in this PR, each compiler emits .BTF directly before linking, so there's no post-link DWARF interpretation needed, and the Rust/C mixing issue goes away entirely.

  1. Please take a look at pahole. pahole generates BTF based on dwarf types but also based on some other vmlinux.o sections (e.g., .BTF_ids, .data..percpu, etc.) and also did some analysis. Also, in pahole, there are some efforts to recover the true signature for some static functions based on locations, where those functions have been optimized. But these are kernel/pahole specific stuff, llvm probably do not like it. Although later kernel/bpf can correct it but it may need to adjust the compiler generated btf. We will need to check how it works and how complex it is.

Yes, pahole handles things beyond type conversion: .BTF_ids, .data..percpu, and the static function signature recovery work. This PR doesn't aim to replace all of pahole right away. The immediate goal is just the type conversion and dedup part, which is the piece that breaks with LTO + Rust. The kernel-specific post-processing (percpu annotations, BTF_ids, signature recovery) can still be done by pahole on top of the linker-generated .BTF, or could be moved into the linker incrementally later. We'll need to work through those cases, but they're separable from the core type merging problem this PR solves.

@yonghong-song

Copy link
Copy Markdown
Contributor

Thanks for -gbtf pull request link, I forgot it. I build a clang binary with -gbtf pull request and this lld pull request and used this build for bpf-next kernel without LTO. I got the following error:

  ld.lld -m elf_x86_64 --fatal-warnings -z noexecstack --btf-merge -r -o vmlinux.o   --whole-archive vmlinux.a --no-whole-archive --start-group  --end-group  ; ./tools/objtool/objtool --hacks=jump_label --hacks=noinstr --hacks=skylake --ibt --mcount --mnop --orc --retpoline --rethunk --static-call --uaccess --prefix=16 --link vmlinux.o
vmlinux.o: warning: objtool: .BTF.ext+0xa4: data relocation to !ENDBR: IS_ERR_OR_NULL+0x0
vmlinux.o: warning: objtool: .BTF.ext+0xd4: data relocation to !ENDBR: get_random_canary+0x0                            
vmlinux.o: warning: objtool: .BTF.ext+0xdc: data relocation to !ENDBR: rcu_read_unlock+0x0
...

There are some thing wrong for objtool.
And subsequently, I see more warnings like

   ./scripts/mod/modpost -M -m -b  -a      -o Module.symvers -T modules.order vmlinux.o
WARNING: modpost: vmlinux (.BTF): unexpected non-allocatable section.
Did you forget to use "ax"/"aw" in a .S file?
Note that for example <linux/init.h> contains
section definitions for use in .S files.

WARNING: modpost: vmlinux (.BTF.ext): unexpected non-allocatable section.
Did you forget to use "ax"/"aw" in a .S file?
Note that for example <linux/init.h> contains
section definitions for use in .S files.
...

and also this failure

+ ld.lld -m elf_x86_64 --fatal-warnings -z noexecstack --btf-merge -z max-page-size=0x200000 --build-id=sha1 --orphan-handling=error --script=./arch/x86/kernel/vmlinux.lds -o .tmp_vmlinux1 --whole-archive vmlinux.o .vmlinux.export.o init/version-timestamp.o --no-whole-archive --start-group --end-group .tmp_vmlinux0.kallsyms.o
ld.lld: error: vmlinux.o:(.BTF.ext) is being placed in '.BTF.ext'

Note that we have not reached pahole yet.
We should ensure end-to-end solution (i.e., including pahole/kernel part) works before merging.

@yonghong-song

yonghong-song commented Mar 16, 2026

Copy link
Copy Markdown
Contributor
  1. You mentioned it is a moving part for rust to generate some debuginfo. I am not sure how much rust may change their debuginfo for the same construct. But I think hopefully pahole can learn it. I am not familiar with rust. but for each file, the dwarf will have its own cu. So I do not understand how one cu can have both rust and C. Could you give an example for this?

With LTO, the linker performs cross-module inlining and merging before emitting DWARF. When you have a mixed C + Rust kernel build with LTO enabled, the linker can inline a Rust function into a C function (or vice versa), and the resulting DWARF ends up in a single CU that contains types from both languages. Pahole's PAHOLE_HAS_LANG_EXCLUDE filters entire CUs by DW_AT_language, so it can't selectively remove just the Rust types from a mixed CU. That's the fundamental problem. With the approach in this PR, each compiler emits .BTF directly before linking, so there's no post-link DWARF interpretation needed, and the Rust/C mixing issue goes away entirely.

One suggestion. Can we add an option in FunctionImport.cpp to avoid cross file inlining between C and Rust language (or broadly between different languages)?
This option can be enabled in linux kernel.
Can this solve your problems?

Does dwarf for rust have stable dwarf generation or the dwarf generation quite different from versions to versions. If it is stable, I guess pahole can try to learn those dwarf. If not stable, then probably we can use the above option for a while until rust dwarf generation becomes stable.

@caniko

caniko commented Mar 16, 2026

Copy link
Copy Markdown
Author

I build a clang binary with -gbtf pull request and this lld pull request and used this build for bpf-next kernel without LTO. I got the following error: [...] objtool [...] modpost [...] orphan-handling=error

These are all caused by .BTF.ext sections passing through the link untouched. Right now --btf-merge only handles .BTF sections. After merge+dedup the type IDs are remapped, which makes any surviving .BTF.ext sections stale (they reference the old per-object type IDs). The linker should discard .BTF.ext input sections when --btf-merge is active. I'll add that.

For the kernel side, .BTF.ext in vmlinux was never needed in the pahole flow either. Once we discard it in the linker, the objtool, modpost, and orphan-handling errors all go away.

We should ensure end-to-end solution (i.e., including pahole/kernel part) works before merging.

Agreed that integration testing matters. I'll put together a working kernel build (bpf-next, non-LTO) with both PRs and post the results here before this is ready to land. The kernel Makefile/Kconfig changes needed to wire up -gbtf + --btf-merge will be a separate kernel patch series.

Can we add an option in FunctionImport.cpp to avoid cross file inlining between C and Rust language (or broadly between different languages)? This option can be enabled in linux kernel. Can this solve your problems?

It wouldn't be sufficient. The problem isn't just cross-language inlining. With full LTO, the linker merges IR modules into one before codegen, so you end up with a single CU containing types from both languages regardless of inlining decisions. Blocking cross-language inlining would also give up a key optimization benefit of LTO in the first place.

More fundamentally, even without LTO, pahole still can't convert Rust DWARF to BTF. Blocking inlining would only help with the mixed-CU filtering issue, not the underlying "pahole doesn't understand Rust types" issue.

Does dwarf for rust have stable dwarf generation or the dwarf generation quite different from versions to versions. If it is stable, I guess pahole can try to learn those dwarf. If not stable, then probably we can use the above option for a while until rust dwarf generation becomes stable.

It is not stable. rustc has changed its DWARF representation for enums multiple times (the move to DW_TAG_variant_part was relatively recent). Trait objects, closures, async state machines, and dyn types all use DWARF patterns that have no C equivalent and have shifted between Rust editions and compiler versions. Teaching pahole to track a moving target across two independent projects is fragile by design. With -gbtf, the compiler that understands its own types emits BTF directly, so that coupling goes away.

Add support for merging and deduplicating .BTF (BPF Type Format) sections
in lld's ELF linker, enabling the LTO + Rust + BTF pipeline in the Linux
kernel.

The implementation consists of:

1. BTFBuilder (llvm/lib/DebugInfo/BTF/BTFBuilder.cpp): A mutable BTF
   container that can parse, merge, and serialize BTF sections. Supports
   merging multiple .BTF sections with automatic type ID and string
   offset remapping.

2. BTFDedup (llvm/lib/DebugInfo/BTF/BTFDedup.cpp): A 5-pass dedup
   algorithm ported from libbpf's btf_dedup:
   - String deduplication via hash-based interning
   - Primitive type dedup (INT, FLOAT, ENUM, ENUM64, FWD)
   - Composite type dedup (STRUCT, UNION) with DFS cycle handling
   - Reference type dedup (PTR, TYPEDEF, CONST, VOLATILE, etc.)
   - Type compaction and ID remapping

3. BtfSection (lld/ELF/SyntheticSections.cpp): A SyntheticSection that
   collects .BTF input sections, merges and deduplicates them, and emits
   a single merged .BTF output section. Enabled via --btf-merge flag.
@4ast

4ast commented Apr 14, 2026

Copy link
Copy Markdown
Member

Agreed that integration testing matters. I'll put together a working kernel build (bpf-next, non-LTO) with both PRs and post the results here before this is ready to land.

what is the status of it?
If clang LTO can build the kernel with -gbtf and deduped vmlinux BTF is equivalent to what pahole does then I think it's a good long term direction.
The copy of libbpf's dedup logic is almost a show stopper, but if the linker can handle it fine, then maybe we can deprecate dedup in pahole/libbpf and use only this version. It would need to work with both gcc -gbtf and clang -gbtf input .o-s.

Trait objects, closures, async state machines, and dyn types all use DWARF patterns that have no C equivalent and have shifted between Rust editions and compiler versions.

What is the story of adding support for all of them to BTF ?
BTF is fully extensible and it should express all Rust types in the long run.

@caniko

caniko commented Apr 20, 2026

Copy link
Copy Markdown
Author

Status as of April 20, 2026: I now have a working bpf-next x86_64 non-LTO build at 1f5ffc672165ff851063a5fd044b727ab2517ae3 with the current patch stack, final vmlinux linked with native .BTF and no pahole, and a QEMU boot smoke is green (/sys/kernel/btf/vmlinux is present in the guest).

What I do not have green yet is the full reviewer-complete receipt set: full selftests/bpf is still blocked in the native path, I do not have ThinLTO/FullLTO receipts yet, and the final native vmlinux BTF is not yet equivalent to the pahole baseline. The remaining work has moved past the earlier final-link plumbing issue and is now in native-BTF fidelity.

On the dedup copy concern: the intent is still to make linker-side merge/dedup the consolidation point rather than carry a permanent third implementation. I do have small receipts for clang-only, gcc-only, and mixed clang+gcc -gbtf objects linking successfully with ld.lld --btf-merge, with merged .BTF surviving and parsing. But the kernel-level receipts are not complete enough yet to claim this can replace pahole/libbpf today. The current native blockers I am debugging are a final merged BTF FWD/phy issue and missing iterator-context types in the native path.

On Rust types: agreed that BTF should grow to express Rust-specific constructs in the long run. I see that as complementary to this work rather than competing with it. Even with future BTF extensions for Rust, we still need linker-level merge/dedup for native .BTF sections. I am keeping the Rust-type expressiveness question separate from getting the current kernel build/LTO/selftests receipts into shape.

My next receipts are: full selftests/bpf on the native non-LTO build, ThinLTO/FullLTO probes, and final native-vs-pahole BTF comparison. I’ll post those before calling this ready.

@4ast

4ast commented Apr 22, 2026

Copy link
Copy Markdown
Member

My next receipts are: full selftests/bpf on the native non-LTO build, ThinLTO/FullLTO probes, and final native-vs-pahole BTF comparison. I’ll post those before calling this ready.

All sounds great. Thanks for the update

@caniko

caniko commented Apr 29, 2026

Copy link
Copy Markdown
Author

Status update as of April 29, 2026, following the April 20 update.

I have a fresh native-BTF validation rollup for bpf-next 1f5ffc672165ff851063a5fd044b727ab2517ae3. The receipts are now published through btf-linker-validation at 8cf6607:

The build and runtime plumbing is now in much better shape:

  • native non-LTO kernel build completes with pahole_invoked=no
  • pahole baseline non-LTO build completes with pahole_invoked=yes
  • QEMU boot smoke passes and /sys/kernel/btf/vmlinux is visible in the guest
  • static native BTF checks pass: no zero-sized final DATASEC, no stale input DATASEC leakage (.apicdrivers, .bss, .data), exactly one rebuilt nonzero .data..percpu
  • generated vmlinux.h no longer contains cgroup_subsys_state___2
  • selftests/bpf install passes
  • focused QEMU selftests smoke passes with selftests_smoke_rc=0
  • native ThinLTO and FullLTO builds complete
  • clang-only, gcc-only, and mixed clang/gcc -gbtf input matrix passes

Broad selftests/bpf is not fully green yet, but the current blockers are paired against pahole and do not look native-BTF-specific. fentry_attach_stress panics on both native and pahole kernels, so the broad run excludes it explicitly. With that exclusion, both native and pahole broad runs later panic at bpf_prog_get_assoc_struct_ops; the paired classification is shared_kernel_selftest_runtime, not an LLVM native-BTF blocker.

I am not claiming native-vs-pahole BTF equivalence yet. The current classified C-dump comparison is:

  • native declarations parsed: 21575
  • pahole declarations parsed: 22807
  • exact declaration matches: 19990
  • param-name-only/order differences: 15
  • same-key text differences: 1651
  • native-only declarations: 25
  • pahole-only declarations: 1155

The dominant residual buckets are anonymous enum text differences and pahole-only struct tags. The validation harness now emits btf-compare/classified-summary.md and btf-compare/classified-details.tsv via classify-btf-diff.py, so the remaining fidelity work is categorized rather than left as a raw diff.

@caniko

caniko commented May 8, 2026

Copy link
Copy Markdown
Author

Greetings @4ast, I would really value your feedback; guidance on next steps is appreciated! Thank you

@4ast

4ast commented May 8, 2026

Copy link
Copy Markdown
Member

Greetings @4ast, I would really value your feedback; guidance on next steps is appreciated! Thank you

generated vmlinux.h no longer contains cgroup_subsys_state___2

I don't see it in vmlinux.h and I don't think anyone reported such issue.

Please post the analysis on bpf @ vger.kernel.org so that broad community can participate.

Without pahole some difference in vmlinux.h is expected. Like kfuncs are probably not there,
but overall it should match. If there is a difference it's likely a bug in your copy pasted dedup logic.
For example see this recent fix:
https://lore.kernel.org/bpf/20260417083319.32716-1-atenart@kernel.org/

and also this patch and the thread:
https://lore.kernel.org/bpf/20251203191507.55565-1-alan.maguire@oracle.com/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants