llvm / llvm/llvm-project

[llvm-dwarfutil] Missing DW_AT_addr_base on GCC DWARF5 CUs without .debug_addr: classic crashes, parallel emits invalid DWARF

Open
#218,825 0 comments 0 reactions 0 assignees View on GitHub
crash llvm-dwarfutil
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

## Summary

`llvm-dwarfutil` mishandles any GCC-produced DWARF v5 object whose compile unit has no `.debug_addr` / `DW_AT_addr_base` but does carry `DW_AT_ranges`. In practice this is every GCC binary compiled at `-O2` or higher with default debug settings.

The default `classic` linker crashes with `llvm_unreachable("Didn't find a DW_AT_addr_base in cloned DIE!")`. `--linker parallel` does not crash, but silently produces DWARF that `llvm-dwarfdump --verify` rejects. Both symptoms have the same cause.

## Reproducer

```c
// c.c
int main(void) { return 0; }
```

```console
$ gcc -g -O2 -o c c.c
$ llvm-dwarfutil c c.out
PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace and instructions to reproduce the bug.
#0 0x00007f40fb43cd19 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int)
...
#3 llvm::dwarf_linker::classic::DWARFLinker::DIECloner::emitDebugAddrSection(llvm::dwarf_linker::classic::CompileUnit&, unsigned short) const
#4 llvm::dwarf_linker::classic::DWARFLinker::DIECloner::cloneAllCompileUnits(llvm::DWARFContext&, llvm::dwarf_linker::DWARFFile const&, bool)
...
Illegal instruction (core dumped)
```

Exit status 132 (SIGILL — `llvm_unreachable` compiled to `ud2` in a release build).

`--linker parallel` on the same input exits 0 and produces a file that does not verify:

```console
$ llvm-dwarfutil --linker parallel c c.parallel
$ llvm-dwarfdump --verify c.parallel
error: DIE address ranges are not contained in its parent's ranges:
0x0000000c: DW_TAG_compile_unit
DW_AT_ranges [DW_FORM_sec_offset] (0x0000000c
[0x0000000000000000, 0x0000000000000003))
DW_AT_low_pc [DW_FORM_addr] (0x0000000000400360)
0x00000030: DW_TAG_subprogram
DW_AT_low_pc [DW_FORM_addr] (0x0000000000400360)
DW_AT_high_pc [DW_FORM_data8] (0x0000000000000003)
error: Aggregated error counts:
error: DIE address ranges are not contained by parent ranges occurred 1 time(s).
Errors detected.
```

## Conditions

| input | `classic` (default) | `--linker parallel` |
|---|---|---|
| `gcc -g` | OK | OK |
| `gcc -g -O1` | OK | OK |
| `gcc -g -O2` | **crash** | **invalid output** |
| `gcc -g -O2 -gdwarf-4` | OK | OK |
| `clang -g -O2` | OK | OK |

The crash happens equally on a `--only-keep-debug` extraction of the same binary.

## Analysis

The relevant difference is in the compile unit DIE:

```console
$ llvm-dwarfdump --debug-info c_g # gcc -g, works
0x00000000: Compile Unit: ... version = 0x0005, unit_type = DW_UT_compile
0x0000000c: DW_TAG_compile_unit
DW_AT_low_pc (0x0000000000400446)

$ llvm-dwarfdump --debug-info c_g_o2 # gcc -g -O2, crashes
0x00000000: Compile Unit: ... version = 0x0005, unit_type = DW_UT_compile
0x0000000c: DW_TAG_compile_unit
DW_AT_ranges (0x0000000c
DW_AT_low_pc (0x0000000000000000)
```

Neither CU has `DW_AT_addr_base` — GCC does not use `.debug_addr` here at all. But at `-O2` the CU gains `DW_AT_ranges`, and rewriting those ranges makes the linker's own `AddrPool` non-empty. `emitDebugAddrSection` therefore falls through its three early returns:

https://github.com/llvm/llvm-project/blob/3e8996717946a90a5373c6af465143ccaa0b842b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp#L2210-L2229

```cpp
if (DwarfVersion < 5)
return Error::success();

if (AddrPool.getValues().empty())
return Error::success();
...
patchAddrBase(*Unit.getOutputUnitDIE(), DIEInteger(AddrOffset));
```

