llvm / llvm/llvm-project

[BOLT] `--update-debug-sections`: an absolute address in a `DW_AT_high_pc` length field, and invented `[0x0, 0x0)` scope ranges

Open
#217,966 1 comment 0 reactions 0 assignees View on GitHub
BOLT
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

## Description

`llvm-bolt --update-debug-sections` writes a wrong address range for a scope DIE (`DW_TAG_lexical_block`, `DW_TAG_inlined_subroutine`, `DW_TAG_try_block`, `DW_TAG_catch_block`) in two distinct ways. The first is reported by `llvm-dwarfdump --verify` as `error: DIE address ranges are not contained by parent ranges occurred 1 time(s).`; the second passes silently through `llvm-dwarfdump` and `readelf`. Both live in the same `if`/`else` chain, and each fix is one or two lines.

### Main defect

For the `DW_TAG_inlined_subroutine` DIE of the reproducer, BOLT sets `DW_AT_low_pc` to `0`, as the deleted-code convention intends, but leaves the input **end address** in `DW_AT_high_pc`. In a class-constant form (`DW_FORM_data4` under clang, `DW_FORM_data8` under GCC) that field is a length, not an end address, so the DIE ends up claiming a multi-kilobyte range at address zero.

### Secondary defect

For two `DW_TAG_lexical_block` DIEs of the reproducer, BOLT **creates** `DW_AT_low_pc`/`DW_AT_high_pc` as `[0x0, 0x0)`, although the input DIEs carry no address attribute at all. No tool reports it; it was found while reproducing the main defect.

They are reported together because they are **five lines apart in adjacent branches of the same `else` chain** - `DWARFRewriter::updateUnitDebugInfo()`, the `case dwarf::DW_TAG_lexical_block:` block and its fall-through labels, lines 1289-1298 of `bd6adfe` - and because the same two sentences of the standard decide both.

## DWARF 5 conformance

### Main defect

- **2.17.2 Contiguous Address Range**: *"if it is of class constant, the value is an unsigned integer offset which when added to the low PC gives the address of the first location past the last instruction associated with the entity."* `DW_FORM_data8` and `DW_FORM_data4` are both class constant (see **7.5.5 Classes and Forms**), so the stored value must be an offset; BOLT stores an absolute input address.

### Secondary defect

- **2.17 Code Addresses, Ranges and Base Addresses**: *"If an entity has no associated machine code, none of these attributes are specified."* §3.5 says the same for lexical blocks in permissive form ("**may** have either a `DW_AT_low_pc` and `DW_AT_high_pc` pair … or a `DW_AT_ranges` attribute").
- **3.3.8.1 Abstract Instances**: *"A debugging information entry that is a member of an abstract instance tree **may not** contain any attributes which describe aspects of the subroutine which vary between distinct inlined expansions or distinct out-of-line expansions. For example, the `DW_AT_low_pc`, `DW_AT_high_pc`, `DW_AT_ranges`, `DW_AT_entry_pc`, `DW_AT_location` … attributes typically should be omitted".*

## Environment

- **BOLT:** `llvm-bolt` built from LLVM `bd6adfedc776c07caf158e59367d9c246c933510`
- **Target arch:** x86-64
- **Compilers:** `clang++ 23.0.0git`, built from the same LLVM tree, or `g++ (GCC) 14.2.0`
- **`llvm-dwarfdump`:** built from the same LLVM tree.

## Reproducer

```cpp
// main.cpp
volatile int sink;

__attribute__((noinline)) int opaque(int v) { return v * 3 + 1; }
__attribute__((noinline)) int other(int v) { return v * 5 + 2; }

// Secondary defect: `k` and `k2` need no storage of their own, so neither scope
// gets an address attribute in the abstract instance tree emitted for this body.
static inline __attribute__((always_inline)) int folded_scopes(int a) {
int t = opaque(a);
{
const int k = 11;
{
const int k2 = k * 2;
t += k2;
}
}
return t;
}

// Main defect: the whole inline instance is a single tail call, so it occupies
// one basic block of its own - the block SCTC erases when it patches the branch.
static inline __attribute__((always_inline)) int inlined_helper(int a) {
return other(a);
}

__attribute__((noinline)) int live_scopes(int x) { return folded_scopes(x); }

__attribute__((noinline)) int live_tail(int x) {
if (x > 7) {
if (x > 11)
return opaque(x);
return inlined_helper(x);
}
return x - 1;
}

int main(int argc, char **) {
sink = live_scopes(argc);
return live_tail(argc) & 1;
}
```

