BPF List
 help / color / mirror / Atom feed
* [PATCHES 00/12] pahole: Support more rust tags and references to dwz alternate debug files
@ 2026-07-31 19:30 Arnaldo Carvalho de Melo
  2026-07-31 19:30 ` [PATCH 01/12] dwarf_loader: Initial support for DW_TAG_variant_part Arnaldo Carvalho de Melo
                   ` (11 more replies)
  0 siblings, 12 replies; 13+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-07-31 19:30 UTC (permalink / raw)
  To: Alan Maguire
  Cc: Jiri Olsa, Clark Williams, dwarves, bpf, Andrii Nakryiko,
	Yonghong Song, Mark Wieelard, Arnaldo Carvalho de Melo

Hi,

Add support for Rust discriminated unions (enums with explicit
discriminants).

Rust enums like Option<u32> and Result<i32,u8> are represented in DWARF
as DW_TAG_variant_part containing DW_TAG_variant children with
discriminant values. These patches:

  - Load DW_TAG_variant_part containers with discriminant tracking
  - Populate DW_TAG_variant children with discriminant values
  - Handle DW_FORM_block encoding for discriminant values
  - Handle DW_TAG_subprogram inside DW_TAG_enumeration (Rust methods)
  - Encode variant parts as BTF_KIND_UNION members

Before: Rust Option<u32> showed as empty struct (0 members)

After:  Shows None and Some variants with correct layout

  === pahole --expand_types output ===
  struct Option<u32> {
      struct None {
      } __attribute__((__aligned__(4)));
      struct Some {
          /* XXX 4 bytes hole, try to pack */
          u32  __0 __attribute__((__aligned__(4)));  /*  4  4 */
      } __attribute__((__aligned__(4)));
  } __attribute__((__aligned__(4)));

BTF has no discriminated-union kind, so variant parts are tentatively
encoded as BTF_KIND_UNION with overlapping members at offset 0. Further
discussion is needed to see if we can keep that.

DW_FORM_block byte order fix: uses CU-recorded endianness instead of
assuming little-endian when decoding DW_AT_const_value and
DW_AT_default_value. Fixes cross-endian DWARF processing (e.g.  reading
big-endian s390x debug info on x86).

Also add support for cross-CU type references and dwz alternate debug
files.

This is needed to improve the support for, among other things, Rust, as
noticed in the pretty printing or a perf binary that has rust objects
linked, built by clang/llvm that use cross-CU references.

DW_TAG_imported_unit support:

  Handle same-file partial units where types are shared across CUs
  via DW_TAG_imported_unit. Force-merge CUs that contain inter-CU
  references (DW_FORM_ref_addr) so type lookups resolve correctly.

  Fix cus__merging_cu failing to detect DW_FORM_ref_addr when
  DW_FORM_implicit_const causes dwarf_getabbrevattr() to fail.

dwz alternate debug file support:

  Handle DW_FORM_GNU_ref_alt references to dwz-compressed alternate
  debug files (.dwz). Pre-processes all alternate partial units with
  separate hash tables and dedup tracking.

  Before (Firefox): 1774 "has no entry in cu" errors
  After:  processes cleanly with 0 errors

  Firefox uses dwz compression which moves common types into a
  separate .dwz file and replaces duplicates with DW_FORM_GNU_ref_alt
  references. pahole now pre-loads the alternate file's partial units,
  resolves all cross-file references, and processes Firefox DWARF
  cleanly.

Includes inter-CU type reference comparison test and
vmlinux_comparison.py for DWARF/BTF analysis across kernel configs.

Known limitations:
  - Pruning of unreferenced alt PUs is conservatively over-approximated
  - Merged-CU path is single-threaded by design

This makes pahole to be able to support more of the CONFIG_DEBUG_ DWARF
options, including DWARF5 and compression, see the latest patch in the
series for more details about how this table is produced:

  ┌───────────────────────────┬─────────┬─────┬───────────────────────────────────┬─────────┬───────┬──────────┬────────┬──────────┬─────────┐
  │ File                      │  Size   │ Ver │ Producer                          │  DWARF  │  BTF  │ DW-Hash  │ DW-Out │ BTF-Hash │ BTF-Out │
  ├───────────────────────────┼─────────┼─────┼───────────────────────────────────┼─────────┼───────┼──────────┼────────┼──────────┼─────────┤
  │ vmlinux.dwarf4            │ 835.7MB │   4 │ GNU C11 16.1.1 -gdwarf-4          │ 599.2MB │ 7.3MB │ 86e1b611 │  7.6MB │ 52123c24 │   7.4MB │
  │ vmlinux.dwarf5            │ 736.3MB │   5 │ GNU C11 16.1.1 -gdwarf-5          │ 499.7MB │ 7.3MB │ 86e1b611 │  7.6MB │ 52123c24 │   7.4MB │
  │ vmlinux.dwarf5.zlib       │ 497.8MB │   5 │ GNU C11 16.1.1 -gdwarf-5 -gz=zlib │ 261.2MB │ 7.3MB │ 86e1b611 │  7.6MB │ 52123c24 │   7.4MB │
  │ vmlinux.dwarf5.zstd       │ 454.7MB │   5 │ GNU C11 16.1.1 -gdwarf-5 -gz=zstd │ 218.1MB │ 7.3MB │ 86e1b611 │  7.6MB │ 52123c24 │   7.4MB │
  │ vmlinux.toolchain_default │ 736.3MB │   5 │ GNU C11 16.1.1                    │ 499.7MB │ 7.3MB │ 86e1b611 │  7.6MB │ 52123c24 │   7.4MB │
  └───────────────────────────┴─────────┴─────┴───────────────────────────────────┴─────────┴───────┴──────────┴────────┴──────────┴─────────┘

Split DWARF support (skeleton CUs) will be supported in upcoming work,
so as to cover all the possibilities the kernel build system offers
for generating DWARF.

- Arnaldo

Arnaldo Carvalho de Melo (12):
  dwarf_loader: Initial support for DW_TAG_variant_part
  dwarf_loader: Initial support for DW_TAG_subprogram in DW_TAG_enumeration
  dwarf_loader: Populate DW_TAG_variant children in DW_TAG_variant_part
  btf_encoder: Encode variant parts as union members in BTF
  dwarf_loader: Handle DW_FORM_block in attr_numeric for Rust discriminant values
  dwarf_loader: Allow forcing the merge of CUs for solving inter CU tag references
  dwarf_loader: Support DW_TAG_imported_unit for same-file partial units
  dwarf_loader: Fix cus__merging_cu failing to detect DW_FORM_ref_addr
  dwarf_loader: Add cu parameter to tag__set_spec() and dwarf_tag__set_attr_type()
  dwarf_loader: Support DW_FORM_GNU_ref_alt references to dwz alternate debug files
  tests: Add inter-CU type reference comparison test
  scripts: Add vmlinux_comparison.py for DWARF/BTF analysis

 btf_encoder.c                 |  66 ++-
 ctf_encoder.c                 |  34 +-
 dwarf_loader.c                | 873 ++++++++++++++++++++++++++++++----
 dwarves.c                     |  77 ++-
 dwarves.h                     |  63 ++-
 dwarves_emit.c                |  10 +-
 dwarves_fprintf.c             |  54 ++-
 man-pages/pahole.1            |  18 +-
 pahole.c                      |  20 +-
 scripts/vmlinux_comparison.py | 367 ++++++++++++++
 tests/block_endian.sh         | 146 ++++++
 tests/dwz_alt_file.sh         | 204 ++++++++
 tests/inter_cu_refs.sh        |  50 ++
 tests/prettify_perf.data.sh   |   4 +-
 14 files changed, 1854 insertions(+), 132 deletions(-)
 create mode 100755 scripts/vmlinux_comparison.py
 create mode 100755 tests/block_endian.sh
 create mode 100755 tests/dwz_alt_file.sh
 create mode 100755 tests/inter_cu_refs.sh

-- 
2.55.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH 01/12] dwarf_loader: Initial support for DW_TAG_variant_part
  2026-07-31 19:30 [PATCHES 00/12] pahole: Support more rust tags and references to dwz alternate debug files Arnaldo Carvalho de Melo
@ 2026-07-31 19:30 ` Arnaldo Carvalho de Melo
  2026-07-31 19:30 ` [PATCH 02/12] dwarf_loader: Initial support for DW_TAG_subprogram in DW_TAG_enumeration Arnaldo Carvalho de Melo
                   ` (10 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-07-31 19:30 UTC (permalink / raw)
  To: Alan Maguire
  Cc: Jiri Olsa, Clark Williams, dwarves, bpf, Andrii Nakryiko,
	Yonghong Song, Mark Wieelard, Arnaldo Carvalho de Melo

From: Arnaldo Carvalho de Melo <acme@redhat.com>

Still doesn't handle its sub hierarchy, i.e. the DW_TAG_variant entries
and its underlying DW_TAG_member entries.

This was noticed when running the regression test that uses a debug
build of perf to process a perf.data file and test pahole's pretty
printing features, as now perf has a synthetic workload that is written
in rust:

 <0><20c95a>: Abbrev Number: 1 (DW_TAG_compile_unit)
    <20c95b>   DW_AT_producer    : (indirect string, offset: 0x4ee5a): clang LLVM (rustc version 1.93.1 (01f6ddf75 2026-02-11) (Fedora 1.93.1-1.fc43))
    <20c95f>   DW_AT_language    : 28	(Rust)
    <20c961>   DW_AT_name        : (indirect string, offset: 0x4eeaa): tests/workloads/code_with_type.rs/@/code_with_type.d6e680867bfb8b27-cgu.0
    <20c965>   DW_AT_stmt_list   : 0x5e1ed
    <20c969>   DW_AT_comp_dir    : (indirect string, offset: 0x487f1): /home/acme/git/perf-tools/tools/perf
    <20c96d>   DW_AT_low_pc      : 0
    <20c975>   DW_AT_ranges      : 0x2d0
⬢ [acme@toolbx pahole]$

So lets add some scaffolding for the Rust DWARF constructs involved for
us to be able to continue using perf with DWARF to test the pretty
printing features.

Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 dwarf_loader.c | 26 +++++++++++++++++++++++++-
 dwarves.c      | 27 +++++++++++++++++++++++++++
 dwarves.h      | 16 ++++++++++++++++
 3 files changed, 68 insertions(+), 1 deletion(-)

diff --git a/dwarf_loader.c b/dwarf_loader.c
index 61d55bd70d643731..af166ec9064e82c6 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -522,6 +522,8 @@ static void tag__init(struct tag *tag, struct cu *cu, Dwarf_Die *die)
 
 	if (tag->tag == DW_TAG_imported_module || tag->tag == DW_TAG_imported_declaration)
 		dwarf_tag__set_attr_type(dtag, type, die, DW_AT_import);
+	else if (tag->tag == DW_TAG_variant_part)
+		dwarf_tag__set_attr_type(dtag, type, die, DW_AT_discr);
 	else
 		dwarf_tag__set_attr_type(dtag, type, die, DW_AT_type);
 
@@ -1238,6 +1240,18 @@ static struct template_parameter_pack *template_parameter_pack__new(Dwarf_Die *d
 	return pack;
 }
 
+static struct variant_part *variant_part__new(Dwarf_Die *die, struct cu *cu, struct conf_load *conf)
+{
+	struct variant_part *vpart = tag__alloc(cu, sizeof(*vpart));
+
+	if (vpart != NULL) {
+		tag__init(&vpart->tag, cu, die);
+		INIT_LIST_HEAD(&vpart->variants);
+	}
+
+	return vpart;
+}
+
 /* Returns number of locations found or negative value for errors. */
 static ptrdiff_t __dwarf_getlocations(Dwarf_Attribute *attr,
 				      ptrdiff_t offset, Dwarf_Addr *basep,
@@ -2357,9 +2371,19 @@ static int die__process_class(Dwarf_Die *die, struct type *class,
 		case DW_TAG_GNU_template_template_param:
 #endif
 		case DW_TAG_subrange_type: // XXX: ADA stuff, its a type tho, will have other entries referencing it...
-		case DW_TAG_variant_part: // XXX: Rust stuff
 			tag__print_not_supported(die);
 			continue;
+		case DW_TAG_variant_part: {
+			struct variant_part *vpart = variant_part__new(die, cu, conf);
+
+			if (vpart == NULL)
+				return -ENOMEM;
+
+			// For rust it seems we have just one, but DWARF, according to Gemini, support having
+			// more than one DW_TAG_variant_part for a given DW_TAG_structure_type, so future proof it
+			type__add_variant_part(class, vpart);
+			continue;
+		}
 		case DW_TAG_template_type_parameter: {
 			struct template_type_param *ttparm = template_type_param__new(die, cu, conf);
 
diff --git a/dwarves.c b/dwarves.c
index 1748889bc051fcb4..b48d9a3b9c43eebc 100644
--- a/dwarves.c
+++ b/dwarves.c
@@ -405,6 +405,7 @@ void __type__init(struct type *type)
 	INIT_LIST_HEAD(&type->type_enum);
 	INIT_LIST_HEAD(&type->template_type_params);
 	INIT_LIST_HEAD(&type->template_value_params);
+	INIT_LIST_HEAD(&type->variant_parts);
 	type->template_parameter_pack = NULL;
 	type->sizeof_member = NULL;
 	type->member_prefix = NULL;
@@ -1210,12 +1211,31 @@ static void type__delete_class_members(struct type *type, struct cu *cu)
 	}
 }
 
+static void variant_part__delete(struct variant_part *vpart, struct cu *cu)
+{
+	if (vpart == NULL)
+		return;
+
+	cu__tag_free(cu, &vpart->tag);
+}
+
+static void type__delete_variant_parts(struct type *type, struct cu *cu)
+{
+	struct variant_part *pos, *next;
+
+	type__for_each_variant_part_safe_reverse(type, pos, next) {
+		list_del_init(&pos->tag.node);
+		variant_part__delete(pos, cu);
+	}
+}
+
 void class__delete(struct class *class, struct cu *cu)
 {
 	if (class == NULL)
 		return;
 
 	type__delete_class_members(&class->type, cu);
+	type__delete_variant_parts(&class->type, cu);
 	cu__tag_free(cu, class__tag(class));
 }
 
@@ -1225,6 +1245,7 @@ void type__delete(struct type *type, struct cu *cu)
 		return;
 
 	type__delete_class_members(type, cu);
+	type__delete_variant_parts(type, cu);
 
 	if (type->suffix_disambiguation)
 		zfree(&type->namespace.name);
@@ -1288,6 +1309,11 @@ void type__add_template_value_param(struct type *type, struct template_value_par
 	list_add_tail(&tvparam->tag.node, &type->template_value_params);
 }
 
+void type__add_variant_part(struct type *type, struct variant_part *vpart)
+{
+	list_add_tail(&vpart->tag.node, &type->variant_parts);
+}
+
 struct class_member *type__last_member(struct type *type)
 {
 	struct class_member *pos;
@@ -1308,6 +1334,7 @@ static int type__clone_members(struct type *type, const struct type *from, struc
 	INIT_LIST_HEAD(&type->type_enum);
 	INIT_LIST_HEAD(&type->template_type_params);
 	INIT_LIST_HEAD(&type->template_value_params);
+	INIT_LIST_HEAD(&type->variant_parts);
 
 	type__for_each_member(from, pos) {
 		struct class_member *clone = class_member__clone(pos, cu);
diff --git a/dwarves.h b/dwarves.h
index 99e9d183c853ea8b..0fc57ed0c7576c01 100644
--- a/dwarves.h
+++ b/dwarves.h
@@ -1021,6 +1021,11 @@ static inline struct formal_parameter_pack *tag__formal_parameter_pack(const str
 
 void formal_parameter_pack__add(struct formal_parameter_pack *pack, struct parameter *param);
 
+struct variant_part {
+	struct tag	 tag;
+	struct list_head variants;
+};
+
 /*
  * tag.tag can be DW_TAG_subprogram_type or DW_TAG_subroutine_type.
  */
@@ -1292,6 +1297,7 @@ struct type {
 	uint8_t		 is_signed_enum:1;
 	struct list_head template_type_params;
 	struct list_head template_value_params;
+	struct list_head variant_parts;
 	struct template_parameter_pack *template_parameter_pack;
 };
 
@@ -1409,9 +1415,19 @@ static inline struct class_member *class_member__next(struct class_member *membe
 #define type__for_each_tag_safe_reverse(type, pos, n) \
 	list_for_each_entry_safe_reverse(pos, n, &(type)->namespace.tags, tag.node)
 
+/**
+ * type__for_each_variant_part_safe_reverse - safely iterate thru all variant_parts in a type, in reverse order
+ * @type: struct type instance to iterate
+ * @pos: struct variant_part iterator
+ * @n: struct variant_part temp iterator
+ */
+#define type__for_each_variant_part_safe_reverse(type, pos, n) \
+	list_for_each_entry_safe_reverse(pos, n, &(type)->variant_parts, tag.node)
+
 void type__add_member(struct type *type, struct class_member *member);
 void type__add_template_type_param(struct type *type, struct template_type_param *ttparm);
 void type__add_template_value_param(struct type *type, struct template_value_param *tvparam);
+void type__add_variant_part(struct type *type, struct variant_part *vpart);
 
 struct class_member *
 	type__find_first_biggest_size_base_type_member(struct type *type,
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 02/12] dwarf_loader: Initial support for DW_TAG_subprogram in DW_TAG_enumeration
  2026-07-31 19:30 [PATCHES 00/12] pahole: Support more rust tags and references to dwz alternate debug files Arnaldo Carvalho de Melo
  2026-07-31 19:30 ` [PATCH 01/12] dwarf_loader: Initial support for DW_TAG_variant_part Arnaldo Carvalho de Melo
@ 2026-07-31 19:30 ` Arnaldo Carvalho de Melo
  2026-07-31 19:30 ` [PATCH 03/12] dwarf_loader: Populate DW_TAG_variant children in DW_TAG_variant_part Arnaldo Carvalho de Melo
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-07-31 19:30 UTC (permalink / raw)
  To: Alan Maguire
  Cc: Jiri Olsa, Clark Williams, dwarves, bpf, Andrii Nakryiko,
	Yonghong Song, Mark Wieelard, Arnaldo Carvalho de Melo

From: Arnaldo Carvalho de Melo <acme@redhat.com>

In Rust enums can have subprograms, add initial support for it.

Example of a Rust enumeration with a DW_TAG_subprogram tag.

  $ pahole -C ProgramKind /tmp/build/perf-tools-next/tests/workloads/code_with_type.a
  enum ProgramKind {
  	PathLookup = 0,
  	Relative   = 1,
  	Absolute   = 2,
  	enum ProgramKind new(struct &std::ffi::os_str::OsStr),
  } __attribute__((__packed__));
  $

Reviewed-by: Alan Maguire <alan.maguire@oracle.com>
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 btf_encoder.c     | 26 +++++++++++++++++------
 ctf_encoder.c     | 23 ++++++++++++++------
 dwarf_loader.c    | 39 +++++++++++++++++++++++++++-------
 dwarves.c         | 10 ++++-----
 dwarves.h         |  2 +-
 dwarves_emit.c    | 10 ++++-----
 dwarves_fprintf.c | 54 +++++++++++++++++++++++++++++++++++------------
 pahole.c          |  9 +++++---
 8 files changed, 123 insertions(+), 50 deletions(-)

