All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v2 0/9] pahole: Support cross-CU type references and dwz alternate debug files
@ 2026-08-21 21:34 Arnaldo Carvalho de Melo
  2026-08-21 21:35 ` [PATCH v2 1/9] dwarf_loader: Initial support for DW_TAG_variant_part Arnaldo Carvalho de Melo
                   ` (8 more replies)
  0 siblings, 9 replies; 10+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-08-21 21:34 UTC (permalink / raw)
  To: Alan Maguire; +Cc: Jiri Olsa, Clark Williams, dwarves, Arnaldo Carvalho de Melo

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

Add support for cross-CU type references and dwz alternate debug
files so pahole can process binaries (e.g. a perf binary with rust
objects, or Firefox) that use DW_FORM_ref_addr inter-CU references or
dwz-compressed .dwz alternate debug files.

Cross-CU type references:

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

Also add an inter-CU type reference comparison test and
scripts/vmlinux_comparison.py for DWARF/BTF analysis across kernel
configs.

What changed from v1:

  v1 (12 patches) also carried the Rust enum discriminant support
  (DW_TAG_variant_part population, BTF union-member encoding and
  DW_FORM_block discriminant handling, plus tests/block_endian.sh).
  That work is being submitted as a separate series based on this one,
  so v2 drops those 3 patches and the block_endian.sh test.  The
  remaining 9 patches are the cross-CU reference and dwz alternate
  debug file support, now self-contained (tests/dwz_alt_file.sh and
  tests/inter_cu_refs.sh are included).

Best regards,

- Arnaldo

Assisted-by: opencode:hy3-free

Arnaldo Carvalho de Melo (9):
  dwarf_loader: Initial support for DW_TAG_variant_part
  dwarf_loader: Initial support for DW_TAG_subprogram in
    DW_TAG_enumeration
  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
  tests: Add inter-CU type reference comparison test
  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
  scripts: Add vmlinux_comparison.py for DWARF/BTF analysis

 btf_encoder.c                 |  22 +-
 ctf_encoder.c                 |  26 +-
 dwarf_loader.c                | 829 ++++++++++++++++++++++++++++++----
 dwarves.c                     |  44 +-
 dwarves.h                     |  20 +-
 dwarves_emit.c                |  10 +-
 dwarves_fprintf.c             |  54 ++-
 man-pages/pahole.1            |  19 +-
 pahole.c                      |  28 +-
 scripts/vmlinux_comparison.py | 612 +++++++++++++++++++++++++
 tests/dwz_alt_file.sh         | 204 +++++++++
 tests/inter_cu_refs.sh        |  50 ++
 tests/prettify_perf.data.sh   |   4 +-
 13 files changed, 1801 insertions(+), 121 deletions(-)
 create mode 100755 scripts/vmlinux_comparison.py
 create mode 100755 tests/dwz_alt_file.sh
 create mode 100755 tests/inter_cu_refs.sh

-- 
2.55.0


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

* [PATCH v2 1/9] dwarf_loader: Initial support for DW_TAG_variant_part
  2026-08-21 21:34 [PATCH v2 0/9] pahole: Support cross-CU type references and dwz alternate debug files Arnaldo Carvalho de Melo
@ 2026-08-21 21:35 ` Arnaldo Carvalho de Melo
  2026-08-21 21:35 ` [PATCH v2 2/9] dwarf_loader: Initial support for DW_TAG_subprogram in DW_TAG_enumeration Arnaldo Carvalho de Melo
                   ` (7 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-08-21 21:35 UTC (permalink / raw)
  To: Alan Maguire; +Cc: Jiri Olsa, Clark Williams, dwarves, 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.

Assisted-by: opencode:deepseek-v4-flash
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 dwarf_loader.c | 25 ++++++++++++++++++++++++-
 dwarves.c      | 27 +++++++++++++++++++++++++++
 dwarves.h      | 16 ++++++++++++++++
 3 files changed, 67 insertions(+), 1 deletion(-)

diff --git a/dwarf_loader.c b/dwarf_loader.c
index 61d55bd70d643731..ac576f0936ac86a9 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,18 @@ 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;
+
+			/* DWARF permits more than one DW_TAG_variant_part for a structure. */
+			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] 10+ messages in thread

* [PATCH v2 2/9] dwarf_loader: Initial support for DW_TAG_subprogram in DW_TAG_enumeration
  2026-08-21 21:34 [PATCH v2 0/9] pahole: Support cross-CU type references and dwz alternate debug files Arnaldo Carvalho de Melo
  2026-08-21 21:35 ` [PATCH v2 1/9] dwarf_loader: Initial support for DW_TAG_variant_part Arnaldo Carvalho de Melo
@ 2026-08-21 21:35 ` Arnaldo Carvalho de Melo
  2026-08-21 21:35 ` [PATCH v2 3/9] dwarf_loader: Allow forcing the merge of CUs for solving inter CU tag references Arnaldo Carvalho de Melo
                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-08-21 21:35 UTC (permalink / raw)
  To: Alan Maguire; +Cc: Jiri Olsa, Clark Williams, dwarves, 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__));
  $

The subprograms are added to the enumeration namespace but are not
counted in type->nr_members, which the CTF encoder uses as the enum
vlen and the fprintf/emit paths use to detect forward declarations.

Assisted-by: opencode:deepseek-v4-flash
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 btf_encoder.c     | 19 ++++++++++++++---
 ctf_encoder.c     | 15 +++++++++++--
 dwarf_loader.c    | 42 +++++++++++++++++++++++++++++-------
 dwarves.c         | 17 +++++++++------
 dwarves.h         |  2 +-
 dwarves_emit.c    | 10 ++++-----
 dwarves_fprintf.c | 54 +++++++++++++++++++++++++++++++++++------------
 pahole.c          |  9 +++++---
 8 files changed, 126 insertions(+), 42 deletions(-)

diff --git a/btf_encoder.c b/btf_encoder.c
index 4b422e09800f3fbb..8779db65f428b2ac 100644
--- a/btf_encoder.c
+++ b/btf_encoder.c
@@ -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;
diff --git a/ctf_encoder.c b/ctf_encoder.c
index b761287d45348c59..f00e4607cbae7875 100644
--- a/ctf_encoder.c
+++ b/ctf_encoder.c
@@ -154,8 +154,19 @@ 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;
 }
diff --git a/dwarf_loader.c b/dwarf_loader.c
index ac576f0936ac86a9..aa584a0939ff842c 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,42 @@ 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;
 
-		if (dwarf_tag(die) != DW_TAG_enumerator) {
+			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;
+			}
+
+			struct dwarf_tag *dtag = tag__dwarf(tag);
+			dtag->small_id = id;
+			/* Not counted in enumeration->type.nr_members: it is the
+			 * enumerator count (CTF enum vlen, forward-decl heuristic).
+			 */
+			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..1d03554d6a26f264 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)
@@ -1373,6 +1368,13 @@ struct class *class__clone(const struct class *from, const char *new_class_name,
 
 void enumeration__add(struct type *type, struct enumerator *enumerator)
 {
+	/*
+	 * nr_members is the enumerator count, not the number of entries in the
+	 * enumeration namespace: DW_TAG_subprogram members of Rust enumerations
+	 * are added to the namespace but must not be counted here, since the
+	 * CTF encoder uses nr_members as the enum vlen and the fprintf/emit
+	 * paths rely on nr_members == 0 to detect forward declarations.
+	 */
 	++type->nr_members;
 	namespace__add_tag(&type->namespace, &enumerator->tag);
 }
@@ -1928,6 +1930,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] 10+ messages in thread