```bash
clang++ -gdwarf-5 -O3 -gz=none main.cpp -o main -Wl,-q
# g++ -gdwarf-5 -O3 -gz=none main.cpp -o main -Wl,-q # cross-check, same result

llvm-bolt main -o main.bolt --update-debug-sections
```

## Analysis

Every dump below is from the clang++ build. The addresses depend on the environment; what carries the argument is the arithmetic and the DIE offsets.

### Main defect - an absolute address ends up in a length field

```bash
llvm-dwarfdump --show-form --debug-info --name live_tail --show-children main
llvm-dwarfdump --show-form --debug-info --name live_tail --show-children main.bolt
```

In the `main` binary `DW_AT_high_pc` is the instance's 5-byte length, which is correct:

``` bash
0x00000123: DW_TAG_inlined_subroutine
DW_AT_abstract_origin [DW_FORM_ref4] (0x000000f8 "_ZL14inlined_helperi")
DW_AT_low_pc [DW_FORM_addrx] (0x000000000000115e)
DW_AT_high_pc [DW_FORM_data4] (0x00000005)
```

After `llvm-bolt` usage the `low_pc` is zeroed as the deleted-code convention intends, but the length field now holds the input **end** address, `0x115e + 0x5`:

```bash
0x0000012e: DW_TAG_inlined_subroutine
DW_AT_abstract_origin [DW_FORM_ref4] (0x00000103 "_ZL14inlined_helperi")
DW_AT_low_pc [DW_FORM_addrx] (0x0000000000000000)
DW_AT_high_pc [DW_FORM_data4] (0x00001163)
```

The five-byte instance now spans `[0x0, 0x1163)` - 4451 bytes starting at address zero, over a region that in the output holds the ELF headers and the archived copy of the input code. Its parent `live_tail` was translated correctly, to `[0x40011a, 0x40012b)` in the new `.text`, so the child is no longer inside it - the containment check `--verify` performs is between exactly these two ranges:

```bash
llvm-dwarfdump --verify main.bolt

error: DIE address ranges are not contained in its parent's ranges:
0x00000115: DW_TAG_subprogram [2] * (0x0000000c)
DW_AT_low_pc [DW_FORM_addrx] (indexed (00000006) address = 0x000000000040011a)
DW_AT_high_pc [DW_FORM_data4] (0x00000011)
DW_AT_name [DW_FORM_strx1] (indexed (00000014) string = "live_tail")
0x0000012e: DW_TAG_inlined_subroutine [18] (0x00000115)
DW_AT_abstract_origin [DW_FORM_ref4] (cu + 0x0103 => {0x00000103} "_ZL14inlined_helperi")
DW_AT_low_pc [DW_FORM_addrx] (indexed (00000001) address = 0x0000000000000000)
DW_AT_high_pc [DW_FORM_data4] (0x00001163)
```

> NOTE 1: The input is not error-free, but it fails only on two unrelated string offsets (two here; the count depends on the build path); the output has those same two plus exactly one new error:

```bash
llvm-dwarfdump --verify main | grep occurred

error: Section contribution contains invalid string offset occurred 2 time(s).

llvm-dwarfdump --verify main.bolt | grep occurred
error: DIE address ranges are not contained by parent ranges occurred 1 time(s).
error: Section contribution contains invalid string offset occurred 2 time(s).
```

> NOTE 2: The GCC build behaves the same way with `DW_FORM_data8` in place of `DW_FORM_data4`

SCTC erases the instance's basic block, so `translateInputToOutputRanges()` returns an empty vector and the DIE takes the `else if (OutputRanges.empty())` branch in `bolt/lib/Rewrite/DWARFRewriter.cpp`, which pushes the input end address into a field that is then written as a length:

```cpp
// bolt/lib/Rewrite/DWARFRewriter.cpp:1139
const uint32_t Size = HighPC - LowPC;
```

With `LowPC` forced to `0`, `Size` is the input end address - `0x1163` here. Before that branch was changed in #73464, the pair kept both input values, so `Size` came out as a real length - hence the follow-up below.

### Secondary defect - a scope with no machine code is given `[0x0, 0x0)`

No tool reports this one: the attributes are well-formed, an empty range is contained in anything, and the enclosing DIE is an abstract instance root that has no range of its own to check against. The evidence is therefore the input/output `llvm-bolt` diff below, read against DWARF 5 §2.17 and §3.3.8.1.

The two nested scopes of `folded_scopes` are emitted inside its **abstract instance tree** - the `DW_TAG_subprogram` at `0x5d` carries `DW_AT_inline` and no address, and these are its children - with **no address attributes at all**:

```bash
llvm-dwarfdump --debug-info --name folded_scopes --show-children main
llvm-dwarfdump --debug-info --name folded_scopes --show-children main.bolt
```

Input, abbreviated to the relevant attributes before `llvm-bolt` usage:

```bash
0x0000005d: DW_TAG_subprogram
DW_AT_name ("folded_scopes")
DW_AT_inline (DW_INL_inlined)

0x00000076: DW_TAG_lexical_block <- no address attributes
0x00000077: DW_TAG_variable
DW_AT_name ("k")

0x0000007f: DW_TAG_lexical_block <- no address attributes
0x00000080: DW_TAG_variable
DW_AT_name ("k2")
```

After `llvm-bolt` usage both blocks have acquired an address range:

```bash
0x00000077: DW_TAG_lexical_block
DW_AT_low_pc (0x0000000000000000)
DW_AT_high_pc (0x0000000000000000)
0x0000007d: DW_TAG_variable
DW_AT_name ("k")

0x00000085: DW_TAG_lexical_block
DW_AT_low_pc (0x0000000000000000)
DW_AT_high_pc (0x0000000000000000)
0x0000008b: DW_TAG_variable
DW_AT_name ("k2")
```

In `bolt/lib/Rewrite/DWARFRewriter.cpp` the `getDIEAddressRanges()` succeeds with an **empty** vector, and an empty vector lands in the same `else` branch as "this DIE has a range but no known function":

```cpp
// bolt/lib/Rewrite/DWARFRewriter.cpp:1292-1298
} else if (!RangesOrError) {
consumeError(RangesOrError.takeError());
} else {
OutputRanges.push_back({0, !RangesOrError->empty()
? RangesOrError.get().front().HighPC
: 0}); // => {0, 0}
}
```

`updateLowPCHighPC()` then *creates* attributes the DIE never had, because its branches are selected on the presence of an old value rather than on whether the DIE should carry one at all, see lines 1158-1167.

## Proposed fix

```diff
--- a/bolt/lib/Rewrite/DWARFRewriter.cpp
+++ b/bolt/lib/Rewrite/DWARFRewriter.cpp
@@ -1287,14 +1287,18 @@
std::move(OutputRanges), CachedRanges);
OutputRanges.clear();
} else if (OutputRanges.empty()) {
- OutputRanges.push_back({0, RangesOrError.get().front().HighPC});
+ // Deleted code: an empty scope at 0, not a range whose length is the
+ // input end address.
+ OutputRanges.push_back({0, 0});
}
} else if (!RangesOrError) {
consumeError(RangesOrError.takeError());
} else {
- OutputRanges.push_back({0, !RangesOrError->empty()
- ? RangesOrError.get().front().HighPC
- : 0});
+ // No address information at all is legal DWARF for a scope without
+ // machine code (DWARF 5 sections 2.17 and 3.3.8.1); do not invent any.
+ if (RangesOrError->empty())
+ break;
+ OutputRanges.push_back({0, RangesOrError.get().front().HighPC});
}
DIEValue LowPCVal = Die->findAttribute(dwarf::DW_AT_low_pc);
DIEValue HighPCVal = Die->findAttribute(dwarf::DW_AT_high_pc);
```

It fix both described defects. The `break` leaves the `switch` for this DIE only, and the ternary is dead once the empty case returns early.

## Follow-up

Follow-up fix for #73464 (it's regarding main defect reported under this ticket but secondary as noted above could be easily included) — cc @maksfb @ayermolo

Contributor guide

Open the contributing guide

Research direction

Start in bolt/lib/Rewrite/DWARFRewriter.cpp, especially updateUnitDebugInfo() around lines 1289-1298 and updateLowPCHighPC() around lines 1139 and 1158-1167. Build the provided main.cpp reproducer with DWARF 5, run llvm-bolt --update-debug-sections, and inspect the input/output with llvm-dwarfdump; done means no invented ranges for addressless scopes and no invalid DW_AT_high_pc length or parent-range verification error.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 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.