and reaches `patchAddrBase`, which assumes the cloned CU DIE already has a `DW_AT_addr_base` to overwrite:

https://github.com/llvm/llvm-project/blob/3e8996717946a90a5373c6af465143ccaa0b842b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp#L2200-L2208

```cpp
static void patchAddrBase(DIE &Die, DIEInteger Offset) {
for (auto &V : Die.values())
if (V.getAttribute() == dwarf::DW_AT_addr_base) {
V = DIEValue(V.getAttribute(), V.getForm(), Offset);
return;
}

llvm_unreachable("Didn't find a DW_AT_addr_base in cloned DIE!");
}
```

That assumption holds for clang output (clang emits `DW_AT_addr_base` whenever it emits DWARF v5) but not for GCC's. The guard on `AddrPool` non-emptiness is not equivalent to "the input CU had an addr base".

`DWARF_VERSION < 5` is why `-gdwarf-4` escapes, and an empty `AddrPool` is why `-O0`/`-O1` escape.

`--linker parallel` reaches the same dead end from the other side. It rewrites the ranges into `DW_RLE_base_addressx` exactly as the classic linker does, emits the `.debug_addr` contribution, and then never emits a `DW_AT_addr_base` at all — its `DIEAttributeCloner` only patches one inherited from the input:

```console
$ llvm-dwarfdump --debug-info c.parallel | grep -c DW_AT_addr_base
0
$ llvm-dwarfdump --debug-addr c.parallel
Addrs: [
0x0000000000400360
]
$ llvm-dwarfdump --debug-rnglists --verbose c.parallel
0x0000000c: [DW_RLE_base_addressx]: 0x0000000000000000
```

DWARF v5 section 3.1.1 interprets `DW_RLE_base_addressx` indices relative to `DW_AT_addr_base`, so with no base the index resolves from 0 and the CU range is read as `[0x0, 0x3)` while `main` sits at `0x400360`. That is the containment error above. Both backends therefore share one root cause: the linker re-encodes absolute range entries into addrx-indexed ones without ensuring the output unit has an addr base to resolve them against. Classic asserts; parallel corrupts silently.

Of the two obvious fixes, only one holds up. Skipping `.debug_addr` emission for units with no addr base would leave the rewritten rnglists referencing a table that is not there, so the attribute has to be added to the cloned DIE when the input lacked one. Note, that it cannot be added at `patchAddrBase` time: by then the abbreviation has been generated and `computeNextUnitOffset()` has frozen the DIE offsets. It has to happen while the unit DIE is cloned, which is where the existing `DW_AT_str_offsets_base` synthesis already does the same thing.

Reaching an `llvm_unreachable` on perfectly valid input seems worth turning into a diagnostic regardless.

## Versions

- **Crashes**: 22.1.8 (Fedora 44, `llvm-dwarfutil` linked against `libLLVM.so.22.1`)
- **Does not crash**: 18.1.8

`patchAddrBase` and `emitDebugAddrSection` are byte-identical in 22.1.8, on `release/23.x` at 21cd7726af6f743cd33557fb5311babac83fab05, and on `main` at 3e8996717946a90a5373c6af465143ccaa0b842b, which the links above are pinned to. Line numbers differ between those two: `patchAddrBase` is at `DWARFLinker.cpp:2155` on `release/23.x` and at 2200 on `main`. The `llvm_unreachable` was introduced in fe48801feca0411f5ecfa37fe8802a2b3eece98f ("Emit a .debug_addr section with dsymutil", 2023-06-28).

Producer: gcc (GCC) 16.2.1 20260819 (Red Hat 16.2.1-2), x86_64, Fedora 44.

Contributor guide

Open the contributing guide

Research direction

Start in llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp, especially patchAddrBase and emitDebugAddrSection, then compare the parallel linker’s DIEAttributeCloner with the existing DW_AT_str_offsets_base synthesis. Reproduce with gcc -g -O2 and verify outputs using llvm-dwarfdump --verify. Done means both linkers avoid the crash and produce DWARF that verifies, including inputs without DW_AT_addr_base.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
compilers, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.