* [PATCH v2 3/9] dwarf_loader: Allow forcing the merge of CUs for solving inter CU tag references
  2026-08-21 21:34 [PATCH v2 0/9] pahole: Support cross-CU type references and dwz alternate debug files Arnaldo Carvalho de Melo
  2026-08-21 21:35 ` [PATCH v2 1/9] dwarf_loader: Initial support for DW_TAG_variant_part Arnaldo Carvalho de Melo
  2026-08-21 21:35 ` [PATCH v2 2/9] dwarf_loader: Initial support for DW_TAG_subprogram in DW_TAG_enumeration Arnaldo Carvalho de Melo
@ 2026-08-21 21:35 ` Arnaldo Carvalho de Melo
  2026-08-21 21:35 ` [PATCH v2 4/9] dwarf_loader: Support DW_TAG_imported_unit for same-file partial units Arnaldo Carvalho de Melo
                   ` (5 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-08-21 21:35 UTC (permalink / raw)
  To: Alan Maguire; +Cc: Jiri Olsa, Clark Williams, dwarves, 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 but 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 available 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                    | 19 ++++++++++++++++++-
 tests/prettify_perf.data.sh |  4 ++--
 5 files changed, 38 insertions(+), 6 deletions(-)

diff --git a/dwarf_loader.c b/dwarf_loader.c
index aa584a0939ff842c..a4c54d569e0788cd 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -4586,7 +4586,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 f5368f68c2460488..79b10725fec08a44 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..372b9c3bd78ae227 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
@@ -1312,6 +1314,14 @@ static void btf_features__enable_all(void)
 		 * want distilled_base must request it explicitly. */
 		if (btf_features[i].conf_value == &conf_load.btf_gen_distilled_base)
 			continue;
+		/* force_cu_merging forces the serialized, single-threaded
+		 * merged-CU loading path.  Enabling it unconditionally
+		 * with --btf_features=all or --btf_gen_all changes output
+		 * and slows loading for files that don't need CU merging.
+		 * Users who want force_cu_merging must request it
+		 * explicitly. */
+		if (btf_features[i].conf_value == &conf_load.force_cu_merging)
+			continue;
 		enable_btf_feature(&btf_features[i]);
 	}
 }
@@ -1816,6 +1826,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 +2044,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] 10+ messages in thread

* [PATCH v2 4/9] dwarf_loader: Support DW_TAG_imported_unit for same-file partial units
  2026-08-21 21:34 [PATCH v2 0/9] pahole: Support cross-CU type references and dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (2 preceding siblings ...)
  2026-08-21 21:35 ` [PATCH v2 3/9] dwarf_loader: Allow forcing the merge of CUs for solving inter CU tag references Arnaldo Carvalho de Melo
@ 2026-08-21 21:35 ` Arnaldo Carvalho de Melo
  2026-08-21 21:35 ` [PATCH v2 5/9] dwarf_loader: Fix cus__merging_cu failing to detect DW_FORM_ref_addr Arnaldo Carvalho de Melo
                   ` (4 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-08-21 21:35 UTC (permalink / raw)
  To: Alan Maguire; +Cc: Jiri Olsa, Clark Williams, dwarves, 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     | 155 ++++++++++++++++++++++++++++++++++++++-------
 man-pages/pahole.1 |   7 +-
 2 files changed, 136 insertions(+), 26 deletions(-)

diff --git a/dwarf_loader.c b/dwarf_loader.c
index a4c54d569e0788cd..abe43ee0f6f816f1 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -153,6 +153,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)
@@ -178,6 +181,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;
@@ -202,6 +208,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;
@@ -450,12 +457,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;
@@ -686,7 +713,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)
@@ -1000,9 +1028,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;
@@ -2002,9 +2035,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;
@@ -2831,7 +2868,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
@@ -2899,9 +2936,92 @@ 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, 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;
+		if (new_size <= dcu->allocated_imported_units)
+			return -ENOMEM;
+		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, 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;
@@ -3715,17 +3835,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",
@@ -3747,7 +3858,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;
 	}
@@ -4519,7 +4630,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..577c4e8b76185aba 100644
--- a/man-pages/pahole.1
+++ b/man-pages/pahole.1
@@ -373,10 +373,9 @@ 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.
+	                   Same-file imported units (DW_TAG_imported_unit) are
+	                   auto-detected, so force_cu_merging is rarely needed with
+	                   current pahole.
 
 .fi
 
-- 
2.55.0


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

* [PATCH v2 5/9] dwarf_loader: Fix cus__merging_cu failing to detect DW_FORM_ref_addr
  2026-08-21 21:34 [PATCH v2 0/9] pahole: Support cross-CU type references and dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (3 preceding siblings ...)
  2026-08-21 21:35 ` [PATCH v2 4/9] dwarf_loader: Support DW_TAG_imported_unit for same-file partial units Arnaldo Carvalho de Melo
@ 2026-08-21 21:35 ` Arnaldo Carvalho de Melo
  2026-08-21 21:35 ` [PATCH v2 6/9] tests: Add inter-CU type reference comparison test Arnaldo Carvalho de Melo
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-08-21 21:35 UTC (permalink / raw)
  To: Alan Maguire; +Cc: Jiri Olsa, Clark Williams, dwarves, 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 abe43ee0f6f816f1..086dd2bb9161dcae 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -4182,7 +4182,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;
@@ -4190,10 +4190,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] 10+ messages in thread

* [PATCH v2 6/9] tests: Add inter-CU type reference comparison test
  2026-08-21 21:34 [PATCH v2 0/9] pahole: Support cross-CU type references and dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (4 preceding siblings ...)
  2026-08-21 21:35 ` [PATCH v2 5/9] dwarf_loader: Fix cus__merging_cu failing to detect DW_FORM_ref_addr Arnaldo Carvalho de Melo
@ 2026-08-21 21:35 ` Arnaldo Carvalho de Melo
  2026-08-21 21:35 ` [PATCH v2 7/9] dwarf_loader: Add cu parameter to tag__set_spec() and dwarf_tag__set_attr_type() Arnaldo Carvalho de Melo
                   ` (2 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-08-21 21:35 UTC (permalink / raw)
  To: Alan Maguire; +Cc: Jiri Olsa, Clark Williams, dwarves, 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 cus__merging_cu() fix earlier in this series, 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)));

An earlier commit in this series 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..0df0ce13cd948f1f
--- /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.
+
+. "$(dirname "$0")/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] 10+ messages in thread

* [PATCH v2 7/9] dwarf_loader: Add cu parameter to tag__set_spec() and dwarf_tag__set_attr_type()
  2026-08-21 21:34 [PATCH v2 0/9] pahole: Support cross-CU type references and dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (5 preceding siblings ...)
  2026-08-21 21:35 ` [PATCH v2 6/9] tests: Add inter-CU type reference comparison test Arnaldo Carvalho de Melo
@ 2026-08-21 21:35 ` Arnaldo Carvalho de Melo
  2026-08-21 21:35 ` [PATCH v2 8/9] dwarf_loader: Support DW_FORM_GNU_ref_alt references to dwz alternate debug files Arnaldo Carvalho de Melo
  2026-08-21 21:35 ` [PATCH v2 9/9] scripts: Add vmlinux_comparison.py for DWARF/BTF analysis Arnaldo Carvalho de Melo
  8 siblings, 0 replies; 10+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-08-21 21:35 UTC (permalink / raw)
  To: Alan Maguire; +Cc: Jiri Olsa, Clark Williams, dwarves, 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 | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/dwarf_loader.c b/dwarf_loader.c
