[llvm] [DWARFLinker] Scope type uniquing on the unit root, not on its tag (PR #219619)

via llvm-commits llvm-commits at lists.llvm.org
Fri Aug 28 20:38:52 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-debuginfo

Author: Arcadiy Ivanov (arcivanov)

<details>
<summary>Changes</summary>

Depends on #<!-- -->219615.

ODR deduplication gives a type one definition shared by the units that use it. Both backends decided which types were candidates by looking for an enclosing `DW_TAG_compile_unit`, so under a `DW_TAG_partial_unit` root — what `dwz` emits, and what Fedora, RHEL and Debian debuginfo packaging therefore ships — nothing was a candidate, every unit kept its own copy, and the parallel backend produced no artificial type unit at all. That is on exactly the inputs where duplicate definitions are most abundant, since hoisting them into partial units is the whole point of `dwz`.

**Classic.** `getChildDeclContext()` returned a null context for any root that was not a compile unit, and a null context propagates to every descendant, so the entire unit dropped out of uniquing. It now returns the root context for any `dwarf::isUnitType()` tag. The local is retyped from `unsigned` to `dwarf::Tag` because `isUnitType` is overloaded on `uint8_t` as well, and an `unsigned` argument binds that overload silently — comparing tag numbers against `DW_UT_*` codes, which is false for every unit tag and true for `DW_TAG_class_type`.

That half is not only a missed optimization. With the definition excluded from uniquing it could be dropped while another unit still referenced it through `DW_FORM_ref_addr`, and emitting that reference asserted in `DIE::getDebugSectionOffset()` — `"DIE must be owned by a DIEUnit to get its absolute offset"`. The compile unit control on the same input is clean.

**Parallel.** `isNamespaceLikeEntry()` named the tag, and two callers wanted the unit root:

1. `getRootForSpecifiedEntry()` climbed past a partial unit root instead of stopping under it, so a type at unit scope was never offered to the type table.
2. `markParentsAsKeepingChildren()` asked the same predicate, and answering no for a partial unit root enqueued `MarkLiveChildrenRec` on it, which force-kept every type below it in plain DWARF. Fixing only the first leaves each type deduplicated into the artificial type unit *and* still present in its original unit.

Both now call `isUnitRootOrNamespaceLikeEntry()`, which reuses the `CompileUnit::isUnitRootDIE()` added by #<!-- -->219615 rather than spelling the same invariant a second way.

`isNamespaceLikeEntry()` itself stays narrow, and is now documented as deliberately narrow, because `maybeAddReferencedRoots()` uses it on the `DW_AT_import` branch to choose single-entry marking over recursive marking. Widening it there makes a `DW_TAG_imported_unit` of a partial unit mark only the root, so the imported unit loses everything it holds and the import dangles — a regression on the same `dwz` output this change is for. I measured that before splitting the predicates.

Deduplicating types under a partial unit root is also what makes the assertion #<!-- -->219615 fixes reachable on ordinary input, rather than only on the contrived shape that found it. That is the dependency, and it is why the two are ordered rather than independent.

**Tests.** `dwarf5-partial-unit-odr.test` is the new behaviour test: two units each defining a `struct S` at unit scope and an unrelated `S` in `namespace ns`, asserting exactly two definitions survive with all four references pointing at them, both backends, with a `DW_TAG_compile_unit` control and a `--no-odr-deduplication` arm. `dwarf5-partial-unit-imported-unit.test` is a guard rather than a repro — it passes before and after, and fails only if the two predicates are merged back together; there was previously no `DW_TAG_imported_unit` coverage anywhere in `llvm-dwarfutil` or `dsymutil`. `dwarf5-partial-unit-types-only.test` is amended: with deduplication reaching partial units its two links are now byte identical, so the block pinning the old asymmetry becomes `cmp`, and the classic backend is linked there now that it no longer aborts.

One expectation worth stating rather than discovering: on the classic side a partial unit root used to yield a null context that made its whole subtree skip `getChildDeclContext` entirely. On `dwz` output, where most types live under partial units, `DeclContextTree` therefore goes from nearly empty to a full tree. That is the fix doing its job, and it is repaid in output size, but it is a real change in peak memory for `llvm-dwarfutil` on those inputs.

Fixes #<!-- -->219393.


---

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


8 Files Affected:

- (modified) llvm/lib/DWARFLinker/Classic/DWARFLinkerDeclContext.cpp (+13-4) 
- (modified) llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp (+2-4) 
- (modified) llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h (+5) 
- (modified) llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp (+18-3) 
- (added) llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-imported-unit.test (+162) 
- (added) llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-namespace.test (+178) 
- (added) llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-odr.test (+334) 
- (added) llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-types-only.test (+191) 


``````````diff
diff --git a/llvm/lib/DWARFLinker/Classic/DWARFLinkerDeclContext.cpp b/llvm/lib/DWARFLinker/Classic/DWARFLinkerDeclContext.cpp
index bfec342526ea4..a965416ea8ec1 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFLinkerDeclContext.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFLinkerDeclContext.cpp
@@ -60,22 +60,31 @@ bool DeclContext::setLastSeenDIE(CompileUnit &U, const DWARFDie &Die) {
 PointerIntPair<DeclContext *, 1>
 DeclContextTree::getChildDeclContext(DeclContext &Context, const DWARFDie &DIE,
                                      CompileUnit &U, bool InClangModule) {
-  unsigned Tag = DIE.getTag();
+  dwarf::Tag Tag = DIE.getTag();
 
   // FIXME: dsymutil-classic compat: We should bail out here if we
   // have a specification or an abstract_origin. We will get the
   // parent context wrong here.
 
+  // A unit root is the root of the unit's context tree, whatever tag it
+  // carries: a null context here would propagate to every descendant and take
+  // the whole unit out of uniquing. The tag recognizes the root because this
+  // API is handed a DIE rather than its index, and it degrades safely - a
+  // nested DIE carrying a unit tag would merely be transparent to its
+  // enclosing context.
+  if (dwarf::isUnitType(Tag))
+    return PointerIntPair<DeclContext *, 1>(&Context);
+
   switch (Tag) {
   default:
     // By default stop gathering child contexts.
     return PointerIntPair<DeclContext *, 1>(nullptr);
   case dwarf::DW_TAG_module:
     break;
-  case dwarf::DW_TAG_compile_unit:
-    return PointerIntPair<DeclContext *, 1>(&Context);
   case dwarf::DW_TAG_subprogram:
-    // Do not unique anything inside CU local functions.
+    // Do not unique anything inside CU local functions. The root context is
+    // default constructed, and DeclContext's Tag defaults to
+    // DW_TAG_compile_unit, so this covers a partial unit root too.
     if ((Context.getTag() == dwarf::DW_TAG_namespace ||
          Context.getTag() == dwarf::DW_TAG_compile_unit) &&
         !dwarf::toUnsigned(DIE.find(dwarf::DW_AT_external), 0))
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
index 4138a9fd69aca..126cac340a49d 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
@@ -1419,8 +1419,7 @@ std::pair<DIE *, TypeEntry *> CompileUnit::cloneDIE(
 
   bool NeedToClonePlainDIE = Info.needToKeepInPlainDwarf();
   bool NeedToCloneTypeDIE =
-      (InputDieEntry->getTag() != dwarf::DW_TAG_compile_unit) &&
-      Info.needToPlaceInTypeTable();
+      !isUnitRootDIE(InputDieIdx) && Info.needToPlaceInTypeTable();
   std::pair<DIE *, TypeEntry *> ClonedDIE;
 
   DIEGenerator PlainDIEGenerator(Allocator, *this);
@@ -1449,8 +1448,7 @@ std::pair<DIE *, TypeEntry *> CompileUnit::cloneDIE(
       (ClonedDIE.first && Info.getKeepPlainChildren());
 
   bool HasTypeChildrenToClone =
-      ((ClonedDIE.second ||
-        InputDieEntry->getTag() == dwarf::DW_TAG_compile_unit) &&
+      ((ClonedDIE.second || isUnitRootDIE(InputDieIdx)) &&
        Info.getKeepTypeChildren());
 
   // Recursively clone children.
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
index c8c1b8822aa64..70b90727f4df4 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
@@ -335,6 +335,11 @@ class alignas(8) CompileUnit : public DwarfUnit {
   ///
   /// @{
 
+  /// \p DieIdx index of the DIE.
+  /// \returns true if the DIE is the unit's root, which is always at index
+  /// zero, whatever tag it carries.
+  static bool isUnitRootDIE(uint32_t DieIdx) { return DieIdx == 0; }
+
   /// \p Idx index of the DIE.
   /// \returns DieInfo descriptor.
   DIEInfo &getDIEInfo(unsigned Idx) { return DieInfoArray[Idx]; }
diff --git a/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp b/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp
index 669ea2ceb16c9..5e001be605aa4 100644
--- a/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp
@@ -106,6 +106,11 @@ void DependencyTracker::verifyKeepChain() {
 #endif
 }
 
+/// \returns true if \p Entry names a scope a DW_AT_import may refer to, whose
+/// contents that import then pulls in wholesale. Deliberately narrow: widening
+/// it to the other unit root tags makes a DW_TAG_imported_unit of a partial
+/// unit mark only the root and drop everything the imported unit holds. See
+/// dwarf5-partial-unit-imported-unit.test.
 static bool isNamespaceLikeEntry(const DWARFDebugInfoEntry *Entry) {
   switch (Entry->getTag()) {
   case dwarf::DW_TAG_compile_unit:
@@ -118,6 +123,14 @@ static bool isNamespaceLikeEntry(const DWARFDebugInfoEntry *Entry) {
   }
 }
 
+/// \returns true if the DIE at \p Idx bounds the scope of the DIEs below it.
+/// The unit root bounds that scope whatever tag it carries, and
+/// DW_TAG_partial_unit - what dwz emits - is not namespace-like.
+static bool isUnitRootOrNamespaceLikeEntry(uint32_t Idx,
+                                           const DWARFDebugInfoEntry *Entry) {
+  return CompileUnit::isUnitRootDIE(Idx) || isNamespaceLikeEntry(Entry);
+}
+
 bool DependencyTracker::resolveDependenciesAndMarkLiveness(
     bool InterCUProcessingStarted, std::atomic<bool> &HasNewInterconnectedCUs) {
   RootEntriesWorkList.clear();
@@ -482,7 +495,8 @@ void DependencyTracker::markParentsAsKeepingChildren(
         bool AddToWorklist = !isAlreadyMarked(
             ParentInfo, CompileUnit::DieOutputPlacement::TypeTable);
         ParentInfo.setKeepTypeChildren();
-        if (AddToWorklist && !isNamespaceLikeEntry(ParentEntry)) {
+        if (AddToWorklist &&
+            !isUnitRootOrNamespaceLikeEntry(*ParentIdx, ParentEntry)) {
           addActionToRootEntriesWorkList(
               LiveRootWorklistActionTy::MarkTypeChildrenRec,
               UnitEntryPairTy{Entry.CU, ParentEntry}, std::nullopt);
@@ -497,7 +511,8 @@ void DependencyTracker::markParentsAsKeepingChildren(
         bool AddToWorklist = !isAlreadyMarked(
             ParentInfo, CompileUnit::DieOutputPlacement::PlainDwarf);
         ParentInfo.setKeepPlainChildren();
-        if (AddToWorklist && !isNamespaceLikeEntry(ParentEntry)) {
+        if (AddToWorklist &&
+            !isUnitRootOrNamespaceLikeEntry(*ParentIdx, ParentEntry)) {
           addActionToRootEntriesWorkList(
               LiveRootWorklistActionTy::MarkLiveChildrenRec,
               UnitEntryPairTy{Entry.CU, ParentEntry}, std::nullopt);
@@ -952,7 +967,7 @@ DependencyTracker::getRootForSpecifiedEntry(UnitEntryPairTy Entry) {
 
     const DWARFDebugInfoEntry *ParentEntry =
         Result.CU->getDebugInfoEntry(*ParentIdx);
-    if (isNamespaceLikeEntry(ParentEntry))
+    if (isUnitRootOrNamespaceLikeEntry(*ParentIdx, ParentEntry))
       break;
     Result.DieEntry = ParentEntry;
   } while (true);
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-imported-unit.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-imported-unit.test
new file mode 100644
index 0000000000000..7ee174105c71b
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-imported-unit.test
@@ -0,0 +1,162 @@
+## Deciding which types are deduplication candidates means asking where the
+## scope holding a type ends, and a unit root ends it whatever tag it carries.
+## Answering that with "is this entry namespace-like", which the linker already
+## had, would also answer a second question asked of the same predicate: how
+## much of an imported unit a DW_TAG_imported_unit drags in. A partial unit is
+## what dwz emits and what DW_TAG_imported_unit names, so this test pins the
+## import path against the scope change.
+##
+## U01:  namespace ns { struct S {}; }
+## U02:  // DW_TAG_imported_unit of U01
+##       void foo1() { ... }
+##
+## Nothing in U01 is referenced from U02, so the import is the only thing
+## keeping it alive and its contents disappear if the import stops recursing.
+## This test passes both before and after the fix: it exists to fail if the two
+## predicates are ever merged back together.
+##
+## Only the parallel backend is linked. The classic backend aborts on
+## DW_TAG_imported_unit in fixupForwardReferences() with "Offset being queried
+## before it's been computed", for a DW_TAG_compile_unit root just as much as
+## for a DW_TAG_partial_unit one, which makes it a separate defect from
+## anything about partial units.
+
+# RUN: yaml2obj %s -o %t.o
+# RUN: llvm-dwarfutil --linker parallel %t.o %t.out
+# RUN: llvm-dwarfdump --debug-info %t.out | FileCheck %s
+
+# CHECK:      0x[[U01:[0-9a-f]{8}]]: DW_TAG_partial_unit
+# CHECK-NEXT:   DW_AT_producer ("by_hand")
+# CHECK-NEXT:   DW_AT_name ("U01")
+# CHECK:        DW_TAG_namespace
+# CHECK-NEXT:     DW_AT_name ("ns")
+# CHECK:          DW_TAG_structure_type
+# CHECK-NEXT:       DW_AT_name ("S")
+# CHECK:      DW_TAG_compile_unit
+# CHECK-NEXT:   DW_AT_producer ("by_hand")
+# CHECK-NEXT:   DW_AT_name ("U02")
+# CHECK:        DW_TAG_imported_unit
+# CHECK-NEXT:     DW_AT_import (0x00000000[[U01]] "U01")
+# CHECK:        DW_TAG_subprogram
+# CHECK-NEXT:     DW_AT_name ("foo1")
+
+## Two DWARFv5 units. The first is a partial unit defining struct S in
+## namespace ns and holding no code; the second is a compile unit that imports
+## it and has the only function. Each root carries its own
+## DW_AT_str_offsets_base, so the strings the linker rewrites into
+## DW_FORM_strx stay resolvable on output; the linker does not synthesize that
+## attribute for a partial unit root by itself.
+--- !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:            0x10
+  - 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_str_offsets_base
+            Form:      DW_FORM_sec_offset
+      - 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_low_pc
+            Form:      DW_FORM_addr
+          - Attribute: DW_AT_high_pc
+            Form:      DW_FORM_data8
+      - Tag:      DW_TAG_imported_unit
+        Children: DW_CHILDREN_no
+        Attributes:
+          - Attribute: DW_AT_import
+            Form:      DW_FORM_ref_addr
+  debug_info:
+    - Version:  5
+      UnitType: DW_UT_partial
+      AbbrevTableID: 0
+      Entries:
+        - AbbrCode: 1
+          Values:
+            - CStr: by_hand
+            - CStr: U01
+            - Value: 0x04
+            - Value: 0x8
+        - AbbrCode: 2
+          Values:
+            - CStr: ns
+        - AbbrCode: 3
+          Values:
+            - CStr: S
+            - Value: 0x8
+        - AbbrCode: 0
+        - AbbrCode: 0
+    - Version:  5
+      UnitType: DW_UT_compile
+      AbbrevTableID: 0
+      Entries:
+        - AbbrCode: 4
+          Values:
+            - CStr: by_hand
+            - CStr: U02
+            - Value: 0x04
+            - Value: 0x1130
+            - Value: 0x10
+            - Value: 0x8
+        - AbbrCode: 6
+          Values:
+            - Value: 0x0c
+        - AbbrCode: 5
+          Values:
+            - CStr: foo1
+            - Value: 0x1130
+            - Value: 0x10
+        - AbbrCode: 0
+...
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-namespace.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-namespace.test
new file mode 100644
index 0000000000000..357d829a34b2c
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-namespace.test
@@ -0,0 +1,178 @@
+## A DW_TAG_partial_unit root - what dwz emits, and what Fedora, RHEL and
+## Debian debuginfo packaging therefore ships - was kept out of the artificial
+## type unit by nothing but a test on its tag, so it was cloned as a type DIE
+## and asked for the type entry that no unit root is ever assigned. Reaching
+## that point needs a scope between the type and the unit root, because the
+## walk that looks for a type's enclosing scope stops at a namespace but runs
+## past a unit root. A DW_TAG_compile_unit root is linked alongside as the
+## control.
+##
+## namespace ns {
+##   struct S { int m1; };
+## }
+##
+## ns::S foo1() { ... }
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -o %t.cu.o
+# RUN: llvm-dwarfutil %t.cu.o %t.cu.classic
+# RUN: llvm-dwarfdump --debug-info %t.cu.classic | FileCheck %s --check-prefixes=CLASSIC,CLASSIC-CU
+# RUN: llvm-dwarfutil --linker parallel %t.cu.o %t.cu.parallel
+# RUN: llvm-dwarfdump --debug-info %t.cu.parallel | FileCheck %s --check-prefixes=PARALLEL,PARALLEL-CU
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -o %t.pu.o
+# RUN: llvm-dwarfutil %t.pu.o %t.pu.classic
+# RUN: llvm-dwarfdump --debug-info %t.pu.classic | FileCheck %s --check-prefixes=CLASSIC,CLASSIC-PU
+# RUN: llvm-dwarfutil --linker parallel %t.pu.o %t.pu.parallel
+# RUN: llvm-dwarfdump --debug-info %t.pu.parallel | FileCheck %s --check-prefixes=PARALLEL,PARALLEL-PU
+
+## The classic backend leaves the type where it found it, and the root tag
+## survives linking. llvm-dwarfdump qualifies a type name with the names of its
+## enclosing scopes and a partial unit root is one, which a compile unit root
+## is not, so the type is printed as "U01::ns::S" for the partial unit run and
+## as "ns::S" for the control. The reference target is the same DIE either way.
+# CLASSIC-CU:     DW_TAG_compile_unit
+# CLASSIC-PU:     DW_TAG_partial_unit
+# CLASSIC:        DW_TAG_subprogram
+# CLASSIC-NEXT:     DW_AT_name ("foo1")
+# CLASSIC-CU:       DW_AT_type (0x00000000[[S:[0-9a-f]{8}]] "ns::S")
+# CLASSIC-PU:       DW_AT_type (0x00000000[[S:[0-9a-f]{8}]] "U01::ns::S")
+# CLASSIC:        DW_TAG_namespace
+# CLASSIC-NEXT:     DW_AT_name ("ns")
+# CLASSIC:      0x[[S]]:     DW_TAG_structure_type
+# CLASSIC-NEXT:   DW_AT_name ("S")
+# CLASSIC:          DW_TAG_member
+# CLASSIC-NEXT:       DW_AT_name ("m1")
+
+## The parallel backend puts the type in the artificial type unit, under a copy
+## of the namespace that held it, and the unit's reference resolves there. The
+## unit root itself stays out of that unit, which is what this pins: it has no
+## name of its own to be deduplicated under, and cloning it as a type DIE is
+## what used to crash.
+# PARALLEL:      DW_AT_name ("__artificial_type_unit")
+# PARALLEL:      DW_TAG_namespace
+# PARALLEL-NEXT:   DW_AT_name ("ns")
+# PARALLEL:      0x[[S:[0-9a-f]{8}]]:     DW_TAG_structure_type
+# PARALLEL-NEXT:   DW_AT_name ("S")
+# PARALLEL:          DW_TAG_member
+# PARALLEL-NEXT:       DW_AT_name ("m1")
+# PARALLEL-CU:   DW_TAG_compile_unit
+# PARALLEL-PU:   DW_TAG_partial_unit
+# PARALLEL-NEXT:   DW_AT_producer ("by_hand")
+# PARALLEL-NEXT:   DW_AT_name ("U01")
+# PARALLEL:      DW_TAG_subprogram
+# PARALLEL-NEXT:   DW_AT_name ("foo1")
+# PARALLEL:        DW_AT_type (0x00000000[[S]] "ns::S")
+
+## One DWARFv5 unit with a function in .text returning struct S, which is
+## defined in namespace ns. The root carries its own DW_AT_str_offsets_base, so
+## the strings the linker rewrites into DW_FORM_strx stay resolvable on output;
+## the linker does not synthesize that attribute for a partial unit root by
+## itself.
+--- !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:            0x10
+  - Name:            .debug_str_offsets
+    Type:            SHT_PROGBITS
+    Flags:           [  ]
+    Content:        "0400000005000000"
+DWARF:
+  debug_abbrev:
+    - Table:
+      - Tag:      [[ROOT]]
+        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
+          - Attribute: DW_AT_type
+            Form:      DW_FORM_ref4
+      - 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_yes
+        Attributes:
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_byte_size
+            Form:      DW_FORM_data1
+      - Tag:      DW_TAG_member
+        Children: DW_CHILDREN_no
+        Attributes:
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+          - Attribute: DW_AT_type
+            Form:      DW_FORM_ref4
+          - Attribute: DW_AT_data_member_location
+            Form:      DW_FORM_data1
+      - Tag:      DW_TAG_base_type
+        Children: DW_CHILDREN_no
+        Attributes:
+          - Attribute: DW_AT_name
+            Form:      DW_FORM_string
+  debug_info:
+    - Version:  5
+      UnitType: [[UNITTYPE]]
+      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
+            - Value: 0x4d
+        - AbbrCode: 3
+          Values:
+            - CStr: ns
+        - AbbrCode: 4
+          Values:
+            - CStr: S
+            - Value: 0x8
+        - AbbrCode: 5
+          Values:
+            - CStr: m1
+            - Value: 0x5c
+            - Value: 0x0
+        - AbbrCode: 0
+        - AbbrCode: 0
+        - AbbrCode: 6
+          Values:
+            - CStr: int
+        - AbbrCode: 0
+...
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-odr.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-odr.test
new file mode 100644
index 0000000000000..f2b82f56f4f41
--- /dev/null
+++...
[truncated]

``````````

</details>


https://github.com/llvm/llvm-project/pull/219619


More information about the llvm-commits mailing list