diff --git a/btf_encoder.c b/btf_encoder.c
index 4b422e09800f3fbb..83ce21186ea7da36 100644
--- a/btf_encoder.c
+++ b/btf_encoder.c
@@ -1888,7 +1888,7 @@ static int32_t btf_encoder__add_struct_type(struct btf_encoder *encoder, struct
 }
 
 static int32_t btf_encoder__add_enum_type(struct btf_encoder *encoder, struct tag *tag,
-					  struct conf_load *conf_load)
+					  const struct cu *cu, struct conf_load *conf_load)
 {
 	struct type *etype = tag__type(tag);
 	struct enumerator *pos;
@@ -1900,9 +1900,22 @@ static int32_t btf_encoder__add_enum_type(struct btf_encoder *encoder, struct ta
 		return type_id;
 
 	type__for_each_enumerator(etype, pos) {
-		name = enumerator__name(pos);
-		if (btf_encoder__add_enum_val(encoder, name, pos->value, etype, conf_load))
-			return -1;
+		switch (pos->tag.tag) {
+		case DW_TAG_enumerator:
+			name = enumerator__name(pos);
+			if (btf_encoder__add_enum_val(encoder, name, pos->value, etype, conf_load))
+				return -1;
+			break;
+		case DW_TAG_subprogram:
+			if (encoder->verbose)
+				fprintf(stderr, "BTF: DW_TAG_subprogram in enumeration '%s' not supported, skipping\n",
+					type__name(etype) ?: "(anonymous)");
+			break;
+		default:
+			fprintf(stderr, "BTF: unexpected DW_TAG_%s in enumeration '%s', skipping\n",
+				dwarf_tag_name(pos->tag.tag), type__name(etype) ?: "(anonymous)");
+			break;
+		}
 	}
 
 	return type_id;
@@ -1929,8 +1942,7 @@ static int tag__nr_btf_ids(const struct tag *tag)
 }
 
 static int btf_encoder__encode_tag(struct btf_encoder *encoder, struct tag *tag,
-				   const struct cu *cu __maybe_unused,
-				   struct conf_load *conf_load)
+				   const struct cu *cu, struct conf_load *conf_load)
 {
 	/* single out type 0 as it represents special type "void" */
 	uint32_t ref_type_id = btf_encoder__tag_type(encoder, tag->type);
@@ -2000,7 +2012,7 @@ static int btf_encoder__encode_tag(struct btf_encoder *encoder, struct tag *tag,
 		return id;
 	}
 	case DW_TAG_enumeration_type:
-		return btf_encoder__add_enum_type(encoder, tag, conf_load);
+		return btf_encoder__add_enum_type(encoder, tag, cu, conf_load);
 	case DW_TAG_subroutine_type:
 		return btf_encoder__add_func_proto_for_ftype(encoder, tag__ftype(tag));
         case DW_TAG_unspecified_type:
diff --git a/ctf_encoder.c b/ctf_encoder.c
index b761287d45348c59..f9e75a5821ce4e83 100644
--- a/ctf_encoder.c
+++ b/ctf_encoder.c
@@ -142,7 +142,7 @@ static int subroutine_type__encode(struct tag *tag, uint32_t core_id, struct ctf
 	return 0;
 }
 
-static int enumeration_type__encode(struct tag *tag, uint32_t core_id, struct ctf *ctf)
+static int enumeration_type__encode(struct tag *tag, const struct cu *cu, uint32_t core_id, struct ctf *ctf)
 {
 	struct type *etype = tag__type(tag);
 	int64_t position;
@@ -154,13 +154,24 @@ static int enumeration_type__encode(struct tag *tag, uint32_t core_id, struct ct
 		return -1;
 
 	struct enumerator *pos;
-	type__for_each_enumerator(etype, pos)
-		ctf__add_enumerator(ctf, pos->name, pos->value, &position);
+	type__for_each_enumerator(etype, pos) {
+		switch (pos->tag.tag) {
+		case DW_TAG_enumerator:
+			ctf__add_enumerator(ctf, pos->name, pos->value, &position);
+			break;
+		case DW_TAG_subprogram:
+			break;
+		default:
+			fprintf(stderr, "CTF: unexpected DW_TAG_%s in enumeration '%s', skipping\n",
+				dwarf_tag_name(pos->tag.tag), type__name(etype) ?: "(anonymous)");
+			break;
+		}
+	}
 
 	return 0;
 }
 
-static void tag__encode_ctf(struct tag *tag, uint32_t core_id, struct ctf *ctf)
+static void tag__encode_ctf(struct tag *tag, const struct cu *cu, uint32_t core_id, struct ctf *ctf)
 {
 	switch (tag->tag) {
 	case DW_TAG_base_type:
@@ -190,7 +201,7 @@ static void tag__encode_ctf(struct tag *tag, uint32_t core_id, struct ctf *ctf)
 		subroutine_type__encode(tag, core_id, ctf);
 		break;
 	case DW_TAG_enumeration_type:
-		enumeration_type__encode(tag, core_id, ctf);
+		enumeration_type__encode(tag, cu, core_id, ctf);
 		break;
 	}
 }
@@ -253,7 +264,7 @@ int cu__encode_ctf(struct cu *cu, int verbose)
 	uint32_t id;
 	struct tag *pos;
 	cu__for_each_type(cu, id, pos)
-		tag__encode_ctf(pos, id, ctf);
+		tag__encode_ctf(pos, cu, id, ctf);
 
 	struct hlist_head hash_addr[HASHADDR__SIZE];
 
diff --git a/dwarf_loader.c b/dwarf_loader.c
index af166ec9064e82c6..11477dcd7373a844 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -2310,6 +2310,8 @@ out_delete:
 	return NULL;
 }
 
+static struct tag *die__create_new_function(Dwarf_Die *die, struct cu *cu, struct conf_load *conf);
+
 static struct tag *die__create_new_enumeration(Dwarf_Die *die, struct cu *cu, struct conf_load *conf)
 {
 	Dwarf_Die child;
@@ -2331,18 +2333,39 @@ static struct tag *die__create_new_enumeration(Dwarf_Die *die, struct cu *cu, st
 
 	die = &child;
 	do {
-		struct enumerator *enumerator;
+		switch (dwarf_tag(die)) {
+		case DW_TAG_enumerator: {
+			struct enumerator *enumerator = enumerator__new(die, cu, conf);
+
+			if (enumerator == NULL)
+				goto out_delete;
+
+			enumeration__add(enumeration, enumerator);
+			cu__hash(cu, &enumerator->tag);
+		}
+			continue;
+		case DW_TAG_subprogram: {
+			struct tag *tag = die__create_new_function(die, cu, conf);
+			uint32_t id;
+
+			if (tag == NULL)
+				goto out_delete;
+
+			if (cu__table_add_tag(cu, tag, &id) < 0) {
+				tag__delete(tag, cu);
+				goto out_delete;
+			}
 
-		if (dwarf_tag(die) != DW_TAG_enumerator) {
+			struct dwarf_tag *dtag = tag__dwarf(tag);
+			dtag->small_id = id;
+			namespace__add_tag(&enumeration->namespace, tag);
+			cu__hash(cu, tag);
+			break;
+		}
+		default:
 			cu__tag_not_handled(cu, die);
 			continue;
 		}
-		enumerator = enumerator__new(die, cu, conf);
-		if (enumerator == NULL)
-			goto out_delete;
-
-		enumeration__add(enumeration, enumerator);
-		cu__hash(cu, &enumerator->tag);
 	} while (dwarf_siblingof(die, die) == 0);
 out:
 	return &enumeration->namespace.tag;
diff --git a/dwarves.c b/dwarves.c
index b48d9a3b9c43eebc..58852d3783b894ff 100644
--- a/dwarves.c
+++ b/dwarves.c
@@ -1256,11 +1256,6 @@ void type__delete(struct type *type, struct cu *cu)
 	cu__tag_free(cu, type__tag(type));
 }
 
-static void enumerator__delete(struct enumerator *enumerator, struct cu *cu)
-{
-	cu__tag_free(cu, &enumerator->tag);
-}
-
 void enumeration__delete(struct type *type, struct cu *cu)
 {
 	struct enumerator *pos, *n;
@@ -1270,7 +1265,7 @@ void enumeration__delete(struct type *type, struct cu *cu)
 
 	type__for_each_enumerator_safe_reverse(type, pos, n) {
 		list_del_init(&pos->tag.node);
-		enumerator__delete(pos, cu);
+		tag__delete(&pos->tag, cu);
 	}
 
 	if (type->suffix_disambiguation)
@@ -1928,6 +1923,9 @@ static void enumeration__calc_prefix(struct type *enumeration)
 	struct enumerator *entry;
 
 	type__for_each_enumerator(enumeration, entry) {
+		if (entry->tag.tag != DW_TAG_enumerator)
+			continue;
+
 		const char *curr_name = enumerator__name(entry);
 
 		if (previous_name) {
diff --git a/dwarves.h b/dwarves.h
index 0fc57ed0c7576c01..f5368f68c2460488 100644
--- a/dwarves.h
+++ b/dwarves.h
@@ -1646,7 +1646,7 @@ static inline const char *enumerator__name(const struct enumerator *enumerator)
 
 void enumeration__delete(struct type *type, struct cu *cu);
 void enumeration__add(struct type *type, struct enumerator *enumerator);
-size_t enumeration__fprintf(const struct tag *tag_enum,
+size_t enumeration__fprintf(const struct tag *tag_enum, const struct cu *cu,
 			    const struct conf_fprintf *conf, FILE *fp);
 
 int dwarves__init(void);
diff --git a/dwarves_emit.c b/dwarves_emit.c
index 01b33b7ec41eb947..aaf0f8f9a7ea815a 100644
--- a/dwarves_emit.c
+++ b/dwarves_emit.c
@@ -100,7 +100,7 @@ static struct type *type_emissions__find_fwd_decl(const struct type_emissions *e
 	return NULL;
 }
 
-static int enumeration__emit_definitions(struct tag *tag,
+static int enumeration__emit_definitions(struct tag *tag, const struct cu *cu,
 					 struct type_emissions *emissions,
 					 const struct conf_fprintf *conf,
 					 FILE *fp)
@@ -121,7 +121,7 @@ static int enumeration__emit_definitions(struct tag *tag,
 		return 0;
 	}
 
-	enumeration__fprintf(tag, conf, fp);
+	enumeration__fprintf(tag, cu, conf, fp);
 	fputs(";\n", fp);
 
 	// See comment on enumeration__fprintf(), it seems this happens with DWARF as well
@@ -198,10 +198,10 @@ static int typedef__emit_definitions(struct tag *tdef, struct cu *cu,
 		if (type__name(ctype) == NULL) {
 			fputs("typedef ", fp);
 			conf.suffix = type__name(def);
-			enumeration__emit_definitions(type, emissions, &conf, fp);
+			enumeration__emit_definitions(type, cu, emissions, &conf, fp);
 			goto out;
 		} else
-			enumeration__emit_definitions(type, emissions, &conf, fp);
+			enumeration__emit_definitions(type, cu, emissions, &conf, fp);
 	}
 		break;
 	case DW_TAG_structure_type:
@@ -380,7 +380,7 @@ next_indirection:
 			struct conf_fprintf conf = {
 				.suffix = NULL,
 			};
-			return enumeration__emit_definitions(type, emissions, &conf, fp);
+			return enumeration__emit_definitions(type, cu, emissions, &conf, fp);
 		}
 		break;
 	case DW_TAG_structure_type:
diff --git a/dwarves_fprintf.c b/dwarves_fprintf.c
index d7edb0cb14a5b803..75615072d03d32a0 100644
--- a/dwarves_fprintf.c
+++ b/dwarves_fprintf.c
@@ -155,6 +155,8 @@ const char tabs[] = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";
 
 static size_t union__fprintf(struct type *type, const struct cu *cu,
 			     const struct conf_fprintf *conf, FILE *fp);
+static size_t function__fprintf(const struct tag *tag, const struct cu *cu,
+				const struct conf_fprintf *conf, FILE *fp);
 
 /*
  * In dwarves_emit.c we can call type__emit() using a locally setup conf_fprintf for which
@@ -406,7 +408,7 @@ next_type:
 		struct conf_fprintf tconf = *pconf;
 
 		tconf.suffix = type__name(type);
-		return printed + enumeration__fprintf(tag_type, &tconf, fp);
+		return printed + enumeration__fprintf(tag_type, cu, &tconf, fp);
 	}
 	}
 
@@ -450,7 +452,15 @@ static int enumeration__max_entry_name_len(struct type *type)
 	struct enumerator *pos;
 
 	type__for_each_enumerator(type, pos) {
-		int len = strlen(enumerator__name(pos));
+		int len = 0;
+
+		if (pos->tag.tag == DW_TAG_enumerator)
+			len = strlen(enumerator__name(pos));
+		else if (pos->tag.tag == DW_TAG_subprogram) {
+			const char *fname = function__name(tag__function(&pos->tag));
+			if (fname)
+				len = strlen(fname);
+		}
 
 		if (type->max_tag_name_len < len)
 			type->max_tag_name_len = len;
@@ -459,7 +469,8 @@ out:
 	return type->max_tag_name_len;
 }
 
-size_t enumeration__fprintf(const struct tag *tag, const struct conf_fprintf *conf, FILE *fp)
+size_t enumeration__fprintf(const struct tag *tag, const struct cu *cu,
+			    const struct conf_fprintf *conf, FILE *fp)
 {
 	struct type *type = tag__type(tag);
 	struct enumerator *pos;
@@ -480,13 +491,25 @@ size_t enumeration__fprintf(const struct tag *tag, const struct conf_fprintf *co
 	}
 
 	type__for_each_enumerator(type, pos) {
-		printed += fprintf(fp, "%.*s\t%-*s = ", indent, tabs,
-				   max_entry_name_len, enumerator__name(pos));
-		if (conf->hex_fmt)
-			printed += fprintf(fp, "%#llx", (unsigned long long)pos->value);
-		else
-			printed += fprintf(fp, type->is_signed_enum ?  "%lld" : "%llu",
-					   (unsigned long long)pos->value);
+		printed += fprintf(fp, "%.*s\t", indent, tabs);
+
+		switch (pos->tag.tag) {
+		case DW_TAG_subprogram:
+			printed += function__fprintf(&pos->tag, cu, conf, fp);
+			break;
+		case DW_TAG_enumerator:
+			printed += fprintf(fp, "%-*s = ", max_entry_name_len, enumerator__name(pos));
+			if (conf->hex_fmt)
+				printed += fprintf(fp, "%#llx", (unsigned long long)pos->value);
+			else
+				printed += fprintf(fp, type->is_signed_enum ?  "%lld" : "%llu",
+						   (unsigned long long)pos->value);
+			break;
+		default:
+			printed += fprintf(fp, "/* Unexpected %s <%llx> */\n", dwarf_tag_name(pos->tag.tag),
+					   tag__orig_id(&pos->tag, cu));
+			continue;
+		}
 		printed += fprintf(fp, ",\n");
 	}
 
@@ -586,9 +609,12 @@ static const char *__tag__name(const struct tag *tag, const struct cu *cu,
 		strncpy(bf, name, len);
 	}
 		break;
-	case DW_TAG_subprogram:
-		strncpy(bf, function__name(tag__function(tag)), len);
+	case DW_TAG_subprogram: {
+		const char *fname = function__name(tag__function(tag));
+		if (fname)
+			strncpy(bf, fname, len);
 		break;
+	}
 	case DW_TAG_pointer_type:
 		return tag__ptr_name(tag, cu, bf, len, "*", conf);
 	case DW_TAG_reference_type:
@@ -937,7 +963,7 @@ print_modifier: {
 		if (type__name(ctype) != NULL && !expand_types)
 			printed += fprintf(fp, "enum %-*s %s", tconf.type_spacing - 5, type__name(ctype), name ?: "");
 		else
-			printed += enumeration__fprintf(type, &tconf, fp);
+			printed += enumeration__fprintf(type, cu, &tconf, fp);
 		break;
 	case DW_TAG_LLVM_annotation:
 	case DW_TAG_GNU_annotation: {
@@ -2173,7 +2199,7 @@ size_t tag__fprintf(struct tag *tag, const struct cu *cu,
 		printed += array_type__fprintf(tag, cu, "array", pconf, fp);
 		break;
 	case DW_TAG_enumeration_type:
-		printed += enumeration__fprintf(tag, pconf, fp);
+		printed += enumeration__fprintf(tag, cu, pconf, fp);
 		break;
 	case DW_TAG_typedef:
 		printed += typedef__fprintf(tag, cu, pconf, fp);
diff --git a/pahole.c b/pahole.c
index 5cf92833e6e84df9..0a7b810568f0dfeb 100644
--- a/pahole.c
+++ b/pahole.c
@@ -2162,7 +2162,7 @@ static const char *enumeration__lookup_value(struct type *enumeration, uint64_t
 	struct enumerator *entry;
 
 	type__for_each_enumerator(enumeration, entry) {
-		if (entry->value == value)
+		if (entry->tag.tag == DW_TAG_enumerator && entry->value == value)
 			return enumerator__name(entry);
 	}
 
@@ -2187,7 +2187,7 @@ static struct enumerator *enumeration__lookup_entry_from_value(struct type *enum
 	struct enumerator *entry;
 
 	type__for_each_enumerator(enumeration, entry) {
-		if (entry->value == value)
+		if (entry->tag.tag == DW_TAG_enumerator && entry->value == value)
 			return entry;
 	}
 
@@ -2213,6 +2213,9 @@ static struct enumerator *enumeration__find_enumerator(struct type *enumeration,
 	struct enumerator *entry;
 
 	type__for_each_enumerator(enumeration, entry) {
+		if (entry->tag.tag != DW_TAG_enumerator)
+			continue;
+
 		const char *entry_name = enumerator__name(entry);
 
 		if (!strcmp(entry_name, name))
@@ -3268,7 +3271,7 @@ static bool print_enumeration_with_enumerator(struct cu *cu, const char *name)
 
 	cu__for_each_enumeration(cu, id, enumeration) {
 		if (enumeration__find_enumerator(enumeration, name) != NULL) {
-			enumeration__fprintf(type__tag(enumeration), &conf, stdout);
+			enumeration__fprintf(type__tag(enumeration), cu, &conf, stdout);
 			fputc('\n', stdout);
 			return true;
 		}
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 03/12] dwarf_loader: Populate DW_TAG_variant children in DW_TAG_variant_part
  2026-07-31 19:30 [PATCHES 00/12] pahole: Support more rust tags and references to dwz alternate debug files Arnaldo Carvalho de Melo
  2026-07-31 19:30 ` [PATCH 01/12] dwarf_loader: Initial support for DW_TAG_variant_part Arnaldo Carvalho de Melo
  2026-07-31 19:30 ` [PATCH 02/12] dwarf_loader: Initial support for DW_TAG_subprogram in DW_TAG_enumeration Arnaldo Carvalho de Melo
@ 2026-07-31 19:30 ` Arnaldo Carvalho de Melo
  2026-07-31 19:30 ` [PATCH 04/12] btf_encoder: Encode variant parts as union members in BTF Arnaldo Carvalho de Melo
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-07-31 19:30 UTC (permalink / raw)
  To: Alan Maguire
  Cc: Jiri Olsa, Clark Williams, dwarves, bpf, Andrii Nakryiko,
	Yonghong Song, Mark Wieelard, Arnaldo Carvalho de Melo

From: Arnaldo Carvalho de Melo <acme@redhat.com>

Rust discriminated unions (enums like Option<T> and Result<T,E>) are
represented in DWARF as:

  DW_TAG_structure_type
    DW_TAG_variant_part (DW_AT_discr -> discriminant member)
      DW_TAG_variant (DW_AT_discr_value = 0)
        DW_TAG_member "None" -> struct None
      DW_TAG_variant (DW_AT_discr_value = 1)
        DW_TAG_member "Some" -> struct Some

Commit 7b135647cbf22e0c ("dwarf_loader: Initial support for
DW_TAG_variant_part") added the variant_part container to pahole's
internal representation, but did not process its DW_TAG_variant children.
This meant the variant parts were always empty, and any Rust struct
backed by a variant_part appeared as a zero-member struct in the output.

Add a struct variant with name and discriminant value fields, a
variant__new() loader that extracts the DW_AT_discr_value and the child
DW_TAG_member's name and type reference, and wire it into
variant_part__new() so DW_TAG_variant children are populated at load
time.

Also add type recoding for variant type references in
namespace__recode_dwarf_types, so variant member types are resolved from
DWARF offsets to CU-local type indices, following the same pattern used
for regular struct/union members.

The variant_part__delete destructor is made non-static and extended to
clean up variant children, and helper functions and iterator macros are
added for the new types.

Before, with a Rust binary like sashiko-cli:

  $ pahole -F btf -C 'Option<u32>' code_with_type.o
  struct Option<u32> {

          /* size: 8, cachelines: 1, members: 0 */
          /* padding: 8 */
          /* last cacheline: 8 bytes */
  } __attribute__((__aligned__(16)));

After:

  $ pahole -F dwarf -C 'Option<u32>' code_with_type.o
  struct Option<u32> {
          struct None { ... } __attribute__((__aligned__(4)));
          struct Some {
                  u32 __0 __attribute__((__aligned__(4)));  /*  4  4 */
          } __attribute__((__aligned__(4)));

          /* size: 8, cachelines: 1, members: 0 */
  } __attribute__((__aligned__(4)));

The variant parts are now populated in pahole's internal representation
and available for downstream consumers (BTF/CTF encoders, pretty
printers).

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 dwarf_loader.c | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++
 dwarves.c      | 42 ++++++++++++++++++++++++++++-
 dwarves.h      | 24 +++++++++++++++++
 3 files changed, 137 insertions(+), 1 deletion(-)

diff --git a/dwarf_loader.c b/dwarf_loader.c
index 11477dcd7373a844..4979968828c63d8f 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -1240,6 +1240,32 @@ static struct template_parameter_pack *template_parameter_pack__new(Dwarf_Die *d
 	return pack;
 }
 
+static struct variant *variant__new(Dwarf_Die *die, struct cu *cu, struct conf_load *conf)
+{
+	struct variant *var = tag__alloc(cu, sizeof(*var));
+
+	if (var != NULL) {
+		tag__init(&var->tag, cu, die);
+		var->discr_value = attr_numeric(die, DW_AT_discr_value);
+		var->name = NULL;
+
+		Dwarf_Die child;
+		if (dwarf_child(die, &child) == 0) {
+			do {
+				if (dwarf_tag(&child) == DW_TAG_member) {
+					struct dwarf_tag *dtag = tag__dwarf(&var->tag);
+
+					var->name = attr_string(&child, DW_AT_name, conf);
+					dwarf_tag__set_attr_type(dtag, type, &child, DW_AT_type);
+					break;
+				}
+			} while (dwarf_siblingof(&child, &child) == 0);
+		}
+	}
+
+	return var;
+}
+
 static struct variant_part *variant_part__new(Dwarf_Die *die, struct cu *cu, struct conf_load *conf)
 {
 	struct variant_part *vpart = tag__alloc(cu, sizeof(*vpart));
@@ -1247,6 +1273,20 @@ static struct variant_part *variant_part__new(Dwarf_Die *die, struct cu *cu, str
 	if (vpart != NULL) {
 		tag__init(&vpart->tag, cu, die);
 		INIT_LIST_HEAD(&vpart->variants);
+
+		Dwarf_Die child;
+		if (dwarf_child(die, &child) == 0) {
+			do {
+				if (dwarf_tag(&child) == DW_TAG_variant) {
+					struct variant *var = variant__new(&child, cu, conf);
+					if (var == NULL) {
+						variant_part__delete(vpart, cu);
+						return NULL;
+					}
+					variant_part__add_variant(vpart, var);
+				}
+			} while (dwarf_siblingof(&child, &child) == 0);
+		}
 	}
 
 	return vpart;
@@ -3003,6 +3043,38 @@ check_type:
 next:
 		pos->type = dtype->small_id;
 	}
+
+	if (tag__is_struct(tag) || tag__is_union(tag)) {
+		struct type *type = tag__type(tag);
+		struct variant_part *vpart;
+		struct dwarf_cu *dcu = cu->priv;
+
+		type__for_each_variant_part(type, vpart) {
+			struct variant *variant;
+			struct dwarf_tag *dvpart = tag__dwarf(&vpart->tag);
+
+			if (dvpart->type != 0) {
+				struct dwarf_tag *dtype = dwarf_cu__find_tag_by_ref(dcu, dvpart, type);
+				if (dtype != NULL)
+					vpart->tag.type = dtype->small_id;
+			}
+
+			variant_part__for_each_variant(vpart, variant) {
+				struct dwarf_tag *dvar = tag__dwarf(&variant->tag);
+
+				if (dvar->type == 0)
+					continue;
+
+				struct dwarf_tag *dtype = dwarf_cu__find_type_by_ref(dcu, dvar, type);
+				if (dtype == NULL) {
+					tag__print_type_not_found(&variant->tag);
+					continue;
+				}
+				variant->tag.type = dtype->small_id;
+			}
+		}
+	}
+
 	return 0;
 }
 
diff --git a/dwarves.c b/dwarves.c
index 58852d3783b894ff..8bcc0860e97d95f5 100644
--- a/dwarves.c
+++ b/dwarves.c
@@ -1211,11 +1211,18 @@ static void type__delete_class_members(struct type *type, struct cu *cu)
 	}
 }
 
-static void variant_part__delete(struct variant_part *vpart, struct cu *cu)
+void variant_part__delete(struct variant_part *vpart, struct cu *cu)
 {
+	struct variant *pos, *next;
+
 	if (vpart == NULL)
 		return;
 
+	list_for_each_entry_safe(pos, next, &vpart->variants, tag.node) {
+		list_del_init(&pos->tag.node);
+		cu__tag_free(cu, &pos->tag);
+	}
+
 	cu__tag_free(cu, &vpart->tag);
 }
 
@@ -1309,6 +1316,11 @@ void type__add_variant_part(struct type *type, struct variant_part *vpart)
 	list_add_tail(&vpart->tag.node, &type->variant_parts);
 }
 
+void variant_part__add_variant(struct variant_part *vpart, struct variant *var)
+{
+	list_add_tail(&var->tag.node, &vpart->variants);
+}
+
 struct class_member *type__last_member(struct type *type)
 {
 	struct class_member *pos;
@@ -1339,6 +1351,34 @@ static int type__clone_members(struct type *type, const struct type *from, struc
 		type__add_member(type, clone);
 	}
 
+	struct variant_part *vpart;
+
+	type__for_each_variant_part(from, vpart) {
+		struct variant_part *vp_clone = cu__tag_alloc(cu, sizeof(*vp_clone));
+
+		if (vp_clone == NULL)
+			return -1;
+
+		memcpy(vp_clone, vpart, sizeof(*vp_clone));
+		INIT_LIST_HEAD(&vp_clone->variants);
+
+		struct variant *variant;
+
+		variant_part__for_each_variant(vpart, variant) {
+			struct variant *v_clone = cu__tag_alloc(cu, sizeof(*v_clone));
+
+			if (v_clone == NULL) {
+				variant_part__delete(vp_clone, cu);
+				return -1;
+			}
+
+			memcpy(v_clone, variant, sizeof(*v_clone));
+			variant_part__add_variant(vp_clone, v_clone);
+		}
+
+		type__add_variant_part(type, vp_clone);
+	}
+
 	return 0;
 }
 
diff --git a/dwarves.h b/dwarves.h
index f5368f68c2460488..dd9aa332e61c77f0 100644
--- a/dwarves.h
+++ b/dwarves.h
@@ -1021,6 +1021,12 @@ static inline struct formal_parameter_pack *tag__formal_parameter_pack(const str
 
 void formal_parameter_pack__add(struct formal_parameter_pack *pack, struct parameter *param);
 
+struct variant {
+	struct tag	 tag;
+	const char	 *name;
+	uint64_t	 discr_value;
+};
+
 struct variant_part {
 	struct tag	 tag;
 	struct list_head variants;
@@ -1424,10 +1430,28 @@ static inline struct class_member *class_member__next(struct class_member *membe
 #define type__for_each_variant_part_safe_reverse(type, pos, n) \
 	list_for_each_entry_safe_reverse(pos, n, &(type)->variant_parts, tag.node)
 
+/**
+ * type__for_each_variant_part - iterate thru all variant_parts in a type
+ * @type: struct type instance to iterate
+ * @pos: struct variant_part iterator
+ */
+#define type__for_each_variant_part(type, pos) \
+	list_for_each_entry(pos, &(type)->variant_parts, tag.node)
+
 void type__add_member(struct type *type, struct class_member *member);
 void type__add_template_type_param(struct type *type, struct template_type_param *ttparm);
 void type__add_template_value_param(struct type *type, struct template_value_param *tvparam);
 void type__add_variant_part(struct type *type, struct variant_part *vpart);
+void variant_part__delete(struct variant_part *vpart, struct cu *cu);
+void variant_part__add_variant(struct variant_part *vpart, struct variant *var);
+
+/**
+ * variant_part__for_each_variant - iterate thru all variants in a variant_part
+ * @vpart: struct variant_part instance to iterate
+ * @pos: struct variant iterator
+ */
+#define variant_part__for_each_variant(vpart, pos) \
+	list_for_each_entry(pos, &(vpart)->variants, tag.node)
 
 struct class_member *
 	type__find_first_biggest_size_base_type_member(struct type *type,
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 04/12] btf_encoder: Encode variant parts as union members in BTF
  2026-07-31 19:30 [PATCHES 00/12] pahole: Support more rust tags and references to dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (2 preceding siblings ...)
  2026-07-31 19:30 ` [PATCH 03/12] dwarf_loader: Populate DW_TAG_variant children in DW_TAG_variant_part Arnaldo Carvalho de Melo
@ 2026-07-31 19:30 ` Arnaldo Carvalho de Melo
  2026-07-31 19:30 ` [PATCH 05/12] dwarf_loader: Handle DW_FORM_block in attr_numeric for Rust discriminant values Arnaldo Carvalho de Melo
                   ` (7 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-07-31 19:30 UTC (permalink / raw)
  To: Alan Maguire
  Cc: Jiri Olsa, Clark Williams, dwarves, bpf, Andrii Nakryiko,
	Yonghong Song, Mark Wieelard, Arnaldo Carvalho de Melo

From: Arnaldo Carvalho de Melo <acme@redhat.com>

With the DWARF loader now populating DW_TAG_variant children (previous
commit), wire them into BTF encoding so Rust discriminated unions
(Option<T>, Result<T,E>, etc.) are no longer emitted as empty structs.

Two changes:

1. Struct-to-union promotion: when a DW_TAG_structure_type has
   variant_parts but no regular data members, encode it as BTF_KIND_UNION
   instead of BTF_KIND_STRUCT, since the variants overlap at offset 0.

2. Variant member encoding: after encoding regular data members, iterate
   the variant_parts and emit each variant as a BTF union field with the
   variant's name and resolved type reference.

Testing with the sashiko-cli Rust binary (a real-world async HTTP client
using tokio, hyper, serde, etc.):

Before:

  $ bpftool btf dump file sashiko-cli | grep -c UNION
  2073
  $ bpftool btf dump file sashiko-cli | grep 'STRUCT.*vlen=0' | grep -vc 'size=0'
  24335

After:

  $ bpftool btf dump file sashiko-cli | grep -c UNION
  25750
  $ bpftool btf dump file sashiko-cli | grep 'STRUCT.*vlen=0' | grep -vc 'size=0'
  2236

22,099 types that were previously encoded as empty structs are now
properly represented as unions with their variant members:

Before:

  $ bpftool btf dump file code_with_type.o | grep -A1 'Option<u32>'
  [11] STRUCT 'Option<u32>' size=8 vlen=0

After:

  $ bpftool btf dump file code_with_type.o | grep -A3 'Option<u32>'
  [11] UNION 'Option<u32>' size=8 vlen=2
          'None' type_id=9 bits_offset=0
          'Some' type_id=10 bits_offset=0

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 btf_encoder.c | 37 +++++++++++++++++++++++++++++++++++--
 1 file changed, 35 insertions(+), 2 deletions(-)

diff --git a/btf_encoder.c b/btf_encoder.c
index 83ce21186ea7da36..acf0a5ade5e29014 100644
--- a/btf_encoder.c
+++ b/btf_encoder.c
@@ -1855,6 +1855,12 @@ static void dump_invalid_symbol(const char *msg, const char *sym,
 	fprintf(stderr, "PAHOLE: Error: Use '--btf_encode_force' to ignore such symbols and force emit the btf.\n");
 }
 
+static bool type__has_variant_parts(const struct type *type)
+{
+	return !list_empty(&type->variant_parts);
+}
+
+
 static int32_t btf_encoder__add_struct_type(struct btf_encoder *encoder, struct tag *tag)
 {
 	struct type *type = tag__type(tag);
@@ -1863,8 +1869,18 @@ static int32_t btf_encoder__add_struct_type(struct btf_encoder *encoder, struct
 	int32_t type_id;
 	uint8_t kind;
 
-	kind = (tag->tag == DW_TAG_union_type) ?
-		BTF_KIND_UNION : BTF_KIND_STRUCT;
+	/*
+	 * Rust discriminated unions (enums) are represented in DWARF as
+	 * DW_TAG_structure_type with DW_TAG_variant_part children.
+	 * If the struct has only variant parts and no regular data members,
+	 * encode it as a BTF union since the variants overlap at offset 0.
+	 */
+	if (tag->tag == DW_TAG_union_type)
+		kind = BTF_KIND_UNION;
+	else if (type__has_variant_parts(type) && type->nr_members == 0)
+		kind = BTF_KIND_UNION;
+	else
+		kind = BTF_KIND_STRUCT;
 
 	type_id = btf_encoder__add_struct(encoder, kind, name, type->size);
 	if (type_id < 0)
@@ -1882,6 +1898,23 @@ static int32_t btf_encoder__add_struct_type(struct btf_encoder *encoder, struct
 			return -1;
 	}
 
+	if (type__has_variant_parts(type) && kind == BTF_KIND_UNION) {
+		struct variant_part *vpart;
+
+		type__for_each_variant_part(type, vpart) {
+			struct variant *variant;
+
+			variant_part__for_each_variant(vpart, variant) {
+				if (variant->tag.type == 0)
+					continue;
+
+				uint32_t ref_type_id = btf_encoder__tag_type(encoder, variant->tag.type);
+
+				if (btf_encoder__add_field(encoder, variant->name, ref_type_id, 0, 0))
+					return -1;
+			}
+		}
+	}
 
 
 	return type_id;
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 05/12] dwarf_loader: Handle DW_FORM_block in attr_numeric for Rust discriminant values
  2026-07-31 19:30 [PATCHES 00/12] pahole: Support more rust tags and references to dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (3 preceding siblings ...)
  2026-07-31 19:30 ` [PATCH 04/12] btf_encoder: Encode variant parts as union members in BTF Arnaldo Carvalho de Melo
@ 2026-07-31 19:30 ` Arnaldo Carvalho de Melo
  2026-07-31 19:30 ` [PATCH 06/12] dwarf_loader: Allow forcing the merge of CUs for solving inter CU tag references Arnaldo Carvalho de Melo
                   ` (6 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-07-31 19:30 UTC (permalink / raw)
  To: Alan Maguire
  Cc: Jiri Olsa, Clark Williams, dwarves, bpf, Andrii Nakryiko,
	Yonghong Song, Mark Wieelard, Arnaldo Carvalho de Melo

From: Arnaldo Carvalho de Melo <acme@redhat.com>

Rust enums with 128-bit discriminant types (i128/u128) encode their
DW_AT_discr_value as a DW_FORM_block1 containing a 16-byte little-endian
integer, rather than using a standard data form like DW_FORM_data*.

Since attr_numeric did not handle DW_FORM_block*, these discriminant
values were not read and produced 311 warnings on a real Rust binary like
sashiko-cli:

  DW_AT_<0x16>=0xa
  DW_AT_<0x16>=0xa
  ...

(0x16 = DW_AT_discr_value, 0xa = DW_FORM_block1)

Add DW_FORM_block1/block2/block4/block to attr_numeric, reading up to 8
bytes from the block into a uint64_t via memcpy and converting from
little-endian with le64toh() so the result is correct on big-endian hosts
as well.  This covers all practical discriminant values.

Before (sashiko-cli):

  $ pahole --btf_encode sashiko-cli 2>&1 | grep -c 'DW_AT_'
  311

After:

  $ pahole --btf_encode sashiko-cli 2>&1 | wc -l
  0

All warnings from Rust DWARF encoding of sashiko-cli are now resolved.

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 dwarf_loader.c        |  51 +++++++++++++--
 tests/block_endian.sh | 146 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 190 insertions(+), 7 deletions(-)
 create mode 100755 tests/block_endian.sh

diff --git a/dwarf_loader.c b/dwarf_loader.c
index 4979968828c63d8f..934d410b2f983828 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -7,6 +7,7 @@
 #include <assert.h>
 #include <dirent.h>
 #include <dwarf.h>
+#include <endian.h>
 #include <elfutils/libdwfl.h>
 #include <elfutils/version.h>
 #include <errno.h>
@@ -335,7 +336,7 @@ static uint64_t __libdw_get_uleb128(uint64_t acc, uint32_t i,
 		var = __libdw_get_uleb128 (var, 1, &(addr));	\
 	} while (0)
 
-static uint64_t attr_numeric(Dwarf_Die *die, uint32_t name)
+static uint64_t __attr_numeric(Dwarf_Die *die, uint32_t name, bool little_endian)
 {
 	Dwarf_Attribute attr;
 	uint32_t form;
@@ -371,6 +372,31 @@ static uint64_t attr_numeric(Dwarf_Die *die, uint32_t name)
 			return value;
 	}
 		break;
+	case DW_FORM_block1:
+	case DW_FORM_block2:
+	case DW_FORM_block4:
+	case DW_FORM_block: {
+		/* Block data is in target byte order, convert to host */
+		Dwarf_Block block;
+		if (dwarf_formblock(&attr, &block) == 0 && block.length > 0) {
+			uint64_t value = 0;
+			size_t n = block.length > sizeof(value) ? sizeof(value) : block.length;
+			if (little_endian) {
+				memcpy(&value, block.data, n);
+				return le64toh(value);
+			}
+			/*
+			 * BE: the least-significant bytes are at the END of
+			 * the block.  Skip any high bytes that don't fit in
+			 * uint64_t, then accumulate the rest MSB-first.
+			 */
+			size_t off = block.length - n;
+			for (size_t i = 0; i < n; i++)
+				value = (value << 8) | (uint8_t)block.data[off + i];
+			return value;
+		}
+	}
+		break;
 	default:
 		fprintf(stderr, "DW_AT_<0x%x>=0x%x\n", name, form);
 		break;
@@ -379,6 +405,16 @@ static uint64_t attr_numeric(Dwarf_Die *die, uint32_t name)
 	return 0;
 }
 
+/*
+ * Most callers don't use DW_FORM_block, so this wrapper keeps them unchanged.
+ * When the attribute can be a block (e.g. DW_AT_discr_value), use
+ * __attr_numeric() with the CU's endianness instead.
+ */
+static uint64_t attr_numeric(Dwarf_Die *die, uint32_t name)
+{
+	return __attr_numeric(die, name, true);
+}
+
 static uint64_t attr_alignment(Dwarf_Die *die, struct conf_load *conf)
 {
 	return conf->ignore_alignment_attr ? 0 : attr_numeric(die, DW_AT_alignment);
@@ -712,7 +748,7 @@ static struct enumerator *enumerator__new(Dwarf_Die *die, struct cu *cu, struct
 	if (enumerator != NULL) {
 		tag__init(&enumerator->tag, cu, die);
 		enumerator->name = attr_string(die, DW_AT_name, conf);
-		enumerator->value = attr_numeric(die, DW_AT_const_value);
+		enumerator->value = __attr_numeric(die, DW_AT_const_value, cu->little_endian);
 	}
 
 	return enumerator;
@@ -801,7 +837,7 @@ static struct constant *constant__new(Dwarf_Die *die, struct cu *cu, struct conf
 	if (constant != NULL) {
 		tag__init(&constant->tag, cu, die);
 		constant->name = attr_string(die, DW_AT_name, conf);
-		constant->value = attr_numeric(die, DW_AT_const_value);
+		constant->value = __attr_numeric(die, DW_AT_const_value, cu->little_endian);
 	}
 
 	return constant;
@@ -1080,7 +1116,7 @@ static struct class_member *class_member__new(Dwarf_Die *die, struct cu *cu,
 
 		if (!cu__is_c(cu)) {
 			member->accessibility = attr_numeric(die, DW_AT_accessibility);
-			member->const_value   = attr_numeric(die, DW_AT_const_value);
+			member->const_value   = __attr_numeric(die, DW_AT_const_value, cu->little_endian);
 			member->virtuality    = attr_numeric(die, DW_AT_virtuality);
 		}
 		member->hole = 0;
@@ -1189,8 +1225,8 @@ static struct template_value_param *template_value_param__new(Dwarf_Die *die, st
 	if (tvparm != NULL) {
 		tag__init(&tvparm->tag, cu, die);
 		tvparm->name = attr_string(die, DW_AT_name, conf);
-		tvparm->const_value = attr_numeric(die, DW_AT_const_value);
-		tvparm->default_value = attr_numeric(die, DW_AT_default_value);
+		tvparm->const_value = __attr_numeric(die, DW_AT_const_value, cu->little_endian);
+		tvparm->default_value = __attr_numeric(die, DW_AT_default_value, cu->little_endian);
 	}
 
 	return tvparm;
@@ -1246,7 +1282,8 @@ static struct variant *variant__new(Dwarf_Die *die, struct cu *cu, struct conf_l
 
 	if (var != NULL) {
 		tag__init(&var->tag, cu, die);
-		var->discr_value = attr_numeric(die, DW_AT_discr_value);
+		/* DW_AT_discr_value uses DW_FORM_block, needs target endianness */
+		var->discr_value = __attr_numeric(die, DW_AT_discr_value, cu->little_endian);
 		var->name = NULL;
 
 		Dwarf_Die child;
diff --git a/tests/block_endian.sh b/tests/block_endian.sh
new file mode 100755
index 0000000000000000..9ede49a9a133e5a1
--- /dev/null
+++ b/tests/block_endian.sh
@@ -0,0 +1,146 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright © 2026 Red Hat Inc, Arnaldo Carvalho de Melo <acme@redhat.com>
+#
+# Test the DW_FORM_block byte order conversion logic used by
+# __attr_numeric() in dwarf_loader.c.  Exercises both LE and BE
+# conversion paths with block lengths 1, 2, 4, and 8 to catch
+# endianness mistakes like using be64toh on sub-8-byte blocks.
+
+. "$(dirname "$0")/test_lib.sh"
+
+outdir=$(make_tmpdir)
+trap cleanup EXIT
+
+title_log "DW_FORM_block byte order conversion."
+
+CC=${CC:-gcc}
+if ! command -v ${CC%% *} > /dev/null 2>&1; then
+	info_log "skip: $CC not available"
+	test_skip
+fi
+
+cat > "$outdir/block_endian_test.c" << 'EOF'
+#define _DEFAULT_SOURCE
+#include <endian.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+/*
+ * These two functions replicate the exact conversion logic from
+ * __attr_numeric() in dwarf_loader.c for the DW_FORM_block case.
+ * Any change to that code must be mirrored here.
+ */
+static uint64_t block_to_u64_le(const uint8_t *data, size_t len)
+{
+	uint64_t value = 0;
+	size_t n = len > sizeof(value) ? sizeof(value) : len;
+
+	memcpy(&value, data, n);
+	return le64toh(value);
+}
+
+static uint64_t block_to_u64_be(const uint8_t *data, size_t len)
+{
+	uint64_t value = 0;
+	size_t n = len > sizeof(value) ? sizeof(value) : len;
+
+	for (size_t i = 0; i < n; i++)
+		value = (value << 8) | data[i];
+	return value;
+}
+
+struct test_case {
+	const char  *name;
+	int          is_be;       /* 0 = LE target, 1 = BE target */
+	uint64_t     expected;
+	size_t       len;
+	uint8_t      data[8];
+};
+
+static const struct test_case tests[] = {
+	/* LE target, 1 byte */
+	{ "LE n=1 val=5",     0, 5,     1, {0x05} },
+	{ "LE n=1 val=0",     0, 0,     1, {0x00} },
+	{ "LE n=1 val=255",   0, 255,   1, {0xff} },
+
+	/* LE target, 2 bytes */
+	{ "LE n=2 val=256",   0, 256,   2, {0x00, 0x01} },
+	{ "LE n=2 val=0x0102",0, 0x0102,2, {0x02, 0x01} },
+
+	/* LE target, 4 bytes */
+	{ "LE n=4 val=66051", 0, 66051, 4, {0x03, 0x02, 0x01, 0x00} },
+	{ "LE n=4 val=1",     0, 1,     4, {0x01, 0x00, 0x00, 0x00} },
+
+	/* LE target, 8 bytes */
+	{ "LE n=8",           0, 0x0807060504030201ULL, 8,
+	  {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} },
+
+	/* BE target, 1 byte (single byte has no endianness) */
+	{ "BE n=1 val=5",     1, 5,     1, {0x05} },
+	{ "BE n=1 val=0",     1, 0,     1, {0x00} },
+	{ "BE n=1 val=255",   1, 255,   1, {0xff} },
+
+	/* BE target, 2 bytes */
+	{ "BE n=2 val=256",   1, 256,   2, {0x01, 0x00} },
+	{ "BE n=2 val=0x0102",1, 0x0102,2, {0x01, 0x02} },
+	{ "BE n=2 val=1",     1, 1,     2, {0x00, 0x01} },
+
+	/* BE target, 4 bytes */
+	{ "BE n=4 val=66051", 1, 66051, 4, {0x00, 0x01, 0x02, 0x03} },
+	{ "BE n=4 val=1",     1, 1,     4, {0x00, 0x00, 0x00, 0x01} },
+
+	/* BE target, 8 bytes */
+	{ "BE n=8",           1, 0x0102030405060708ULL, 8,
+	  {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} },
+};
+
+int main(void)
+{
+	int failures = 0;
+	size_t n = sizeof(tests) / sizeof(tests[0]);
+
+	for (size_t i = 0; i < n; i++) {
+		const struct test_case *t = &tests[i];
+		uint64_t got;
+
+		if (t->is_be)
+			got = block_to_u64_be(t->data, t->len);
+		else
+			got = block_to_u64_le(t->data, t->len);
+
+		if (got != t->expected) {
+			fprintf(stderr, "FAIL: %s: expected 0x%llx, got 0x%llx\n",
+				t->name,
+				(unsigned long long)t->expected,
+				(unsigned long long)got);
+			failures++;
+		}
+	}
+
+	if (failures) {
+		fprintf(stderr, "%d/%zu tests failed\n", failures, n);
+		return 1;
+	}
+	return 0;
+}
+EOF
+
+$CC -std=c11 -Wall -Werror -o "$outdir/block_endian_test" \
+	"$outdir/block_endian_test.c" 2>/dev/null
+if [ $? -ne 0 ]; then
+	error_log "FAIL: failed to compile block_endian_test"
+	test_fail
+fi
+
+output=$("$outdir/block_endian_test" 2>&1)
+if [ $? -ne 0 ]; then
+	error_log "FAIL: block endian conversion test failed:"
+	error_log "$output"
+	test_fail
+fi
+
+info_log "   block form LE+BE conversion: ok"
+
+test_pass
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 06/12] dwarf_loader: Allow forcing the merge of CUs for solving inter CU tag references
  2026-07-31 19:30 [PATCHES 00/12] pahole: Support more rust tags and references to dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (4 preceding siblings ...)
  2026-07-31 19:30 ` [PATCH 05/12] dwarf_loader: Handle DW_FORM_block in attr_numeric for Rust discriminant values Arnaldo Carvalho de Melo
@ 2026-07-31 19:30 ` Arnaldo Carvalho de Melo
  2026-07-31 19:30 ` [PATCH 07/12] dwarf_loader: Support DW_TAG_imported_unit for same-file partial units Arnaldo Carvalho de Melo
                   ` (5 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-07-31 19:30 UTC (permalink / raw)
  To: Alan Maguire
  Cc: Jiri Olsa, Clark Williams, dwarves, bpf, Andrii Nakryiko,
	Yonghong Song, Mark Wieelard, Arnaldo Carvalho de Melo

From: Arnaldo Carvalho de Melo <acme@redhat.com>

The Linux perf tool now includes some Rust code that then gets linked
into perf and comes with its DWARF that has tags referencing tags in
different CUs, and as the current DWARF loading algorithm uses
parallelization and recodes the big DWARF types (DWARF_off, usually
64-bit) into smaller ones as a step into converting to CTF (initially)
and later BTF, the resolution fails.

There is a case where this inter CU happens, LTO builds, and so there is
an alternative algorithm for that case, that serializes DWARF CU loading
and merges all the CUs into just one meta/mega-CU, which then has all
the types and thus doesn't have a problem with inter CU references, as
the recoding into smaller ids is done only after all CUs are loaded.

So while we don't refactor the loading in a way that allows for inter CU
while allowing parallelization, maybe by doing the recoding just at the
end of parallel loading, add minimal code to force this CU merging for
experimentation in such cases, getting back the regression test
prettify_perf.data.sh to work, making it force CU merging.

  $ pahole ~/bin/perf > unmerged.txt
  <Suppress lots of warnings when recoding DWARF types.>
  $ pahole --features=force_cu_merging ~/bin/perf > merged.txt
  $

With the current set of Rust types that are representable with the
pahole data structures and then pretty printed as if they were C we see
12 differences:

  $ diff -u unmerged.txt merged.txt | grep ^@@ | wc -l
  12
  $ diff -u unmerged.txt merged.txt | wc -l
  198

Of this kind, due to some types not being resolved as tags are
referencing tags in other CUs.

  $ diff -u unmerged.txt merged.txt | head
  --- unmerged.txt	2026-03-23 17:56:54.971785023 -0300
  +++ merged.txt	2026-03-23 17:56:59.826872178 -0300
  @@ -9643,10 +9643,11 @@
   	u64                        __0 __attribute__((__aligned__(8))); /*     0     8 */
   	struct Abbreviation        __1 __attribute__((__aligned__(8))); /*     8   112 */

  -	/* XXX last struct has 5 bytes of padding */
  +	/* XXX last struct has 16 bytes of padding, 1 hole */

   	/* size: 120, cachelines: 2, members: 2 */
  $

Now the pretty printing perf.data test case passes:

  ⬢ [acme@toolbx tests]$ ./prettify_perf.data.sh
  Pretty printing of files using DWARF type information.
  Test ./prettify_perf.data.sh passed
  ⬢ [acme@toolbx tests]$

This was implemented reusing the --btf_features mechanism that now can
be accessed as well via --features, as this is not strictly a BTF
feature but, as Alan Maguire suggested, it is desirable to ask for that
feature to be enabled when we know it is needed bug can't guarantee that
the available pahole version has the feature and not have it fail
because it doesn't implement --force_cu_merging, which the
--btf_features=force_cu_merging, now also avaialbe as
--features=force_cu_merging, allows as it ignores unknown features.

Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 dwarf_loader.c              |  2 +-
 dwarves.h                   |  1 +
 man-pages/pahole.1          | 18 ++++++++++++++++--
 pahole.c                    | 11 ++++++++++-
 tests/prettify_perf.data.sh |  4 ++--
 5 files changed, 30 insertions(+), 6 deletions(-)

diff --git a/dwarf_loader.c b/dwarf_loader.c
index 934d410b2f983828..99bc121a377fbd8b 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -4693,7 +4693,7 @@ static int cus__load_module(struct cus *cus, struct conf_load *conf,
 			cus__remove(cus, type_cu);
 	}
 
-	if (cus__merging_cu(dw, elf)) {
+	if (conf->force_cu_merging || cus__merging_cu(dw, elf)) {
 		res = cus__merge_and_process_cu(cus, conf, mod, dw, elf, filename,
 						build_id, build_id_len,
 						type_cu ? type_dcu : NULL);
diff --git a/dwarves.h b/dwarves.h
index dd9aa332e61c77f0..38fec59ca78990df 100644
--- a/dwarves.h
+++ b/dwarves.h
@@ -112,6 +112,7 @@ struct conf_load {
 	const char		*kabi_prefix;
 	struct btf		*base_btf;
 	struct conf_fprintf	*conf_fprintf;
+	bool			force_cu_merging;
 };
 
 /** struct conf_fprintf - hints to the __fprintf routines
diff --git a/man-pages/pahole.1 b/man-pages/pahole.1
index 21378a224847eb45..e3ce737460efec72 100644
--- a/man-pages/pahole.1
+++ b/man-pages/pahole.1
@@ -314,8 +314,10 @@ Generate BTF for functions with optimization-related suffixes (.isra, .constprop
 Allow using all the BTF features supported by pahole.
 
 .TP
-.B \-\-btf_features=FEATURE_LIST
-Encode BTF using the specified feature list, or specify 'default' for all
+.B \-\-features=FEATURE_LIST
+This is also available as \-\-btf_features=FEATURE_LIST, unknown features,
+like those added in newer versions of pahole, are ignored.  BTF encoding
+configuration is the major user. Using 'default' will enable all
 standard features supported.
 This option can be used as an alternative to using multiple BTF-related options,
 and 'default' represents the standard set of BTF features that are in use for
@@ -364,6 +366,18 @@ Supported non-standard features (not enabled for 'default')
 	layout             Encode information about BTF kinds available at encoding     
 	                   time in layout section in BTF.
 
+Non-standard, non-BTF related features:
+
+	force_cu_merging   Force merging all CUs into one. Use when there are
+	                   references across CUs. This happens in some LTO cases
+	                   and was observed with Rust CUs, where types tags
+	                   (function parameters, abstract origins for inlines, etc)
+	                   reference types in another CU.
+	                   For LTO this is being autodetected and the merging of
+	                   cus is done automatically, but for the Rust case, and
+	                   maybe others this is needed with the current DWARF
+	                   loading algorithm.
+
 .fi
 
 So for example, specifying \-\-btf_encode=var,enum64 will result in a BTF
diff --git a/pahole.c b/pahole.c
index 0a7b810568f0dfeb..b7559a124a86d722 100644
--- a/pahole.c
+++ b/pahole.c
@@ -1154,6 +1154,7 @@ ARGP_PROGRAM_VERSION_HOOK_DEF = dwarves_print_version;
 #define ARG_padding		   348
 #define ARGP_with_embedded_flexible_array 349
 #define ARGP_btf_attributes	   350
+#define ARGP_features		   351
 
 /* --btf_features=feature1[,feature2,..] allows us to specify
  * a list of requested BTF features or "default" to enable all default
@@ -1241,7 +1242,8 @@ struct btf_feature {
 	BTF_NON_DEFAULT_FEATURE_CHECK(attributes, btf_attributes, false,
 				      attributes_check),
 	BTF_NON_DEFAULT_FEATURE(true_signature, true_signature, false),
-	BTF_NON_DEFAULT_FEATURE_CHECK(layout, btf_gen_layout, false, layout_check)
+	BTF_NON_DEFAULT_FEATURE_CHECK(layout, btf_gen_layout, false, layout_check),
+	BTF_NON_DEFAULT_FEATURE(force_cu_merging, force_cu_merging, false),
 };
 
 #define BTF_MAX_FEATURE_STR	1024
@@ -1816,6 +1818,12 @@ static const struct argp_option pahole__options[] = {
 		.arg = "FEATURE_LIST",
 		.doc = "Specify supported BTF features in FEATURE_LIST or 'default' for default set of supported features. See the pahole manual page for the list of supported, default features."
 	},
+	{
+		.name = "features",
+		.key = ARGP_features,
+		.arg = "FEATURE_LIST",
+		.doc = "Specify supported features in FEATURE_LIST or 'default' for default set of supported features. See the pahole manual page for the list of supported, default features."
+	},
 	{
 		.name = "supported_btf_features",
 		.key = ARGP_supported_btf_features,
@@ -2028,6 +2036,7 @@ static error_t pahole__options_parser(int key, char *arg,
 		conf_load.reproducible_build = true;	break;
 	case ARGP_running_kernel_vmlinux:
 		show_running_kernel_vmlinux = true;	break;
+	case ARGP_features:
 	case ARGP_btf_features:
 		parse_btf_features(arg, false);		break;
 	case ARGP_supported_btf_features:
diff --git a/tests/prettify_perf.data.sh b/tests/prettify_perf.data.sh
index 1fae95154d710aae..384c250ff4e01a4c 100755
--- a/tests/prettify_perf.data.sh
+++ b/tests/prettify_perf.data.sh
@@ -25,7 +25,7 @@ fi
 perf_lacks_type_info() {
 	local type_keyword=$1
 	local type_name=$2
-	if ! pahole -C $type_name $perf | grep -q "^$type_keyword $type_name {"; then
+	if ! pahole --features=force_cu_merging -C $type_name $perf | grep -q "^$type_keyword $type_name {"; then
 		info_log "skip: $perf doesn't have '$type_keyword $type_name' type info"
 		test_skip
 	fi
@@ -41,7 +41,7 @@ $perf record --quiet -o $perf_data sleep 0.00001
 
 number_of_filtered_perf_record_metadata() {
 	local metadata_record=$1
-	local count=$(pahole -F dwarf -V $perf --header=perf_file_header --seek_bytes '$header.data.offset' --size_bytes='$header.data.size' -C "perf_event_header(sizeof,type,type_enum=perf_event_type+perf_user_event_type,filter=type==PERF_RECORD_$metadata_record)" --prettify $perf_data | grep ".type = PERF_RECORD_$metadata_record," | wc -l)
+	local count=$(pahole --features=force_cu_merging -F dwarf -V $perf --header=perf_file_header --seek_bytes '$header.data.offset' --size_bytes='$header.data.size' -C "perf_event_header(sizeof,type,type_enum=perf_event_type+perf_user_event_type,filter=type==PERF_RECORD_$metadata_record)" --prettify $perf_data | grep ".type = PERF_RECORD_$metadata_record," | wc -l)
 	echo "$count"
 }
 
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 07/12] dwarf_loader: Support DW_TAG_imported_unit for same-file partial units
  2026-07-31 19:30 [PATCHES 00/12] pahole: Support more rust tags and references to dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (5 preceding siblings ...)
  2026-07-31 19:30 ` [PATCH 06/12] dwarf_loader: Allow forcing the merge of CUs for solving inter CU tag references Arnaldo Carvalho de Melo
@ 2026-07-31 19:30 ` Arnaldo Carvalho de Melo
  2026-07-31 19:30 ` [PATCH 08/12] dwarf_loader: Fix cus__merging_cu failing to detect DW_FORM_ref_addr Arnaldo Carvalho de Melo
                   ` (4 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-07-31 19:30 UTC (permalink / raw)
  To: Alan Maguire
  Cc: Jiri Olsa, Clark Williams, dwarves, bpf, Andrii Nakryiko,
	Yonghong Song, Mark Wieelard, Arnaldo Carvalho de Melo

From: Arnaldo Carvalho de Melo <acme@redhat.com>

Binaries processed by the dwz(1) tool have their DWARF type information
deduplicated into DW_TAG_partial_unit entries that are then referenced
via DW_TAG_imported_unit from each DW_TAG_compile_unit that uses those
types. This is the standard DWARF mechanism for cross-CU type sharing.

On Fedora/RHEL, most debuginfo packages are built with dwz, making this
a common pattern. For instance, bash-debuginfo has 10,486
DW_TAG_partial_unit, 384 DW_TAG_compile_unit, and 8,572
DW_TAG_imported_unit entries — all using same-file references (no .dwz
alternate DWARF file involved).

Before this patch, pahole skipped DW_TAG_partial_unit with a warning:

  WARNING: DW_TAG_partial_unit used, some types will not be considered!
           Probably this was optimized using a tool like 'dwz'
           A future version of pahole will support this.

And DW_TAG_imported_unit was silently ignored (returned NULL), causing
pahole to report "file has no dwarf type information" for binaries like
bash and glibc.

The fix adds die__process_imported_unit(), called from die__process_unit()
when encountering DW_TAG_imported_unit. It follows DW_AT_import to the
referenced DW_TAG_partial_unit DIE and processes its children inline into
the importing compile unit's type tables. This works because
dwarf_formref_die() already handles all DWARF reference forms, and each
CU maintains its own independent hash tables — so the same partial unit
can be safely imported by multiple CUs, each getting its own copy of the
types.

Since imported units can themselves contain DW_TAG_imported_unit entries
(nested imports), a depth limit of 64 is enforced to prevent stack
overflow from pathological or corrupted DWARF.  A warning is emitted if
the limit is reached.

Some binaries (e.g. chromium-browser on Fedora 44, built with Rust
components) also have DW_TAG_imported_unit entries that reference partial
units in an alternate debug file via DW_FORM_GNU_ref_alt (the
.gnu_debugaltlink mechanism). When elfutils resolves such a reference, it
returns DIEs from the alternate file whose offsets are in a different
address space — processing these into the main CU's hash tables corrupts
type references and causes a crash during type recoding.

The same DW_FORM_GNU_ref_alt form can also appear on regular type
attributes (DW_AT_type, DW_AT_abstract_origin, DW_AT_specification,
etc.), not just on DW_TAG_imported_unit's DW_AT_import. Guard all paths
via attr_form_is_ref_alt(), which skips the reference and warns once, so
users know why some types are missing rather than getting a crash.

The korg/alt_dwarf branch had a previous attempt at this that also
handled the .dwz alternate DWARF file case (DW_FORM_GNU_ref_alt), but it
was never merged and is now 294 commits behind master. This patch takes a
simpler approach focused on the same-file case first, which covers dwz
output on Fedora/RHEL where all partial units are within the same .debug
file.

Before (bash-5.3.9-3.fc44.x86_64 debuginfo):
  $ pahole -F dwarf /usr/lib/debug/usr/bin/bash-5.3.9-3.fc44.x86_64.debug
  WARNING: DW_TAG_partial_unit used, some types will not be considered!
  pahole: /usr/lib/debug/usr/bin/bash-5.3.9-3.fc44.x86_64.debug: file has no dwarf type information

After:
  $ pahole -F dwarf /usr/lib/debug/usr/bin/bash-5.3.9-3.fc44.x86_64.debug | wc -l
  1605
  $ pahole -F dwarf -C variable /usr/lib/debug/usr/bin/bash-5.3.9-3.fc44.x86_64.debug
  struct variable {
  	char *                     name;                 /*     0     8 */
  	char *                     value;                /*     8     8 */
  	...
  	/* size: 48, cachelines: 1, members: 7 */
  };

Before (chromium-browser debuginfo, Fedora 44):
  $ pahole /usr/lib/debug/.../chromium-browser-149.0.7827.155-1.fc44.x86_64.debug
  Segmentation fault

After:
  $ pahole /usr/lib/debug/.../chromium-browser-149.0.7827.155-1.fc44.x86_64.debug
  WARNING: DW_FORM_GNU_ref_alt (dwz alternate debug file) not yet supported,
           some types will not be available.

Reported-by: Sashiko:gemini-3-1-pro-preview # Running on a local machine
Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 dwarf_loader.c     | 153 ++++++++++++++++++++++++++++++++++++++-------
 man-pages/pahole.1 |   8 +--
 2 files changed, 135 insertions(+), 26 deletions(-)

diff --git a/dwarf_loader.c b/dwarf_loader.c
index 99bc121a377fbd8b..ffdbd6c9fa3e6401 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -154,6 +154,9 @@ struct dwarf_cu {
 	struct dwarf_tag *last_type_lookup;
 	struct cu *cu;
 	struct dwarf_cu *type_unit;
+	Dwarf_Off *imported_units;
+	uint32_t  nr_imported_units;
+	uint32_t  allocated_imported_units;
 };
 
 static int dwarf_cu__init(struct dwarf_cu *dcu, struct cu *cu)
@@ -179,6 +182,9 @@ static int dwarf_cu__init(struct dwarf_cu *dcu, struct cu *cu)
 		INIT_HLIST_HEAD(&dcu->hash_types[i]);
 	}
 	dcu->type_unit = NULL;
+	dcu->imported_units = NULL;
+	dcu->nr_imported_units = 0;
+	dcu->allocated_imported_units = 0;
 	// To avoid a per-lookup check against NULL in dwarf_cu__find_type_by_ref()
 	dcu->last_type_lookup = &sentinel_dtag;
 	return 0;
@@ -203,6 +209,7 @@ static void dwarf_cu__delete(struct cu *cu)
 
 	struct dwarf_cu *dcu = cu->priv;
 
+	free(dcu->imported_units);
 	// dcu->hash_tags & dcu->hash_types are on cu->obstack
 	cu__free(cu, dcu);
 	cu->priv = NULL;
@@ -486,12 +493,32 @@ static const char *attr_string(Dwarf_Die *die, uint32_t name, struct conf_load *
 	return str;
 }
 
+static bool attr_form_is_ref_alt(Dwarf_Attribute *attr)
+{
+	if (attr->form == DW_FORM_GNU_ref_alt) {
+		static bool warned;
+
+		if (!warned) {
+			fprintf(stderr,
+				"WARNING: DW_FORM_GNU_ref_alt (dwz alternate debug file) not yet supported,\n"
+				"         some types will not be available.\n");
+			warned = true;
+		}
+		return true;
+	}
+	return false;
+}
+
 static bool attr_type(Dwarf_Die *die, uint32_t attr_name, Dwarf_Off *offset)
 {
 	Dwarf_Attribute attr;
 
 	if (dwarf_attr(die, attr_name, &attr) != NULL) {
 		Dwarf_Die type_die;
+		if (attr_form_is_ref_alt(&attr)) {
+			*offset = 0;
+			return 0;
+		}
 		if (dwarf_formref_die(&attr, &type_die) != NULL) {
 			*offset = dwarf_dieoffset(&type_die);
 			return attr.form == DW_FORM_ref_sig8;
@@ -722,7 +749,8 @@ static void type__init(struct type *type, Dwarf_Die *die, struct cu *cu, struct
 	Dwarf_Attribute attr;
 	if (dwarf_attr(die, DW_AT_type, &attr) != NULL) {
 		Dwarf_Die type_die;
-		if (dwarf_formref_die(&attr, &type_die) != NULL) {
+		if (!attr_form_is_ref_alt(&attr) &&
+		    dwarf_formref_die(&attr, &type_die) != NULL) {
 			uint64_t encoding = attr_numeric(&type_die, DW_AT_encoding);
 
 			if (encoding == DW_ATE_signed || encoding == DW_ATE_signed_char)
@@ -1036,9 +1064,14 @@ static int add_gnu_annotation_chain(Dwarf_Die *die, int component_idx,
 	Dwarf_Attribute attr;
 	Dwarf_Die annot_die;
 
-	while (dwarf_attr(die, DW_AT_GNU_annotation, &attr) != NULL &&
-	       dwarf_formref_die(&attr, &annot_die) != NULL &&
-	       dwarf_tag(&annot_die) == DW_TAG_GNU_annotation) {
+	while (dwarf_attr(die, DW_AT_GNU_annotation, &attr) != NULL) {
+		if (attr_form_is_ref_alt(&attr))
+			break;
+		if (dwarf_formref_die(&attr, &annot_die) == NULL)
+			break;
+		if (dwarf_tag(&annot_die) != DW_TAG_GNU_annotation)
+			break;
+
 		int ret = add_tag_annotation(&annot_die, component_idx, conf, head);
 		if (ret)
 			return ret;
@@ -2079,9 +2112,13 @@ check_gnu_attr:
 		goto out;
 
 	/* Handle GCC-style DW_AT_GNU_annotation attribute */
-	while (dwarf_attr(die, DW_AT_GNU_annotation, &attr) != NULL &&
-	       dwarf_formref_die(&attr, &annot_die) != NULL &&
-	       dwarf_tag(&annot_die) == DW_TAG_GNU_annotation) {
+	while (dwarf_attr(die, DW_AT_GNU_annotation, &attr) != NULL) {
+		if (attr_form_is_ref_alt(&attr))
+			break;
+		if (dwarf_formref_die(&attr, &annot_die) == NULL)
+			break;
+		if (dwarf_tag(&annot_die) != DW_TAG_GNU_annotation)
+			break;
 		name = attr_string(&annot_die, DW_AT_name, conf);
 		if (strcmp(name, "btf_type_tag") != 0)
 			break;
@@ -2906,7 +2943,7 @@ static struct tag *__die__process_tag(Dwarf_Die *die, struct cu *cu,
 
 	switch (dwarf_tag(die)) {
 	case DW_TAG_imported_unit:
-		return NULL; // We don't support imported units yet, so to avoid segfaults
+		return &unsupported_tag; // Handled in die__process_unit()
 	case DW_TAG_array_type:
 		tag = die__create_new_array(die, cu);		break;
 	case DW_TAG_string_type: // FORTRAN stuff, looks like an array
@@ -2974,9 +3011,90 @@ static struct tag *__die__process_tag(Dwarf_Die *die, struct cu *cu,
 	return tag;
 }
 
-static int die__process_unit(Dwarf_Die *die, struct cu *cu, struct conf_load *conf)
+#define MAX_IMPORTED_UNIT_DEPTH 64
+
+static int die__process_unit(Dwarf_Die *die, struct cu *cu, struct conf_load *conf, int import_depth);
+
+static bool dwarf_cu__imported_unit_visited(struct dwarf_cu *dcu, Dwarf_Off offset)
+{
+	for (uint32_t i = 0; i < dcu->nr_imported_units; i++)
+		if (dcu->imported_units[i] == offset)
+			return true;
+	return false;
+}
+
+static int dwarf_cu__mark_imported_unit(struct dwarf_cu *dcu, struct cu *cu, Dwarf_Off offset)
+{
+	if (dcu->nr_imported_units == dcu->allocated_imported_units) {
+		uint32_t new_size = dcu->allocated_imported_units ? dcu->allocated_imported_units * 2 : 16;
+		Dwarf_Off *new_array = realloc(dcu->imported_units, new_size * sizeof(Dwarf_Off));
+		if (new_array == NULL)
+			return -ENOMEM;
+		dcu->imported_units = new_array;
+		dcu->allocated_imported_units = new_size;
+	}
+	dcu->imported_units[dcu->nr_imported_units++] = offset;
+	return 0;
+}
+
+static int die__process_imported_unit(Dwarf_Die *die, struct cu *cu, struct conf_load *conf, int import_depth)
+{
+	Dwarf_Attribute attr;
+
+	if (dwarf_attr(die, DW_AT_import, &attr) == NULL)
+		return 0;
+
+	if (attr_form_is_ref_alt(&attr))
+		return 0;
+
+	Dwarf_Die imported_die;
+
+	if (dwarf_formref_die(&attr, &imported_die) == NULL)
+		return 0;
+
+	if (dwarf_tag(&imported_die) != DW_TAG_partial_unit)
+		return 0;
+
+	if (import_depth >= MAX_IMPORTED_UNIT_DEPTH) {
+		static bool warned;
+
+		if (!warned) {
+			fprintf(stderr,
+				"WARNING: DW_TAG_imported_unit nesting too deep (>%d), "
+				"some types will not be available.\n",
+				MAX_IMPORTED_UNIT_DEPTH);
+			warned = true;
+		}
+		return 0;
+	}
+
+	Dwarf_Off offset = dwarf_dieoffset(&imported_die);
+	struct dwarf_cu *dcu = cu->priv;
+
+	if (dwarf_cu__imported_unit_visited(dcu, offset))
+		return 0;
+
+	if (dwarf_cu__mark_imported_unit(dcu, cu, offset))
+		return -ENOMEM;
+
+	Dwarf_Die child;
+
+	if (dwarf_child(&imported_die, &child) == 0)
+		return die__process_unit(&child, cu, conf, import_depth + 1);
+
+	return 0;
+}
+
+static int die__process_unit(Dwarf_Die *die, struct cu *cu, struct conf_load *conf, int import_depth)
 {
 	do {
+		if (dwarf_tag(die) == DW_TAG_imported_unit) {
+			int err = die__process_imported_unit(die, cu, conf, import_depth);
+			if (err)
+				return err;
+			continue;
+		}
+
 		struct tag *tag = die__process_tag(die, cu, 1, conf);
 		if (tag == NULL)
 			return -ENOMEM;
@@ -3822,17 +3940,8 @@ static int die__process(Dwarf_Die *die, struct cu *cu, struct conf_load *conf)
 		return 0; // so that other units can be processed
 	}
 
-	if (tag == DW_TAG_partial_unit) {
-		static bool warned;
-
-		if (!warned) {
-			fprintf(stderr, "WARNING: DW_TAG_partial_unit used, some types will not be considered!\n"
-					"         Probably this was optimized using a tool like 'dwz'\n"
-					"         A future version of pahole will support this.\n");
-			warned = true;
-		}
-		return 0; // so that other units can be processed
-	}
+	if (tag == DW_TAG_partial_unit)
+		return 0; // Processed inline when reached via DW_TAG_imported_unit
 
 	if (tag != DW_TAG_compile_unit && tag != DW_TAG_type_unit) {
 		fprintf(stderr, "%s: DW_TAG_compile_unit, DW_TAG_type_unit, DW_TAG_partial_unit or DW_TAG_skeleton_unit expected got %s (0x%x) @ %llx!\n",
@@ -3854,7 +3963,7 @@ static int die__process(Dwarf_Die *die, struct cu *cu, struct conf_load *conf)
 		return DWARF_CB_OK;
 
 	if (dwarf_child(die, &child) == 0) {
-		int err = die__process_unit(&child, cu, conf);
+		int err = die__process_unit(&child, cu, conf, 0);
 		if (err)
 			return err;
 	}
@@ -4626,7 +4735,7 @@ static int cus__merge_and_process_cu(struct cus *cus, struct conf_load *conf,
 				filtered = conf->early_cu_filter(&unmerged_cu) == NULL;
 			}
 
-			if (!filtered && die__process_unit(&child, cu, conf) != 0)
+			if (!filtered && die__process_unit(&child, cu, conf, 0) != 0)
 				goto out_abort;
 		}
 
diff --git a/man-pages/pahole.1 b/man-pages/pahole.1
index e3ce737460efec72..96ed80069e1e6d80 100644
--- a/man-pages/pahole.1
+++ b/man-pages/pahole.1
@@ -373,10 +373,10 @@ Non-standard, non-BTF related features:
 	                   and was observed with Rust CUs, where types tags
 	                   (function parameters, abstract origins for inlines, etc)
 	                   reference types in another CU.
-	                   For LTO this is being autodetected and the merging of
-	                   cus is done automatically, but for the Rust case, and
-	                   maybe others this is needed with the current DWARF
-	                   loading algorithm.
+	                   Imported units (DW_TAG_imported_unit for LTO and Rust)
+	                   and .dwz alternate debug files (DW_FORM_GNU_ref_alt) are
+	                   now auto-detected and processed automatically, so
+	                   force_cu_merging is rarely needed with current pahole.
 
 .fi
 
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 08/12] dwarf_loader: Fix cus__merging_cu failing to detect DW_FORM_ref_addr
  2026-07-31 19:30 [PATCHES 00/12] pahole: Support more rust tags and references to dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (6 preceding siblings ...)
  2026-07-31 19:30 ` [PATCH 07/12] dwarf_loader: Support DW_TAG_imported_unit for same-file partial units Arnaldo Carvalho de Melo
@ 2026-07-31 19:30 ` Arnaldo Carvalho de Melo
  2026-07-31 19:30 ` [PATCH 09/12] dwarf_loader: Add cu parameter to tag__set_spec() and dwarf_tag__set_attr_type() Arnaldo Carvalho de Melo
                   ` (3 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-07-31 19:30 UTC (permalink / raw)
  To: Alan Maguire
  Cc: Jiri Olsa, Clark Williams, dwarves, bpf, Andrii Nakryiko,
	Yonghong Song, Mark Wieelard, Arnaldo Carvalho de Melo

From: Arnaldo Carvalho de Melo <acme@redhat.com>

cus__merging_cu() scans abbreviation tables looking for DW_FORM_ref_addr
to detect binaries with inter-CU type references (like Rust CUs in
perf). When found, it triggers the merged CU loading path that can
resolve cross-CU references.

However, dwarf_getabbrevattr() can fail on certain attributes, notably
when DW_FORM_implicit_const is used (DWARF5). The function was treating
this failure as terminal, returning false immediately without scanning
the remaining abbreviations. This prevented detection of
DW_FORM_ref_addr in later CUs, causing the parallel path to be taken
instead — which cannot resolve cross-CU references.

For example, with the perf binary containing 507 CUs where 7 Rust CUs
(CU 209-215) use DW_FORM_ref_addr, the function was failing at CU 0
abbreviation 20 attribute 8 and returning false, never reaching the
Rust CUs.

Before:

  $ pahole -F dwarf ~/bin/perf 2>&1 | grep "couldn't find" | wc -l
  314
  $ diff <(pahole -F dwarf ~/bin/perf 2>/dev/null) \
         <(pahole --features=force_cu_merging -F dwarf ~/bin/perf 2>/dev/null) \
    | grep '^[<>]' | wc -l
  70

After:

  $ pahole -F dwarf ~/bin/perf 2>&1 | grep "couldn't find" | wc -l
  0
  $ diff <(pahole -F dwarf ~/bin/perf 2>/dev/null) \
         <(pahole --features=force_cu_merging -F dwarf ~/bin/perf 2>/dev/null) \
    | wc -l
  0

The fix changes dwarf_getattrcnt() failure to skip the current
abbreviation (goto next_abbrev) and dwarf_getabbrevattr() failure to
skip to the next attribute (continue), both continuing to scan for
DW_FORM_ref_addr instead of aborting the entire detection.

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 dwarf_loader.c | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/dwarf_loader.c b/dwarf_loader.c
index ffdbd6c9fa3e6401..ab4036dc32cdda61 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -4287,7 +4287,7 @@ static bool cus__merging_cu(Dwarf *dw, Elf *elf)
 
 			size_t attrcnt;
 			if (dwarf_getattrcnt (abbrev, &attrcnt) != 0)
-				return false;
+				goto next_abbrev;
 
 			unsigned int attr_num, attr_form;
 			Dwarf_Off aboffset;
@@ -4295,10 +4295,11 @@ static bool cus__merging_cu(Dwarf *dw, Elf *elf)
 			for (j = 0; j < attrcnt; ++j) {
 				if (dwarf_getabbrevattr (abbrev, j, &attr_num, &attr_form,
 							 &aboffset))
-					return false;
+					continue;
 				if (attr_form == DW_FORM_ref_addr)
 					return true;
 			}
+next_abbrev:
 
 			offset += length;
 		}
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 09/12] dwarf_loader: Add cu parameter to tag__set_spec() and dwarf_tag__set_attr_type()
  2026-07-31 19:30 [PATCHES 00/12] pahole: Support more rust tags and references to dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (7 preceding siblings ...)
  2026-07-31 19:30 ` [PATCH 08/12] dwarf_loader: Fix cus__merging_cu failing to detect DW_FORM_ref_addr Arnaldo Carvalho de Melo
@ 2026-07-31 19:30 ` Arnaldo Carvalho de Melo
  2026-07-31 19:30 ` [PATCH 10/12] dwarf_loader: Support DW_FORM_GNU_ref_alt references to dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (2 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-07-31 19:30 UTC (permalink / raw)
  To: Alan Maguire
  Cc: Jiri Olsa, Clark Williams, dwarves, bpf, Andrii Nakryiko,
	Yonghong Song, Mark Wieelard, Arnaldo Carvalho de Melo

From: Arnaldo Carvalho de Melo <acme@redhat.com>

Thread the cu parameter through the reference-setting helpers, in
preparation for using it to track which offset space (main file vs dwz
alternate file) each reference targets.

The cu parameter is currently unused: dwarf_tag__set_attr_type() silently
ignores the extra macro argument, and tag__set_spec() marks it
__maybe_unused.

No functional change.

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 dwarf_loader.c | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/dwarf_loader.c b/dwarf_loader.c
index ab4036dc32cdda61..39a49e9a5048dee7 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -572,7 +572,7 @@ static void tag__free(struct tag *tag, struct cu *cu)
 	cu__free(cu, dtag);
 }
 
-#define dwarf_tag__set_attr_type(dtag, field, die, attr_name) \
+#define dwarf_tag__set_attr_type(dtag, field, die, attr_name, cu) \
 	dtag->from_types_section.field = attr_type(die, attr_name, &dtag->field)
 
 static void tag__init(struct tag *tag, struct cu *cu, Dwarf_Die *die)
@@ -584,13 +584,13 @@ static void tag__init(struct tag *tag, struct cu *cu, Dwarf_Die *die)
 	dtag->id  = dwarf_dieoffset(die);
 
 	if (tag->tag == DW_TAG_imported_module || tag->tag == DW_TAG_imported_declaration)
-		dwarf_tag__set_attr_type(dtag, type, die, DW_AT_import);
+		dwarf_tag__set_attr_type(dtag, type, die, DW_AT_import, cu);
 	else if (tag->tag == DW_TAG_variant_part)
-		dwarf_tag__set_attr_type(dtag, type, die, DW_AT_discr);
+		dwarf_tag__set_attr_type(dtag, type, die, DW_AT_discr, cu);
 	else
-		dwarf_tag__set_attr_type(dtag, type, die, DW_AT_type);
+		dwarf_tag__set_attr_type(dtag, type, die, DW_AT_type, cu);
 
-	dwarf_tag__set_attr_type(dtag, abstract_origin, die, DW_AT_abstract_origin);
+	dwarf_tag__set_attr_type(dtag, abstract_origin, die, DW_AT_abstract_origin, cu);
 	tag->recursivity_level = 0;
 	tag->attributes = NULL;
 
@@ -633,10 +633,10 @@ static struct tag *tag__new(Dwarf_Die *die, struct cu *cu)
 	return tag;
 }
 
-static void tag__set_spec(struct tag *tag, Dwarf_Die *die)
+static void tag__set_spec(struct tag *tag, Dwarf_Die *die, struct cu *cu __maybe_unused)
 {
 	struct dwarf_tag *dtag = tag__dwarf(tag);
-	dwarf_tag__set_attr_type(dtag, specification, die, DW_AT_specification);
+	dwarf_tag__set_attr_type(dtag, specification, die, DW_AT_specification, cu);
 }
 
 static struct ptr_to_member_type *ptr_to_member_type__new(Dwarf_Die *die,
@@ -647,7 +647,7 @@ static struct ptr_to_member_type *ptr_to_member_type__new(Dwarf_Die *die,
 	if (ptr != NULL) {
 		tag__init(&ptr->tag, cu, die);
 		struct dwarf_tag *dtag = tag__dwarf(&ptr->tag);
-		dwarf_tag__set_attr_type(dtag, containing_type, die, DW_AT_containing_type);
+		dwarf_tag__set_attr_type(dtag, containing_type, die, DW_AT_containing_type, cu);
 	}
 
 	return ptr;
@@ -738,7 +738,7 @@ static void type__init(struct type *type, Dwarf_Die *die, struct cu *cu, struct
 	type->size		 = attr_numeric(die, DW_AT_byte_size);
 	type->alignment		 = attr_alignment(die, conf);
 	type->declaration	 = attr_numeric(die, DW_AT_declaration);
-	tag__set_spec(&type->namespace.tag, die);
+	tag__set_spec(&type->namespace.tag, die, cu);
 	type->definition_emitted = 0;
 	type->fwd_decl_emitted	 = 0;
 	type->resized		 = 0;
@@ -851,7 +851,7 @@ static struct variable *variable__new(Dwarf_Die *die, struct cu *cu, struct conf
 		if (!var->declaration && cu->has_addr_info)
 			var->scope = dwarf__location(die, &var->ip.addr, &var->location);
 		if (has_specification) {
-			tag__set_spec(&var->ip.tag, die);
+			tag__set_spec(&var->ip.tag, die, cu);
 		}
 	}
 
@@ -1326,7 +1326,7 @@ static struct variant *variant__new(Dwarf_Die *die, struct cu *cu, struct conf_l
 					struct dwarf_tag *dtag = tag__dwarf(&var->tag);
 
 					var->name = attr_string(&child, DW_AT_name, conf);
-					dwarf_tag__set_attr_type(dtag, type, &child, DW_AT_type);
+					dwarf_tag__set_attr_type(dtag, type, &child, DW_AT_type, cu);
 					break;
 				}
 			} while (dwarf_siblingof(&child, &child) == 0);
@@ -1779,7 +1779,7 @@ static struct inline_expansion *inline_expansion__new(Dwarf_Die *die, struct cu
 		tag__init(&exp->ip.tag, cu, die);
 		dtag->decl_file = attr_string(die, DW_AT_call_file, conf);
 		dtag->decl_line = attr_numeric(die, DW_AT_call_line);
-		dwarf_tag__set_attr_type(dtag, type, die, DW_AT_abstract_origin);
+		dwarf_tag__set_attr_type(dtag, type, die, DW_AT_abstract_origin, cu);
 		exp->ip.addr = 0;
 		exp->high_pc = 0;
 
@@ -1941,7 +1941,7 @@ static struct function *function__new(Dwarf_Die *die, struct cu *cu, struct conf
 		func->declaration     = dwarf_hasattr(die, DW_AT_declaration);
 		func->external	      = dwarf_hasattr(die, DW_AT_external);
 		func->abstract_origin = dwarf_hasattr(die, DW_AT_abstract_origin);
-		tag__set_spec(&func->proto.tag, die);
+		tag__set_spec(&func->proto.tag, die, cu);
 		func->accessibility   = attr_numeric(die, DW_AT_accessibility);
 		func->virtuality      = attr_numeric(die, DW_AT_virtuality);
 		INIT_LIST_HEAD(&func->vtable_node);
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 10/12] dwarf_loader: Support DW_FORM_GNU_ref_alt references to dwz alternate debug files
  2026-07-31 19:30 [PATCHES 00/12] pahole: Support more rust tags and references to dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (8 preceding siblings ...)
  2026-07-31 19:30 ` [PATCH 09/12] dwarf_loader: Add cu parameter to tag__set_spec() and dwarf_tag__set_attr_type() Arnaldo Carvalho de Melo
@ 2026-07-31 19:30 ` Arnaldo Carvalho de Melo
  2026-07-31 19:30 ` [PATCH 11/12] tests: Add inter-CU type reference comparison test Arnaldo Carvalho de Melo
  2026-07-31 19:31 ` [PATCH 12/12] scripts: Add vmlinux_comparison.py for DWARF/BTF analysis Arnaldo Carvalho de Melo
  11 siblings, 0 replies; 13+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-07-31 19:30 UTC (permalink / raw)
  To: Alan Maguire
  Cc: Jiri Olsa, Clark Williams, dwarves, bpf, Andrii Nakryiko,
	Yonghong Song, Mark Wieelard, Arnaldo Carvalho de Melo

From: Arnaldo Carvalho de Melo <acme@redhat.com>

Binaries processed by dwz(1) can have shared types deduplicated into a
separate alternate debug file (.dwz), referenced via DW_FORM_GNU_ref_alt
through the .gnu_debugaltlink ELF section.  elfutils resolves these
references transparently via dwarf_getalt(), but pahole was skipping
them with a warning because the alt file's DWARF offset space is
separate from the main file's — mixing them in the same hash tables
caused type corruption and crashes.

Add a separate dwarf_cu (dcu->alt) with its own hash tables for the alt
file's offset space.  This mirrors the existing type_unit pattern but
uses direct lookup instead of fallback: the DWARF form tells us exactly
which hash table to use.

Key changes:

  - Add 'from_alt' bitfield to struct dwarf_tag, parallel to
    from_types_section, marking references that point to the alt file.

  - Add 'alt' pointer and 'processing_alt' flag to struct dwarf_cu.
    When processing children of an alt-file imported unit,
    processing_alt directs cu__hash() to use dcu->alt hash tables
    and causes all type references to be marked from_alt.

  - Update attr_type() to resolve DW_FORM_GNU_ref_alt via
    dwarf_formref_die() instead of skipping.  Returns the alt offset
    and sets is_alt so the caller marks from_alt on the dwarf_tag.
    When the alt file cannot be resolved (missing .gnu_debugaltlink
    target or build-id mismatch), emit a one-shot warning naming the
    artifact, the likely cause, and the fix (installing the matching
    debuginfo package).

  - Update dwarf_cu__find_{tag,type}_by_ref() to check from_alt and
    go directly to dcu->alt for lookup (not a fallback — the form
    is unambiguous).

  - Create the alt dwarf_cu in cus__merge_and_process_cu() via
    dwarf_getalt() before processing any CUs, so it's available
    when die__process_imported_unit() encounters the first alt ref.

  - Trigger the merged CU path when dwarf_getalt() finds an alt file,
    since the alt hash tables are only set up in that path.

  - Prune unreferenced alt partial units after processing all CUs:
    PUs directly imported by main-file CUs, or imported by any
    other PU, are conservatively retained — the hit-marking
    during pre-processing is not limited to main-CU-reachable
    PUs, so some unreferenced PUs may survive pruning.  Pruning
    creates NULL holes in types_table; a prefix-sum adjustment
    array in btf_encoder lets
    btf_encoder__tag_type() translate small_ids to dense BTF IDs in
    O(1).  This relies on a documented dwz invariant: inter-PU
    dependencies use DW_TAG_imported_unit, not bare DW_FORM_ref_addr.
    tag__check_pruned_alt_ref() detects violations at recode time by
    checking whether a failed from_alt lookup targets a pruned PU.

  - Alt PU ranges are kept in a sorted array and located via binary
    search (dwarf_cu__find_alt_pu), since large dwz alt files can
    contain thousands of partial units and every type reference needs
    to identify which PU it belongs to.

  - dwarf_cu__delete() now explicitly frees hash_tags and
    hash_types via cu__free() for all dwarf_cu instances, not just
    the alt one.  Under obstack this is a no-op, but in the
    non-obstack path it fixes a pre-existing leak.

  - Remove attr_form_is_ref_alt() and all its callers — no longer
    needed since alt refs are now resolved instead of skipped.

  - Add tests/dwz_alt_file.sh: creates two binaries sharing types,
    runs dwz to produce an alt file, verifies pahole resolves the
    shared types.  Includes a negative sub-test that hides the alt
    file and verifies the one-shot warning fires exactly once and
    pahole does not crash.

The btf_encoder__tag_type() call site consolidation and the
dwarf_tag__set_attr_type() / tag__set_spec() cu parameter threading
were split into separate prep patches.

Before (chromium-browser debuginfo, Fedora 44):

  $ pahole -F dwarf .../chromium-browser-149.0.7827.155-1.fc44.x86_64.debug
  WARNING: DW_FORM_GNU_ref_alt (dwz alternate debug file) not yet supported,
           some types will not be available.
  [no output]

After:

  $ pahole -F dwarf .../chromium-browser-149.0.7827.155-1.fc44.x86_64.debug | wc -l
  6378

This was based in previous, not AI assisted work back in 2023, as stored
in these branches:

  https://git.kernel.org/pub/scm/devel/pahole/pahole.git/log/?h=alt_dwarf and
  https://git.kernel.org/pub/scm/devel/pahole/pahole.git/commit/?h=WIP-imported-unit

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 btf_encoder.c         |   3 +-
 ctf_encoder.c         |  11 +
 dwarf_loader.c        | 579 +++++++++++++++++++++++++++++++++++++-----
 dwarves.h             |  20 ++
 tests/dwz_alt_file.sh | 204 +++++++++++++++
 5 files changed, 748 insertions(+), 69 deletions(-)
 create mode 100755 tests/dwz_alt_file.sh

diff --git a/btf_encoder.c b/btf_encoder.c
index acf0a5ade5e29014..1d645977319c9991 100644
--- a/btf_encoder.c
+++ b/btf_encoder.c
@@ -768,7 +768,8 @@ static int32_t btf_encoder__tag_type(struct btf_encoder *encoder, uint32_t tag_t
 	if (encoder->btf_id_map && tag_type < encoder->btf_id_map_sz)
 		return encoder->btf_id_map[tag_type];
 
-	/* Fallback for map-absent CUs (nr == 0) or out-of-range ids */
+	/* Fallback for map-absent CUs (nr == 0) or out-of-range ids;
+	 * adjust for NULL holes left by dwz alt PU pruning */
 	uint32_t adj = encoder->type_id_null_adj ? encoder->type_id_null_adj[tag_type] : 0;
 
 	return encoder->type_id_off + tag_type - adj;
diff --git a/ctf_encoder.c b/ctf_encoder.c
index f9e75a5821ce4e83..d7fc1c1f94875b16 100644
--- a/ctf_encoder.c
+++ b/ctf_encoder.c
@@ -263,6 +263,17 @@ int cu__encode_ctf(struct cu *cu, int verbose)
 
 	uint32_t id;
 	struct tag *pos;
+
+	/* CTF IDs must be sequential — NULL holes in the types table
+	 * (from dwz alternate debug files) would desync core vs CTF IDs.
+	 * Bail out rather than generate corrupt CTF. */
+	for (id = 1; id < cu->types_table.nr_entries; ++id) {
+		if (cu->types_table.entries[id] == NULL) {
+			fprintf(stderr, "ctf_encoder: NULL holes in types table not supported for CTF encoding\n");
+			goto out_delete;
+		}
+	}
+
 	cu__for_each_type(cu, id, pos)
 		tag__encode_ctf(pos, cu, id, ctf);
 
diff --git a/dwarf_loader.c b/dwarf_loader.c
index 39a49e9a5048dee7..aca886a6f675343e 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -118,6 +118,16 @@ static struct tag unsupported_tag;
 
 #define cu__tag_not_handled(cu, die) __cu__tag_not_handled(cu, die, __FUNCTION__)
 
+/*
+ * Each DWARF reference attribute can point to one of three offset spaces:
+ *  1. Main file — the default, looked up in dcu->hash_*
+ *  2. .debug_types — DW_FORM_ref_sig8, looked up in dcu->type_unit->hash_*
+ *  3. dwz alt file — DW_FORM_GNU_ref_alt, looked up in dcu->alt->hash_*
+ *
+ * The from_types_section and from_alt bitfields record which space each
+ * reference attribute targets, so the lookup functions go directly to the
+ * right hash table without fallback searches.
+ */
 struct dwarf_tag {
 	struct hlist_node hash_node;
 	Dwarf_Off	 type;
@@ -133,6 +143,12 @@ struct dwarf_tag {
 		bool		 containing_type:1;
 		bool		 specification:1;
 	} from_types_section;
+	struct {
+		bool		 type:1;
+		bool		 abstract_origin:1;
+		bool		 containing_type:1;
+		bool		 specification:1;
+	} from_alt;
 	uint16_t         decl_line;
 	uint32_t         small_id;
 	const char	 *decl_file;
@@ -148,15 +164,26 @@ static inline struct dwarf_tag *tag__dwarf(const struct tag *tag)
 	return ((struct dwarf_tag *)tag) - 1;
 }
 
+struct alt_pu {
+	Dwarf_Off start;	/* first DIE offset in this PU */
+	Dwarf_Off end;		/* first offset past this PU (next CU header) */
+	bool	  hit;		/* referenced by main CUs? */
+};
+
 struct dwarf_cu {
-	struct hlist_head *hash_tags;
-	struct hlist_head *hash_types;
+	struct hlist_head *hash_tags;	/* tags (functions, etc.) keyed by DWARF offset */
+	struct hlist_head *hash_types;	/* types keyed by DWARF offset */
 	struct dwarf_tag *last_type_lookup;
 	struct cu *cu;
-	struct dwarf_cu *type_unit;
-	Dwarf_Off *imported_units;
+	struct dwarf_cu *type_unit;	/* .debug_types offset space */
+	struct dwarf_cu *alt;		/* dwz alt file offset space */
+	Dwarf_Off *imported_units;	/* dedup: visited same-file partial unit offsets */
 	uint32_t  nr_imported_units;
 	uint32_t  allocated_imported_units;
+	struct alt_pu *alt_pus;
+	uint32_t  nr_alt_pus;
+	uint32_t  allocated_alt_pus;
+	bool	  processing_alt;	/* true while processing alt-file DIEs */
 };
 
 static int dwarf_cu__init(struct dwarf_cu *dcu, struct cu *cu)
@@ -182,9 +209,21 @@ static int dwarf_cu__init(struct dwarf_cu *dcu, struct cu *cu)
 		INIT_HLIST_HEAD(&dcu->hash_types[i]);
 	}
 	dcu->type_unit = NULL;
+	dcu->alt = NULL;
 	dcu->imported_units = NULL;
 	dcu->nr_imported_units = 0;
 	dcu->allocated_imported_units = 0;
+	dcu->alt_pus = NULL;
+	dcu->nr_alt_pus = 0;
+	dcu->allocated_alt_pus = 0;
+	/*
+	 * processing_alt is per-merged-CU state, not thread-safe.
+	 * The merged-CU path (cus__merge_and_process_cu) is
+	 * single-threaded; do not parallelize merged loading
+	 * without addressing this and the alt hash routing in
+	 * cu__hash().
+	 */
+	dcu->processing_alt = false;
 	// To avoid a per-lookup check against NULL in dwarf_cu__find_type_by_ref()
 	dcu->last_type_lookup = &sentinel_dtag;
 	return 0;
@@ -209,8 +248,16 @@ static void dwarf_cu__delete(struct cu *cu)
 
 	struct dwarf_cu *dcu = cu->priv;
 
-	free(dcu->imported_units);
-	// dcu->hash_tags & dcu->hash_types are on cu->obstack
+	zfree(&dcu->imported_units);
+	zfree(&dcu->alt_pus);
+	if (dcu->alt) {
+		cu__free(cu, dcu->alt->hash_tags);
+		cu__free(cu, dcu->alt->hash_types);
+		cu__free(cu, dcu->alt);
+		dcu->alt = NULL;
+	}
+	cu__free(cu, dcu->hash_tags);
+	cu__free(cu, dcu->hash_types);
 	cu__free(cu, dcu);
 	cu->priv = NULL;
 }
@@ -252,39 +299,198 @@ static struct dwarf_tag *hashtags__find(const struct hlist_head *hashtable,
 	return NULL;
 }
 
+/*
+ * cu__hash - insert a tag into the appropriate hash table
+ *
+ * Three hash table sets exist per merged CU:
+ *  - dcu->hash_types / hash_tags:  main-file DWARF offsets
+ *  - dcu->alt->hash_types / hash_tags:  dwz alt-file offsets
+ *  - dcu->type_unit->hash_types:  .debug_types offsets
+ *
+ * When dcu->processing_alt is set (i.e. we are processing children of
+ * an alt-file partial unit), tags are hashed into dcu->alt so that
+ * their alt-file offsets don't collide with main-file offsets.
+ */
 static void cu__hash(struct cu *cu, struct tag *tag)
 {
 	struct dwarf_cu *dcu = cu->priv;
+	struct dwarf_cu *target = (dcu->processing_alt && dcu->alt) ? dcu->alt : dcu;
 	struct hlist_head *hashtable = tag__is_tag_type(tag) ?
-							dcu->hash_types :
-							dcu->hash_tags;
+							target->hash_types :
+							target->hash_tags;
 	hashtags__hash(hashtable, tag__dwarf(tag));
 }
 
+/* Binary search for the alt PU whose [start, end) range contains offset.
+ * The array is sorted by .start (built monotonically by dwarf_nextcu)
+ * with non-overlapping ranges. Returns NULL if offset is not in any PU. */
+static struct alt_pu *dwarf_cu__find_alt_pu(struct dwarf_cu *dcu, Dwarf_Off offset)
+{
+	uint32_t lo = 0, hi = dcu->nr_alt_pus;
+
+	while (lo < hi) {
+		uint32_t mid = lo + (hi - lo) / 2;
+
+		if (offset < dcu->alt_pus[mid].start)
+			hi = mid;
+		else if (offset >= dcu->alt_pus[mid].end)
+			lo = mid + 1;
+		else
+			return &dcu->alt_pus[mid];
+	}
+	return NULL;
+}
+
+static bool dwarf_cu__alt_pu_is_hit(struct dwarf_cu *dcu, Dwarf_Off offset)
+{
+	struct alt_pu *pu = dwarf_cu__find_alt_pu(dcu, offset);
+	return pu ? pu->hit : false;
+}
+
+/* True when offset falls inside an alt PU that was not marked as hit —
+ * i.e. it was pruned by dwarf_cu__prune_unreferenced_alt_pus(). */
+static bool dwarf_cu__offset_in_pruned_alt_pu(struct dwarf_cu *dcu, Dwarf_Off offset)
+{
+	struct alt_pu *pu = dwarf_cu__find_alt_pu(dcu, offset);
+	return pu ? !pu->hit : false;
+}
+
+/* After a from_alt lookup miss during recode, check whether the target
+ * offset falls in a PU that was pruned.  This catches dwz invariant
+ * violations where an inter-PU dependency was not expressed via
+ * DW_TAG_imported_unit.
+ *
+ * Works for plain-form refs (ref4 etc.) within the alt file too:
+ * dwarf_tag__set_attr_type ORs processing_alt into from_alt, so
+ * intra-alt refs carry the bit even though their DWARF form is not
+ * DW_FORM_GNU_ref_alt. */
+static void tag__check_pruned_alt_ref(struct dwarf_cu *dcu, Dwarf_Off ref,
+				      bool from_alt)
+{
+	if (!from_alt || dcu == NULL || dcu->nr_alt_pus == 0)
+		return;
+	if (dwarf_cu__offset_in_pruned_alt_pu(dcu, ref))
+		fprintf(stderr,
+			"         reference %#llx is in a pruned dwz alternate partial unit — "
+			"possible dwz invariant violation\n"
+			"         (inter-PU dependency not expressed via DW_TAG_imported_unit)\n",
+			(unsigned long long)ref);
+}
+
+/*
+ * Remove types/functions from dwz alternate partial units that were
+ * never marked as hit.  A PU is marked hit when:
+ *
+ *  (a) a main-file CU references it via DW_FORM_GNU_ref_alt
+ *      (attribute ref or DW_TAG_imported_unit), or
+ *  (b) any other alt PU imports it via DW_TAG_imported_unit —
+ *      regardless of whether that importing PU is itself reachable
+ *      from a main CU, since the pre-processing pass in
+ *      cus__merge_and_process_cu() walks ALL alt PUs with
+ *      hit-marking enabled.
+ *
+ * (b) makes this a conservative over-approximation of "transitively
+ * reachable from main CUs": clusters of PUs that only import each
+ * other survive pruning even if no main CU references the cluster.
+ * Correctness is unaffected — only the amount pruned.  Tightening
+ * this to true main-rooted reachability would require recording
+ * PU→PU import edges during pre-processing and propagating hits
+ * from main-referenced roots before pruning.
+ *
+ * Safety invariant (why pruning can't remove something needed):
+ * dwz expresses inter-PU dependencies via DW_TAG_imported_unit,
+ * not bare DW_FORM_ref_addr.  If dwz violated this, the target PU
+ * could be pruned while still referenced; tag__check_pruned_alt_ref()
+ * detects that at recode time and prints a diagnostic.
+ */
+static void dwarf_cu__prune_unreferenced_alt_pus(struct dwarf_cu *dcu)
+{
+	struct dwarf_cu *alt = dcu->alt;
+	struct cu *cu = dcu->cu;
+
+	if (alt == NULL || dcu->nr_alt_pus == 0)
+		return;
+
+	uint64_t hashtags_size = 1UL << hashtags__bits;
+
+	for (uint64_t i = 0; i < hashtags_size; i++) {
+		struct dwarf_tag *dtag;
+		struct hlist_node *pos, *tmp;
+
+		hlist_for_each_entry_safe(dtag, pos, tmp, &alt->hash_types[i], hash_node) {
+			if (!dwarf_cu__alt_pu_is_hit(dcu, dtag->id)) {
+				struct tag *tag = dtag__tag(dtag);
+				hlist_del(&dtag->hash_node);
+				list_del_init(&tag->node);
+				cu->types_table.entries[dtag->small_id] = NULL;
+			}
+		}
+		hlist_for_each_entry_safe(dtag, pos, tmp, &alt->hash_tags[i], hash_node) {
+			if (!dwarf_cu__alt_pu_is_hit(dcu, dtag->id)) {
+				struct tag *tag = dtag__tag(dtag);
+				hlist_del(&dtag->hash_node);
+				list_del_init(&tag->node);
+				if (tag__is_function(tag)) {
+					rb_erase(&tag__function(tag)->rb_node, &cu->functions);
+					cu->functions_table.entries[dtag->small_id] = NULL;
+				} else
+					cu->tags_table.entries[dtag->small_id] = NULL;
+			}
+		}
+		/*
+		 * Don't free pruned tags here: child tags (members) have
+		 * list nodes linked through their parent's namespace.tags,
+		 * so freeing parents in hash_types before processing children
+		 * in hash_tags would cause use-after-free on list_del_init().
+		 * All tag memory is freed when the CU is destroyed.
+		 */
+	}
+}
+
+/*
+ * Lookup a tag (function, lexblock, etc.) by DWARF offset.
+ *
+ * The from_alt bit tells us the reference came from DW_FORM_GNU_ref_alt
+ * or was inside an alt partial unit (processing_alt), so we go directly
+ * to dcu->alt->hash_tags.  This is a direct lookup, not a fallback —
+ * the DWARF form tells us which offset space the reference belongs to.
+ */
 static struct dwarf_tag *__dwarf_cu__find_tag_by_ref(const struct dwarf_cu *cu,
-						     const Dwarf_Off ref, bool from_types)
+						     const Dwarf_Off ref,
+						     bool from_types, bool from_alt)
 {
 	if (cu == NULL)
 		return NULL;
-	if (from_types) {
+	if (from_types)
 		return NULL;
+	if (from_alt) {
+		cu = cu->alt;
+		if (cu == NULL)
+			return NULL;
 	}
 	return hashtags__find(cu->hash_tags, ref);
 }
 
 #define dwarf_cu__find_tag_by_ref(cu, dtag, field) \
-	__dwarf_cu__find_tag_by_ref(cu, dtag->field, dtag->from_types_section.field)
+	__dwarf_cu__find_tag_by_ref(cu, dtag->field, \
+				    dtag->from_types_section.field, \
+				    dtag->from_alt.field)
 
+/* Same as __dwarf_cu__find_tag_by_ref but for type lookups (hash_types). */
 static struct dwarf_tag *__dwarf_cu__find_type_by_ref(struct dwarf_cu *dcu,
-						      const Dwarf_Off ref, bool from_types)
+						      const Dwarf_Off ref,
+						      bool from_types, bool from_alt)
 {
 	if (dcu == NULL)
 		return NULL;
 	if (from_types) {
 		dcu = dcu->type_unit;
-		if (dcu == NULL) {
+		if (dcu == NULL)
+			return NULL;
+	} else if (from_alt) {
+		dcu = dcu->alt;
+		if (dcu == NULL)
 			return NULL;
-		}
 	}
 
 	if (dcu->last_type_lookup->id == ref)
@@ -299,7 +505,9 @@ static struct dwarf_tag *__dwarf_cu__find_type_by_ref(struct dwarf_cu *dcu,
 }
 
 #define dwarf_cu__find_type_by_ref(dcu, dtag, field) \
-	__dwarf_cu__find_type_by_ref(dcu, dtag->field, dtag->from_types_section.field)
+	__dwarf_cu__find_type_by_ref(dcu, dtag->field, \
+				     dtag->from_types_section.field, \
+				     dtag->from_alt.field)
 
 static void *memdup(const void *src, size_t len, struct cu *cu)
 {
@@ -493,31 +701,65 @@ static const char *attr_string(Dwarf_Die *die, uint32_t name, struct conf_load *
 	return str;
 }
 
-static bool attr_form_is_ref_alt(Dwarf_Attribute *attr)
-{
-	if (attr->form == DW_FORM_GNU_ref_alt) {
-		static bool warned;
-
-		if (!warned) {
-			fprintf(stderr,
-				"WARNING: DW_FORM_GNU_ref_alt (dwz alternate debug file) not yet supported,\n"
-				"         some types will not be available.\n");
-			warned = true;
-		}
-		return true;
-	}
-	return false;
-}
-
-static bool attr_type(Dwarf_Die *die, uint32_t attr_name, Dwarf_Off *offset)
+/**
+ * attr_type - extract a type reference attribute from a DWARF DIE
+ * @die: the DWARF DIE to read the attribute from
+ * @attr_name: the attribute to read (DW_AT_type, DW_AT_import, etc)
+ * @offset: output DWARF offset of the referenced DIE
+ * @is_alt: output flag, set when the reference points to a dwz alt file
+ *
+ * Resolves a DWARF reference attribute to a DIE offset.  Handles three
+ * reference forms:
+ *
+ *  - DW_FORM_GNU_ref_alt: reference into a dwz alternate debug file.
+ *    Resolved transparently by elfutils via dwarf_getalt().  Sets
+ *    @is_alt so callers use the alt hash tables for lookup.
+ *
+ *  - DW_FORM_ref_sig8: reference into a .debug_types section (DWARF4).
+ *    Returns true so callers use the type_unit hash tables.
+ *
+ *  - All other ref forms (ref1/ref2/ref4/ref8/ref_addr): same-file
+ *    references resolved by dwarf_formref_die().
+ *
+ * Return: true if the reference is from the types section (DW_FORM_ref_sig8),
+ *         false otherwise.  @offset and @is_alt are set on output.
+ */
+static bool attr_type(Dwarf_Die *die, uint32_t attr_name, Dwarf_Off *offset,
+		      bool *is_alt)
 {
 	Dwarf_Attribute attr;
 
+	*is_alt = false;
+
 	if (dwarf_attr(die, attr_name, &attr) != NULL) {
 		Dwarf_Die type_die;
-		if (attr_form_is_ref_alt(&attr)) {
+		if (attr.form == DW_FORM_GNU_ref_alt) {
+			if (dwarf_formref_die(&attr, &type_die) != NULL) {
+				*offset = dwarf_dieoffset(&type_die);
+				*is_alt = true;
+				return false;
+			}
+			/* elfutils couldn't resolve the alt reference —
+			 * .gnu_debugaltlink target is missing or has a
+			 * build-id mismatch.  Warn once per process:
+			 * when loading multiple files in batch mode,
+			 * only the first missing alt triggers the
+			 * message — subsequent files with the same
+			 * problem are silent.  This trades completeness
+			 * for avoiding stderr spam on large debuginfo
+			 * directories where many files share the same
+			 * missing alt package. */
+			static bool alt_resolve_warned;
+			if (!alt_resolve_warned) {
+				fprintf(stderr,
+					"WARNING: could not resolve dwz alternate debug file\n"
+					"         (.gnu_debugaltlink target missing or build-id mismatch?)\n"
+					"         Some types will not be available.\n"
+					"         Installing the matching debuginfo package may fix this.\n");
+				alt_resolve_warned = true;
+			}
 			*offset = 0;
-			return 0;
+			return false;
 		}
 		if (dwarf_formref_die(&attr, &type_die) != NULL) {
 			*offset = dwarf_dieoffset(&type_die);
@@ -525,7 +767,7 @@ static bool attr_type(Dwarf_Die *die, uint32_t attr_name, Dwarf_Off *offset)
 		}
 	}
 	*offset = 0;
-	return 0;
+	return false;
 }
 
 static int attr_location(Dwarf_Die *die, Dwarf_Op **expr, size_t *exprlen)
@@ -572,8 +814,44 @@ static void tag__free(struct tag *tag, struct cu *cu)
 	cu__free(cu, dtag);
 }
 
-#define dwarf_tag__set_attr_type(dtag, field, die, attr_name, cu) \
-	dtag->from_types_section.field = attr_type(die, attr_name, &dtag->field)
+static void dwarf_cu__mark_alt_pu_hit(struct dwarf_cu *dcu, Dwarf_Off offset)
+{
+	struct alt_pu *pu = dwarf_cu__find_alt_pu(dcu, offset);
+	if (pu)
+		pu->hit = true;
+}
+
+/*
+ * Extract a reference attribute and record which offset space it targets.
+ *
+ * from_alt is set when either:
+ *  (a) attr_type() saw DW_FORM_GNU_ref_alt — an explicit alt reference, OR
+ *  (b) we are inside an alt partial unit (processing_alt) — a same-file
+ *      ref form like DW_FORM_ref4 that still refers to the alt offset space.
+ *
+ * This bit drives the lookup functions to search dcu->alt hash tables.
+ */
+#define dwarf_tag__set_attr_type(dtag, field, die, attr_name, cu) do {	\
+	bool __is_alt;							\
+	struct dwarf_cu *__dcu = (cu)->priv;				\
+	dtag->from_types_section.field = attr_type(die, attr_name,	\
+						   &dtag->field, &__is_alt); \
+	dtag->from_alt.field = __is_alt ||				\
+			       (__dcu && __dcu->processing_alt);	\
+	/* Only mark alt PU as hit for actual cross-file refs	\
+	 * (DW_FORM_GNU_ref_alt).  During processing_alt,	\
+	 * internal refs within alt PUs use same-file forms	\
+	 * (ref4 etc.) and must NOT mark PUs as hit — doing	\
+	 * so would mark every PU via its own internal refs	\
+	 * and defeat pruning entirely.  Transitive deps are	\
+	 * handled by DW_TAG_imported_unit chains: if PU A	\
+	 * imports PU B, then B gets marked hit during the	\
+	 * alt pre-processing pass in			\
+	 * cus__merge_and_process_cu().			\
+	 */							\
+	if (__dcu && __is_alt)					\
+		dwarf_cu__mark_alt_pu_hit(__dcu, dtag->field);		\
+} while (0)
 
 static void tag__init(struct tag *tag, struct cu *cu, Dwarf_Die *die)
 {
@@ -633,7 +911,7 @@ static struct tag *tag__new(Dwarf_Die *die, struct cu *cu)
 	return tag;
 }
 
-static void tag__set_spec(struct tag *tag, Dwarf_Die *die, struct cu *cu __maybe_unused)
+static void tag__set_spec(struct tag *tag, Dwarf_Die *die, struct cu *cu)
 {
 	struct dwarf_tag *dtag = tag__dwarf(tag);
 	dwarf_tag__set_attr_type(dtag, specification, die, DW_AT_specification, cu);
@@ -749,8 +1027,7 @@ static void type__init(struct type *type, Dwarf_Die *die, struct cu *cu, struct
 	Dwarf_Attribute attr;
 	if (dwarf_attr(die, DW_AT_type, &attr) != NULL) {
 		Dwarf_Die type_die;
-		if (!attr_form_is_ref_alt(&attr) &&
-		    dwarf_formref_die(&attr, &type_die) != NULL) {
+		if (dwarf_formref_die(&attr, &type_die) != NULL) {
 			uint64_t encoding = attr_numeric(&type_die, DW_AT_encoding);
 
 			if (encoding == DW_ATE_signed || encoding == DW_ATE_signed_char)
@@ -1065,8 +1342,6 @@ static int add_gnu_annotation_chain(Dwarf_Die *die, int component_idx,
 	Dwarf_Die annot_die;
 
 	while (dwarf_attr(die, DW_AT_GNU_annotation, &attr) != NULL) {
-		if (attr_form_is_ref_alt(&attr))
-			break;
 		if (dwarf_formref_die(&attr, &annot_die) == NULL)
 			break;
 		if (dwarf_tag(&annot_die) != DW_TAG_GNU_annotation)
@@ -1520,7 +1795,9 @@ static void parameter__record_true_sig_member(struct parameter *parm, Dwarf_Die
 	if (!parm->true_sig_member_name)
 		return;
 
-	parm->true_sig_type_from_types = attr_type(&member_die, DW_AT_type, &parm->true_sig_type);
+	bool is_alt;
+	parm->true_sig_type_from_types = attr_type(&member_die, DW_AT_type, &parm->true_sig_type, &is_alt);
+	parm->true_sig_type_from_alt = is_alt;
 	if (parm->true_sig_type == 0)
 		parm->true_sig_member_name = NULL;
 }
@@ -2112,23 +2389,55 @@ check_gnu_attr:
 		goto out;
 
 	/* Handle GCC-style DW_AT_GNU_annotation attribute */
-	while (dwarf_attr(die, DW_AT_GNU_annotation, &attr) != NULL) {
-		if (attr_form_is_ref_alt(&attr))
-			break;
-		if (dwarf_formref_die(&attr, &annot_die) == NULL)
-			break;
-		if (dwarf_tag(&annot_die) != DW_TAG_GNU_annotation)
-			break;
-		name = attr_string(&annot_die, DW_AT_name, conf);
-		if (strcmp(name, "btf_type_tag") != 0)
-			break;
+	{
+		struct dwarf_cu *annot_dcu = cu->priv;
+		bool was_alt = annot_dcu->processing_alt;
 
-		/* GCC chain is already in BTF order; append to preserve it. */
-		tag = die__add_btf_type_tag(tag, die, &annot_die, cu, conf, false);
-		if (tag == NULL)
-			return NULL;
+		while (dwarf_attr(die, DW_AT_GNU_annotation, &attr) != NULL) {
+			bool is_alt_annot = (attr.form == DW_FORM_GNU_ref_alt);
 
-		die = &annot_die;
+			if (dwarf_formref_die(&attr, &annot_die) == NULL)
+				break;
+			if (dwarf_tag(&annot_die) != DW_TAG_GNU_annotation)
+				break;
+			name = attr_string(&annot_die, DW_AT_name, conf);
+			if (strcmp(name, "btf_type_tag") != 0)
+				break;
+
+			/*
+			 * Create the base wrapper before entering alt context
+			 * so the pointer's type ref stays in the main file's
+			 * hash tables.  Then set processing_alt so the
+			 * annotation itself is hashed into the alt tables.
+			 */
+			/* Mark annotation as hit for both cross-file refs
+			 * (DW_FORM_GNU_ref_alt) and intra-alt refs when
+			 * already in alt context, so annotations aren't
+			 * pruned by dwarf_cu__prune_unreferenced_alt_pus */
+			if (is_alt_annot || annot_dcu->processing_alt) {
+				dwarf_cu__mark_alt_pu_hit(annot_dcu,
+							  dwarf_dieoffset(&annot_die));
+				if (tag == NULL) {
+					tag = die__create_new_btf_type_tag_ptr_type(die, cu);
+					if (tag == NULL) {
+						annot_dcu->processing_alt = was_alt;
+						return NULL;
+					}
+				}
+				annot_dcu->processing_alt = true;
+			}
+
+			/* GCC chain is already in BTF order; append to preserve it. */
+			tag = die__add_btf_type_tag(tag, die, &annot_die, cu, conf, false);
+			if (tag == NULL) {
+				annot_dcu->processing_alt = was_alt;
+				return NULL;
+			}
+
+			die = &annot_die;
+		}
+
+		annot_dcu->processing_alt = was_alt;
 	}
 
 out:
@@ -3037,6 +3346,45 @@ static int dwarf_cu__mark_imported_unit(struct dwarf_cu *dcu, struct cu *cu, Dwa
 	return 0;
 }
 
+/*
+ * Defensive: dwarf_nextcu iterates monotonically so duplicates
+ * should not occur, but guard against it anyway.
+ */
+static bool dwarf_cu__alt_pu_visited(struct dwarf_cu *dcu, Dwarf_Off offset)
+{
+	/* Exact start match — reuse the range search since PU starts
+	 * are unique and searching for a start offset will land in
+	 * the PU whose start == offset (if it exists). */
+	struct alt_pu *pu = dwarf_cu__find_alt_pu(dcu, offset);
+	return pu && pu->start == offset;
+}
+
+static int dwarf_cu__add_alt_pu(struct dwarf_cu *dcu, Dwarf_Off start, Dwarf_Off end)
+{
+	if (dcu->nr_alt_pus == dcu->allocated_alt_pus) {
+		uint32_t new_size = dcu->allocated_alt_pus ? dcu->allocated_alt_pus * 2 : 16;
+		if (new_size <= dcu->allocated_alt_pus)
+			return -ENOMEM;
+		struct alt_pu *new_array = realloc(dcu->alt_pus, new_size * sizeof(*new_array));
+		if (new_array == NULL)
+			return -ENOMEM;
+		dcu->alt_pus = new_array;
+		dcu->allocated_alt_pus = new_size;
+	}
+	dcu->alt_pus[dcu->nr_alt_pus++] = (struct alt_pu){ .start = start, .end = end, .hit = false };
+	return 0;
+}
+
+/**
+ * die__process_imported_unit - process a DW_TAG_imported_unit reference
+ *
+ * For alt-file imports (DW_FORM_GNU_ref_alt / dwz): marks the partial
+ * unit as referenced for pruning.  Alt PUs are pre-processed in bulk
+ * by cus__merge_and_process_cu().
+ *
+ * For same-file imports: processes children inline into the CU's type
+ * tables, with dedup to avoid re-processing.
+ */
 static int die__process_imported_unit(Dwarf_Die *die, struct cu *cu, struct conf_load *conf, int import_depth)
 {
 	Dwarf_Attribute attr;
@@ -3044,8 +3392,7 @@ static int die__process_imported_unit(Dwarf_Die *die, struct cu *cu, struct conf
 	if (dwarf_attr(die, DW_AT_import, &attr) == NULL)
 		return 0;
 
-	if (attr_form_is_ref_alt(&attr))
-		return 0;
+	bool is_alt = (attr.form == DW_FORM_GNU_ref_alt);
 
 	Dwarf_Die imported_die;
 
@@ -3071,6 +3418,18 @@ static int die__process_imported_unit(Dwarf_Die *die, struct cu *cu, struct conf
 	Dwarf_Off offset = dwarf_dieoffset(&imported_die);
 	struct dwarf_cu *dcu = cu->priv;
 
+	if (is_alt || dcu->processing_alt) {
+		/*
+		 * Alt PUs are pre-processed in cus__merge_and_process_cu(),
+		 * so just mark this one as referenced for pruning.
+		 * Mark hits from both main CU imports (is_alt) and
+		 * inter-alt-PU imports (processing_alt) to handle
+		 * transitive dependencies between partial units.
+		 */
+		dwarf_cu__mark_alt_pu_hit(dcu, offset);
+		return 0;
+	}
+
 	if (dwarf_cu__imported_unit_visited(dcu, offset))
 		return 0;
 
@@ -3391,7 +3750,8 @@ static bool parameter__apply_true_sig_member(struct parameter *parm, struct cu *
 
 	tmp.type = parm->true_sig_type;
 	tmp.from_types_section.type = parm->true_sig_type_from_types;
-	dtype = __dwarf_cu__find_type_by_ref(cu->priv, tmp.type, tmp.from_types_section.type);
+	dtype = __dwarf_cu__find_type_by_ref(cu->priv, tmp.type, tmp.from_types_section.type,
+					     parm->true_sig_type_from_alt);
 	if (!dtype)
 		return false;
 
@@ -3547,10 +3907,15 @@ static void lexblock__recode_dwarf_types(struct lexblock *tag, struct cu *cu)
 			else
 				dtype = dwarf_cu__find_tag_by_ref(dcu, dpos, abstract_origin);
 			if (dtype == NULL) {
-				if (dpos->type != 0)
+				if (dpos->type != 0) {
 					tag__print_type_not_found(pos);
-				else
+					tag__check_pruned_alt_ref(dcu, dpos->type,
+								  dpos->from_alt.type);
+				} else {
 					tag__print_abstract_origin_not_found(pos);
+					tag__check_pruned_alt_ref(dcu, dpos->abstract_origin,
+								  dpos->from_alt.abstract_origin);
+				}
 				continue;
 			}
 			ftype__recode_dwarf_types(dtag__tag(dtype), cu);
@@ -3612,6 +3977,7 @@ static void lexblock__recode_dwarf_types(struct lexblock *tag, struct cu *cu)
 		dtype = dwarf_cu__find_type_by_ref(dcu, dpos, type);
 		if (dtype == NULL) {
 			tag__print_type_not_found(pos);
+			tag__check_pruned_alt_ref(dcu, dpos->type, dpos->from_alt.type);
 			continue;
 		}
 		pos->type = dtype->small_id;
@@ -3732,10 +4098,14 @@ static int tag__recode_dwarf_type(struct tag *tag, struct cu *cu)
 
 	case DW_TAG_namespace:
 		return namespace__recode_dwarf_types(tag, cu);
-	/* Damn, DW_TAG_inlined_subroutine is an special case
-           as dwarf_tag->id is in fact an abtract origin, i.e. must be
-	   looked up in the tags_table, not in the types_table.
-	   The others also point to routines, so are in tags_table */
+	/*
+	 * DW_TAG_inlined_subroutine uses DW_AT_abstract_origin to
+	 * reference the out-of-line subprogram.  inline_expansion__new()
+	 * stores this in dtag->type (not dtag->abstract_origin) via
+	 * dwarf_tag__set_attr_type(dtag, type, die, DW_AT_abstract_origin).
+	 * For dwz binaries, the target subprogram lives in the alt file,
+	 * so dtag->from_alt.type drives the lookup to dcu->alt->hash_tags.
+	 */
 	case DW_TAG_inlined_subroutine:
 	case DW_TAG_imported_module:
 		dtype = dwarf_cu__find_tag_by_ref(cu->priv, dtag, type);
@@ -3773,6 +4143,7 @@ find_type:
 check_type:
 	if (dtype == NULL) {
 		tag__print_type_not_found(tag);
+		tag__check_pruned_alt_ref(cu->priv, dtag->type, dtag->from_alt.type);
 		return 0;
 	}
 out:
@@ -4718,6 +5089,75 @@ static int cus__merge_and_process_cu(struct cus *cus, struct conf_load *conf,
 			dcu->type_unit = type_dcu;
 			cu->priv = dcu;
 			cu->dfops = &dwarf__ops;
+
+			/*
+			 * Check for a dwz alternate debug file.  Create
+			 * a separate dwarf_cu with its own hash tables
+			 * for the alt file's offset space, so that
+			 * DW_FORM_GNU_ref_alt references during
+			 * die__process_unit() can be hashed and looked
+			 * up without colliding with main-file offsets.
+			 *
+			 * Pre-process all partial units in the alt file
+			 * so that DW_FORM_GNU_ref_alt references from
+			 * regular attributes (DW_AT_abstract_origin,
+			 * DW_AT_type, etc. on inlined_subroutines and
+			 * other DIEs) can be resolved.  These direct
+			 * references can point to any DIE in the alt
+			 * file, not just those in explicitly imported
+			 * partial units.
+			 */
+			Dwarf *alt_dw = dwarf_getalt(dw);
+
+			if (alt_dw != NULL) {
+				struct dwarf_cu *alt_dcu = dwarf_cu__new(cu);
+
+				if (alt_dcu == NULL)
+					goto out_abort;
+
+				alt_dcu->cu = cu;
+				dcu->alt = alt_dcu;
+
+				Dwarf_Off alt_off = 0, alt_noff;
+				size_t alt_cuhl;
+
+				/*
+				 * First pass: register all alt PUs so that
+				 * forward references between them can be
+				 * resolved during processing.
+				 */
+				while (dwarf_nextcu(alt_dw, alt_off, &alt_noff,
+						   &alt_cuhl, NULL, NULL, NULL) == 0) {
+					Dwarf_Die alt_die_mem;
+
+					if (dwarf_offdie(alt_dw, alt_off + alt_cuhl,
+							 &alt_die_mem) != NULL &&
+					    !dwarf_cu__alt_pu_visited(dcu, alt_off + alt_cuhl)) {
+						if (dwarf_cu__add_alt_pu(dcu, alt_off + alt_cuhl, alt_noff))
+							goto out_abort;
+					}
+					alt_off = alt_noff;
+				}
+
+				/* Second pass: process DIEs now that all PUs
+				 * are in the array and can be marked as hit.
+				 */
+				dcu->processing_alt = true;
+
+				for (uint32_t i = 0; i < dcu->nr_alt_pus; i++) {
+					Dwarf_Die alt_cu_die, alt_child;
+
+					if (dwarf_offdie(alt_dw, dcu->alt_pus[i].start,
+							 &alt_cu_die) != NULL &&
+					    dwarf_child(&alt_cu_die, &alt_child) == 0) {
+						if (die__process_unit(&alt_child, cu, conf, 0) != 0)
+							goto out_abort;
+					}
+				}
+
+				dcu->processing_alt = false;
+			}
+
 			cu->language = attr_numeric(cu_die, DW_AT_language);
 			cu->producer_clang = attr_producer_clang(cu_die);
 			cus__add(cus, cu);
@@ -4746,6 +5186,9 @@ static int cus__merge_and_process_cu(struct cus *cus, struct conf_load *conf,
 	if (cu == NULL)
 		return 0;
 
+	if (dcu)
+		dwarf_cu__prune_unreferenced_alt_pus(dcu);
+
 	/* process merged cu */
 	if (cu__recode_dwarf_types(cu) != LSK__KEEPIT)
 		goto out_abort;
@@ -4803,7 +5246,7 @@ static int cus__load_module(struct cus *cus, struct conf_load *conf,
 			cus__remove(cus, type_cu);
 	}
 
-	if (conf->force_cu_merging || cus__merging_cu(dw, elf)) {
+	if (conf->force_cu_merging || cus__merging_cu(dw, elf) || dwarf_getalt(dw) != NULL) {
 		res = cus__merge_and_process_cu(cus, conf, mod, dw, elf, filename,
 						build_id, build_id_len,
 						type_cu ? type_dcu : NULL);
diff --git a/dwarves.h b/dwarves.h
index 38fec59ca78990df..e7dc8a447bd13b5b 100644
--- a/dwarves.h
+++ b/dwarves.h
@@ -393,6 +393,25 @@ bool languages__cu_filtered(struct languages *languages, struct cu *cu, bool ver
 			continue;				\
 		else
 
+/**
+ * cu__for_each_type_dense - iterate types, tracking NULL holes
+ * @cu: struct cu instance to iterate
+ * @id: type_id_t raw table index
+ * @pos: struct tag iterator
+ * @nulls: uint32_t counter incremented for each NULL entry
+ *
+ * Like cu__for_each_type but counts NULL entries in @nulls so
+ * callers that need a dense (hole-free) index can compute it
+ * as (id - nulls).  Use this when the table index must map to
+ * a sequential output ID (e.g. BTF/CTF encoding).
+ */
+#define cu__for_each_type_dense(cu, id, pos, nulls)		\
+	for (id = 1, nulls = 0;					\
+	     id < cu->types_table.nr_entries; ++id)		\
+		if (!(pos = cu->types_table.entries[id]))	\
+			++nulls;				\
+		else
+
 /**
  * cu__for_each_struct - iterate thru all the struct tags
  * @cu: struct cu instance to iterate
@@ -949,6 +968,7 @@ struct parameter {
 	int loc_reg;
 	uint16_t type_byte_size;
 	uint8_t true_sig_type_from_types:1;
+	uint8_t true_sig_type_from_alt:1;
 	uint8_t has_const_value:1;
 	uint8_t loc_const_value:1;
 	uint8_t loc_stack:1;
diff --git a/tests/dwz_alt_file.sh b/tests/dwz_alt_file.sh
new file mode 100755
index 0000000000000000..fc94f3618d86938b
--- /dev/null
+++ b/tests/dwz_alt_file.sh
@@ -0,0 +1,204 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright © 2026 Red Hat Inc, Arnaldo Carvalho de Melo <acme@redhat.com>
+#
+# Test that pahole correctly resolves types from dwz alternate debug
+# files (.gnu_debugaltlink / DW_FORM_GNU_ref_alt).
+#
+# Creates two binaries sharing common types, runs dwz in multifile
+# mode to deduplicate them into a .dwz alt file, then verifies pahole
+# can resolve the shared types.
+
+. "$(dirname "$0")/test_lib.sh"
+
+outdir=$(make_tmpdir)
+trap cleanup EXIT
+
+title_log "DWZ alternate debug file type resolution."
+
+CC=${CC:-gcc}
+if ! command -v ${CC%% *} > /dev/null 2>&1; then
+	info_log "skip: $CC not available"
+	test_skip
+fi
+
+if ! command -v dwz > /dev/null 2>&1; then
+	info_log "skip: dwz not available"
+	test_skip
+fi
+
+READELF=${READELF:-$(command -v readelf || command -v eu-readelf || true)}
+if [ -z "$READELF" ]; then
+	info_log "skip: neither readelf nor eu-readelf available"
+	test_skip
+fi
+
+cat > "$outdir/shared_types.h" << 'HEOF'
+struct shared_base {
+	int id;
+	long flags;
+	char *name;
+	void *data;
+};
+
+struct shared_nested {
+	struct shared_base base;
+	int count;
+	double value;
+	char buffer[64];
+};
+
+struct shared_list {
+	struct shared_nested item;
+	struct shared_list *next;
+	struct shared_list *prev;
+};
+
+enum shared_state {
+	STATE_INIT = 0,
+	STATE_RUNNING = 1,
+	STATE_PAUSED = 2,
+	STATE_STOPPED = 3,
+	STATE_ERROR = 4,
+};
+
+typedef unsigned long shared_handle_t;
+typedef int (*shared_callback_t)(struct shared_base *, enum shared_state);
+
+struct shared_context {
+	struct shared_base base;
+	struct shared_list *items;
+	shared_callback_t callback;
+	shared_handle_t handle;
+	enum shared_state state;
+	int refcount;
+	char description[128];
+};
+HEOF
+
+cat > "$outdir/prog1.c" << 'EOF'
+#include "shared_types.h"
+
+int lib_init(struct shared_context *ctx) {
+	ctx->state = STATE_INIT;
+	ctx->refcount = 1;
+	return 0;
+}
+
+int main(void) {
+	struct shared_context ctx = { .base = { .id = 1, .name = "prog1" } };
+	struct shared_nested item = { .count = 42, .value = 3.14 };
+	lib_init(&ctx);
+	return item.count;
+}
+EOF
+
+cat > "$outdir/prog2.c" << 'EOF'
+#include "shared_types.h"
+
+shared_handle_t prog2_work(struct shared_context *ctx) {
+	struct shared_list node = {
+		.item = { .base = { .id = 2 }, .count = 10 },
+	};
+	ctx->items = &node;
+	ctx->state = STATE_PAUSED;
+	return ctx->handle;
+}
+
+int main(void) {
+	struct shared_context ctx = { .base = { .id = 99, .name = "prog2" } };
+	return (int)prog2_work(&ctx);
+}
+EOF
+
+# Try DWARF 4 first (old dwz chokes on DWARF 5 input), then fall back
+# to default -g so we do not silently lose coverage on distros where
+# gcc defaults to DWARF 5 and dwz is too old for it.
+dwz_ok=0
+for dwarf_flag in -gdwarf-4 -g; do
+	if ! $CC $dwarf_flag -I"$outdir" -o "$outdir/prog1" "$outdir/prog1.c" 2>"$outdir/cc.log"; then
+		info_log "   compile prog1 ($dwarf_flag) failed:"
+		info_log "   $(cat "$outdir/cc.log")"
+		continue
+	fi
+
+	if ! $CC $dwarf_flag -I"$outdir" -o "$outdir/prog2" "$outdir/prog2.c" 2>"$outdir/cc.log"; then
+		info_log "   compile prog2 ($dwarf_flag) failed:"
+		info_log "   $(cat "$outdir/cc.log")"
+		continue
+	fi
+
+	if ! dwz -m "$outdir/dwz_alt" "$outdir/prog1" "$outdir/prog2" 2>/dev/null; then
+		info_log "   dwz multifile mode failed with $dwarf_flag (types too small?)"
+		continue
+	fi
+
+	if ! $READELF -p .gnu_debugaltlink "$outdir/prog1" 2>/dev/null | grep -q dwz_alt; then
+		info_log "   dwz ($dwarf_flag) did not produce .gnu_debugaltlink"
+		continue
+	fi
+
+	info_log "   dwz multifile with $dwarf_flag: ok"
+	dwz_ok=1
+	break
+done
+
+if [ "$dwz_ok" -eq 0 ]; then
+	info_log "skip: could not produce dwz alt file with any DWARF version"
+	test_skip
+fi
+
+# Verify pahole can resolve the shared type from the alt file
+if ! pahole -F dwarf -C shared_context "$outdir/prog1" 2>/dev/null | grep -q "struct shared_context {"; then
+	error_log "FAIL: pahole could not resolve shared_context from prog1"
+	test_fail
+fi
+
+if ! pahole -F dwarf -C shared_context "$outdir/prog2" 2>/dev/null | grep -q "struct shared_context {"; then
+	error_log "FAIL: pahole could not resolve shared_context from prog2"
+	test_fail
+fi
+
+# Verify member resolution (shared_base is in the alt file)
+if ! pahole -F dwarf -C shared_context "$outdir/prog1" 2>/dev/null | grep -q "shared_base"; then
+	error_log "FAIL: shared_base member not resolved in shared_context"
+	test_fail
+fi
+
+# Verify no unexpected stderr output — catches warnings, errors, and
+# any new class of dwz-related diagnostics regardless of format
+pahole -F dwarf "$outdir/prog1" >/dev/null 2>"$outdir/pahole_stderr.log"
+if test -s "$outdir/pahole_stderr.log"; then
+	error_log "FAIL: pahole produced unexpected stderr processing dwz file:"
+	error_log "$(cat "$outdir/pahole_stderr.log")"
+	test_fail
+fi
+
+# Negative test: hide the alt file and verify pahole warns exactly once
+# and does not crash.  This is the scenario users hit in the wild with
+# partially installed debuginfo packages.
+mv "$outdir/dwz_alt" "$outdir/dwz_alt.hidden"
+
+pahole -F dwarf "$outdir/prog1" >/dev/null 2>"$outdir/pahole_noalt_stderr.log"
+noalt_rc=$?
+
+mv "$outdir/dwz_alt.hidden" "$outdir/dwz_alt"
+
+if [ "$noalt_rc" -ne 0 ]; then
+	error_log "FAIL: pahole crashed (rc=$noalt_rc) with missing alt file"
+	test_fail
+fi
+
+if ! grep -q "could not resolve dwz alternate debug file" "$outdir/pahole_noalt_stderr.log"; then
+	error_log "FAIL: missing-alt warning not emitted"
+	error_log "$(cat "$outdir/pahole_noalt_stderr.log")"
+	test_fail
+fi
+
+noalt_warn_count=$(grep -c "could not resolve dwz alternate debug file" "$outdir/pahole_noalt_stderr.log")
+if [ "$noalt_warn_count" -ne 1 ]; then
+	error_log "FAIL: missing-alt warning emitted $noalt_warn_count times, expected 1"
+	test_fail
+fi
+
+test_pass
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 11/12] tests: Add inter-CU type reference comparison test
  2026-07-31 19:30 [PATCHES 00/12] pahole: Support more rust tags and references to dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (9 preceding siblings ...)
  2026-07-31 19:30 ` [PATCH 10/12] dwarf_loader: Support DW_FORM_GNU_ref_alt references to dwz alternate debug files Arnaldo Carvalho de Melo
@ 2026-07-31 19:30 ` Arnaldo Carvalho de Melo
  2026-07-31 19:31 ` [PATCH 12/12] scripts: Add vmlinux_comparison.py for DWARF/BTF analysis Arnaldo Carvalho de Melo
  11 siblings, 0 replies; 13+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-07-31 19:30 UTC (permalink / raw)
  To: Alan Maguire
  Cc: Jiri Olsa, Clark Williams, dwarves, bpf, Andrii Nakryiko,
	Yonghong Song, Mark Wieelard, Arnaldo Carvalho de Melo

From: Arnaldo Carvalho de Melo <acme@redhat.com>

When the perf binary includes Rust CUs, some DWARF types reference types
in other CUs via DW_FORM_ref_addr. The parallel loading path processes
each CU independently, so cross-CU references fail during
cu__recode_dwarf_types() and the types become unresolved (void).

The force_cu_merging feature worked around this by serializing loading
into a mega-CU, but disables parallel DIE processing. This test
compares both outputs to validate that cross-CU references are
correctly resolved without needing force_cu_merging.

Before the preceding fix, 314 cross-CU references failed, producing 70
lines of output difference across 12 diff hunks:

  $ pahole -F dwarf ~/bin/perf 2>/dev/null > parallel.txt
  $ pahole --features=force_cu_merging -F dwarf ~/bin/perf 2>/dev/null > merged.txt
  $ diff parallel.txt merged.txt | grep '^[<>]' | wc -l
  70
  $ diff -u parallel.txt merged.txt | head -20
  --- parallel.txt
  +++ merged.txt
  @@ -10821,7 +10821,11 @@
   	usize                      __0 __attribute__((__aligned__(8)));
   	struct ThreadInfo          __1 __attribute__((__aligned__(8)));

  +	/* XXX last struct has 8 bytes of padding, 1 hole */
  +
   	/* size: 48, cachelines: 1, members: 2 */
  +	/* member types with holes: 1, total: 1 */
  +	/* paddings: 1, sum paddings: 8 */
   	/* forced alignments: 2 */
   	/* last cacheline: 48 bytes */
   } __attribute__((__aligned__(8)));

The preceding commit fixed the detection of DW_FORM_ref_addr in
cus__merging_cu(), making this test pass.

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 tests/inter_cu_refs.sh | 50 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 50 insertions(+)
 create mode 100755 tests/inter_cu_refs.sh

diff --git a/tests/inter_cu_refs.sh b/tests/inter_cu_refs.sh
new file mode 100755
index 0000000000000000..6827bf83931eb3ed
--- /dev/null
+++ b/tests/inter_cu_refs.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright © 2026 Red Hat Inc, Arnaldo Carvalho de Melo <acme@redhat.com>
+#
+# Compare default CU loading with explicit force_cu_merging for binaries
+# with inter-CU type references (e.g. Rust CUs in perf).
+#
+# When cus__merging_cu() correctly detects DW_FORM_ref_addr usage, it
+# automatically falls back to the merged CU path.  This test verifies
+# that the automatic detection produces the same output as explicitly
+# forcing CU merging.
+
+. ./test_lib.sh
+
+outdir=$(make_tmpdir)
+trap cleanup EXIT
+
+title_log "Compare parallel vs merged CU loading for inter-CU type references."
+
+perf=$(which perf 2>/dev/null)
+if [ -z "$perf" ] ; then
+	info_log "skip: No 'perf' binary available"
+	test_skip
+fi
+
+if ! pahole --features=force_cu_merging -F dwarf -C perf_event_header "$perf" 2>/dev/null | grep -q "^struct perf_event_header {" ; then
+	info_log "skip: $perf doesn't have 'struct perf_event_header' type info"
+	test_skip
+fi
+
+parallel_out=$outdir/parallel.txt
+merged_out=$outdir/merged.txt
+
+if ! pahole -F dwarf "$perf" > "$parallel_out" 2>/dev/null ; then
+	error_log "FAIL: pahole failed processing $perf (parallel mode)"
+	test_fail
+fi
+
+if ! pahole --features=force_cu_merging -F dwarf "$perf" > "$merged_out" 2>/dev/null ; then
+	error_log "FAIL: pahole failed processing $perf (merged mode)"
+	test_fail
+fi
+
+if diff -u "$parallel_out" "$merged_out" > /dev/null 2>&1 ; then
+	test_pass
+else
+	nr_diff=$(diff "$parallel_out" "$merged_out" | grep '^[<>]' | wc -l)
+	error_log "FAIL: $nr_diff lines differ between parallel and merged CU loading"
+	test_fail
+fi
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH 12/12] scripts: Add vmlinux_comparison.py for DWARF/BTF analysis
  2026-07-31 19:30 [PATCHES 00/12] pahole: Support more rust tags and references to dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (10 preceding siblings ...)
  2026-07-31 19:30 ` [PATCH 11/12] tests: Add inter-CU type reference comparison test Arnaldo Carvalho de Melo
@ 2026-07-31 19:31 ` Arnaldo Carvalho de Melo
  11 siblings, 0 replies; 13+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-07-31 19:31 UTC (permalink / raw)
  To: Alan Maguire
  Cc: Jiri Olsa, Clark Williams, dwarves, bpf, Andrii Nakryiko,
	Yonghong Song, Mark Wieelard, Arnaldo Carvalho de Melo

From: Arnaldo Carvalho de Melo <acme@redhat.com>

Analyzes vmlinux files to compare DWARF versions, compilers, section
sizes, and pahole output across different builds. Validates that
different DWARF encodings (v4 vs v5, compressed vs uncompressed)
produce equivalent pahole output.

The kernel offers several CONFIG_DEBUG_INFO options that affect DWARF
encoding:
- CONFIG_DEBUG_INFO_DWARF4: DWARF version 4 format
- CONFIG_DEBUG_INFO_DWARF5: DWARF version 5 (~15% smaller sections)
- CONFIG_DEBUG_INFO_COMPRESSED_ZLIB: zlib-compressed debug sections
- CONFIG_DEBUG_INFO_COMPRESSED_ZSTD: zstd-compressed (smaller/faster)
- CONFIG_DEBUG_INFO_SPLIT: split DWARF with .dwo files (NOT supported yet)

This script validates pahole correctly handles all supported modes by
comparing output hashes across different vmlinux builds.

Features:
- Extracts DWARF version and DW_AT_producer from debug info
- Reports file sizes and .debug_*/.BTF* section sizes
- Runs pahole -F dwarf and -F btf, computing output hashes
- Streams pahole output for memory efficiency (handles 800MB+ files)
- Optimized readelf: reads only first 100-200 lines for metadata
  instead of processing entire debug sections (minutes → seconds)
- Displays results in ASCII table similar to coverage-table

Example output shows DWARF v4 (599MB sections) vs v5 (500MB) vs
v5+zlib (261MB) all produce identical pahole output (hash 86e1b611),
validating pahole's consistency across different DWARF encodings.

Usage: scripts/vmlinux_comparison.py [directory]
Default directory: ~/dws/

Test vmlinux files covering supported configurations are available in
~/dws/.  Split DWARF (DW_TAG_skeleton_unit) support is deferred to the
next patch series.

Example:

  acme@number:~/git/pahole$ ls -la ~/dws/vmlinux.*
  -rwxr-xr-x. 1 acme acme 876343656 Jul 16 20:25 /home/acme/dws/vmlinux.dwarf4
  -rwxr-xr-x. 1 acme acme 772024544 Jul 16 21:43 /home/acme/dws/vmlinux.dwarf5
  -rwxr-xr-x. 1 acme acme 521968168 Jul 17 08:19 /home/acme/dws/vmlinux.dwarf5.zlib
  -rwxr-xr-x. 1 acme acme 476736632 Jul 17 12:58 /home/acme/dws/vmlinux.dwarf5.zstd
  -rwxr-xr-x. 1 acme acme 772024248 Jul 16 20:32 /home/acme/dws/vmlinux.toolchain_default
  acme@number:~/git/pahole$ scripts/vmlinux_comparison.py ~/dws/
  Scanning /home/acme/dws/ for vmlinux files...
  Found 5 vmlinux file(s)

    Analyzing vmlinux.dwarf4...
      Running pahole -F dwarf...
      Running pahole -F btf...
    Analyzing vmlinux.dwarf5...
      Running pahole -F dwarf...
      Running pahole -F btf...
    Analyzing vmlinux.dwarf5.zlib...
      Running pahole -F dwarf...
      Running pahole -F btf...
    Analyzing vmlinux.dwarf5.zstd...
      Running pahole -F dwarf...
      Running pahole -F btf...
    Analyzing vmlinux.toolchain_default...
      Running pahole -F dwarf...
      Running pahole -F btf...

  ┌───────────────────────────┬─────────┬─────┬───────────────────────────────────┬─────────┬───────┬──────────┬────────┬──────────┬─────────┐
  │ File                      │  Size   │ Ver │ Producer                          │  DWARF  │  BTF  │ DW-Hash  │ DW-Out │ BTF-Hash │ BTF-Out │
  ├───────────────────────────┼─────────┼─────┼───────────────────────────────────┼─────────┼───────┼──────────┼────────┼──────────┼─────────┤
  │ vmlinux.dwarf4            │ 835.7MB │   4 │ GNU C11 16.1.1 -gdwarf-4          │ 599.2MB │ 7.3MB │ 86e1b611 │  7.6MB │ 52123c24 │   7.4MB │
  │ vmlinux.dwarf5            │ 736.3MB │   5 │ GNU C11 16.1.1 -gdwarf-5          │ 499.7MB │ 7.3MB │ 86e1b611 │  7.6MB │ 52123c24 │   7.4MB │
  │ vmlinux.dwarf5.zlib       │ 497.8MB │   5 │ GNU C11 16.1.1 -gdwarf-5 -gz=zlib │ 261.2MB │ 7.3MB │ 86e1b611 │  7.6MB │ 52123c24 │   7.4MB │
  │ vmlinux.dwarf5.zstd       │ 454.7MB │   5 │ GNU C11 16.1.1 -gdwarf-5 -gz=zstd │ 218.1MB │ 7.3MB │ 86e1b611 │  7.6MB │ 52123c24 │   7.4MB │
  │ vmlinux.toolchain_default │ 736.3MB │   5 │ GNU C11 16.1.1                    │ 499.7MB │ 7.3MB │ 86e1b611 │  7.6MB │ 52123c24 │   7.4MB │
  └───────────────────────────┴─────────┴─────┴───────────────────────────────────┴─────────┴───────┴──────────┴────────┴──────────┴─────────┘

  Column descriptions:
    File       : vmlinux filename
    Size       : Total file size
    Ver        : DWARF version
    Producer   : Compiler and flags (truncated)
    DWARF      : Total size of .debug_* sections
    BTF        : Total size of .BTF* sections
    DW-Hash    : SHA256 hash (first 8 chars) of pahole -F dwarf output
    DW-Out     : Size of pahole -F dwarf output
    BTF-Hash   : SHA256 hash (first 8 chars) of pahole -F btf output
    BTF-Out    : Size of pahole -F btf output

  Note: Different hashes indicate different pahole output between files.

  The output shows that vmlinux variants (different DWARF versions, compression)
  produce identical pahole output despite different file sizes and debug section
  sizes, validating pahole's consistency across different DWARF encodings.
  acme@number:~/git/pahole$

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 scripts/vmlinux_comparison.py | 367 ++++++++++++++++++++++++++++++++++
 1 file changed, 367 insertions(+)
 create mode 100755 scripts/vmlinux_comparison.py

diff --git a/scripts/vmlinux_comparison.py b/scripts/vmlinux_comparison.py
new file mode 100755
index 0000000000000000..1633bf8394098e02
--- /dev/null
+++ b/scripts/vmlinux_comparison.py
@@ -0,0 +1,367 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+# Arnaldo Carvalho de Melo <acme@redhat.com>
+# Assisted-by: Claude:claude-sonnet-4-5
+#
+# Compare vmlinux files: DWARF versions, producers, section sizes, and pahole output.
+#
+# Prompt used to generate this script:
+#
+# Given a directory with a series of vmlinux files, look at the DW_AT_producer
+# DWARF tags to notice what's unique for each of the vmlinux files, look at the
+# DWARF version as well, then run pahole -F dwarf and btf to see if the output
+# produced changes for both formats in the different vmlinux files, create a
+# table like the one in the upcoming coverage-table make target. Add columns for
+# each row (DWARF files) with the size of the vmlinux files and the size of the
+# DWARF and BTF related ELF sections on each of the vmlinux files.
+#
+# Usage:
+#   scripts/vmlinux_comparison.py [DIRECTORY]
+#
+#   Default directory: ~/dws/
+
+import argparse
+import hashlib
+import os
+import re
+import subprocess
+import sys
+import tempfile
+
+def format_size(size_bytes):
+    """Format byte size in human-readable format."""
+    for unit in ['B', 'KB', 'MB', 'GB']:
+        if size_bytes < 1024.0:
+            return f"{size_bytes:.1f}{unit}"
+        size_bytes /= 1024.0
+    return f"{size_bytes:.1f}TB"
+
+def get_file_size(path):
+    """Get file size in bytes."""
+    return os.path.getsize(path)
+
+def extract_dwarf_version(vmlinux_path):
+    """Extract DWARF version using readelf (only first 100 lines)."""
+    try:
+        # Only read first 100 lines of debug info to find version quickly
+        cmd = f"readelf --debug-dump=info '{vmlinux_path}' 2>/dev/null | head -100"
+        result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
+        if result.returncode != 0:
+            return 'N/A'
+
+        # Look for first "Version:" in output
+        for line in result.stdout.splitlines():
+            if 'Version:' in line:
+                match = re.search(r'Version:\s+(\d+)', line)
+                if match:
+                    return match.group(1)
+        return 'N/A'
+    except Exception as e:
+        return 'N/A'
+
+
+def extract_producer(vmlinux_path):
+    """Extract DW_AT_producer using readelf (only first 200 lines)."""
+    try:
+        # Only read first 200 lines of debug info to find producer quickly
+        cmd = f"readelf --debug-dump=info '{vmlinux_path}' 2>/dev/null | head -200"
+        result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
+        if result.returncode != 0:
+            return 'N/A'
+
+        # Look for first DW_AT_producer
+        for line in result.stdout.splitlines():
+            if 'DW_AT_producer' in line:
+                # Extract the actual producer string after the colon
+                match = re.search(r'DW_AT_producer\s*:\s*(?:\([^)]+\):\s*)?(.+)', line)
+                if match:
+                    producer = match.group(1).strip()
+                    # Extract key parts: compiler name, version, DWARF flag
+                    # Example: "GNU C11 16.1.1 20260515 (Red Hat 16.1.1-2) ... -gdwarf-4 ..."
+
+                    # Get compiler and version
+                    compiler_match = re.search(r'(GNU C\d+|clang)\s+([\d.]+)', producer)
+                    compiler = compiler_match.group(0) if compiler_match else 'unknown'
+
+                    # Get DWARF version flag
+                    dwarf_match = re.search(r'-gdwarf-(\d+)', producer)
+                    dwarf_flag = f"-gdwarf-{dwarf_match.group(1)}" if dwarf_match else ''
+
+                    # Get compression if present
+                    compress_match = re.search(r'-gz(=\w+)?', producer)
+                    compress = compress_match.group(0) if compress_match else ''
+
+                    parts = [compiler, dwarf_flag, compress]
+                    return ' '.join(p for p in parts if p)
+        return 'N/A'
+    except Exception as e:
+        return f'Error: {e}'
+
+
+def get_section_sizes(vmlinux_path):
+    """Get sizes of DWARF and BTF sections using readelf."""
+    try:
+        cmd = ['readelf', '-S', vmlinux_path]
+        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
+        if result.returncode != 0:
+            return 0, 0
+
+        dwarf_size = 0
+        btf_size = 0
+
+        # Parse section table - readelf outputs sections across two lines:
+        # Line 1: [Nr] Name Type Address Offset
+        # Line 2:      Size EntSize Flags Link Info Align
+        lines = result.stdout.splitlines()
+        i = 0
+        while i < len(lines):
+            line = lines[i]
+
+            # Match section header line: [Nr] Name ...
+            match = re.match(r'\s*\[\s*\d+\]\s+(\S+)', line)
+            if match:
+                section_name = match.group(1)
+
+                # Size is on the next line
+                if i + 1 < len(lines):
+                    next_line = lines[i + 1]
+                    # Extract first hex number from next line (the size)
+                    size_match = re.search(r'^\s+([0-9a-f]+)', next_line)
+                    if size_match:
+                        size = int(size_match.group(1), 16)
+
+                        if section_name.startswith('.debug_'):
+                            dwarf_size += size
+                        elif section_name.startswith('.BTF'):
+                            btf_size += size
+
+            i += 1
+
+        return dwarf_size, btf_size
+    except Exception as e:
+        return 0, 0
+
+
+def run_pahole_and_hash(vmlinux_path, format_type):
+    """Run pahole with given format and return output hash."""
+    try:
+        # Run pahole and stream output for hashing (avoids loading into memory)
+        cmd = ['pahole', '-F', format_type, vmlinux_path]
+        result = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+                                 stderr=subprocess.DEVNULL)  # Ignore stderr to avoid blocking
+
+        # Stream output and compute hash on the fly
+        hasher = hashlib.sha256()
+        file_size = 0
+
+        while True:
+            chunk = result.stdout.read(8192)
+            if not chunk:
+                break
+            hasher.update(chunk)
+            file_size += len(chunk)
+
+        result.wait(timeout=600)  # Increase timeout to 10 minutes for large files
+
+        if result.returncode != 0:
+            return 'ERROR', 0
+
+        hash_short = hasher.hexdigest()[:8]
+        return hash_short, file_size
+    except subprocess.TimeoutExpired:
+        result.kill()
+        result.wait()  # Clean up zombie process
+        return 'TIMEOUT', 0
+    except Exception as e:
+        return 'ERROR', 0
+
+
+def find_vmlinux_files(directory):
+    """Find all vmlinux* files in directory, excluding text files."""
+    vmlinux_files = []
+
+    if not os.path.isdir(directory):
+        print(f"Error: {directory} is not a directory", file=sys.stderr)
+        return []
+
+    for filename in os.listdir(directory):
+        path = os.path.join(directory, filename)
+
+        # Skip non-files
+        if not os.path.isfile(path):
+            continue
+
+        # Only include files starting with "vmlinux"
+        if not filename.startswith('vmlinux'):
+            continue
+
+        # Skip text files (.c, .txt, etc.)
+        if filename.endswith(('.c', '.txt', '.log', '.md')):
+            continue
+
+        # Check if it's an ELF file
+        try:
+            with open(path, 'rb') as f:
+                magic = f.read(4)
+                if magic != b'\x7fELF':
+                    continue
+        except:
+            continue
+
+        vmlinux_files.append(path)
+
+    return sorted(vmlinux_files)
+
+
+def analyze_vmlinux(vmlinux_path):
+    """Analyze a single vmlinux file and return metrics."""
+    filename = os.path.basename(vmlinux_path)
+
+    print(f"  Analyzing {filename}...", file=sys.stderr)
+
+    file_size = get_file_size(vmlinux_path)
+    dwarf_version = extract_dwarf_version(vmlinux_path)
+    producer = extract_producer(vmlinux_path)
+    dwarf_size, btf_size = get_section_sizes(vmlinux_path)
+
+    print(f"    Running pahole -F dwarf...", file=sys.stderr)
+    dwarf_hash, dwarf_out_size = run_pahole_and_hash(vmlinux_path, 'dwarf')
+
+    print(f"    Running pahole -F btf...", file=sys.stderr)
+    btf_hash, btf_out_size = run_pahole_and_hash(vmlinux_path, 'btf')
+
+    return {
+        'filename': filename,
+        'file_size': file_size,
+        'dwarf_version': dwarf_version,
+        'producer': producer,
+        'dwarf_section_size': dwarf_size,
+        'btf_section_size': btf_size,
+        'dwarf_output_hash': dwarf_hash,
+        'dwarf_output_size': dwarf_out_size,
+        'btf_output_hash': btf_hash,
+        'btf_output_size': btf_out_size,
+    }
+
+
+def print_table(results):
+    """Print comparison table with box-drawing characters."""
+    if not results:
+        print("No vmlinux files found.")
+        return
+
+    # Column widths
+    W_FILE = 30
+    W_SIZE = 8
+    W_VER = 4
+    W_PROD = 35
+    W_SECT = 10
+    W_HASH = 10
+    W_OUT = 10
+
+    # Header
+    def print_sep(char='─'):
+        print(f"┌{char*(W_FILE+2)}┬{char*(W_SIZE+2)}┬{char*(W_VER+2)}┬{char*(W_PROD+2)}┬"
+              f"{char*(W_SECT+2)}┬{char*(W_SECT+2)}┬"
+              f"{char*(W_HASH+2)}┬{char*(W_OUT+2)}┬"
+              f"{char*(W_HASH+2)}┬{char*(W_OUT+2)}┐")
+
+    def print_row_sep():
+        print(f"├{'─'*(W_FILE+2)}┼{'─'*(W_SIZE+2)}┼{'─'*(W_VER+2)}┼{'─'*(W_PROD+2)}┼"
+              f"{'─'*(W_SECT+2)}┼{'─'*(W_SECT+2)}┼"
+              f"{'─'*(W_HASH+2)}┼{'─'*(W_OUT+2)}┼"
+              f"{'─'*(W_HASH+2)}┼{'─'*(W_OUT+2)}┤")
+
+    print_sep()
+
+    # Column headers
+    print(f"│ {'File':<{W_FILE}} │ {'Size':>{W_SIZE}} │ {'Ver':>{W_VER}} │ {'Producer':<{W_PROD}} │ "
+          f"{'DWARF':>{W_SECT}} │ {'BTF':>{W_SECT}} │ "
+          f"{'DW-Hash':>{W_HASH}} │ {'DW-Out':>{W_OUT}} │ "
+          f"{'BTF-Hash':>{W_HASH}} │ {'BTF-Out':>{W_OUT}} │")
+
+    print_row_sep()
+
+    # Data rows
+    for r in results:
+        fname = r['filename']
+        if len(fname) > W_FILE:
+            fname = fname[:W_FILE-3] + '...'
+
+        fsize = format_size(r['file_size'])
+        ver = r['dwarf_version']
+
+        prod = r['producer']
+        if len(prod) > W_PROD:
+            prod = prod[:W_PROD-3] + '...'
+
+        dwarf_sect = format_size(r['dwarf_section_size'])
+        btf_sect = format_size(r['btf_section_size'])
+
+        dw_hash = r['dwarf_output_hash']
+        dw_out = format_size(r['dwarf_output_size'])
+
+        btf_hash = r['btf_output_hash']
+        btf_out = format_size(r['btf_output_size'])
+
+        print(f"│ {fname:<{W_FILE}} │ {fsize:>{W_SIZE}} │ {ver:>{W_VER}} │ {prod:<{W_PROD}} │ "
+              f"{dwarf_sect:>{W_SECT}} │ {btf_sect:>{W_SECT}} │ "
+              f"{dw_hash:>{W_HASH}} │ {dw_out:>{W_OUT}} │ "
+              f"{btf_hash:>{W_HASH}} │ {btf_out:>{W_OUT}} │")
+
+    # Footer
+    print(f"└{'─'*(W_FILE+2)}┴{'─'*(W_SIZE+2)}┴{'─'*(W_VER+2)}┴{'─'*(W_PROD+2)}┴"
+          f"{'─'*(W_SECT+2)}┴{'─'*(W_SECT+2)}┴"
+          f"{'─'*(W_HASH+2)}┴{'─'*(W_OUT+2)}┴"
+          f"{'─'*(W_HASH+2)}┴{'─'*(W_OUT+2)}┘")
+
+    print()
+    print("Column descriptions:")
+    print("  File       : vmlinux filename")
+    print("  Size       : Total file size")
+    print("  Ver        : DWARF version")
+    print("  Producer   : Compiler and flags (truncated)")
+    print("  DWARF      : Total size of .debug_* sections")
+    print("  BTF        : Total size of .BTF* sections")
+    print("  DW-Hash    : SHA256 hash (first 8 chars) of pahole -F dwarf output")
+    print("  DW-Out     : Size of pahole -F dwarf output")
+    print("  BTF-Hash   : SHA256 hash (first 8 chars) of pahole -F btf output")
+    print("  BTF-Out    : Size of pahole -F btf output")
+    print()
+    print("Note: Different hashes indicate different pahole output between files.")
+    print()
+    print("The output shows that vmlinux variants (different DWARF versions, compression)")
+    print("produce identical pahole output despite different file sizes and debug section")
+    print("sizes, validating pahole's consistency across different DWARF encodings.")
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description='Compare vmlinux files: DWARF, BTF, and pahole output')
+    parser.add_argument('directory', nargs='?',
+                       default=os.path.expanduser('~/dws'),
+                       help='Directory containing vmlinux files (default: ~/dws)')
+    args = parser.parse_args()
+
+    directory = os.path.expanduser(args.directory)
+
+    print(f"Scanning {directory} for vmlinux files...", file=sys.stderr)
+    vmlinux_files = find_vmlinux_files(directory)
+
+    if not vmlinux_files:
+        print(f"No vmlinux files found in {directory}", file=sys.stderr)
+        sys.exit(1)
+
+    print(f"Found {len(vmlinux_files)} vmlinux file(s)\n", file=sys.stderr)
+
+    results = []
+    for vmlinux_path in vmlinux_files:
+        result = analyze_vmlinux(vmlinux_path)
+        results.append(result)
+
+    print(file=sys.stderr)
+    print_table(results)
+
+
+if __name__ == '__main__':
+    main()
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

end of thread, other threads:[~2026-07-31 19:31 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-31 19:30 [PATCHES 00/12] pahole: Support more rust tags and references to dwz alternate debug files Arnaldo Carvalho de Melo
2026-07-31 19:30 ` [PATCH 01/12] dwarf_loader: Initial support for DW_TAG_variant_part Arnaldo Carvalho de Melo
2026-07-31 19:30 ` [PATCH 02/12] dwarf_loader: Initial support for DW_TAG_subprogram in DW_TAG_enumeration Arnaldo Carvalho de Melo
2026-07-31 19:30 ` [PATCH 03/12] dwarf_loader: Populate DW_TAG_variant children in DW_TAG_variant_part Arnaldo Carvalho de Melo
2026-07-31 19:30 ` [PATCH 04/12] btf_encoder: Encode variant parts as union members in BTF Arnaldo Carvalho de Melo
2026-07-31 19:30 ` [PATCH 05/12] dwarf_loader: Handle DW_FORM_block in attr_numeric for Rust discriminant values Arnaldo Carvalho de Melo
2026-07-31 19:30 ` [PATCH 06/12] dwarf_loader: Allow forcing the merge of CUs for solving inter CU tag references Arnaldo Carvalho de Melo
2026-07-31 19:30 ` [PATCH 07/12] dwarf_loader: Support DW_TAG_imported_unit for same-file partial units Arnaldo Carvalho de Melo
2026-07-31 19:30 ` [PATCH 08/12] dwarf_loader: Fix cus__merging_cu failing to detect DW_FORM_ref_addr Arnaldo Carvalho de Melo
2026-07-31 19:30 ` [PATCH 09/12] dwarf_loader: Add cu parameter to tag__set_spec() and dwarf_tag__set_attr_type() Arnaldo Carvalho de Melo
2026-07-31 19:30 ` [PATCH 10/12] dwarf_loader: Support DW_FORM_GNU_ref_alt references to dwz alternate debug files Arnaldo Carvalho de Melo
2026-07-31 19:30 ` [PATCH 11/12] tests: Add inter-CU type reference comparison test Arnaldo Carvalho de Melo
2026-07-31 19:31 ` [PATCH 12/12] scripts: Add vmlinux_comparison.py for DWARF/BTF analysis Arnaldo Carvalho de Melo

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox