llvm / llvm/llvm-project

[BOLT] `--update-debug-sections` leaves `DW_AT_entry_pc` invalid, pointing into code that never runs

Open
#218,361 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

BOLT relocates a scope DIE's address ranges but never touches `DW_AT_entry_pc` on the same DIE. Its value stays as the input had it, which after the rewrite is an address in `.bolt.org.text` - the archived copy of the code that no longer executes. The DIE then contradicts itself, declaring an entry address that lies outside every range the same DIE declares, and dropping the attribute does not recover the entry either (see the conformance section below). Affected DIEs are any scope carrying the attribute - `DW_TAG_inlined_subroutine`, `DW_TAG_lexical_block`, `DW_TAG_subprogram`.

> NOTE No tool reports this - `llvm-dwarfdump --verify` does not check entry addresses, `readelf -w` says nothing, and `llvm-bolt` prints no diagnostic. GDB does: it discards an entry address that falls outside the block's ranges, so the value is not merely stale, it is lost. (see example below)

## DWARF 5 conformance

**DWARF 5 §2.18 (*Entry Address*)**:

> Any debugging information entry describing an entity that has a range of code addresses, which includes compilation units, module initialization, subroutines, lexical blocks, try/catch blocks, and the like, may have a `DW_AT_entry_pc` attribute to indicate the entry address which is the address of the instruction where execution begins within that range of addresses.
>
> If no `DW_AT_entry_pc` attribute is present, then the entry address is assumed to be the same as the base address of the containing scope.

The rewrite breaks the first sentence: the address of the instruction where execution begins is required to be *within that range of addresses*, and after `--update-debug-sections` both instances declare their ranges in `.text` and their entry address in `.bolt.org.text` - in none of those ranges and in a different section. §2.17 (*Code Addresses, Ranges and Base Addresses*) leaves no room for reading the two as consistent.

Deleting the attribute would not repair it either. These DIEs carry `DW_AT_ranges` and no `DW_AT_low_pc`, so §2.18's fallback resolves to the start of their first range - which is not the entry whenever a scope is entered anywhere but at its lowest address, exactly the case the attribute exists for (see Analysis below). So neither reading of the output names the entry address.

## Environment

* **BOLT:** `llvm-bolt` built from LLVM `bd6adfedc776c07caf158e59367d9c246c933510`
* **Target arch:** x86-64 and AArch64.
* **Compiler:** `g++ (GCC) 14.2.0`
* **`llvm-dwarfdump`:** built from the same tree.
* **`GNU gdb 16.3`**

## Reproducer
```cpp
volatile int sink;

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

static inline __attribute__((always_inline)) int inner(int a) {
int t = other(a);
if (__builtin_expect(t > 100, 0))
sink = t;
return t + 1;
}

__attribute__((noinline)) int live(int x) {
int s = 0;
for (int i = 0; i < x; ++i)
s += inner(s) + inner(i);
return s;
}

int main(int argc, char **) {
sink = live(argc);
return 0;
}
```

```bash
g++ -gdwarf-5 -O3 -gz=none main.cpp -o main -Wl,-q
llvm-bolt main -o main.bolt --update-debug-sections # no warning of any kind

llvm-dwarfdump --verify main.bolt # says nothing about the entry address
```

> NOTE `-Wl,-q` puts BOLT in relocation mode, which is what makes the defect provable from the output: the code moves to a new section, so the untranslated entry address ends up outside the ranges of its own DIE and the contradiction is visible.

## Analysis

**A** and **B** label the two expansions, in that order. Their code is interleaved in the loop body of `live`: B's own code starts at `0x40114c` with an instruction of A sitting in the middle of it, while control enters B five bytes higher, at `0x401151` - which is why B states its entry address explicitly instead of leaving it to the range list.

``` bash
llvm-dwarfdump --show-form --debug-info main main.bolt | grep -A8 DW_TAG_inlined_subroutine
```

Before the rewrite each instance's entry agrees with that instance's own code:

``` bash
# DW_AT_call_file / DW_AT_call_line / DW_AT_call_column omitted from both DIEs due to -A8 grep
0x0000010f: DW_TAG_inlined_subroutine <- A
DW_AT_abstract_origin [DW_FORM_ref4] (0x000001a9 "inner")
DW_AT_entry_pc [DW_FORM_addr] (0x0000000000401140)
DW_AT_GNU_entry_view [DW_FORM_data1] (0x01)
DW_AT_ranges [DW_FORM_sec_offset] (0x00000022
[0x0000000000401140, 0x000000000040114c)
[0x000000000040114e, 0x0000000000401151)
[0x0000000000401180, 0x0000000000401190))

0x0000015d: DW_TAG_inlined_subroutine <- B
DW_AT_abstract_origin [DW_FORM_ref4] (0x000001a9 "inner")
DW_AT_entry_pc [DW_FORM_addr] (0x0000000000401151)
DW_AT_GNU_entry_view [DW_FORM_data1] (0x01)
DW_AT_ranges [DW_FORM_sec_offset] (0x00000035
[0x000000000040114c, 0x000000000040114e)
[0x0000000000401151, 0x000000000040115b)
[0x0000000000401170, 0x0000000000401180))
```

After, every range has moved into `.text` and neither entry address has moved at all:

```bash
# same three attributes omitted
0x00000109: DW_TAG_inlined_subroutine <- A
DW_AT_abstract_origin [DW_FORM_ref4] (0x0000018b "inner")
DW_AT_entry_pc [DW_FORM_addr] (0x0000000000401140) <- unchanged
DW_AT_GNU_entry_view [DW_FORM_data1] (0x01)
DW_AT_ranges [DW_FORM_rnglistx] (indexed (0x2) rangelist = 0x00000043
[0x000000000080014b, 0x0000000000800157)
[0x0000000000800159, 0x000000000080015c)
[0x000000000080017f, 0x0000000000800187)) <- relocated

0x0000014b: DW_TAG_inlined_subroutine <- B
DW_AT_abstract_origin [DW_FORM_ref4] (0x0000018b "inner")
DW_AT_entry_pc [DW_FORM_addr] (0x0000000000401151) <- unchanged
DW_AT_GNU_entry_view [DW_FORM_data1] (0x01)
DW_AT_ranges [DW_FORM_rnglistx] (indexed (0x3) rangelist = 0x0000004f
[0x0000000000800157, 0x0000000000800159)
[0x000000000080015c, 0x0000000000800166)
[0x0000000000800177, 0x000000000080017f)) <- relocated
```

Both entry addresses now sit in `.bolt.org.text`, the archived copy of the code that no longer executes, which spans `0x401020` to `0x401195` (`readelf -S main.bolt`), while every range above is in the new `.text` at `0x800000`. So a consumer resolves the entry of these inlined instances to code that never runs - and trusting the attribute over the first range is exactly what `DW_AT_entry_pc` is for.

Ignoring the attribute does not recover the entry either. §2.18's fallback lands on the entry for A by coincidence, its entry being the start of its own first range, but for B it yields `0x800157` while the instruction the producer named as the entry now sits at `0x80015c`, in B's second range - five bytes further on, with code belonging to A in between.

GDB reports it. Complaints are suppressed by default, which is why the limit has to be raised, and `maint expand-symtabs` is what forces GDB to parse the unit at all:

```bash
gdb -batch -q -ex 'set complaints 100' -ex 'maint expand-symtabs' main.bolt 2>&1 | grep entry_pc
```

``` bash
During symbol reading: in main.bolt, DIE 0x109, DW_AT_entry_pc (0x401140) outside block range (0x80014b -> 0x800187)
During symbol reading: in main.bolt, DIE 0x14b, DW_AT_entry_pc (0x401151) outside block range (0x800157 -> 0x80017f)
During symbol reading: in main.bolt, DIE 0xc, DW_AT_entry_pc (0x0) outside block range (0x80001c -> 0x80018c)
```
> NOTE The third line is unrelated to this report: the CU DIE carries no `DW_AT_entry_pc` at all, the value GDB prints is its `DW_AT_low_pc`, which GCC sets to `0x0` on a unit described by ranges, and the same complaint appears on `main` as well.

