llvm / llvm/llvm-project

[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

Open
#219,392 0 comments 0 reactions 0 assignees View on GitHub
llvm-dwarfutil
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.