llvm / llvm/llvm-project

[BOLT] BOLT does not relocate `DW_OP_addr` operands inside call-site value expressions

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

The `DW_OP_addr` operand embedded in an inline `DW_FORM_exprloc` attribute - most visibly a **call-site value expression** (`DW_AT_call_value`) — is left pointing at the **pre-BOLT** address, i.e. into the dead `.bolt.org.text` copy instead of the live `.text` during `llvm-bolt` usage with `--update-debug-sections` flag.

A debugger that evaluates `DW_AT_call_value` (the call-site parameter value expression) therefore obtains a **wrong / stale function pointer**.

> Note: the same class of bug also affects `DW_OP_addr` operands stored inside a `.debug_loclists` location list; that is tracked as a **separate** report because (https://github.com/llvm/llvm-project/issues/210990) it lives in a different BOLT code path. This report covers only the inline `DW_FORM_exprloc` case.

## Environment
- BOLT version: `bd6adfedc776c07caf158e59367d9c246c933510` (2026-06-30)
- 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`
```cpp
volatile void *g_sink;

struct T {
int x;
__attribute__((noinline)) T() { x = 1; g_sink = (void *)&x; }
__attribute__((noinline)) ~T() { g_sink = (void *)&x; }
};

T g_obj;

int main() { 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

The global object with a non-trivial destructor makes the compiler emit a static-initialization call `__cxa_atexit(&T::~T, &g_obj, &__dso_handle)` at the **code** level. The first argument is the **address of the destructor** (a code address). It is recorded in DWARF as `DW_AT_call_value (DW_OP_addr <&T::~T>)` on a `DW_TAG_call_site_parameter` child of `DW_TAG_call_site`.

Before BOLT — dump the call site:

```bash
llvm-dwarfdump --show-form --debug-info main | sed -n '/DW_TAG_call_site/,/NULL/p' | head -30
```

```text
0x0000010f: DW_TAG_call_site
DW_AT_call_return_pc [DW_FORM_addr] (0x0000000000401076)
DW_AT_call_tail_call [DW_FORM_flag_present] (true)

0x00000118: DW_TAG_call_site_parameter
DW_AT_location [DW_FORM_exprloc] (DW_OP_reg5 RDI)
DW_AT_call_value [DW_FORM_exprloc] (DW_OP_addr 0x401170) <- &T::~T

0x00000125: DW_TAG_call_site_parameter
DW_AT_location [DW_FORM_exprloc] (DW_OP_reg4 RSI)
DW_AT_call_value [DW_FORM_exprloc] (DW_OP_addr 0x404020) <- &g_obj

0x00000132: DW_TAG_call_site_parameter
DW_AT_location [DW_FORM_exprloc] (DW_OP_reg1 RDX) <- &__dso_handle
```

The bug is on the **first** `DW_AT_call_value`: `0x401170` is a code address that should be relocated after `llvm-bolt` usage. After `llvm-bolt` the destructor lives at `0x800166`, but `DW_AT_call_value` still holds `DW_OP_addr 0x401170`, which now falls inside `.bolt.org.text` (dead copy):

- **Observed (after BOLT):** `DW_AT_call_value (DW_OP_addr 0x401170)` → stale, inside `.bolt.org.text` `[0x401040, 0x40118e)`
- **Expected (after BOLT):** `DW_AT_call_value (DW_OP_addr 0x800166)` → the relocated destructor inside `.text` `[0x800000, 0x80018d)`

(Addresses are example values from this build; the exact numbers depend on the linker/BOLT layout.)

### DWARF spec context

The expected result is compatible with *DWARF Debugging Information Format, Version 5* (Feb 2017):

**`DW_OP_addr` encodes a machine address.** §2.5.1.1 *Literal Encodings*:
> "The `DW_OP_addr` operation has a single operand that encodes a machine address and whose size is the size of an address on the target machine."

§7.3 *Relocatable, Split, Executable, Shared, Package and Supplementary Object Files*:
> "A DWARF expression may contain a `DW_OP_addr` (see Section 2.5.1.1 …) which contains a location within the virtual address space of the program, and require relocation."

**`DW_AT_call_value` must yield the parameter value at call time.** §3.4.2 *Call Site Parameters*:
> "Each `DW_TAG_call_site_parameter` entry may have a `DW_AT_call_value` attribute which is a DWARF expression which when evaluated yields the value of the parameter at the time of the call."

When that value is a function pointer, the expression is `DW_OP_addr `, and the yielded value must be the address of the function as it exists in the final program.

## LLVM BOLT findings (root cause)

In `bolt/lib/Rewrite/DWARFRewriter.cpp`, `DWARFRewriter::updateUnitDebugInfo()`:

1. `case dwarf::DW_TAG_call_site:` only relocates `DW_AT_call_pc` and `DW_AT_call_return_pc`. Child `DW_TAG_call_site_parameter` DIEs are visited separately in the same loop but are not handled here; `DW_AT_call_value` (nor `DW_AT_call_data_value` / `DW_AT_call_target` / `DW_AT_call_target_clobbered`) is never relocated in this path.

2. `DW_TAG_call_site_parameter` has no dedicated `case`, so it falls into `default:`, which handles `DW_AT_location` (usually as a location list) and `DW_AT_low_pc` but **never looks at `DW_AT_call_value`**. That is the direct root cause for this reproducer.

## Proposed fix

Add a helper `relocateExprLocAddresses` that walks a `DW_FORM_exprloc` byte buffer and, for every `DW_OP_addr` operand whose address resolves to a `BinaryFunction`, overwrites the operand in place with the relocated address (`translateInputToOutputAddress`). Operands that do not resolve to a function (e.g. data addresses such as `&g_obj`) are left untouched. `DW_OP_addr` is a fixed, address-sized operand, so the expression length does not change. The helper is then invoked for the call-site address attributes (`DW_AT_call_value`, `DW_AT_call_data_value`, `DW_AT_call_data_location`, `DW_AT_call_target`, `DW_AT_call_target_clobbered`) and for an exprloc `DW_AT_location`.

```diff
--- a/bolt/lib/Rewrite/DWARFRewriter.cpp
+++ b/bolt/lib/Rewrite/DWARFRewriter.cpp
@@ void DWARFRewriter::updateUnitDebugInfo(
}
};

+ // Relocate a literal DW_OP_addr code operand inside a DW_FORM_exprloc so it
+ // follows BOLT's code relocation. Only operands resolving to a function are
+ // translated; data addresses are left as-is. DW_OP_addr is address-sized and
+ // stored one DW_FORM_data1 byte per DIEInteger, so it is overwritten in place.
+ auto relocateExprLocAddresses = [&](const DIEValue &AttrInfo) {
+ if (!AttrInfo || AttrInfo.getForm() != dwarf::DW_FORM_exprloc)
+ return;
+ DIELoc &ExprLoc = const_cast(AttrInfo.getDIELoc());
+ const bool IsLittleEndian = Unit.getContext().isLittleEndian();
+ const unsigned AddrSize = Unit.getAddressByteSize();
+
+ std::vector Bytes;
+ for (const DIEValue &Val : ExprLoc.values())
+ Bytes.push_back(Val.getDIEInteger().getValue());
+ DataExtractor Data(Bytes, 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));
+ // Overwrite the AddrSize operand bytes that follow the 1-byte opcode.
+ auto It = ExprLoc.values().begin();
+ std::advance(It, Op.getOperandEndOffset(0) - AddrSize);
+ for (unsigned I = 0; I < AddrSize; ++I, ++It) {
+ const unsigned Shift = IsLittleEndian ? I : (AddrSize - 1 - I);
+ *It = DIEValue(static_cast(0), dwarf::DW_FORM_data1,
+ DIEInteger((Address >> (8 * Shift)) & 0xffULL));
+ }
+ }
+ };
+
for (const std::unique_ptr &DI : DIs) {
DIE *Die = DI->Die;
switch (Die->getTag()) {
@@ case dwarf::DW_TAG_call_site: {
DIEValue CallRetPcAttrVal =
Die->findAttribute(dwarf::DW_AT_call_return_pc);
if (CallRetPcAttrVal)
patchPC(Die, CallRetPcAttrVal, "DW_AT_call_return_pc",
/*CheckPastEnd=*/true);

+ // Call target expressions may embed a DW_OP_addr code address that must
+ // follow BOLT's code relocation.
+ relocateExprLocAddresses(Die->findAttribute(dwarf::DW_AT_call_target));
+ relocateExprLocAddresses(
+ Die->findAttribute(dwarf::DW_AT_call_target_clobbered));
+
break;
}
default: {
+ // Relocate DW_OP_addr code operands in call-site value expressions and in
+ // an exprloc DW_AT_location (no-op unless such an operand is present).
+ relocateExprLocAddresses(Die->findAttribute(dwarf::DW_AT_call_value));
+ relocateExprLocAddresses(Die->findAttribute(dwarf::DW_AT_call_data_value));
+ relocateExprLocAddresses(Die->findAttribute(dwarf::DW_AT_call_data_location));
+ relocateExprLocAddresses(Die->findAttribute(dwarf::DW_AT_location));
+
// Handle any tag that can have DW_AT_location attribute.
DIEValue LocAttrInfo = Die->findAttribute(dwarf::DW_AT_location);
DIEValue LowPCAttrInfo = Die->findAttribute(dwarf::DW_AT_low_pc);
```

Contributor guide

Open the contributing guide

Research direction

Reproduce the issue with the provided main.cpp commands and inspect bolt/lib/Rewrite/DWARFRewriter.cpp, especially DWARFRewriter::updateUnitDebugInfo(). Verify the DW_FORM_exprloc call-site expressions after BOLT with llvm-dwarfdump; done means code-address DW_OP_addr operands point into relocated .text while data addresses remain unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers, devtools
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.