[BOLT] BOLT does not relocate `DW_OP_addr` operands inside call-site value expressions
- 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
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