## Proposed fix - reuse the existing `patchPC`

`updateUnitDebugInfo()` already translates a single address exactly this way for `DW_AT_call_pc` and `DW_AT_call_return_pc`; the `patchPC` lambda that does it is merely scoped inside `case dwarf::DW_TAG_call_site:`. Moving it beside `updateLowPCHighPC` - it needs the same `Unit`, `BC`, `DIEBldr` and `AddressWriter`, all captured by `[&]` there - makes each new call site three lines:

```diff
--- a/bolt/lib/Rewrite/DWARFRewriter.cpp
+++ b/bolt/lib/Rewrite/DWARFRewriter.cpp
@@ -1166,6 +1166,18 @@
DIEBldr.addValue(Die, AttrHighPC, FormHighPC, DIEInteger(Size));
}
};

+ // Moved here from case dwarf::DW_TAG_call_site, unchanged apart from the
+ // guard below, so that the scope branches can use it as well.
+ auto patchPC = [&](DIE *Die, DIEValue &AttrVal, StringRef Entry) -> void {
+ std::optional Address = getAsAddress(Unit, AttrVal);
+ if (!Address) {
+ // Not an address form. DWARF 5 section 2.18 also allows DW_AT_entry_pc
+ // of class constant, an offset from the entity's base address, which
+ // this code does not recompute - report it instead of guessing.
+ errs() << "BOLT-ERROR: unsupported form for " << Entry << "\n";
+ return;
+ }
+ const BinaryFunction *Function =
+ BC.getBinaryFunctionContainingAddress(*Address);
+ uint64_t UpdatedAddress = *Address;
+ // ... rest of the body unchanged ...
+ };
+
for (const std::unique_ptr &DI : DIs) {
@@ -1244,6 +1256,9 @@ case dwarf::DW_TAG_subprogram:
DIEValue LowPCVal = Die->findAttribute(dwarf::DW_AT_low_pc);
DIEValue HighPCVal = Die->findAttribute(dwarf::DW_AT_high_pc);
+ DIEValue EntryPCVal = Die->findAttribute(dwarf::DW_AT_entry_pc);
+ if (EntryPCVal)
+ patchPC(Die, EntryPCVal, "DW_AT_entry_pc");
if (FunctionRanges.empty()) {
@@ -1299,6 +1314,9 @@ case dwarf::DW_TAG_lexical_block:
DIEValue LowPCVal = Die->findAttribute(dwarf::DW_AT_low_pc);
DIEValue HighPCVal = Die->findAttribute(dwarf::DW_AT_high_pc);
+ DIEValue EntryPCVal = Die->findAttribute(dwarf::DW_AT_entry_pc);
+ if (EntryPCVal)
+ patchPC(Die, EntryPCVal, "DW_AT_entry_pc");
if (OutputRanges.size() == 1) {
@@ -1310,21 +1328,6 @@
case dwarf::DW_TAG_call_site: {
- auto patchPC = [&](DIE *Die, DIEValue &AttrVal, StringRef Entry) -> void {
- ... moved to the top of the function, see the first hunk ...
- };
```

With this applied, `A` and `B` come out with `DW_AT_entry_pc` `0x80014b` and `0x80015c` - the starts of A's first and B's second output range, matching where the two entry instructions actually moved. Both GDB complaints about the inlined instances disappear.

Contributor guide

Open the contributing guide

Research direction

Start in bolt/lib/Rewrite/DWARFRewriter.cpp at updateUnitDebugInfo() and compare the existing call-site patchPC handling with scope handling. Reproduce the issue with the supplied C++ program and llvm-bolt --update-debug-sections, then inspect the output with llvm-dwarfdump and GDB. Done means relocated DW_AT_entry_pc values match their output ranges and the reported GDB complaints disappear.

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
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.