[llvm] [DWARFLinker] Key the unit's address range on the unit root (PR #219602)
Arcadiy Ivanov via llvm-commits
llvm-commits at lists.llvm.org
Fri Aug 28 16:54:03 PDT 2026
https://github.com/arcivanov created https://github.com/llvm/llvm-project/pull/219602
`llvm-dwarfutil` emits a unit range that does not contain the subprograms it just relocated whenever the unit's root DIE is a `DW_TAG_partial_unit`, which is the shape `dwz` leaves behind. DWARFLinker has two backends, classic and parallel, selected with `--linker` and shared with `dsymutil`; both are affected, and both exit 0:
```console
$ llvm-dwarfutil partial.o partial.out
$ llvm-dwarfdump --verify partial.out
error: DIE address ranges are not contained in its parent's ranges:
0x0000000c: DW_TAG_partial_unit [1] *
DW_AT_low_pc [DW_FORM_addr] (0x0000000000001140)
DW_AT_high_pc [DW_FORM_data8] (0x0000000000000010)
0x00000035: DW_TAG_subprogram [2] (0x0000000c)
DW_AT_name [DW_FORM_strx] (indexed (00000003) string = "foo2")
DW_AT_low_pc [DW_FORM_addr] (0x0000000000001150)
DW_AT_high_pc [DW_FORM_data8] (0x0000000000000010)
error: Aggregated error counts:
error: DIE address ranges are not contained by parent ranges occurred 2 time(s).
Errors detected.
```
`DW_AT_low_pc` and `DW_AT_high_pc` on a unit root bound the unit, so both backends replace them with `getLowPc()`/`getHighPc()`, the accumulated extents of every function that survived. Both gated that on the root being a `DW_TAG_compile_unit`. A `DW_TAG_partial_unit` root fell through to the generic path instead, which adds the DIE's PC offset and nothing else, and that offset is zero at a root because it is reassigned only on `DW_TAG_subprogram`. The root kept the range the input gave it while its children were relocated independently.
`DWARFContext::compile_units()` filters out only type units, so a partial unit reaches this code on exactly the same path a full compilation unit does; the tag test was the only thing separating them. DWARFv5 section 3.1.1 introduces the attribute list with "A full or partial compilation unit entry may have the following attributes", and its first item is "Either a `DW_AT_low_pc` and `DW_AT_high_pc` pair of attributes or a `DW_AT_ranges` attribute whose values encode the contiguous or non-contiguous address ranges, respectively, of the machine instructions generated for the compilation unit".
That item covers both encodings and both were gated the same way, so `DW_AT_ranges` on a partial unit root is the same defect in its other form. It is the louder of the two: the input range list names no function that survived, so mapping it through the surviving function ranges rather than replacing it outright yields nothing at all.
```console
$ llvm-dwarfutil ranges-partial.o ranges-partial.out
ranges-partial.o: warning: inconsistent range data.
$ llvm-dwarfdump --debug-info ranges-partial.out
0x0000000c: DW_TAG_partial_unit
DW_AT_producer ("by_hand")
DW_AT_name ("U1")
DW_AT_ranges (0x0000000c)
0x0000001b: DW_TAG_subprogram
DW_AT_name ("foo1")
DW_AT_low_pc (0x0000000000001130)
DW_AT_high_pc (0x0000000000001140)
```
A `DW_TAG_compile_unit` root on otherwise byte-identical input gets `[0x1130, 0x1140)` and `[0x1150, 0x1160)`, the two functions the linker kept, and no warning.
The unit range now keys on the root DIE itself rather than on its tag, behind a named predicate in each backend so the rule is stated once rather than at every site. `CompileUnit::isUnitRootDIE(const DIE &)` compares against `getOutputUnitDIE()`, `DIEAttributeCloner::isUnitRootDIE()` tests for input DIE index 0 — the same keying as #219398. Five sites per backend move. `DW_AT_high_pc` encoded as an address is cloned by `cloneAddressAttribute()`/`cloneAddressAttr()`, while the same attribute encoded as a length is cloned by `cloneScalarAttribute()`/`cloneScalarAttr()`, so each of those two functions carries its own copy of the test plus the negative one that keeps `constrainHighPC()` off the unit root. `DW_AT_low_pc` is always in the address class and so has a single site, and `DW_AT_ranges` is keyed where the unit's range patch is recorded, in `CompileUnit::noteRangeAttribute()` and in the `DebugRangePatch` constructor. That patch's flag is renamed `IsCompileUnitRanges` → `IsUnitRanges`, since after this change it no longer marks a compile unit specifically; the classic counterpart was already called `UnitRangeAttribute`.
Both tests are DWARFv5 units with two subprograms at `[0x1130, 0x1140)` and `[0x1150, 0x1160)` whose root advertises the gap between them, `[0x1140, 0x1150)`, so the substitution is observable in full: the linked unit has to cover both functions and neither input bound may survive. Each is linked with a `DW_TAG_partial_unit` root and, as a control, with a `DW_TAG_compile_unit` root, through both backends. `dwarf5-partial-unit-pc-range.test` covers the pair in both `DW_AT_high_pc` encodings; `dwarf5-partial-unit-ranges.test` covers `DW_AT_ranges`.
Two properties of the input are load-bearing and worth calling out. The declared root range stays inside `.text` because `llvm-dwarfutil` decides whether a unit is worth linking at all from the root DIE's own ranges, and `--tombstone=universal` treats a root range outside every executable section as dead, which silently skips the unit and empties the output. The root also carries its own `DW_AT_str_offsets_base` and, in the `DW_AT_ranges` test, its own `DW_AT_addr_base`, because the linker synthesizes neither for a partial unit root — the second of those is #218825, and without it the classic backend asserts rather than producing wrong output.
A third pair of runs pins the case where the substitution has nothing to substitute. With both subprograms tombstoned and only a variable keeping the unit alive, there are no linked extents, and the root's `DW_AT_low_pc` and `DW_AT_high_pc` are dropped rather than left at their unrelocated input values. The `DW_TAG_compile_unit` control drops them identically, which is the point of running it: this is behaviour a partial unit root now inherits, not behaviour the change invents.
The tests assert what `--verify` says, not only what is emitted, and that is where this PR meets #219398. The compile unit controls verify clean. A partial unit does not yet: after this change the reported "DIE address ranges are not contained in its parent's ranges" is gone, and the single complaint left is `Mismatched unit type`, because the unit header still says `DW_UT_compile` whatever the root tag was. That is a separate defect, #219365, fixed by #219398. The two changes are independent and can land in either order; the partial unit runs here therefore use `not llvm-dwarfdump --verify` and pin the remaining error exactly, so that neither fix can regress into the other's territory unnoticed. Once both are in, those runs become a plain `llvm-dwarfdump --verify` with no expected errors, and I will send that as a follow-up on whichever of the two lands second.
One adjacent site keeps its tag test deliberately. Both backends gate accelerator table population on `DW_TAG_compile_unit`, so under `--build-accelerator=DWARF` a partial unit root is admitted to the generic name-record path and its `DW_AT_name` — a source file name, not a program identifier — is emitted as a `.debug_names` entry tagged `DW_TAG_partial_unit`. That is a name index defect rather than an address range one, it is orthogonal to everything above, and it wants its own change.
`check-llvm-tools-llvm-dwarfutil`, `check-llvm-tools-dsymutil`, `DWARFLinkerParallelTests` and `DebugInfoDWARFTests` pass.
Fixes: #219392
This change was produced with AI assistance; I have reviewed it and am accountable for it.
>From 32fa457cafa43fb2bc0630d2e9fe49139a222db1 Mon Sep 17 00:00:00 2001
From: Arcadiy Ivanov <arcadiy at ivanov.biz>
Date: Fri, 28 Aug 2026 19:53:25 -0400
Subject: [PATCH] [DWARFLinker] Key the unit's address range on the unit root
A unit root's address range bounds the unit, so the classic and parallel
DWARFLinker backends both replace it with the extents the linker
actually kept, and both gated that replacement on the root being a
DW_TAG_compile_unit. A DW_TAG_partial_unit root fell through to the
generic path instead, which adds the DIE's PC offset and nothing else,
and that offset is zero at a root because it is reassigned only on
DW_TAG_subprogram. The root kept the range the input gave it while its
children were relocated independently, so llvm-dwarfdump --verify
rejected the output with "DIE address ranges are not contained in its
parent's ranges".
DW_TAG_partial_unit is what dwz leaves behind, and dwz is a standard
step in distribution debuginfo packaging. DWARFContext::compile_units()
filters out only type units, so such a unit reaches this code on exactly
the same path a full compilation unit does; the tag test was the only
thing separating them.
DWARFv5 section 3.1.1 lets a full or a partial compilation unit encode
the addresses generated for it either as a DW_AT_low_pc and
DW_AT_high_pc pair or as a DW_AT_ranges attribute, and both encodings
were gated the same way. In the DW_AT_ranges form the input list named
no surviving function, so the linked result was "warning: inconsistent
range data." and an empty range list, leaving a unit that advertised no
code at all while still owning two subprograms.
Key the substitution on the unit root itself instead, behind a named
predicate in each backend: CompileUnit::isUnitRootDIE(const DIE &)
compares against the output unit DIE,
DIEAttributeCloner::isUnitRootDIE() tests for input DIE index 0. Five
sites per backend move. DW_AT_high_pc encoded as an address is cloned as
an address attribute while the same attribute encoded as a length is
cloned as a scalar, so each of those two functions carries its own copy
of the test plus the negative one that keeps constrainHighPC() off the
unit root, and DW_AT_ranges is keyed where the unit's range patch is
recorded. The parallel flag carrying that last decision is renamed from
IsCompileUnitRanges to IsUnitRanges, since it no longer marks a compile
unit specifically.
Fixes: #219392
Assisted-by: Claude Code
---
.../Classic/DWARFLinkerCompileUnit.h | 11 +
llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp | 19 +-
.../Classic/DWARFLinkerCompileUnit.cpp | 4 +-
.../Parallel/DIEAttributeCloner.cpp | 27 ++-
.../DWARFLinker/Parallel/DIEAttributeCloner.h | 9 +
.../Parallel/DWARFLinkerCompileUnit.cpp | 10 +-
.../lib/DWARFLinker/Parallel/OutputSections.h | 4 +-
.../ELF/X86/dwarf5-partial-unit-pc-range.test | 206 ++++++++++++++++++
.../ELF/X86/dwarf5-partial-unit-ranges.test | 146 +++++++++++++
9 files changed, 404 insertions(+), 32 deletions(-)
create mode 100644 llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-pc-range.test
create mode 100644 llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-ranges.test
diff --git a/llvm/include/llvm/DWARFLinker/Classic/DWARFLinkerCompileUnit.h b/llvm/include/llvm/DWARFLinker/Classic/DWARFLinkerCompileUnit.h
index 538572338dc8b..542eca75863b3 100644
--- a/llvm/include/llvm/DWARFLinker/Classic/DWARFLinkerCompileUnit.h
+++ b/llvm/include/llvm/DWARFLinker/Classic/DWARFLinkerCompileUnit.h
@@ -145,6 +145,17 @@ class CompileUnit {
return nullptr;
}
+ /// Check whether \p Die is this unit's root DIE, which owns the attributes
+ /// that bound the unit and so are replaced with the extents the linker kept.
+ /// The root is identified by being the output unit DIE rather than by its
+ /// tag: DWARFv5 section 3.1.1 gives a full or a partial compilation unit
+ /// entry the same address range attributes, encoding the ranges generated
+ /// for that unit, and DWARFContext::compile_units() hands both kinds to the
+ /// linker on the same path.
+ bool isUnitRootDIE(const DIE &Die) const {
+ return &Die == getOutputUnitDIE();
+ }
+
dwarf::Tag getTag() const { return OrigUnit.getUnitDIE().getTag(); }
bool hasODR() const { return HasODR; }
diff --git a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
index 46001d7a8a4d5..e0e13487767bb 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
@@ -1493,14 +1493,15 @@ unsigned DWARFLinker::DIECloner::cloneAddressAttribute(
return 0;
}
- if (InputDIE.getTag() == dwarf::DW_TAG_compile_unit &&
- AttrSpec.Attr == dwarf::DW_AT_low_pc) {
+ // A unit root's DW_AT_low_pc and DW_AT_high_pc bound the unit, so they come
+ // from the extents the linker kept rather than from the input values, which
+ // describe an address range this DIE no longer covers.
+ if (Unit.isUnitRootDIE(Die) && AttrSpec.Attr == dwarf::DW_AT_low_pc) {
if (std::optional<uint64_t> LowPC = Unit.getLowPc())
Addr = *LowPC;
else
return 0;
- } else if (InputDIE.getTag() == dwarf::DW_TAG_compile_unit &&
- AttrSpec.Attr == dwarf::DW_AT_high_pc) {
+ } else if (Unit.isUnitRootDIE(Die) && AttrSpec.Attr == dwarf::DW_AT_high_pc) {
if (uint64_t HighPc = Unit.getHighPc())
Addr = HighPc;
else
@@ -1647,8 +1648,7 @@ unsigned DWARFLinker::DIECloner::cloneScalarAttribute(
Value = *Offset;
AttrSpec.Form = dwarf::DW_FORM_sec_offset;
AttrSize = Unit.getOrigUnit().getFormParams().getDwarfOffsetByteSize();
- } else if (AttrSpec.Attr == dwarf::DW_AT_high_pc &&
- Die.getTag() == dwarf::DW_TAG_compile_unit) {
+ } else if (AttrSpec.Attr == dwarf::DW_AT_high_pc && Unit.isUnitRootDIE(Die)) {
std::optional<uint64_t> LowPC = Unit.getLowPc();
if (!LowPC)
return 0;
@@ -1667,10 +1667,9 @@ unsigned DWARFLinker::DIECloner::cloneScalarAttribute(
return 0;
}
- // A compile unit's high_pc comes from the unit's own linked range and spans
- // every symbol in it.
- if (AttrSpec.Attr == dwarf::DW_AT_high_pc &&
- Die.getTag() != dwarf::DW_TAG_compile_unit)
+ // A unit's high_pc comes from the unit's own linked range and spans every
+ // symbol in it.
+ if (AttrSpec.Attr == dwarf::DW_AT_high_pc && !Unit.isUnitRootDIE(Die))
Value = constrainHighPC(InputDIE, Value, /*IsLength=*/true, Info.PCOffset,
*File.Addresses);
diff --git a/llvm/lib/DWARFLinker/Classic/DWARFLinkerCompileUnit.cpp b/llvm/lib/DWARFLinker/Classic/DWARFLinkerCompileUnit.cpp
index 41dfd5a50f6e7..e17a4ba758954 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFLinkerCompileUnit.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFLinkerCompileUnit.cpp
@@ -170,7 +170,9 @@ void CompileUnit::addFunctionRange(uint64_t FuncLowPc, uint64_t FuncHighPc,
}
void CompileUnit::noteRangeAttribute(const DIE &Die, PatchLocation Attr) {
- if (Die.getTag() == dwarf::DW_TAG_compile_unit) {
+ // The unit root's ranges bound the unit, so they are replaced with the
+ // extents the linker kept rather than mapped through like an ordinary DIE's.
+ if (isUnitRootDIE(Die)) {
UnitRangeAttribute = Attr;
return;
}
diff --git a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
index 68d7b9d72cf61..a20040f92402e 100644
--- a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
@@ -479,8 +479,7 @@ size_t DIEAttributeCloner::cloneScalarAttr(
Value = *Offset;
ResultingForm = dwarf::DW_FORM_sec_offset;
- } else if (AttrSpec.Attr == dwarf::DW_AT_high_pc &&
- InputDieEntry->getTag() == dwarf::DW_TAG_compile_unit) {
+ } else if (AttrSpec.Attr == dwarf::DW_AT_high_pc && isUnitRootDIE()) {
if (!OutUnit.isCompileUnit())
return 0;
@@ -503,11 +502,11 @@ size_t DIEAttributeCloner::cloneScalarAttr(
if (AttrSpec.Attr == dwarf::DW_AT_ranges ||
AttrSpec.Attr == dwarf::DW_AT_start_scope) {
- // Create patch for the range offset value.
+ // Create patch for the range offset value. The unit root's ranges bound
+ // the unit, so they are replaced with the extents the linker kept rather
+ // than mapped through like an ordinary DIE's.
DebugInfoOutputSection.notePatchWithOffsetUpdate(
- DebugRangePatch{{AttrOutOffset},
- InputDieEntry->getTag() == dwarf::DW_TAG_compile_unit},
- PatchesOffsets);
+ DebugRangePatch{{AttrOutOffset}, isUnitRootDIE()}, PatchesOffsets);
AttrInfo.HasRanges = true;
} else if (DWARFAttribute::mayHaveLocationList(AttrSpec.Attr) &&
dwarf::doesFormBelongToClass(AttrSpec.Form,
@@ -550,10 +549,9 @@ size_t DIEAttributeCloner::cloneScalarAttr(
!OutUnit.isCompileUnit())
return 0;
- // A compile unit's high_pc comes from the unit's own linked range and spans
- // every symbol in it.
- if (AttrSpec.Attr == dwarf::DW_AT_high_pc &&
- InputDieEntry->getTag() != dwarf::DW_TAG_compile_unit)
+ // A unit's high_pc comes from the unit's own linked range and spans every
+ // symbol in it.
+ if (AttrSpec.Attr == dwarf::DW_AT_high_pc && !isUnitRootDIE())
Value = constrainHighPC(Value, /*IsLength=*/true);
auto Result =
@@ -697,14 +695,15 @@ size_t DIEAttributeCloner::cloneAddressAttr(
return 0;
}
- if (InputDieEntry->getTag() == dwarf::DW_TAG_compile_unit &&
- AttrSpec.Attr == dwarf::DW_AT_low_pc) {
+ // A unit root's DW_AT_low_pc and DW_AT_high_pc bound the unit, so they come
+ // from the extents the linker kept rather than from the input values, which
+ // describe an address range this DIE no longer covers.
+ if (isUnitRootDIE() && AttrSpec.Attr == dwarf::DW_AT_low_pc) {
if (std::optional<uint64_t> LowPC = OutUnit.getAsCompileUnit()->getLowPc())
Addr = *LowPC;
else
return 0;
- } else if (InputDieEntry->getTag() == dwarf::DW_TAG_compile_unit &&
- AttrSpec.Attr == dwarf::DW_AT_high_pc) {
+ } else if (isUnitRootDIE() && AttrSpec.Attr == dwarf::DW_AT_high_pc) {
if (uint64_t HighPc = OutUnit.getAsCompileUnit()->getHighPc())
Addr = HighPc;
else
diff --git a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.h b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.h
index 2a5a2250cc5b2..dd1be316aae42 100644
--- a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.h
+++ b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.h
@@ -140,6 +140,15 @@ class DIEAttributeCloner {
/// is constrained as well.
uint64_t constrainHighPC(uint64_t HighPC, bool IsLength);
+ /// Returns true if the DIE being cloned is the unit root, which owns the
+ /// attributes that bound the unit and so are replaced with the extents the
+ /// linker kept. Index 0 is that root whatever its tag: DWARFv5 section 3.1.1
+ /// gives a full or a partial compilation unit entry the same address range
+ /// attributes, encoding the ranges generated for that unit, and
+ /// DWARFContext::compile_units() hands both kinds to the linker on the same
+ /// path.
+ bool isUnitRootDIE() const { return InputDIEIdx == 0; }
+
/// Returns true if attribute should be skipped.
bool
shouldSkipAttribute(DWARFAbbreviationDeclaration::AttributeSpec AttrSpec);
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
index 4138a9fd69aca..315fb82f24b35 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
@@ -817,10 +817,10 @@ void CompileUnit::cloneAndEmitRangeList(DebugSectionKind RngSectionKind,
std::optional<AddressRangeValuePair> CachedRange;
uint64_t OffsetAfterUnitLength = emitRangeListHeader(OutRangeSection);
- DebugRangePatch *CompileUnitRangePtr = nullptr;
+ DebugRangePatch *UnitRangePtr = nullptr;
DebugInfoSection.ListDebugRangePatch.forEach([&](DebugRangePatch &Patch) {
- if (Patch.IsCompileUnitRanges) {
- CompileUnitRangePtr = &Patch;
+ if (Patch.IsUnitRanges) {
+ UnitRangePtr = &Patch;
} else {
// Get ranges from the source DWARF corresponding to the current
// attribute.
@@ -859,10 +859,10 @@ void CompileUnit::cloneAndEmitRangeList(DebugSectionKind RngSectionKind,
}
});
- if (CompileUnitRangePtr != nullptr) {
+ if (UnitRangePtr != nullptr) {
// Emit compile unit ranges last to be binary compatible with classic
// dsymutil.
- DebugInfoSection.apply(CompileUnitRangePtr->PatchOffset,
+ DebugInfoSection.apply(UnitRangePtr->PatchOffset,
dwarf::DW_FORM_sec_offset,
OutRangeSection.OS.tell());
emitRangeListFragment(LinkedFunctionRanges, OutRangeSection);
diff --git a/llvm/lib/DWARFLinker/Parallel/OutputSections.h b/llvm/lib/DWARFLinker/Parallel/OutputSections.h
index 10488f8e9f2b6..e7c121d2ed3e3 100644
--- a/llvm/lib/DWARFLinker/Parallel/OutputSections.h
+++ b/llvm/lib/DWARFLinker/Parallel/OutputSections.h
@@ -56,8 +56,8 @@ struct DebugLineStrPatch : SectionPatch {
/// This structure is used to update range list offset into
/// .debug_ranges/.debug_rnglists.
struct DebugRangePatch : SectionPatch {
- /// Indicates patch which points to immediate compile unit's attribute.
- bool IsCompileUnitRanges = false;
+ /// Indicates patch which points to the unit root's attribute.
+ bool IsUnitRanges = false;
};
/// This structure is used to update location list offset into
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-pc-range.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-pc-range.test
new file mode 100644
index 0000000000000..a6688b3acc28d
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-pc-range.test
@@ -0,0 +1,206 @@
+## A unit root's DW_AT_low_pc/DW_AT_high_pc bound the unit, so the linker has to
+## replace them with the range it actually linked rather than copy the input
+## values through. Both backends keyed that on the root being a
+## DW_TAG_compile_unit, which left a DW_TAG_partial_unit root - what dwz emits,
+## and what Fedora, RHEL and Debian debuginfo packaging therefore ships -
+## advertising a range that no longer contains its own subprograms.
+##
+## The input declares the root range as [0x1140, 0x1150), the gap between foo1
+## at [0x1130, 0x1140) and foo2 at [0x1150, 0x1160), so both halves of the
+## substitution are observable: low_pc has to move down to 0x1130 and high_pc
+## out to 0x1160. The declared range stays inside .text because llvm-dwarfutil
+## decides whether a unit has anything worth linking from the root DIE's own
+## ranges, and --tombstone=universal, the default, treats a root range outside
+## every executable section as dead. DW_AT_high_pc reaches a different code path
+## in each backend depending on whether it is encoded as an address or as a
+## length, so both encodings are linked here, and a DW_TAG_compile_unit root is
+## linked alongside as the control.
+
+## DW_TAG_compile_unit root, DW_AT_high_pc as an address.
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -DHIGHPCFORM=DW_FORM_addr -DHIGHPC=0x1150 -DFOO1LOW=0x1130 -DFOO2LOW=0x1150 -o %t.cu-addr.o
+# RUN: llvm-dwarfutil %t.cu-addr.o %t.cu-addr.classic
+# RUN: llvm-dwarfdump --debug-info %t.cu-addr.classic | FileCheck %s --check-prefixes=CHECK,CU
+# RUN: llvm-dwarfdump --verify %t.cu-addr.classic
+# RUN: llvm-dwarfutil --linker parallel %t.cu-addr.o %t.cu-addr.parallel
+# RUN: llvm-dwarfdump --debug-info %t.cu-addr.parallel | FileCheck %s --check-prefixes=CHECK,CU
+# RUN: llvm-dwarfdump --verify %t.cu-addr.parallel
+
+## DW_TAG_compile_unit root, DW_AT_high_pc as a length.
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -DHIGHPCFORM=DW_FORM_data8 -DHIGHPC=0x10 -DFOO1LOW=0x1130 -DFOO2LOW=0x1150 -o %t.cu-len.o
+# RUN: llvm-dwarfutil %t.cu-len.o %t.cu-len.classic
+# RUN: llvm-dwarfdump --debug-info %t.cu-len.classic | FileCheck %s --check-prefixes=CHECK,CU
+# RUN: llvm-dwarfdump --verify %t.cu-len.classic
+# RUN: llvm-dwarfutil --linker parallel %t.cu-len.o %t.cu-len.parallel
+# RUN: llvm-dwarfdump --debug-info %t.cu-len.parallel | FileCheck %s --check-prefixes=CHECK,CU
+# RUN: llvm-dwarfdump --verify %t.cu-len.parallel
+
+## DW_TAG_partial_unit root, DW_AT_high_pc as an address.
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -DHIGHPCFORM=DW_FORM_addr -DHIGHPC=0x1150 -DFOO1LOW=0x1130 -DFOO2LOW=0x1150 -o %t.pu-addr.o
+# RUN: llvm-dwarfutil %t.pu-addr.o %t.pu-addr.classic
+# RUN: llvm-dwarfdump --debug-info %t.pu-addr.classic | FileCheck %s --check-prefixes=CHECK,PU
+# RUN: not llvm-dwarfdump --verify %t.pu-addr.classic | FileCheck %s --check-prefix=VERIFY
+# RUN: llvm-dwarfutil --linker parallel %t.pu-addr.o %t.pu-addr.parallel
+# RUN: llvm-dwarfdump --debug-info %t.pu-addr.parallel | FileCheck %s --check-prefixes=CHECK,PU
+# RUN: not llvm-dwarfdump --verify %t.pu-addr.parallel | FileCheck %s --check-prefix=VERIFY
+
+## DW_TAG_partial_unit root, DW_AT_high_pc as a length.
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -DHIGHPCFORM=DW_FORM_data8 -DHIGHPC=0x10 -DFOO1LOW=0x1130 -DFOO2LOW=0x1150 -o %t.pu-len.o
+# RUN: llvm-dwarfutil %t.pu-len.o %t.pu-len.classic
+# RUN: llvm-dwarfdump --debug-info %t.pu-len.classic | FileCheck %s --check-prefixes=CHECK,PU
+# RUN: not llvm-dwarfdump --verify %t.pu-len.classic | FileCheck %s --check-prefix=VERIFY
+# RUN: llvm-dwarfutil --linker parallel %t.pu-len.o %t.pu-len.parallel
+# RUN: llvm-dwarfdump --debug-info %t.pu-len.parallel | FileCheck %s --check-prefixes=CHECK,PU
+# RUN: not llvm-dwarfdump --verify %t.pu-len.parallel | FileCheck %s --check-prefix=VERIFY
+
+## The same unit shapes with both functions tombstoned to the BFD dead value, so
+## the linker keeps the unit for its live variable but has no function extents
+## to substitute. Every run below is paired with a DW_TAG_compile_unit control,
+## which shows the behaviour a partial unit root now inherits rather than one
+## this change invents.
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -DHIGHPCFORM=DW_FORM_addr -DHIGHPC=0x1150 -DFOO1LOW=0x0 -DFOO2LOW=0x0 -o %t.drop-cu-addr.o
+# RUN: llvm-dwarfutil %t.drop-cu-addr.o %t.drop-cu-addr.classic
+# RUN: llvm-dwarfdump --debug-info %t.drop-cu-addr.classic | FileCheck %s --check-prefixes=CU,DROP
+# RUN: llvm-dwarfutil --linker parallel %t.drop-cu-addr.o %t.drop-cu-addr.parallel
+# RUN: llvm-dwarfdump --debug-info %t.drop-cu-addr.parallel | FileCheck %s --check-prefixes=CU,DROP
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -DHIGHPCFORM=DW_FORM_data8 -DHIGHPC=0x10 -DFOO1LOW=0x0 -DFOO2LOW=0x0 -o %t.drop-cu-len.o
+# RUN: llvm-dwarfutil %t.drop-cu-len.o %t.drop-cu-len.classic
+# RUN: llvm-dwarfdump --debug-info %t.drop-cu-len.classic | FileCheck %s --check-prefixes=CU,DROP
+# RUN: llvm-dwarfutil --linker parallel %t.drop-cu-len.o %t.drop-cu-len.parallel
+# RUN: llvm-dwarfdump --debug-info %t.drop-cu-len.parallel | FileCheck %s --check-prefixes=CU,DROP
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -DHIGHPCFORM=DW_FORM_addr -DHIGHPC=0x1150 -DFOO1LOW=0x0 -DFOO2LOW=0x0 -o %t.drop-pu-addr.o
+# RUN: llvm-dwarfutil %t.drop-pu-addr.o %t.drop-pu-addr.classic
+# RUN: llvm-dwarfdump --debug-info %t.drop-pu-addr.classic | FileCheck %s --check-prefixes=PU,DROP
+# RUN: llvm-dwarfutil --linker parallel %t.drop-pu-addr.o %t.drop-pu-addr.parallel
+# RUN: llvm-dwarfdump --debug-info %t.drop-pu-addr.parallel | FileCheck %s --check-prefixes=PU,DROP
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -DHIGHPCFORM=DW_FORM_data8 -DHIGHPC=0x10 -DFOO1LOW=0x0 -DFOO2LOW=0x0 -o %t.drop-pu-len.o
+# RUN: llvm-dwarfutil %t.drop-pu-len.o %t.drop-pu-len.classic
+# RUN: llvm-dwarfdump --debug-info %t.drop-pu-len.classic | FileCheck %s --check-prefixes=PU,DROP
+# RUN: llvm-dwarfutil --linker parallel %t.drop-pu-len.o %t.drop-pu-len.parallel
+# RUN: llvm-dwarfdump --debug-info %t.drop-pu-len.parallel | FileCheck %s --check-prefixes=PU,DROP
+
+## The root tag survives linking, and the unit range is the one the linker
+## produced: it starts at the first surviving function and ends past the last.
+# CU: DW_TAG_compile_unit
+# PU: DW_TAG_partial_unit
+# CHECK: DW_AT_producer ("by_hand")
+# CHECK-NEXT: DW_AT_name ("U1")
+# CHECK-NEXT: DW_AT_low_pc (0x0000000000001130)
+# CHECK-NEXT: DW_AT_high_pc (0x0000000000001160)
+
+## The subprograms keep the ranges they came in with. Only the unit root is
+## rewritten from the linked extents; a DW_AT_high_pc below the root is still
+## the function's own, so widening it to the unit range would be wrong.
+# CHECK: DW_TAG_subprogram
+# CHECK-NEXT: DW_AT_name ("foo1")
+# CHECK-NEXT: DW_AT_low_pc (0x0000000000001130)
+# CHECK-NEXT: DW_AT_high_pc (0x0000000000001140)
+# CHECK: DW_TAG_subprogram
+# CHECK-NEXT: DW_AT_name ("foo2")
+# CHECK-NEXT: DW_AT_low_pc (0x0000000000001150)
+# CHECK-NEXT: DW_AT_high_pc (0x0000000000001160)
+
+## The compile unit control above verifies clean. A partial unit does not yet,
+## and this pins what is left: the ranges no longer draw "DIE address ranges are
+## not contained in its parent's ranges", and the only remaining complaint is
+## that the unit header still says DW_UT_compile, which is a separate defect.
+## When that one is fixed these four runs become a plain llvm-dwarfdump
+## --verify with no expected errors at all.
+# VERIFY: error: Aggregated error counts:
+# VERIFY-NEXT: error: Mismatched unit type occurred 1 time(s).
+# VERIFY-NEXT: Errors detected.
+
+## With no surviving function the unit has no extents to advertise, so both
+## bounds are dropped rather than left at the unrelocated input values. The
+## DW_AT_name line is followed straight by DW_AT_str_offsets_base, which is what
+## pins the absence.
+# DROP: DW_AT_producer ("by_hand")
+# DROP-NEXT: DW_AT_name ("U1")
+# DROP-NEXT: DW_AT_str_offsets_base (0x00000008)
+# DROP: DW_TAG_variable
+# DROP-NEXT: DW_AT_name ("gvar")
+
+## A DWARFv5 unit with two functions in .text, whose root advertises the gap
+## between them. FOO1LOW and FOO2LOW select whether the functions are live or
+## tombstoned; gvar is a variable in .text that keeps the unit alive once they
+## are gone, since a unit with nothing live is dropped whole and asserts
+## nothing. 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: 0x30
+ - 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_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: [[HIGHPCFORM]]
+ - 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_variable
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_location
+ Form: DW_FORM_exprloc
+ debug_info:
+ - Version: 5
+ UnitType: [[UNITTYPE]]
+ Entries:
+ - AbbrCode: 1
+ Values:
+ - CStr: by_hand
+ - CStr: U1
+ - Value: 0x1140
+ - Value: [[HIGHPC]]
+ - Value: 0x8
+ - AbbrCode: 2
+ Values:
+ - CStr: foo1
+ - Value: [[FOO1LOW]]
+ - Value: 0x10
+ - AbbrCode: 2
+ Values:
+ - CStr: foo2
+ - Value: [[FOO2LOW]]
+ - Value: 0x10
+ - AbbrCode: 3
+ Values:
+ - CStr: gvar
+ - BlockData: [ 0x03, 0x40, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]
+ - AbbrCode: 0
+...
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-ranges.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-ranges.test
new file mode 100644
index 0000000000000..dd6e5392fef5c
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-ranges.test
@@ -0,0 +1,146 @@
+## DWARFv5 section 3.1.1 lets a unit root encode the addresses generated for
+## the unit either as a DW_AT_low_pc/DW_AT_high_pc pair or as DW_AT_ranges, so
+## the linker has to substitute the range it actually linked in both forms.
+## dwarf5-partial-unit-pc-range.test covers the pair; this covers DW_AT_ranges,
+## which both backends keyed on the root being a DW_TAG_compile_unit. A
+## DW_TAG_partial_unit root instead had its input ranges mapped through the
+## surviving function ranges like an ordinary DIE's, and since the input range
+## names no surviving function the result was "warning: inconsistent range
+## data." and an empty range list - a unit advertising no code at all while
+## still owning two subprograms.
+##
+## The input declares the root range as [0x1140, 0x1150), the gap between foo1
+## at [0x1130, 0x1140) and foo2 at [0x1150, 0x1160), so the substitution is
+## observable: the linked unit covers both functions and neither of the input
+## bounds survives. A DW_TAG_compile_unit root is linked alongside as the
+## control.
+##
+## The root carries DW_AT_addr_base because the linker re-encodes range lists
+## into .debug_addr-indexed entries but does not synthesize the attribute that
+## resolves those indices for a unit that came in without one - that is #218825,
+## and it is why the input supplies both DW_AT_addr_base and .debug_addr.
+
+## DW_TAG_compile_unit root.
+# 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=CHECK,CU
+# RUN: llvm-dwarfdump --verify %t.cu.classic
+# RUN: llvm-dwarfutil --linker parallel %t.cu.o %t.cu.parallel
+# RUN: llvm-dwarfdump --debug-info %t.cu.parallel | FileCheck %s --check-prefixes=CHECK,CU
+# RUN: llvm-dwarfdump --verify %t.cu.parallel
+
+## DW_TAG_partial_unit root.
+# 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=CHECK,PU
+# RUN: not llvm-dwarfdump --verify %t.pu.classic | FileCheck %s --check-prefix=VERIFY
+# RUN: llvm-dwarfutil --linker parallel %t.pu.o %t.pu.parallel
+# RUN: llvm-dwarfdump --debug-info %t.pu.parallel | FileCheck %s --check-prefixes=CHECK,PU
+# RUN: not llvm-dwarfdump --verify %t.pu.parallel | FileCheck %s --check-prefix=VERIFY
+
+## The root tag survives linking, and the unit range list is the one the linker
+## produced: one entry per surviving function, not the input's single entry.
+# CU: DW_TAG_compile_unit
+# PU: DW_TAG_partial_unit
+# CHECK: DW_AT_producer ("by_hand")
+# CHECK-NEXT: DW_AT_name ("U1")
+# CHECK-NEXT: DW_AT_ranges (0x0000000c
+# CHECK-NEXT: [0x0000000000001130, 0x0000000000001140)
+# CHECK-NEXT: [0x0000000000001150, 0x0000000000001160))
+
+## The subprograms keep the ranges they came in with. Only the unit root is
+## rewritten from the linked extents.
+# CHECK: DW_TAG_subprogram
+# CHECK-NEXT: DW_AT_name ("foo1")
+# CHECK-NEXT: DW_AT_low_pc (0x0000000000001130)
+# CHECK-NEXT: DW_AT_high_pc (0x0000000000001140)
+# CHECK: DW_TAG_subprogram
+# CHECK-NEXT: DW_AT_name ("foo2")
+# CHECK-NEXT: DW_AT_low_pc (0x0000000000001150)
+# CHECK-NEXT: DW_AT_high_pc (0x0000000000001160)
+
+## The compile unit control above verifies clean. A partial unit does not yet,
+## and this pins what is left: the ranges no longer draw "DIE address ranges are
+## not contained in its parent's ranges", and the only remaining complaint is
+## that the unit header still says DW_UT_compile, which is a separate defect.
+## When that one is fixed these two runs become a plain llvm-dwarfdump --verify
+## with no expected errors at all.
+# VERIFY: error: Aggregated error counts:
+# VERIFY-NEXT: error: Mismatched unit type occurred 1 time(s).
+# VERIFY-NEXT: Errors detected.
+
+## .debug_rnglists holds a v5 header followed by one DW_RLE_start_length entry
+## for [0x1140, 0x1150) and a DW_RLE_end_of_list, so the root's DW_AT_ranges at
+## offset 0xc names the gap between the two functions.
+--- !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: 0x30
+ - Name: .debug_str_offsets
+ Type: SHT_PROGBITS
+ Flags: [ ]
+ Content: "0400000005000000"
+ - Name: .debug_addr
+ Type: SHT_PROGBITS
+ Flags: [ ]
+ Content: "0c000000050008003011000000000000"
+ - Name: .debug_rnglists
+ Type: SHT_PROGBITS
+ Flags: [ ]
+ Content: "1300000005000800000000000740110000000000001000"
+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_ranges
+ Form: DW_FORM_sec_offset
+ - Attribute: DW_AT_str_offsets_base
+ Form: DW_FORM_sec_offset
+ - Attribute: DW_AT_addr_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
+ debug_info:
+ - Version: 5
+ UnitType: [[UNITTYPE]]
+ Entries:
+ - AbbrCode: 1
+ Values:
+ - CStr: by_hand
+ - CStr: U1
+ - Value: 0xc
+ - Value: 0x8
+ - Value: 0x8
+ - AbbrCode: 2
+ Values:
+ - CStr: foo1
+ - Value: 0x1130
+ - Value: 0x10
+ - AbbrCode: 2
+ Values:
+ - CStr: foo2
+ - Value: 0x1150
+ - Value: 0x10
+ - AbbrCode: 0
+...
More information about the llvm-commits
mailing list