index 086dd2bb9161dcae..103381c4dbfdf860 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -536,7 +536,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)
@@ -548,13 +548,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;
 
@@ -597,10 +597,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,
@@ -611,7 +611,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;
@@ -702,7 +702,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;
@@ -815,7 +815,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);
 		}
 	}
 
@@ -1702,7 +1702,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;
 
@@ -1864,7 +1864,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] 10+ messages in thread

* [PATCH v2 8/9] dwarf_loader: Support DW_FORM_GNU_ref_alt references to dwz alternate debug files
  2026-08-21 21:34 [PATCH v2 0/9] pahole: Support cross-CU type references and dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (6 preceding siblings ...)
  2026-08-21 21:35 ` [PATCH v2 7/9] dwarf_loader: Add cu parameter to tag__set_spec() and dwarf_tag__set_attr_type() Arnaldo Carvalho de Melo
@ 2026-08-21 21:35 ` Arnaldo Carvalho de Melo
  2026-08-21 21:35 ` [PATCH v2 9/9] scripts: Add vmlinux_comparison.py for DWARF/BTF analysis Arnaldo Carvalho de Melo
  8 siblings, 0 replies; 10+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-08-21 21:35 UTC (permalink / raw)
  To: Alan Maguire; +Cc: Jiri Olsa, Clark Williams, dwarves, 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.

  - Turn dwarf_tag__set_attr_type() from a macro into a static
    function that selects which offset/flags fields to fill via an
    enum (DWARF_TAG__REF_*).  The previous plain macro form
    (from_types_section.field = attr_type(...)) could not indicate
    which offset/flags to fill for different reference types; now
    explicit.

  - When the alt file cannot be resolved (missing .gnu_debugaltlink
    target or build-id mismatch), warn once per file, naming it.
    cus__merging_cu()'s abbreviation scan records whether the file
    uses DW_FORM_GNU_ref_alt, so cus__load_module() knows to warn
    when dwarf_getalt() fails — a process-wide static would hide
    later files with the same problem when processing many debuginfo
    files in a single invocation.

  - 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; btf_encoder's
    btf_encoder__tag_type() adjusts for these holes via
    type_id_null_adj[].  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 per-file 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 /usr/lib/debug/usr/lib64/chromium-browser/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 /usr/lib/debug/usr/lib64/chromium-browser/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        | 666 +++++++++++++++++++++++++++++++++++++-----
 dwarves.h             |   1 +
 man-pages/pahole.1    |   6 +-
 tests/dwz_alt_file.sh | 204 +++++++++++++
 6 files changed, 808 insertions(+), 83 deletions(-)
 create mode 100755 tests/dwz_alt_file.sh

diff --git a/btf_encoder.c b/btf_encoder.c
index 8779db65f428b2ac..d6b9be6a215df72b 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 f00e4607cbae7875..a1d9052f34c44cf8 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, id, ctf);
 
diff --git a/dwarf_loader.c b/dwarf_loader.c
index 103381c4dbfdf860..61ef52f291dee035 100644
--- a/dwarf_loader.c
+++ b/dwarf_loader.c
@@ -117,6 +117,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;
@@ -132,6 +142,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;
@@ -147,15 +163,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)
@@ -181,9 +208,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;
@@ -208,8 +247,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;
 }
@@ -251,39 +298,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)
@@ -298,7 +504,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)
 {
@@ -457,31 +665,54 @@ 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:
+			 * the .gnu_debugaltlink target is missing or
+			 * has a build-id mismatch.  cus__load_module()
+			 * already warned once for this file after
+			 * detecting DW_FORM_GNU_ref_alt in the abbrevs,
+			 * so stay silent here — this fires once per
+			 * reference.  Report the reference as
+			 * unresolvable (offset 0). */
 			*offset = 0;
-			return 0;
+			return false;
 		}
 		if (dwarf_formref_die(&attr, &type_die) != NULL) {
 			*offset = dwarf_dieoffset(&type_die);
@@ -489,7 +720,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)
@@ -536,8 +767,78 @@ 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.
+ */
+enum dwarf_tag__ref_type {
+	DWARF_TAG__REF_TYPE,
+	DWARF_TAG__REF_ABSTRACT_ORIGIN,
+	DWARF_TAG__REF_CONTAINING_TYPE,
+	DWARF_TAG__REF_SPECIFICATION,
+};
+
+static void dwarf_tag__set_attr_type(struct dwarf_tag *dtag,
+				     enum dwarf_tag__ref_type ref_type,
+				     Dwarf_Die *die, uint32_t attr_name,
+				     struct cu *cu)
+{
+	bool is_alt;
+	Dwarf_Off offset;
+	struct dwarf_cu *dcu = cu->priv;
+
+	bool from_types = attr_type(die, attr_name, &offset, &is_alt);
+	bool from_alt = is_alt || (dcu && dcu->processing_alt);
+
+	switch (ref_type) {
+	case DWARF_TAG__REF_TYPE:
+		dtag->type = offset;
+		dtag->from_types_section.type = from_types;
+		dtag->from_alt.type = from_alt;
+		break;
+	case DWARF_TAG__REF_ABSTRACT_ORIGIN:
+		dtag->abstract_origin = offset;
+		dtag->from_types_section.abstract_origin = from_types;
+		dtag->from_alt.abstract_origin = from_alt;
+		break;
+	case DWARF_TAG__REF_CONTAINING_TYPE:
+		dtag->containing_type = offset;
+		dtag->from_types_section.containing_type = from_types;
+		dtag->from_alt.containing_type = from_alt;
+		break;
+	case DWARF_TAG__REF_SPECIFICATION:
+		dtag->specification = offset;
+		dtag->from_types_section.specification = from_types;
+		dtag->from_alt.specification = from_alt;
+		break;
+	}
+
+	/* 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, offset);
+}
 
 static void tag__init(struct tag *tag, struct cu *cu, Dwarf_Die *die)
 {
@@ -548,13 +849,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, cu);
+		dwarf_tag__set_attr_type(dtag, DWARF_TAG__REF_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, cu);
+		dwarf_tag__set_attr_type(dtag, DWARF_TAG__REF_TYPE, die, DW_AT_discr, cu);
 	else
-		dwarf_tag__set_attr_type(dtag, type, die, DW_AT_type, cu);
+		dwarf_tag__set_attr_type(dtag, DWARF_TAG__REF_TYPE, die, DW_AT_type, cu);
 
-	dwarf_tag__set_attr_type(dtag, abstract_origin, die, DW_AT_abstract_origin, cu);
+	dwarf_tag__set_attr_type(dtag, DWARF_TAG__REF_ABSTRACT_ORIGIN, die, DW_AT_abstract_origin, cu);
 	tag->recursivity_level = 0;
 	tag->attributes = NULL;
 
@@ -597,10 +898,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, 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);
+	dwarf_tag__set_attr_type(dtag, DWARF_TAG__REF_SPECIFICATION, die, DW_AT_specification, cu);
 }
 
 static struct ptr_to_member_type *ptr_to_member_type__new(Dwarf_Die *die,
@@ -611,7 +912,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, cu);
+		dwarf_tag__set_attr_type(dtag, DWARF_TAG__REF_CONTAINING_TYPE, die, DW_AT_containing_type, cu);
 	}
 
 	return ptr;
@@ -713,8 +1014,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)
@@ -1029,8 +1329,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)
@@ -1443,7 +1741,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;
 }
@@ -1702,7 +2002,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, cu);
+		dwarf_tag__set_attr_type(dtag, DWARF_TAG__REF_TYPE, die, DW_AT_abstract_origin, cu);
 		exp->ip.addr = 0;
 		exp->high_pc = 0;
 
@@ -2035,23 +2335,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:
@@ -2964,6 +3296,45 @@ static int dwarf_cu__mark_imported_unit(struct dwarf_cu *dcu, Dwarf_Off offset)
 	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;
@@ -2971,8 +3342,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;
 
@@ -2998,6 +3368,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;
 
@@ -3286,7 +3668,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;
 
@@ -3442,10 +3825,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);
@@ -3507,6 +3895,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;
@@ -3627,10 +4016,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() with 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);
@@ -3668,6 +4061,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:
@@ -3706,9 +4100,13 @@ static int cu__resolve_func_ret_types_optimized(struct cu *cu, struct conf_load
 	for (i = 0; i < pt->nr_entries; ++i) {
 		struct tag *tag = pt->entries[i];
 		struct parameter *pos;
-		struct function *fn = tag__function(tag);
+		struct function *fn;
 		bool has_unexpected_reg = false, has_struct_param = false;
 
+		if (tag == NULL)
+			continue;
+
+		fn = tag__function(tag);
 		function__analyze_parameter_locations(fn, cu, conf);
 
 		/* mark function as optimized if parameter is, or
@@ -4135,8 +4533,23 @@ static int __cus__load_debug_types(struct cus *cus, struct conf_load *conf, Dwfl
 /* Match the define in linux:include/linux/elfnote-lto.h */
 #define LINUX_ELFNOTE_LTO_INFO		0x101
 
-static bool cus__merging_cu(Dwarf *dw, Elf *elf)
+/* Decide whether the CUs in this file must be merged into a single
+ * CU.  Two conditions require it:
+ *
+ *  - LTO-produced binaries carry a Linux ELF note (LINUX_ELFNOTE_LTO_INFO)
+ *    saying so;
+ *  - any DW_FORM_ref_addr attribute means CUs reference each other by
+ *    absolute offset and cannot be processed independently.
+ *
+ * While scanning the abbreviations, also record whether the file uses
+ * DW_FORM_GNU_ref_alt, so cus__load_module() can warn once per file
+ * when the dwz alternate debug file is missing.  */
+static bool cus__merging_cu(Dwarf *dw, Elf *elf, bool *uses_alt_refs)
 {
+	bool merging_cu = false;
+
+	*uses_alt_refs = false;
+
 	Elf_Scn *section = NULL;
 	while ((section = elf_nextscn(elf, section)) != 0) {
 		GElf_Shdr header;
@@ -4158,7 +4571,8 @@ static bool cus__merging_cu(Dwarf *dw, Elf *elf)
 				if (strcmp((char *)data->d_buf + name_off, "Linux") != 0)
 					continue;
 
-				return *(int *)(data->d_buf + desc_off) != 0;
+				if (*(int *)(data->d_buf + desc_off) != 0)
+					merging_cu = true;
 			}
 		}
 	}
@@ -4192,7 +4606,9 @@ static bool cus__merging_cu(Dwarf *dw, Elf *elf)
 							 &aboffset))
 					continue;
 				if (attr_form == DW_FORM_ref_addr)
-					return true;
+					merging_cu = true;
+				else if (attr_form == DW_FORM_GNU_ref_alt)
+					*uses_alt_refs = true;
 			}
 next_abbrev:
 
@@ -4202,7 +4618,7 @@ next_abbrev:
 		off = noff;
 	}
 
-	return false;
+	return merging_cu;
 }
 
 struct dwarf_cus {
@@ -4613,6 +5029,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);
@@ -4641,6 +5126,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;
@@ -4698,7 +5186,25 @@ 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)) {
+	bool uses_alt_refs;
+	bool merging_cu = cus__merging_cu(dw, elf, &uses_alt_refs);
+	Dwarf *alt_dw = dwarf_getalt(dw);
+
+	/* The file was dwz'd with -m (types moved to an alternate
+	 * debug file) but the alt file couldn't be loaded.  Warn once
+	 * per file — a process-wide static would hide later files
+	 * with the same problem when processing many debuginfo files
+	 * in a single invocation. */
+	if (uses_alt_refs && alt_dw == NULL) {
+		fprintf(stderr,
+			"WARNING: could not resolve dwz alternate debug file for %s\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",
+			filename);
+	}
+
+	if (conf->force_cu_merging || merging_cu || alt_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 79b10725fec08a44..fc913a891bf32622 100644
--- a/dwarves.h
+++ b/dwarves.h
@@ -949,6 +949,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/man-pages/pahole.1 b/man-pages/pahole.1
index 577c4e8b76185aba..35ea9f41223e09e7 100644
--- a/man-pages/pahole.1
+++ b/man-pages/pahole.1
@@ -374,8 +374,10 @@ Non-standard, non-BTF related features:
 	                   (function parameters, abstract origins for inlines, etc)
 	                   reference types in another CU.
 	                   Same-file imported units (DW_TAG_imported_unit) are
-	                   auto-detected, so force_cu_merging is rarely needed with
-	                   current pahole.
+	                   auto-detected. Alternate debug files using
+	                   DW_FORM_GNU_ref_alt are also handled automatically when
+	                   the matching .dwz file is available, so force_cu_merging
+	                   is rarely needed with current pahole.
 
 .fi
 
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] 10+ messages in thread

* [PATCH v2 9/9] scripts: Add vmlinux_comparison.py for DWARF/BTF analysis
  2026-08-21 21:34 [PATCH v2 0/9] pahole: Support cross-CU type references and dwz alternate debug files Arnaldo Carvalho de Melo
                   ` (7 preceding siblings ...)
  2026-08-21 21:35 ` [PATCH v2 8/9] dwarf_loader: Support DW_FORM_GNU_ref_alt references to dwz alternate debug files Arnaldo Carvalho de Melo
@ 2026-08-21 21:35 ` Arnaldo Carvalho de Melo
  8 siblings, 0 replies; 10+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-08-21 21:35 UTC (permalink / raw)
  To: Alan Maguire; +Cc: Jiri Olsa, Clark Williams, dwarves, Arnaldo Carvalho de Melo

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

