[llvm] [AsmPrinter] Find a DIE's unit by its owner, not by its tag (PR #219660)

Arcadiy Ivanov via llvm-commits llvm-commits at lists.llvm.org
Sat Aug 29 16:25:05 PDT 2026


https://github.com/arcivanov updated https://github.com/llvm/llvm-project/pull/219660

>From 32b0fe4fc4cde41f8b942221af7e7cd02a01d23b Mon Sep 17 00:00:00 2001
From: Arcadiy Ivanov <arcadiy at ivanov.biz>
Date: Sat, 29 Aug 2026 05:23:27 -0400
Subject: [PATCH] [AsmPrinter] Find a DIE's unit by its owner, not by its tag

DIE::getUnitDie() climbs the parent chain looking for the unit root,
and it recognises that root by comparing its tag against
DW_TAG_compile_unit, DW_TAG_skeleton_unit and DW_TAG_type_unit.
DW_TAG_partial_unit is the fourth unit root tag and is absent from
that list, so the walk runs off the top of the tree and returns null.

DIEUnit's own constructor already asserts that its tag is one of
those four, partial units included. A partial unit root is therefore
accepted into a DIEUnit and then cannot be found again from anything
underneath it.

getUnit() returns null in turn, and getDebugSectionOffset() asserts.
Every DW_FORM_ref_addr reference into a partial unit aborts the
DWARFLinker on an assertions build and dereferences the null on a
released one. Only that one form reaches this code: DIEEntry::emitValue
asks for a section-absolute offset for DW_FORM_ref_addr alone, and the
unit-relative forms emit getOffset() instead.

DWARFv5 section 3.1.1 gives DW_TAG_compile_unit and DW_TAG_partial_unit
alike as unit roots, and section 7.5.5 defines DW_FORM_ref_addr as an
offset from the beginning of the .debug_info section, which is the
offset getDebugSectionOffset() computes. dwz is what emits partial
units, and it is a standard step in Fedora, RHEL and Debian debuginfo
packaging, so hoisted shared types referenced across unit boundaries
are the ordinary shape of a distribution debuginfo file.

The tag is the wrong instrument for the question. A DIEUnit sets
itself as the owner of exactly one DIE, its unit DIE, so the walk is
looking for the DIE that carries a DIEUnit owner. Testing for that
owner answers directly and is correct for every root tag rather than
for an enumerated three.

