[BOLT] BOLT does not relocate `DW_AT_call_return_pc` when it equals the caller's `high_pc`
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Description
When optimizing a binary with `--update-debug-sections`, BOLT can leave `DW_AT_call_return_pc` in `DW_TAG_call_site` entries pointing at the **pre-BOLT** address. This happens when the return PC is exactly the exclusive end of the caller function's address range (`low_pc + size == call_return_pc`).
GDB then fails to resolve `DW_OP_entry_value` for formal parameters and shows `DW_OP_entry_value resolving cannot find DW_TAG_call_site
in ???`## Reproduction
Minimal example (`crash_demo.cpp`):
```cpp
volatile int sink;
__attribute__((noinline))
int boom(int n) {
int a = n * 3;
int b = n + 7;
int *p = nullptr;
sink = *p; // intentional crash
return a + b + sink;
}
int main(int argc, char **) {
return boom(argc + 41);
}
```
Build and optimize:
```bash
g++ -gdwarf-5 -O3 -g -gz=none -fno-reorder-blocks-and-partition crash_demo.cpp -o crash_demo -Wl,-q -Wl,--emit-relocs
llvm-bolt crash_demo -o crash_demo.bolt --update-debug-sections
```
### Before BOLT
Disassembly:
```bash
llvm-objdump -d crash_demo
401020 :
401020: add $0x29,%edi
401023: call boom
401028: nopl ... <- return PC
```
``` bash
llvm-dwarfdump --show-form crash_demo | grep -E 'low_pc|high_pc|call_return_pc|DW_AT_name.*main'
DW_AT_low_pc [DW_FORM_addr] (0x0000000000401020)
DW_AT_high_pc [DW_FORM_data8] (0x0000000000000008)
DW_AT_call_return_pc [DW_FORM_addr] (0x0000000000401028)
main: low_pc = 0x401020, high_pc (size) = 8 -> range [0x401020, 0x401028)
call site: DW_AT_call_return_pc = 0x401028 -> exactly at exclusive end
```
### After BOLT (bug)
Code is relocated:
```bash
llvm-objdump -d crash_demo.bolt
80001c :
80001c: add $0x29,%edi
80001f: call boom
800024: nopl ... <- actual return PC
```
But DWARF is stale:
``` bash
llvm-dwarfdump --show-form crash_demo.bolt | grep -E 'low_pc|high_pc|call_return_pc|DW_AT_name.*main'
llvm-dwarfdump --show-form crash_demo.bolt | grep 'call_return_pc'
DW_AT_low_pc [DW_FORM_addr] (0x000000000080001c)
DW_AT_high_pc [DW_FORM_data8] (0x0000000000000008)
DW_AT_call_return_pc [DW_FORM_addr] (0x0000000000401028) #BUG
DW_AT_call_return_pc = 0x401028 # should be 0x800024
```
GDB impact
```bash
gdb -q -batch crash_demo.bolt -ex 'set debug entry-values 1' -ex 'break boom' -ex run -ex bt
DW_OP_entry_value resolving cannot find DW_TAG_call_site 0x800024 in ???
```
GDB looks for a `DW_TAG_call_site` whose `DW_AT_call_return_pc` matches the relocated return address (`0x800024`), but debug info still contains `0x401028`.
## DWARF 5 specification
The debug info in this example - where `DW_AT_call_return_pc` equals the caller's exclusive `high_pc` — is **valid, standard-compliant DWARF 5**. References: *DWARF Debugging Information Format, Version 5* (February 2017).
**§3.4.1 — Call Site Entries** defines `DW_AT_call_return_pc` as *"the return address after the call"*, i.e. the address of the first instruction **after** the call instruction.
**§2.17.2 — Contiguous Address Range** defines an address-class `DW_AT_high_pc` as *"the first location **past** the last instruction associated with the entity"*, i.e. an **exclusive** upper bound.
When the call is the last instruction of a function, both definitions point at the same address:
```
low_pc = 0x401020
high_pc = 0x401028 (exclusive end, §2.17.2)
call_return_pc = 0x401028 (first byte after the call, §3.4.1)
```
So `call_return_pc == high_pc` is exactly what the standard prescribes — the return address legitimately lands on the exclusive boundary and lies just outside the range `[0x401020, 0x401028)`. This is normal for functions where a call is the final instruction; the DWARF is correct.
## Proposed minimal fix
In `bolt/lib/Rewrite/DWARFRewriter.cpp`, pass `CheckPastEnd = true` only for `DW_AT_call_return_pc`:
```diff
--- a/bolt/lib/Rewrite/DWARFRewriter.cpp
+++ b/bolt/lib/Rewrite/DWARFRewriter.cpp
@@ -1389,10 +1389,11 @@ void DWARFRewriter::updateUnitDebugInfo(
break;
}
case dwarf::DW_TAG_call_site: {
- auto patchPC = [&](DIE *Die, DIEValue &AttrVal, StringRef Entry) -> void {
+ auto patchPC = [&](DIE *Die, DIEValue &AttrVal, StringRef Entry,
+ bool CheckPastEnd = false) -> void {
std::optional Address = getAsAddress(Unit, AttrVal);
const BinaryFunction *Function =
- BC.getBinaryFunctionContainingAddress(*Address);
+ BC.getBinaryFunctionContainingAddress(*Address, CheckPastEnd);
uint64_t UpdatedAddress = *Address;
if (Function)
UpdatedAddress =
@@ -1417,7 +1418,8 @@ void DWARFRewriter::updateUnitDebugInfo(
DIEValue CallRetPcAttrVal =
Die->findAttribute(dwarf::DW_AT_call_return_pc);
if (CallRetPcAttrVal)
- patchPC(Die, CallRetPcAttrVal, "DW_AT_call_return_pc");
+ patchPC(Die, CallRetPcAttrVal, "DW_AT_call_return_pc",
+ /*CheckPastEnd=*/true);
break;
}
```
`DW_AT_call_pc` should keep the default (`false`): per §3.4.1 it is the address of the call instruction itself, which per §2.17.2 always lies inside [low_pc, high_pc)
Tests such as `bolt/test/X86/dwarf5-return-pc-form-addr.test` use `main` with instructions **after** the `call`. There `DW_AT_call_return_pc` lies **inside** `[low_pc, high_pc)`, so `getBinaryFunctionContainingAddress()` succeeds and the bug is hidden. Adaptation of that test should be considered to cover that specific scenario.
Expected after fix:
- `DW_AT_call_return_pc` changes from `0x401028` to `0x800024`
- GDB backtrace shows `boom (n=n@entry=42)`
## Environment
- **Observed on:** BOLT `bd6adfedc776c07caf158e59367d9c246c933510` (2026-06-30)
- **Compiler**: g++ (GCC) 14.2.0 (but that should be reproduced also in clang)
- **Verified on**: x86_64
Contributor guide
Research direction
Start in bolt/lib/Rewrite/DWARFRewriter.cpp at DW_TAG_call_site handling, then reproduce the boundary case with the provided crash_demo.cpp commands. Use bolt/test/X86/dwarf5-return-pc-form-addr.test as the related test and extend coverage for a call_return_pc equal to the caller's exclusive high_pc. Done means the relocated return PC is emitted and the GDB entry-value lookup succeeds.
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
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 75/100