Analyzes vmlinux files to compare DWARF versions, compilers, 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
- Runs pahole -F dwarf and -F btf, computing output hashes
- Times the BTF encoding with perf stat --null -r5, repeating each
  encoding 5 times to rule out variation from system caches, reporting
  mean ± stddev (rel.err) as the BTF-Enc column
- Detects inter-CU references (DW_FORM_ref_addr in .debug_abbrev),
  which pahole auto-detects and switches to the single-threaded
  merged-CU mode when present, reported as the Inter-CU column
- Reports DWARF and BTF ELF section sizes (DW-Sect, BTF-Sect columns)
- 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 compact ASCII table similar to coverage-table
- Configurable timeouts via --timeout-readelf and --timeout-pahole

Example output below shows a real run: DW-Hash and BTF-Hash are the
same across all files, and BTF-Enc shows the mean ± stddev (rel.err)
of 5 perf stat runs of the parallel BTF encoding.

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.*
  Scanning /home/acme/dws for vmlinux files...
  Found 5 vmlinux file(s)

    Analyzing vmlinux.dwarf4...
      Reading ELF sections...
      Extracting DWARF version...
      Extracting producer...
      Running pahole -F dwarf...
      Running pahole -F btf...
      Checking for inter-CU references...
      Running perf stat --null -r5 pahole -j --btf_encode_detached...
    Analyzing vmlinux.dwarf5...
      Reading ELF sections...
      Extracting DWARF version...
      Extracting producer...
      Running pahole -F dwarf...
      Running pahole -F btf...
      Checking for inter-CU references...
      Running perf stat --null -r5 pahole -j --btf_encode_detached...
    Analyzing vmlinux.dwarf5.zlib...
      Reading ELF sections...
      Extracting DWARF version...
      Extracting producer...
      Running pahole -F dwarf...
      Running pahole -F btf...
      Checking for inter-CU references...
      Running perf stat --null -r5 pahole -j --btf_encode_detached...
    Analyzing vmlinux.dwarf5.zstd...
      Reading ELF sections...
      Extracting DWARF version...
      Extracting producer...
      Running pahole -F dwarf...
      Running pahole -F btf...
      Checking for inter-CU references...
      Running perf stat --null -r5 pahole -j --btf_encode_detached...
    Analyzing vmlinux.toolchain_default...
      Reading ELF sections...
      Extracting DWARF version...
      Extracting producer...
      Running pahole -F dwarf...
      Running pahole -F btf...
      Checking for inter-CU references...
      Running perf stat --null -r5 pahole -j --btf_encode_detached...

  ┌──────────────────────────┬────────┬─────┬───────────────────────────────────────────────────────┬────────────┬──────────┬────────┬────────┬────────────────────────┬────────┐
  │File                      │    Size│ Ver │Producer                                               │     DW-Sect│  BTF-Sect│ DW-Hash│BTF-Hash│        BTF-Enc         │Inter-CU│
  ├──────────────────────────┼────────┼─────┼───────────────────────────────────────────────────────┼────────────┼──────────┼────────┼────────┼────────────────────────┼────────┤
  │vmlinux.dwarf4            │ 835.7MB│  4  │GNU C11 -gdwarf-4                                      │     599.2MB│     7.3MB│86e1b611│52123c24│ 2.565s ±0.006 (0.24%)  │   no   │
  │vmlinux.dwarf5            │ 736.3MB│  5  │GNU C11 -gdwarf-5                                      │     499.7MB│     7.3MB│86e1b611│52123c24│ 2.550s ±0.012 (0.48%)  │   no   │
  │vmlinux.dwarf5.zlib       │ 497.8MB│  5  │GNU C11 -gdwarf-5 -gz=zlib                             │     261.2MB│     7.3MB│86e1b611│52123c24│ 3.362s ±0.007 (0.21%)  │   no   │
  │vmlinux.dwarf5.zstd       │ 454.7MB│  5  │GNU C11 -gdwarf-5 -gz=zstd                             │     218.1MB│     7.3MB│86e1b611│52123c24│ 3.004s ±0.004 (0.13%)  │   no   │
  │vmlinux.toolchain_default │ 736.3MB│  5  │GNU C11                                                │     499.7MB│     7.3MB│86e1b611│52123c24│ 2.554s ±0.011 (0.43%)  │   no   │
  └──────────────────────────┴────────┴─────┴───────────────────────────────────────────────────────┴────────────┴──────────┴────────┴────────┴────────────────────────┴────────┘

  Column descriptions:
    File       : vmlinux filename
    Size       : Total file size
    Ver        : DWARF version
    Producer   : Compiler and flags (truncated)
    DW-Sect    : Total size of .debug_* sections
    BTF-Sect   : Total size of .btf* sections
    DW-Hash    : SHA256 hash (first 8 chars) of pahole -F dwarf output
    BTF-Hash   : SHA256 hash (first 8 chars) of pahole -F btf output
    BTF-Enc    : Mean ± stddev (rel.err) of 5 perf stat --null -r5
                 pahole -j --btf_encode_detached runs, e.g. '2.989s ±0.004 (0.13%)'
    Inter-CU   : Whether inter-CU references (DW_FORM_ref_addr) are present
                 in .debug_abbrev, which pahole auto-detects and switches
                 to the single-threaded merged-CU mode

  The DW-Hash values must match across all files, and so must the
  BTF-Hash values: this validates that pahole's output does not change
  with the DWARF version or compression used to build the vmlinux.

  BTF-Enc is the mean of 5 perf stat runs, repeating each encoding to
  rule out variation from system caches.  pahole automatically detects
  inter-CU references (DW_FORM_ref_addr in .debug_abbrev) and uses the
  parallel -j path only when there are none; with inter-CU references
  (Rust, LTO) it switches to the merged-CU mode, which is single-threaded.
  The Inter-CU column reflects which path was taken, so no CONFIG_DEBUG_*
  knowledge is needed to interpret the BTF-Enc numbers.

  The output shows that vmlinux variants (different DWARF versions, compression)
  produce identical pahole output despite different file sizes,
  validating pahole's consistency across different DWARF encodings.

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
---
 scripts/vmlinux_comparison.py | 612 ++++++++++++++++++++++++++++++++++
 1 file changed, 612 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..7c370b8e8776bece