The unit tests cover all four unit root tags, and they also pin the
orphan case: a tree carrying a unit root tag that no DIEUnit owns
belongs to no unit, which is what the documented contract already
said and what merely adding the missing tag to the list would not
give. The lit test links a partial unit root and a compile unit
control, under garbage collection and in update mode. It runs the
classic linker only and without --verify, because the parallel linker
asserts on a type under a partial unit root (#219613) and such a unit
still ships under a DW_UT_compile header (#219651).

Fixes #219650.

Assisted-by: Claude Code
---
 llvm/include/llvm/CodeGen/DIE.h               |  14 +-
 llvm/lib/CodeGen/AsmPrinter/DIE.cpp           |   8 +-
 .../ELF/X86/dwarf5-partial-unit-ref-addr.test | 303 ++++++++++++++++++
 llvm/unittests/CodeGen/DIETest.cpp            |  38 +++
 4 files changed, 352 insertions(+), 11 deletions(-)
 create mode 100644 llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-ref-addr.test

diff --git a/llvm/include/llvm/CodeGen/DIE.h b/llvm/include/llvm/CodeGen/DIE.h
index bddbf9277cfbd..660585ac11399 100644
--- a/llvm/include/llvm/CodeGen/DIE.h
+++ b/llvm/include/llvm/CodeGen/DIE.h
@@ -937,18 +937,16 @@ class DIE : IntrusiveBackListNode, public DIEValueList {
   computeOffsetsAndAbbrevs(const dwarf::FormParams &FormParams,
                            DIEAbbrevSet &AbbrevSet, unsigned CUOffset);
 
-  /// Climb up the parent chain to get the compile unit or type unit DIE that
-  /// this DIE belongs to.
+  /// Climb up the parent chain to get the unit DIE that this DIE belongs to.
   ///
-  /// \returns the compile or type unit DIE that owns this DIE, or NULL if
-  /// this DIE hasn't been added to a unit DIE.
+  /// \returns the unit DIE that owns this DIE, or NULL if this DIE hasn't been
+  /// added to a unit DIE.
   LLVM_ABI const DIE *getUnitDie() const;
 
-  /// Climb up the parent chain to get the compile unit or type unit that this
-  /// DIE belongs to.
+  /// Climb up the parent chain to get the unit that this DIE belongs to.
   ///
-  /// \returns the DIEUnit that represents the compile or type unit that owns
-  /// this DIE, or NULL if this DIE hasn't been added to a unit DIE.
+  /// \returns the DIEUnit that represents the unit that owns this DIE, or NULL
+  /// if this DIE hasn't been added to a unit DIE.
   LLVM_ABI DIEUnit *getUnit() const;
 
   void setOffset(unsigned O) { Offset = O; }
diff --git a/llvm/lib/CodeGen/AsmPrinter/DIE.cpp b/llvm/lib/CodeGen/AsmPrinter/DIE.cpp
index 01e7ae434192d..5cc473e9e8077 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DIE.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DIE.cpp
@@ -191,9 +191,11 @@ uint64_t DIE::getDebugSectionOffset() const {
 const DIE *DIE::getUnitDie() const {
   const DIE *p = this;
   while (p) {
-    if (p->getTag() == dwarf::DW_TAG_compile_unit ||
-        p->getTag() == dwarf::DW_TAG_skeleton_unit ||
-        p->getTag() == dwarf::DW_TAG_type_unit)
+    // A DIEUnit sets itself as the owner of exactly one DIE, its unit DIE, so
+    // the unit root is the DIE carrying a DIEUnit owner. Testing the tag
+    // instead has to enumerate every unit root tag, and misses any it does not
+    // list.
+    if (dyn_cast_if_present<DIEUnit *>(p->Owner))
       return p;
     p = p->getParent();
   }
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-ref-addr.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-ref-addr.test
new file mode 100644
index 0000000000000..56e01be796c78
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-ref-addr.test
@@ -0,0 +1,303 @@
+## This test checks that a cross-unit DW_FORM_ref_addr reference into a
+## DW_TAG_partial_unit is rewritten to the section-absolute offset of the
+## referenced DIE, as it already is for a DW_TAG_compile_unit.
+##
+## The two documents differ only in the root tag and unit type of the unit
+## holding the referenced type, so the compile unit document is the control.
+##
+## Only the classic linker is exercised. The parallel linker asserts on a type
+## under a partial unit root, which is issue #219613, and --verify rejects the
+## output because a partial unit still ships under a DW_UT_compile header,
+## which is issue #219365.
+
+# RUN: yaml2obj --docnum=1 %s -o %t-cu.o
+# RUN: yaml2obj --docnum=2 %s -o %t-pu.o
+
+# RUN: llvm-dwarfutil %t-cu.o %t-cu.out
+# RUN: llvm-dwarfdump --debug-info %t-cu.out | FileCheck %s
+
+# RUN: llvm-dwarfutil %t-pu.o %t-pu.out
+# RUN: llvm-dwarfdump --debug-info %t-pu.out | FileCheck %s
+
+## Update mode keeps every DIE, so the same reference has to be resolved there.
+
+# RUN: llvm-dwarfutil --no-garbage-collection --build-accelerator=DWARF \
+# RUN:   %t-cu.o %t-cu.upd
+# RUN: llvm-dwarfdump --debug-info %t-cu.upd | FileCheck %s
+
+# RUN: llvm-dwarfutil --no-garbage-collection --build-accelerator=DWARF \
+# RUN:   %t-pu.o %t-pu.upd
+# RUN: llvm-dwarfdump --debug-info %t-pu.upd | FileCheck %s
+
+## U01 owns the type, U02 names it across the unit boundary.
+# CHECK: DW_AT_name{{.*}}"U01"
+
+# CHECK: 0x[[S:[0-9a-f]*]]: DW_TAG_structure_type
+# CHECK-NEXT: DW_AT_name{{.*}}"S"
+
+# CHECK: DW_AT_name{{.*}}"U02"
+
+# CHECK: DW_TAG_subprogram
+# CHECK-NEXT: DW_AT_name{{.*}}"foo2"
+# CHECK-NEXT: DW_AT_type{{.*}}(0x00000000[[S]] "{{.*}}S")
+
+## The control: U01 is an ordinary compilation unit.
+--- !ELF
+FileHeader:
+  Class:    ELFCLASS64
+  Data:     ELFDATA2LSB
+  Type:     ET_REL
+  Machine:  EM_X86_64
+Sections:
+  - Name:            .text
+    Type:            SHT_PROGBITS
+    Flags:           [ SHF_ALLOC, SHF_EXECINSTR ]
+    Address:         0x1130
+    Size:            0x20
+  - Name:            .debug_str_offsets
+    Type:            SHT_PROGBITS
+    Flags:           [  ]
+    Content:        "0400000005000000"
+DWARF:
+  debug_abbrev:
+    - ID:    0
+      Table:
+      - Tag:      DW_TAG_partial_unit
+        Children: DW_CHILDREN_yes
+        Attributes:
+          - Attribute: DW_AT_producer
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_language
+            Form:      DW_FORM_data2
+          - Attribute: DW_AT_low_pc
+            Form:      DW_FORM_addr
+          - Attribute: DW_AT_high_pc
+            Form:      DW_FORM_data8
+          - Attribute: DW_AT_str_offsets_base
+            Form:      DW_FORM_sec_offset
+      - Tag:      DW_TAG_subprogram
+        Children: DW_CHILDREN_no
+        Attributes:
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_low_pc
+            Form:      DW_FORM_addr
+          - Attribute: DW_AT_high_pc
+            Form:      DW_FORM_data8
+      - Tag:      DW_TAG_namespace
+        Children: DW_CHILDREN_yes
+        Attributes:
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+      - Tag:      DW_TAG_structure_type
+        Children: DW_CHILDREN_no
+        Attributes:
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_byte_size
+            Form:      DW_FORM_data1
+      - Tag:      DW_TAG_compile_unit
+        Children: DW_CHILDREN_yes
+        Attributes:
+          - Attribute: DW_AT_producer
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_language
+            Form:      DW_FORM_data2
+          - Attribute: DW_AT_low_pc
+            Form:      DW_FORM_addr
+          - Attribute: DW_AT_high_pc
+            Form:      DW_FORM_data8
+          - Attribute: DW_AT_str_offsets_base
+            Form:      DW_FORM_sec_offset
+      - Tag:      DW_TAG_subprogram
+        Children: DW_CHILDREN_no
+        Attributes:
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_type
+            Form:      DW_FORM_ref_addr
+          - Attribute: DW_AT_low_pc
+            Form:      DW_FORM_addr
+          - Attribute: DW_AT_high_pc
+            Form:      DW_FORM_data8
+  debug_info:
+    - Version:  5
+      UnitType: DW_UT_compile
+      AbbrevTableID: 0
+      Entries:
+        - AbbrCode: 5
+          Values:
+            - CStr: by_hand
+            - CStr: U01
+            - Value: 0x04
+            - Value: 0x1130
+            - Value: 0x10
+            - Value: 0x8
+        - AbbrCode: 2
+          Values:
+            - CStr: foo1
+            - Value: 0x1130
+            - Value: 0x10
+        - AbbrCode: 3
+          Values:
+            - CStr: ns
+        - AbbrCode: 4
+          Values:
+            - CStr: S
+            - Value: 0x8
+        - AbbrCode: 0
+        - AbbrCode: 0
+    - Version:  5
+      UnitType: DW_UT_compile
+      AbbrevTableID: 0
+      Entries:
+        - AbbrCode: 5
+          Values:
+            - CStr: by_hand
+            - CStr: U02
+            - Value: 0x04
+            - Value: 0x1140
+            - Value: 0x10
+            - Value: 0x8
+        - AbbrCode: 6
+          Values:
+            - CStr: foo2
+            - Value: 0x49
+            - Value: 0x1140
+            - Value: 0x10
+        - AbbrCode: 0
+
+## The case: U01 is a partial unit, and nothing else changes.
+--- !ELF
+FileHeader:
+  Class:    ELFCLASS64
+  Data:     ELFDATA2LSB
+  Type:     ET_REL
+  Machine:  EM_X86_64
+Sections:
+  - Name:            .text
+    Type:            SHT_PROGBITS
+    Flags:           [ SHF_ALLOC, SHF_EXECINSTR ]
+    Address:         0x1130
+    Size:            0x20
+  - Name:            .debug_str_offsets
+    Type:            SHT_PROGBITS
+    Flags:           [  ]
+    Content:        "0400000005000000"
+DWARF:
+  debug_abbrev:
+    - ID:    0
+      Table:
+      - Tag:      DW_TAG_partial_unit
+        Children: DW_CHILDREN_yes
+        Attributes:
+          - Attribute: DW_AT_producer
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_language
+            Form:      DW_FORM_data2
+          - Attribute: DW_AT_low_pc
+            Form:      DW_FORM_addr
+          - Attribute: DW_AT_high_pc
+            Form:      DW_FORM_data8
+          - Attribute: DW_AT_str_offsets_base
+            Form:      DW_FORM_sec_offset
+      - Tag:      DW_TAG_subprogram
+        Children: DW_CHILDREN_no
+        Attributes:
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_low_pc
+            Form:      DW_FORM_addr
+          - Attribute: DW_AT_high_pc
+            Form:      DW_FORM_data8
+      - Tag:      DW_TAG_namespace
+        Children: DW_CHILDREN_yes
+        Attributes:
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+      - Tag:      DW_TAG_structure_type
+        Children: DW_CHILDREN_no
+        Attributes:
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_byte_size
+            Form:      DW_FORM_data1
+      - Tag:      DW_TAG_compile_unit
+        Children: DW_CHILDREN_yes
+        Attributes:
+          - Attribute: DW_AT_producer
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_language
+            Form:      DW_FORM_data2
+          - Attribute: DW_AT_low_pc
+            Form:      DW_FORM_addr
+          - Attribute: DW_AT_high_pc
+            Form:      DW_FORM_data8
+          - Attribute: DW_AT_str_offsets_base
+            Form:      DW_FORM_sec_offset
+      - Tag:      DW_TAG_subprogram
+        Children: DW_CHILDREN_no
+        Attributes:
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_type
+            Form:      DW_FORM_ref_addr
+          - Attribute: DW_AT_low_pc
+            Form:      DW_FORM_addr
+          - Attribute: DW_AT_high_pc
+            Form:      DW_FORM_data8
+  debug_info:
+    - Version:  5
+      UnitType: DW_UT_partial
+      AbbrevTableID: 0
+      Entries:
+        - AbbrCode: 1
+          Values:
+            - CStr: by_hand
+            - CStr: U01
+            - Value: 0x04
+            - Value: 0x1130
+            - Value: 0x10
+            - Value: 0x8
+        - AbbrCode: 2
+          Values:
+            - CStr: foo1
+            - Value: 0x1130
+            - Value: 0x10
+        - AbbrCode: 3
+          Values:
+            - CStr: ns
+        - AbbrCode: 4
+          Values:
+            - CStr: S
+            - Value: 0x8
+        - AbbrCode: 0
+        - AbbrCode: 0
+    - Version:  5
+      UnitType: DW_UT_compile
+      AbbrevTableID: 0
+      Entries:
+        - AbbrCode: 5
+          Values:
+            - CStr: by_hand
+            - CStr: U02
+            - Value: 0x04
+            - Value: 0x1140
+            - Value: 0x10
+            - Value: 0x8
+        - AbbrCode: 6
+          Values:
+            - CStr: foo2
+            - Value: 0x49
+            - Value: 0x1140
+            - Value: 0x10
+        - AbbrCode: 0
+...
diff --git a/llvm/unittests/CodeGen/DIETest.cpp b/llvm/unittests/CodeGen/DIETest.cpp
index 4eb4a78f7de9d..5311b583be058 100644
--- a/llvm/unittests/CodeGen/DIETest.cpp
+++ b/llvm/unittests/CodeGen/DIETest.cpp
@@ -222,4 +222,42 @@ TEST(DIEValueListTest, DeleteValue) {
   EXPECT_FALSE(List.deleteValue(dwarf::DW_AT_high_pc));
 }
 
+TEST(DIETest, GetUnitDie) {
+  // A DIEUnit owns exactly one DIE, its unit DIE, whatever the root tag is, so
+  // the walk up from a nested DIE has to reach the unit for every unit root
+  // tag.
+  static const dwarf::Tag UnitTags[] = {
+      dwarf::DW_TAG_compile_unit, dwarf::DW_TAG_partial_unit,
+      dwarf::DW_TAG_type_unit, dwarf::DW_TAG_skeleton_unit};
+
+  BumpPtrAllocator Alloc;
+  for (dwarf::Tag UnitTag : UnitTags) {
+    BasicDIEUnit Unit(UnitTag);
+    Unit.setDebugSectionOffset(0x100);
+
+    DIE &Root = Unit.getUnitDie();
+    DIE &Namespace = Root.addChild(DIE::get(Alloc, dwarf::DW_TAG_namespace));
+    DIE &Struct =
+        Namespace.addChild(DIE::get(Alloc, dwarf::DW_TAG_structure_type));
+    Struct.setOffset(0x20);
+
+    EXPECT_EQ(&Root, Struct.getUnitDie());
+    EXPECT_EQ(static_cast<DIEUnit *>(&Unit), Struct.getUnit());
+    EXPECT_EQ(0x120u, Struct.getDebugSectionOffset());
+  }
+}
+
+TEST(DIETest, GetUnitDieWithoutUnit) {
+  // A tree carrying a unit root tag that was never handed to a DIEUnit belongs
+  // to no unit, so there is no absolute offset to compute for anything in it.
+  BumpPtrAllocator Alloc;
+  DIE *Root = DIE::get(Alloc, dwarf::DW_TAG_compile_unit);
+  DIE &Struct = Root->addChild(DIE::get(Alloc, dwarf::DW_TAG_structure_type));
+
+  EXPECT_EQ(nullptr, Root->getUnitDie());
+  EXPECT_EQ(nullptr, Root->getUnit());
+  EXPECT_EQ(nullptr, Struct.getUnitDie());
+  EXPECT_EQ(nullptr, Struct.getUnit());
+}
+
 } // end namespace



More information about the llvm-commits mailing list