llvm / llvm/llvm-project

[BOLT] BOLT does not relocate `DW_OP_addr` operands inside location lists (`.debug_loclists`)

Open
#210,990 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

When a variable is described by a **location list** (a list stored in `.debug_loclists` and referenced from the DIE via `DW_FORM_loclistx` or `DW_FORM_sec_offset`), BOLT translates the range endpoints of each entry but copies the DWARF **expression bytes verbatim**. A literal `DW_OP_addr` code operand inside such an expression (e.g. `DW_OP_addr , DW_OP_stack_value`) is therefore left pointing at the **pre-BOLT** address, i.e. into the dead `.bolt.org.text` copy instead of the live `.text`.

A debugger that evaluates the location then reports a **wrong / stale function pointer** as the variable's value.

> Note: the sibling case - a literal `DW_OP_addr` in an inline `DW_FORM_exprloc` attribute such as `DW_AT_call_value` - is tracked as a **separate** report ( https://github.com/llvm/llvm-project/issues/210339 ) because it lives in a different BOLT code path. This report covers only the `.debug_loclists` location-list case.

## Environment

- BOLT version: `bd6adfedc776c07caf158e59367d9c246c933510` (LLVM 23.0.0git) + the fix for #192084 (PR #192166, "Fix DW_FORM_implicit_const values lost during DWARF5 rewriting", merged 2026-07-08 as c7c7cab
- Target: `x86_64` (architecture-independent; the same DWARF is emitted on AArch64)
- Producer: `g++ (GCC) 14.2.0`
- Build flags: `-gdwarf-5 -O2 -gz=none -fno-reorder-blocks-and-partition -Wl,-q`

## Reproducer

`main.cpp` gives a function-pointer local two different constant values over its lifetime, which forces the compiler to describe it with a location list of `DW_OP_addr … DW_OP_stack_value` entries:

```cpp
volatile int g_sink;

__attribute__((noinline)) static void A() { g_sink += 1; }
__attribute__((noinline)) static void B() { g_sink += 2; }
__attribute__((noinline)) static void use(void (*f)()) { f(); }

int main(int argc, char **) {
void (*p)() = A; // p == &A over the first range
use(p);
g_sink += argc;
p = B; // p == &B over the second range
use(p);
return 0;
}
```

```bash
g++ -gdwarf-5 -O2 -gz=none -fno-reorder-blocks-and-partition -Wl,-q main.cpp -o main
llvm-bolt main -o main.bolt --update-debug-sections
```

## Analysis

```bash
llvm-dwarfdump --show-form --debug-info main | grep -A8 'DW_AT_name.*"p"'
DW_AT_name [DW_FORM_string] ("p")
DW_AT_decl_file [DW_FORM_data1] ("/workspace/main.cpp")
DW_AT_decl_line [DW_FORM_data1] (27)
DW_AT_decl_column [DW_FORM_data1] (10)
DW_AT_type [DW_FORM_ref4] (0x000000e8 "void (*)()")
DW_AT_location [DW_FORM_sec_offset] (0x00000032:
[0x0000000000401020, 0x000000000040103b): DW_OP_addr 0x401140, DW_OP_stack_value
[0x000000000040103b, 0x0000000000401049): DW_OP_addr 0x401150, DW_OP_stack_value)
DW_AT_GNU_locviews [DW_FORM_sec_offset] (0x0000002e)

readelf -sW main | grep -iE '_ZL1Av|_ZL1Bv|g_sink'
40: 0000000000401140 16 FUNC LOCAL DEFAULT 11 _ZL1Av
41: 0000000000401150 16 FUNC LOCAL DEFAULT 11 _ZL1Bv
72: 0000000000404014 4 OBJECT GLOBAL DEFAULT 30 g_sink
```

`DW_OP_stack_value` means the *value* of `p` over each range is the function address itself, so the `DW_OP_addr` operand is a genuine code address. The operand resolves to a **function**: in this build `0x401140` is `A()` (symbol `_ZL1Av`, a `FUNC` in an executable `AX` section) and `0x401150` is `B()` (`_ZL1Bv`). This is exactly why it must be relocated - it points at code that BOLT moves, unlike a data address (e.g. the global `g_sink` at `0x404014` in `.bss`, which BOLT correctly leaves unchanged).

After llvm-bolt usage:

```bash
llvm-dwarfdump --show-form --debug-info main.bolt | grep -A8 'DW_AT_name.*"p"'
DW_AT_name [DW_FORM_string] ("p")
DW_AT_decl_file [DW_FORM_data1] (0x01) # <-- other issue https://github.com/llvm/llvm-project/issues/209184
DW_AT_decl_line [DW_FORM_data1] (27)
DW_AT_decl_column [DW_FORM_data1] (10)
DW_AT_type [DW_FORM_ref4] (0x000000eb "void (*)()")
DW_AT_location [DW_FORM_loclistx] (indexed (0x1) loclist = 0x0000002d:
[0x000000000080001c, 0x0000000000800037): DW_OP_addr 0x401140, DW_OP_stack_value # <-- current issue
[0x0000000000800037, 0x0000000000800045): DW_OP_addr 0x401150, DW_OP_stack_value) # <-- current issue
DW_AT_GNU_locviews [DW_FORM_sec_offset] (0x0000002e)

readelf -sW main.bolt | grep -iE '_ZL1Av|_ZL1Bv|g_sink'
40: 0000000000800126 16 FUNC LOCAL DEFAULT 25 _ZL1Av
41: 0000000000800140 16 FUNC LOCAL DEFAULT 25 _ZL1Bv
72: 0000000000404014 4 OBJECT GLOBAL DEFAULT 24 g_sink
```

After BOLT the functions live at `0x800126` / `0x800140`, but the loclist `DW_OP_addr` still holds the pre-BOLT addresses, which now fall inside `.bolt.org.text` (dead copy).
> Note: https://github.com/llvm/llvm-project/issues/209184 covers problem visible under DW_AT_decl_file [DW_FORM_data1]

- **Observed:**
`[..): DW_OP_addr 0x401140, DW_OP_stack_value` / `0x401150` → stale, in `.bolt.org.text`
- **Expected:**
`[..): DW_OP_addr 0x800126, DW_OP_stack_value` / `0x800140` → the relocated addresses in `.text`

Per *DWARF Debugging Information Format, Version 5* (Feb 2017), §7.3, a DWARF expression containing a `DW_OP_addr` holds a location in the program's virtual address space and requires relocation - so BOLT should translate it in `.debug_loclists` entries too; leaving the pre-BOLT address is the defect.

### Root cause

In `bolt/lib/Rewrite/DWARFRewriter.cpp`, a `DW_AT_location` that uses a location list (form `DW_FORM_loclistx` or `DW_FORM_sec_offset`) is rewritten in `DWARFRewriter::updateUnitDebugInfo()` via `translateInputToOutputLocationList()`:

```cpp
static DebugLocationsVector
translateInputToOutputLocationList(const BinaryFunction &BF,
const DebugLocationsVector &InputLL) {
...
for (const DebugLocationEntry &Entry : InputLL) {
DebugAddressRangesVector OutRanges =
BF.translateInputToOutputRange({Entry.LowPC, Entry.HighPC});
...
llvm::transform(OutRanges, std::back_inserter(OutputLL),
[&Entry](const DebugAddressRange &R) {
return DebugLocationEntry{R.LowPC, R.HighPC, Entry.Expr,
Entry.View};
});
}
...
}
```

Each entry's range is translated via `translateInputToOutputRange()`, but the expression byte buffer (`DebugLocationEntry::Expr`) is passed through unchanged (here and in the subsequent merge loop) and later emitted by `DebugLocWriter::addList()`. Consequently a literal `DW_OP_addr` code operand inside the expression is never translated to its post-BOLT address.

## Proposed fix

```diff
--- a/bolt/lib/Rewrite/DWARFRewriter.cpp
+++ b/bolt/lib/Rewrite/DWARFRewriter.cpp
@@ void DWARFRewriter::updateUnitDebugInfo(
}
};
+ // Translate DW_OP_addr code operands inside a location-list entry's DWARF
+ // expression (a raw byte buffer for .debug_loclists). Each DW_OP_addr operand
+ // that resolves to a function is rewritten to its output address; data
+ // addresses are left unchanged. DW_OP_addr is address-sized, so the operand
+ // bytes are overwritten in place and the expression length is unchanged.
+ auto relocateLocExprAddresses = [&](SmallVectorImpl &ExprBytes) {
+ if (ExprBytes.empty())
+ return;
+ const bool IsLittleEndian = Unit.getContext().isLittleEndian();
+ const unsigned AddrSize = Unit.getAddressByteSize();
+ // Parse from a scratch copy so mutating ExprBytes cannot alias the bytes
+ // the expression iterator is still reading.
+ SmallVector Scratch(ExprBytes.begin(), ExprBytes.end());
+ DataExtractor Data(Scratch, IsLittleEndian);
+ DWARFExpression Expr(Data, AddrSize, Unit.getFormParams().Format);
+ for (const DWARFExpression::Operation &Op : Expr) {
+ if (Op.getCode() != dwarf::DW_OP_addr)
+ continue;
+ const BinaryFunction *Function =
+ BC.getBinaryFunctionContainingAddress(Op.getRawOperand(0));
+ if (!Function) // data address -> leave unchanged
+ continue;
+ const uint64_t Address =
+ Function->translateInputToOutputAddress(Op.getRawOperand(0));
+ const unsigned Start = Op.getOperandEndOffset(0) - AddrSize;
+ for (unsigned I = 0; I < AddrSize; ++I) {
+ const unsigned Shift = IsLittleEndian ? I : (AddrSize - 1 - I);
+ ExprBytes[Start + I] =
+ static_cast((Address >> (8 * Shift)) & 0xffULL);
+ }
+ }
+ };
+
for (const std::unique_ptr &DI : DIs) {
DIE *Die = DI->Die;
switch (Die->getTag()) {
case dwarf::DW_TAG_compile_unit:

.
.
.
} else {
// It's possible for a subprogram to be removed and to have
// address of 0. Adding this entry to output to preserve debug
// information.
OutputLL = InputLL;
}
+ // Translate DW_OP_addr code operands embedded in each location-list
+ // entry's expression (e.g. DW_OP_addr , DW_OP_stack_value).
+ for (DebugLocationEntry &Entry : OutputLL)
+ relocateLocExprAddresses(Entry.Expr);
DebugLocWriter.addList(DIEBldr, *Die, LocAttrInfo, OutputLL,
HasGNULocViews);
```

Contributor guide

Open the contributing guide

Research direction

Start in bolt/lib/Rewrite/DWARFRewriter.cpp at translateInputToOutputLocationList() and follow the expression through DebugLocWriter::addList(). Build the provided main.cpp reproducer, run llvm-bolt --update-debug-sections, and inspect the location list with llvm-dwarfdump; done means DW_OP_addr operands for relocated functions show their post-BOLT addresses while data addresses remain unchanged.

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
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.