--- /dev/null
+++ b/scripts/vmlinux_comparison.py
@@ -0,0 +1,612 @@
+#!/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
+import shutil
+from dataclasses import dataclass
+from typing import Optional, List, Dict, Any
+
+
+@dataclass
+class ToolError(Exception):
+    """Exception for tool execution failures with context."""
+    tool: str
+    message: str
+    returncode: Optional[int] = None
+    stderr: str = ''
+
+
+def format_size(size_bytes: int) -> str:
+    """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: str) -> int:
+    """Get file size in bytes."""
+    return os.path.getsize(path)
+
+
+def check_tool_exists(tool_name: str) -> bool:
+    """Check if a command-line tool exists and is executable."""
+    try:
+        result = subprocess.run(['which', tool_name], capture_output=True)
+        return result.returncode == 0
+    except Exception:
+        return False
+
+
+def run_readelf_sections(vmlinux_path: str, timeout: int = 30) -> Dict[str, int]:
+    """Extract ELF section sizes using readelf -SW (wide output)."""
+    sections = {}
+    try:
+        cmd = ['readelf', '-SW', vmlinux_path]
+        result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
+        if result.returncode != 0:
+            return sections
+
+        # Parse section headers using regex to avoid column alignment issues
+        # Format: [Nr] Name Type Address Off Size ES Flg Lk Inf Al
+        # Example: [14] .BTF PROGBITS ffffffff874cc000 66cc000 755775 00 A 0 0 1
+        for line in result.stdout.splitlines():
+            match = re.search(r'\[\s*\d+\]\s+(\S+)\s+\S+\s+\S+\s+\S+\s+(\S+)', line)
+            if match:
+                name = match.group(1)
+                size_hex = match.group(2)
+                try:
+                    size = int(size_hex, 16)
+                    if size > 0:
+                        sections[name] = size
+                except ValueError:
+                    pass
+    except subprocess.TimeoutExpired:
+        raise ToolError('readelf', f'Timeout after {timeout}s', stderr='')
+    except Exception as e:
+        raise ToolError('readelf', str(e), stderr='')
+    return sections
+
+
+def extract_dwarf_version(vmlinux_path: str, timeout: int = 30, max_lines: int = 100) -> str:
+    """Extract DWARF version using readelf.
+
+    Note: Limited to max_lines lines to avoid timeout issues when reading
+    large debug info sections from vmlinux files.
+    """
+    try:
+        cmd = ['readelf', '--debug-dump=info', vmlinux_path]
+        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+        try:
+            lines_read = 0
+            for line in proc.stdout:
+                if 'Version:' in line:
+                    match = re.search(r'Version:\s+(\d+)', line)
+                    if match:
+                        proc.terminate()
+                        try:
+                            proc.wait(timeout=5)
+                        except subprocess.TimeoutExpired:
+                            proc.kill()
+                            proc.wait()
+                        return match.group(1)
+                lines_read += 1
+                if lines_read >= max_lines:
+                    break
+            proc.terminate()
+            try:
+                proc.wait(timeout=5)
+            except subprocess.TimeoutExpired:
+                proc.kill()
+                proc.wait()
+            return 'N/A'
+        except subprocess.TimeoutExpired:
+            proc.kill()
+            proc.wait()
+            raise ToolError('readelf', f'Timeout after {timeout}s', stderr='')
+    except ToolError:
+        raise
+    except Exception as e:
+        raise ToolError('readelf', str(e), stderr='')
+
+
+def extract_producer(vmlinux_path: str, timeout: int = 30, max_lines: int = 200) -> str:
+    """Extract DW_AT_producer using readelf.
+
+    Note: Limited to max_lines lines to avoid timeout issues when reading
+    large debug info sections from vmlinux files.
+    """
+    try:
+        cmd = ['readelf', '--debug-dump=info', vmlinux_path]
+        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+        try:
+            lines_read = 0
+            for line in proc.stdout:
+                if 'DW_AT_producer' in line:
+                    match = re.search(r'DW_AT_producer\s*:\s*(?:\([^)]+\):\s*)?(.+)', line)
+                    if match:
+                        producer = match.group(1).strip()
+                        proc.terminate()
+                        try:
+                            proc.wait(timeout=5)
+                        except subprocess.TimeoutExpired:
+                            proc.kill()
+                            proc.wait()
+                        # Extract key parts: compiler name, version, DWARF flag, compression
+                        # Try to parse known patterns, but fall back to raw producer string
+                        parts = []
+
+                        # Get compiler and version (GCC, Clang, etc.)
+                        # GCC: "GNU C11 16.1.1 20260515 (Red Hat 16.1.1-2)"
+                        # Clang: "clang version 16.0.0"
+                        # ICC: "Intel(R) C++ Compiler for applications targeting Intel(R) 64, Version 19.0.1.117"
+                        # Vendor GCC: "gcc (GCC) 8.3.0 20191121"
+                        compiler_match = re.search(r'(GNU C\d+|clang|Intel.*C.*Compiler|gcc\s+\()', producer, re.IGNORECASE)
+                        if compiler_match:
+                            parts.append(compiler_match.group(0).strip())
+                        else:
+                            # Try to extract any compiler-like prefix
+                            generic_match = re.search(r'^(clang|icc|gcc|GNU|icc|Apple\s+LLVM)\s+', producer, re.IGNORECASE)
+                            if generic_match:
+                                parts.append(generic_match.group(0).strip())
+
+                        # Get DWARF version flag
+                        dwarf_match = re.search(r'-gdwarf-(\d+)', producer)
+                        if dwarf_match:
+                            parts.append(f"-gdwarf-{dwarf_match.group(1)}")
+
+                        # Get compression if present
+                        compress_match = re.search(r'-gz(=\w+)?', producer)
+                        if compress_match:
+                            parts.append(compress_match.group(0))
+
+                        if parts:
+                            return ' '.join(parts)
+                        # Fallback: return first 80 chars of raw producer
+                        return producer[:80] + ('...' if len(producer) > 80 else '')
+                lines_read += 1
+                if lines_read >= max_lines:
+                    break
+            proc.terminate()
+            try:
+                proc.wait(timeout=5)
+            except subprocess.TimeoutExpired:
+                proc.kill()
+                proc.wait()
+            return 'N/A'
+        except subprocess.TimeoutExpired:
+            proc.kill()
+            proc.wait()
+            raise ToolError('readelf', f'Timeout after {timeout}s', stderr='')
+    except ToolError:
+        raise
+    except Exception as e:
+        raise ToolError('readelf', str(e), stderr='')
+
+
+def run_pahole_and_hash(vmlinux_path: str, format_type: str, timeout: int = 600) -> tuple[str, int]:
+    """Run pahole with given format and return output hash and size."""
+    try:
+        cmd = ['pahole', '-F', format_type, vmlinux_path]
+        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        try:
+            result = proc.communicate(timeout=timeout)
+            if proc.returncode != 0:
+                stderr_msg = result[1].decode('utf-8', errors='replace') if result[1] else 'no stderr'
+                raise ToolError('pahole', f'pahole -F {format_type} failed', proc.returncode, stderr_msg)
+
+            hasher = hashlib.sha256()
+            hasher.update(result[0])
+            hash_short = hasher.hexdigest()[:8]
+            return hash_short, len(result[0])
+        except subprocess.TimeoutExpired:
+            proc.kill()
+            proc.wait()
+            raise ToolError('pahole', f'Timeout after {timeout}s', stderr='')
+    except ToolError:
+        raise
+    except Exception as e:
+        raise ToolError('pahole', str(e), stderr='')
+
+
+def has_inter_cu_references(vmlinux_path: str, timeout: int = 120) -> Optional[bool]:
+    """Detect inter-CU references (DW_FORM_ref_addr) in .debug_abbrev.
+
+    Mirrors cus__merging_cu() in dwarf_loader.c: any DW_FORM_ref_addr
+    attribute means CUs reference each other by absolute offset and
+    cannot be processed independently, so pahole falls back to the
+    merged-CU mode (force_cu_merging), which is single-threaded.
+    """
+    try:
+        cmd = ['readelf', '--debug-dump=abbrev', vmlinux_path]
+        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+                                stderr=subprocess.PIPE)
+        try:
+            for line in proc.stdout:
+                if b'DW_FORM_ref_addr' in line:
+                    proc.terminate()
+                    try:
+                        proc.wait(timeout=5)
+                    except subprocess.TimeoutExpired:
+                        proc.kill()
+                        proc.wait()
+                    return True
+            proc.wait(timeout=timeout)
+            return False
+        except subprocess.TimeoutExpired:
+            proc.kill()
+            proc.wait()
+            raise ToolError('readelf', f'Timeout after {timeout}s', stderr='')
+    except ToolError:
+        raise
+    except Exception as e:
+        raise ToolError('readelf', str(e), stderr='')
+
+
+def run_btf_encode_timed(vmlinux_path: str, threads: Optional[int] = None, timeout: int = 600) -> Optional[tuple[float, float, float]]:
+    """Run perf stat --null -r5 pahole -j --btf_encode_detached and return
+    (mean, stddev, rel_err) elapsed time in seconds, or None on failure.
+
+    Times the BTF encoding, which is what varies across the different
+    CONFIG_DEBUG_* DWARF modes.  Each encoding is repeated 5 times to
+    rule out variation from system caches.  pahole automatically detects
+    inter-CU references (DW_FORM_ref_addr in .debug_abbrev) and switches
+    to the merged-CU mode when present, which is single-threaded.
+    """
+    tmpfile: Optional[str] = None
+    try:
+        with tempfile.NamedTemporaryFile(prefix='vmlinux-cmp-', suffix='.btf', delete=False) as tmp:
+            tmpfile = tmp.name
+
+        try:
+            cmd = ['perf', 'stat', '--null', '-r5']
+            cmd.append('pahole')
+            if threads is not None:
+                cmd.append(f'-j{threads}')
+            else:
+                cmd.append('-j')
+            cmd.append(f'--btf_encode_detached={tmpfile}')
+            cmd.append(vmlinux_path)
+
+            result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
+            if result.returncode != 0:
+                return None
+
+            # perf stat prints to stderr, e.g.:
+            #   3.023371323 +- 0.003831050 seconds time elapsed  ( +-  0.13% )
+            # Also handle locale variations (comma as decimal separator)
+            stderr = result.stderr.replace(',', '.')
+            match = re.search(
+                r'([0-9.]+)\s+\+-\s+([0-9.]+)\s+seconds time elapsed\s+\(\s+\+-\s*([0-9.]+)%\s*\)',
+                stderr)
+            if match:
+                mean = float(match.group(1))
+                stddev = float(match.group(2))
+                rel_err = float(match.group(3))
+                return mean, stddev, rel_err
+            return None
+        finally:
+            if tmpfile is not None:
+                try:
+                    os.unlink(tmpfile)
+                except OSError:
+                    pass
+    except subprocess.TimeoutExpired:
+        return None
+    except Exception:
+        return None
+
+
+def find_vmlinux_files(directory: str) -> List[str]:
+    """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 Exception:
+            continue
+
+        vmlinux_files.append(path)
+
+    return sorted(vmlinux_files)
+
+
+def analyze_vmlinux(vmlinux_path: str, timeouts: Dict[str, int]) -> Dict[str, Any]:
+    """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)
+
+    # Extract ELF section sizes
+    try:
+        print(f"    Reading ELF sections...", file=sys.stderr)
+        sections = run_readelf_sections(vmlinux_path, timeout=timeouts.get('readelf', 30))
+    except ToolError as e:
+        print(f"    Warning: Failed to read ELF sections: {e.message}", file=sys.stderr)
+        sections = {}
+
+    # DWARF version
+    try:
+        print(f"    Extracting DWARF version...", file=sys.stderr)
+        dwarf_version = extract_dwarf_version(vmlinux_path, timeout=timeouts.get('readelf', 30))
+    except ToolError as e:
+        print(f"    Error: Failed to extract DWARF version: {e.message}", file=sys.stderr)
+        dwarf_version = e.message
+
+    # Producer
+    try:
+        print(f"    Extracting producer...", file=sys.stderr)
+        producer = extract_producer(vmlinux_path, timeout=timeouts.get('readelf', 30))
+    except ToolError as e:
+        print(f"    Error: Failed to extract producer: {e.message}", file=sys.stderr)
+        producer = e.message
+
+    # pahole -F dwarf
+    try:
+        print(f"    Running pahole -F dwarf...", file=sys.stderr)
+        dwarf_hash, _ = run_pahole_and_hash(vmlinux_path, 'dwarf', timeout=timeouts.get('pahole', 600))
+    except ToolError as e:
+        print(f"    Error: pahole -F dwarf failed: {e.message}", file=sys.stderr)
+        dwarf_hash = e.message
+
+    # pahole -F btf
+    try:
+        print(f"    Running pahole -F btf...", file=sys.stderr)
+        btf_hash, _ = run_pahole_and_hash(vmlinux_path, 'btf', timeout=timeouts.get('pahole', 600))
+    except ToolError as e:
+        print(f"    Error: pahole -F btf failed: {e.message}", file=sys.stderr)
+        btf_hash = e.message
+
+    # Inter-CU references
+    try:
+        print(f"    Checking for inter-CU references...", file=sys.stderr)
+        inter_cu_refs = has_inter_cu_references(vmlinux_path, timeout=timeouts.get('readelf', 120))
+    except ToolError as e:
+        print(f"    Error: Failed to check inter-CU references: {e.message}", file=sys.stderr)
+        inter_cu_refs = None
+
+    # BTF encoding timing
+    print(f"    Running perf stat --null -r5 pahole -j --btf_encode_detached...", file=sys.stderr)
+    btf_encoding_time = run_btf_encode_timed(vmlinux_path, timeout=timeouts.get('pahole', 600))
+
+    # Compute key section sizes
+    dwarf_sections = {k: v for k, v in sections.items() if k.startswith('.debug_')}
+    btf_sections = {k: v for k, v in sections.items() if k.lower().startswith('.btf')}
+
+    return {
+        'filename': filename,
+        'file_size': file_size,
+        'sections': sections,
+        'dwarf_sections': dwarf_sections,
+        'btf_sections': btf_sections,
+        'dwarf_version': dwarf_version,
+        'producer': producer,
+        'dwarf_output_hash': dwarf_hash,
+        'btf_output_hash': btf_hash,
+        'btf_encoding': btf_encoding_time,
+        'inter_cu_refs': inter_cu_refs,
+    }
+
+
+def print_table(results: List[Dict[str, Any]]) -> None:
+    """Print comparison table with box-drawing characters."""
+    if not results:
+        print("No vmlinux files found.")
+        return
+
+    # Column widths
+    W_FILE = 26
+    W_SIZE = 8
+    W_VER = 5
+    W_PROD = 55
+    W_HASH = 8
+    W_TIME = 24
+    W_ICUREF = 8
+    W_DWSECT = 12
+    W_BTFSECT = 10
+
+    # Header
+    def print_sep(char='─'):
+        print(f"┌{char*W_FILE}┬{char*W_SIZE}┬{char*W_VER}┬{char*W_PROD}┬"
+              f"{char*W_DWSECT}┬{char*W_BTFSECT}┬"
+              f"{char*W_HASH}┬{char*W_HASH}┬"
+              f"{char*W_TIME}┬{char*W_ICUREF}┐")
+
+    def print_row_sep():
+        print(f"├{'─'*W_FILE}┼{'─'*W_SIZE}┼{'─'*W_VER}┼{'─'*W_PROD}┼"
+              f"{'─'*W_DWSECT}┼{'─'*W_BTFSECT}┼"
+              f"{'─'*W_HASH}┼{'─'*W_HASH}┼"
+              f"{'─'*W_TIME}┼{'─'*W_ICUREF}┤")
+
+    print_sep()
+
+    # Column headers
+    print(f"│{'File':<{W_FILE}}│{'Size':>{W_SIZE}}│{'Ver':^{W_VER}}│{'Producer':<{W_PROD}}│"
+          f"{'DW-Sect':>{W_DWSECT}}│{'BTF-Sect':>{W_BTFSECT}}│"
+          f"{'DW-Hash':>{W_HASH}}│{'BTF-Hash':>{W_HASH}}│"
+          f"{'BTF-Enc':^{W_TIME}}│{'Inter-CU':>{W_ICUREF}}│")
+
+    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] + '...'
+
+        # Sum of DWARF section sizes
+        dw_sect_total = sum(r['dwarf_sections'].values())
+        dw_sect_str = format_size(dw_sect_total)
+
+        # Sum of BTF section sizes
+        btf_sect_total = sum(r['btf_sections'].values())
+        btf_sect_str = format_size(btf_sect_total)
+
+        dw_hash = r['dwarf_output_hash']
+        btf_hash = r['btf_output_hash']
+
+        enc = r['btf_encoding']
+        if enc is None:
+            enc_str = 'N/A'
+        else:
+            mean, stddev, rel_err = enc
+            enc_str = f"{mean:.3f}s ±{stddev:.3f} ({rel_err:.2f}%)"
+
+        icuref = r['inter_cu_refs']
+        if icuref is None:
+            icuref_str = 'N/A'
+        elif icuref:
+            icuref_str = 'yes'
+        else:
+            icuref_str = 'no'
+
+        print(f"│{fname:<{W_FILE}}│{fsize:>{W_SIZE}}│{ver:^{W_VER}}│{prod:<{W_PROD}}│"
+              f"{dw_sect_str:>{W_DWSECT}}│{btf_sect_str:>{W_BTFSECT}}│"
+              f"{dw_hash:>{W_HASH}}│{btf_hash:>{W_HASH}}│"
+              f"{enc_str:>{W_TIME-2}}  │{icuref_str:^{W_ICUREF}}│")
+
+    # Footer
+    print(f"└{'─'*W_FILE}┴{'─'*W_SIZE}┴{'─'*W_VER}┴{'─'*W_PROD}┴"
+          f"{'─'*W_DWSECT}┴{'─'*W_BTFSECT}┴"
+          f"{'─'*W_HASH}┴{'─'*W_HASH}┴"
+          f"{'─'*W_TIME}┴{'─'*W_ICUREF}┘")
+
+    print()
+    print("Column descriptions:")
+    print("  File       : vmlinux filename")
+    print("  Size       : Total file size")
+    print("  Ver        : DWARF version")
+    print("  Producer   : Compiler and flags (truncated)")
+    print("  DW-Sect    : Total size of .debug_* sections")
+    print("  BTF-Sect   : Total size of .btf* sections")
+    print("  DW-Hash    : SHA256 hash (first 8 chars) of pahole -F dwarf output")
+    print("  BTF-Hash   : SHA256 hash (first 8 chars) of pahole -F btf output")
+    print("  BTF-Enc    : Mean ± stddev (rel.err) of 5 perf stat --null -r5")
+    print("               pahole -j --btf_encode_detached runs, e.g. '2.989s ±0.004 (0.13%)'")
+    print("  Inter-CU   : Whether inter-CU references (DW_FORM_ref_addr) are present")
+    print("               in .debug_abbrev, which pahole auto-detects and switches")
+    print("               to the single-threaded merged-CU mode")
+    print()
+    print("The DW-Hash values must match across all files, and so must the")
+    print("BTF-Hash values: this validates that pahole's output does not change")
+    print("with the DWARF version or compression used to build the vmlinux.")
+    print()
+    print("BTF-Enc is the mean of 5 perf stat runs, repeating each encoding to")
+    print("rule out variation from system caches.  pahole automatically detects")
+    print("inter-CU references (DW_FORM_ref_addr in .debug_abbrev) and uses the")
+    print("parallel -j path only when there are none; with inter-CU references")
+    print("(Rust, LTO) it switches to the merged-CU mode, which is single-threaded.")
+    print("The Inter-CU column reflects which path was taken, so no CONFIG_DEBUG_*")
+    print("knowledge is needed to interpret the BTF-Enc numbers.")
+    print()
+    print("The output shows that vmlinux variants (different DWARF versions, compression)")
+    print("produce identical pahole output despite different file sizes,")
+    print("validating pahole's consistency across different DWARF encodings.")
+
+
+def main() -> None:
+    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)')
+    parser.add_argument('--timeout-readelf', type=int, default=30,
+                       help='Timeout for readelf commands in seconds (default: 30)')
+    parser.add_argument('--timeout-pahole', type=int, default=600,
+                       help='Timeout for pahole commands in seconds (default: 600)')
+    parser.add_argument('--threads', type=int, default=None,
+                       help='Number of threads for pahole -j (default: auto)')
+    args = parser.parse_args()
+
+    directory = os.path.expanduser(args.directory)
+
+    timeouts = {
+        'readelf': args.timeout_readelf,
+        'pahole': args.timeout_pahole,
+    }
+
+    tools_to_check = ['readelf', 'pahole', 'perf']
+    for tool in tools_to_check:
+        if not check_tool_exists(tool):
+            print(f"Error: Required tool '{tool}' not found in PATH", file=sys.stderr)
+            sys.exit(1)
+
+    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)
+        print("Hint: Build kernel with CONFIG_DEBUG_INFO and look for vmlinux in build 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, timeouts)
+        results.append(result)
+
+    print(file=sys.stderr)
+    print_table(results)
+
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file
-- 
2.55.0


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

end of thread, other threads:[~2026-08-21 21:35 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-21 21:34 [PATCH v2 0/9] pahole: Support cross-CU type references and dwz alternate debug files Arnaldo Carvalho de Melo
2026-08-21 21:35 ` [PATCH v2 1/9] dwarf_loader: Initial support for DW_TAG_variant_part Arnaldo Carvalho de Melo
2026-08-21 21:35 ` [PATCH v2 2/9] dwarf_loader: Initial support for DW_TAG_subprogram in DW_TAG_enumeration Arnaldo Carvalho de Melo
2026-08-21 21:35 ` [PATCH v2 3/9] dwarf_loader: Allow forcing the merge of CUs for solving inter CU tag references Arnaldo Carvalho de Melo
2026-08-21 21:35 ` [PATCH v2 4/9] dwarf_loader: Support DW_TAG_imported_unit for same-file partial units Arnaldo Carvalho de Melo
2026-08-21 21:35 ` [PATCH v2 5/9] dwarf_loader: Fix cus__merging_cu failing to detect DW_FORM_ref_addr Arnaldo Carvalho de Melo
2026-08-21 21:35 ` [PATCH v2 6/9] tests: Add inter-CU type reference comparison test Arnaldo Carvalho de Melo
2026-08-21 21:35 ` [PATCH v2 7/9] dwarf_loader: Add cu parameter to tag__set_spec() and dwarf_tag__set_attr_type() Arnaldo Carvalho de Melo
2026-08-21 21:35 ` [PATCH v2 8/9] dwarf_loader: Support DW_FORM_GNU_ref_alt references to dwz alternate debug files Arnaldo Carvalho de Melo
2026-08-21 21:35 ` [PATCH v2 9/9] scripts: Add vmlinux_comparison.py for DWARF/BTF analysis Arnaldo Carvalho de Melo

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.