[llvm-dwarfutil] DW_TAG_partial_unit roots keep unrelocated DW_AT_low_pc/DW_AT_high_pc, producing ranges that do not contain their children
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
The classic and parallel DWARFLinker backends both substitute the linked unit range into `DW_AT_low_pc`/`DW_AT_high_pc` only when the unit root is a `DW_TAG_compile_unit`. A `DW_TAG_partial_unit` root keeps its input values verbatim, so the emitted unit range no longer covers the subprograms the linker relocated underneath it, and `llvm-dwarfdump --verify` rejects the output.
`DW_TAG_partial_unit` is what `dwz` leaves behind, and `dwz` is a standard step in distribution debuginfo packaging (Fedora and RHEL run it from `find-debuginfo`, Debian from `dh_dwz`), so this is not a hypothetical shape.
## Reproducer
The partial unit root below declares `DW_AT_low_pc = 0x1130`, `DW_AT_high_pc = 0x10`, so it spans `[0x1130, 0x1140)` while its second subprogram sits at `[0x1150, 0x1160)`, outside it.
```yaml
--- !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_addr
Type: SHT_PROGBITS
Flags: [ ]
Content: "0c000000050008003011000000000000"
DWARF:
debug_abbrev:
- 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_low_pc
Form: DW_FORM_addr
- Attribute: DW_AT_high_pc
Form: DW_FORM_data8
- 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
- Attribute: DW_AT_type
Form: DW_FORM_ref4
- Tag: DW_TAG_base_type
Children: DW_CHILDREN_no
Attributes:
- Attribute: DW_AT_name
Form: DW_FORM_string
debug_info:
- Version: 5
UnitType: DW_UT_partial
Entries:
- AbbrCode: 1
Values:
- CStr: by_hand
- CStr: PU1
- Value: 0x1130
- Value: 0x10
- Value: 0x8
- AbbrCode: 2
Values:
- CStr: foo1
- Value: 0x1130
- Value: 0x10
- Value: 0x61
- AbbrCode: 2
Values:
- CStr: foo2
- Value: 0x1150
- Value: 0x10
- Value: 0x61
- AbbrCode: 3
Values:
- CStr: int
- AbbrCode: 0
...
```
```console
$ yaml2obj partial.yaml -o partial.o
$ 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_producer [DW_FORM_strx] (indexed (00000000) string = "by_hand")
DW_AT_name [DW_FORM_strx] (indexed (00000001) string = "PU1")
DW_AT_low_pc [DW_FORM_addr] (0x0000000000001130)
DW_AT_high_pc [DW_FORM_data8] (0x0000000000000010)
DW_AT_addr_base [DW_FORM_sec_offset] (0x00000008)
DW_AT_str_offsets_base [DW_FORM_sec_offset] (0x00000008)
0x0000003d: 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)
DW_AT_type [DW_FORM_ref4] (cu + 0x0053 => {0x00000053} "int")
error: Aggregated error counts:
error: DIE address ranges are not contained by parent ranges occurred 1 time(s).
Errors detected.
```
Substituting `DW_TAG_compile_unit`/`DW_UT_compile` for `DW_TAG_partial_unit`/`DW_UT_partial` in the same input verifies clean, and the emitted root then carries `DW_AT_high_pc (0x0000000000001160)`, recomputed to cover both subprograms rather than the `0x10` that was declared. `--linker parallel` fails exactly as the classic backend does.
## Analysis
The classic backend gates the substitution on the root tag:
https://github.com/llvm/llvm-project/blob/a7831dcce98893f3514f889a827f0d38da46621a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp#L1496-L1512
```cpp
if (InputDIE.getTag() == dwarf::DW_TAG_compile_unit &&
AttrSpec.Attr == dwarf::DW_AT_low_pc) {
if (std::optional LowPC = Unit.getLowPc())
Addr = *LowPC;
else
return 0;
} else if (InputDIE.getTag() == dwarf::DW_TAG_compile_unit &&
AttrSpec.Attr == dwarf::DW_AT_high_pc) {
```
The parallel backend has the same gate:
https://github.com/llvm/llvm-project/blob/a7831dcce98893f3514f889a827f0d38da46621a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp#L700-L712
`Unit.getLowPc()` and `getHighPc()` accumulate the linked extents of every surviving function. A partial unit root falls into the `else` branch, which only adds `Info.PCOffset`, and that is 0 at the root because it is reassigned only on `DW_TAG_subprogram`. The root therefore keeps whatever the input declared while its children are relocated independently.
`DWARFContext::compile_units()` filters out only type units, so a partial unit root reaches this code on exactly the same path a full compilation unit does. The tag test is the only thing that distinguishes them.
## Expected behavior
The unit range is derived from the linked function ranges for any unit root, not only for `DW_TAG_compile_unit`.
## Versions
Reproduced at a7831dcce98893f3514f889a827f0d38da46621a on `main`, x86_64 Linux, on both the classic (default) and `--linker parallel` backends. `dsymutil` shares the same code.
This report was produced with AI assistance; I have reviewed it and am accountable for it.
Contributor guide
Research direction
Start in llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp around the root-range attribute handling, then compare the corresponding logic in llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp. Run the supplied yaml2obj, llvm-dwarfutil, and llvm-dwarfdump --verify reproducer with a partial unit. Done means both linker backends recompute partial-unit root ranges to contain relocated children and verification passes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100