llvm / llvm/llvm-project

[BOLT] `DW_OP_implicit_pointer` keeps a stale `.debug_info` offset after rewriting

Open
#215,398 2 comments 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 operand of `DW_OP_implicit_pointer` is *"an offset of a debugging information entry in the* `.debug_info` *section"* (DWARF 5, section 2.6.1.1.4). When BOLT rebuilds `.debug_info`, every DIE moves, but the operand is copied verbatim, so it keeps the offset the DIE had in the input. After the run it points into the middle of an unrelated DIE, or at nothing at all.

> Nothing reports this. `llvm-bolt` prints no diagnostic, the expression stays well-formed, and `llvm-dwarfdump --verify` does not check this operand, so the output looks healthy while the debugger silently resolves the pointer to the wrong object.

## Environment

- **BOLT:** `llvm-bolt` (`BOLT version:` bd6adfedc776c07caf158e59367d9c246c933510) + fix for https://github.com/llvm/llvm-project/issues/214721 explained below
- **Target arch:** AArch64 and x86_64
- **Compiler:** g++ (GCC) 14.2.0
- **`llvm-dwarfdump`:** built from the same tree as the `llvm-bolt` above.

## Reproducer

`main.cpp`:

```cpp
volatile int sink;

struct Point {
int x, y;
void emit(int k) const {
sink = x + k;
sink = y - k;
}
};

// One inlined call site: the location of `this` covers a single range.
__attribute__((noinline)) static int one(int n) {
Point p{n, 7};
p.emit(n);
return sink;
}

// Two inlined call sites: the location of `this` becomes a multi-entry list.
__attribute__((noinline)) static int many(int n) {
Point q{n, 9};
q.emit(n);
q.emit(n + 1);
return sink;
}

int main(int argc, char **) { return one(argc) + many(argc); }
```

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

```bash
llvm-dwarfdump --debug-info main | grep -B4 implicit_pointer && llvm-dwarfdump --name q --name p main
```

The interesting parts:

```bash
0x0000017b: DW_TAG_formal_parameter
DW_AT_location (0x00000069:
[0x0000000000401140, 0x0000000000401158): DW_OP_implicit_pointer 0x146 +0)
...
0x0000022e: DW_TAG_formal_parameter
DW_AT_location (0x000000c4:
[0x0000000000401120, 0x0000000000401136): DW_OP_implicit_pointer 0x1ff +0)
...
0x00000146: DW_TAG_variable
DW_AT_name ("q")

0x000001ff: DW_TAG_variable
DW_AT_name ("p")
```

```bash
llvm-dwarfdump --debug-info main.bolt | grep -B4 implicit_pointer && llvm-dwarfdump --name q --name p main.bolt
```

After `llvm-bolt`, `q` has moved from `0x146` to `0x14c` and `p` from `0x1ff` to `0x1f6`, but the operands still hold the input offsets:

```bash
0x0000017e: DW_TAG_formal_parameter
DW_AT_location (indexed (0x3) loclist = 0x0000007c:
[0x000000000080015e, 0x0000000000800176): DW_OP_implicit_pointer 0x146 +0) <-- stale
...
0x00000222: DW_TAG_formal_parameter
DW_AT_location (indexed (0x8) loclist = 0x000000b4:
[0x0000000000800140, 0x0000000000800156): DW_OP_implicit_pointer 0x1ff +0) <-- stale

0x0000014c: DW_TAG_variable
DW_AT_name ("q")
0x000001f6: DW_TAG_variable
DW_AT_name ("p")
```

## Analysis

`DIEBuilder::cloneExpression()` (`bolt/lib/Core/DIEBuilder.cpp`) is the only place that rewrites a DIE reference held inside a DWARF expression, and it reacts to exactly one operand encoding:

```cpp
if ((Description.Op.size() == 1 &&
Description.Op[0] == Encoding::BaseTypeRef) ||
(Description.Op.size() == 2 &&
Description.Op[1] == Encoding::BaseTypeRef && ...)) {
... // relocate
} else {
// Copy over everything else unmodified.
const StringRef Bytes = Data.getData().slice(OpOffset, Op.getEndOffset());
OutputBuffer.append(Bytes.begin(), Bytes.end());
}
```

`DW_OP_implicit_pointer` is described as `Desc(Op::Dwarf5, Op::SizeRefAddr, Op::SignedSizeLEB)` in `llvm/lib/DebugInfo/DWARF/LowLevel/DWARFExpression.cpp`, so its reference is `Encoding::SizeRefAddr`, never `Encoding::BaseTypeRef`, and it takes the `else` branch. The descriptor itself is present and correct, so the expression is decoded and re-emitted intact - only the offset inside it is never translated.

Two things differ from `BaseTypeRef`: the operand is a section offset, not a unit-relative one, so the new value is `DIE offset within the unit + unit offset` - the same arithmetic `DIEBuilder::updateReferences()` already does for `DW_FORM_ref_addr`; and it has the fixed width of a section offset (`dwarf::getDwarfOffsetByteSize`), so it is rewritten in place, without padding and without shifting a single byte.

## Proposed fix

`bolt/lib/Core/DIEBuilder.cpp`, one new branch in `cloneExpression()`:

```diff
encodeULEB128(Offset, ULEB, 4);
ArrayRef ULEBbytes(ULEB, 4);
OutputBuffer.append(ULEBbytes.begin(), ULEBbytes.end());
+ } else if (!Description.Op.empty() &&
+ Description.Op[0] == Encoding::SizeRefAddr) {
+ // DW_OP_implicit_pointer and DW_OP_call_ref
+ // name a DIE by its offset in .debug_info (DWARF 5 2.5.1.5 and
+ // 2.6.1.1.4). Unlike Encoding::BaseTypeRef the operand is not relative to
+ // the owning unit, and it is written with the fixed width of a section
+ // offset, so rewriting it never changes the size of the expression.
+ DoesContainReference = true;
+ OutputBuffer.push_back(Op.getCode());
+
+ const uint64_t RefOffset = Op.getRawOperand(0);
+ uint64_t NewOffset = RefOffset;
+ if (Stage == CloneExpressionStage::PATCH) {
+ const DWARFAbbreviationDeclaration::AttributeSpec RefAddrSpec(
+ dwarf::DW_AT_location, dwarf::DW_FORM_ref_addr, std::nullopt);
+ DWARFUnit *RefUnit =
+ getUnitForOffset(*this, *DwarfContext, RefOffset, RefAddrSpec);
+ std::optional RefUnitID =
+ RefUnit ? getUnitId(*RefUnit) : std::nullopt;
+ std::optional RefDieID =
+ RefUnit ? getAllocDIEId(*RefUnit, RefOffset) : std::nullopt;
+ if (RefUnitID && RefDieID) {
+ DIEInfo &RefDieInfo = getDIEInfo(*RefUnitID, *RefDieID);
+ if (DIE *Clone = RefDieInfo.Die)
+ NewOffset =
+ Clone->getOffset() + getUnitInfo(RefDieInfo.UnitId).UnitOffset;
+ } else {
+ BC.errs() << "BOLT-WARNING: [internal-dwarf-error]: DW_OP operand "
+ "does not point to a known DIE at offset: "
+ << Twine::utohexstr(RefOffset) << ".\n";
+ }
+ }
+
+ const uint8_t RefSize = U.getFormParams().getDwarfOffsetByteSize();
+ for (uint8_t I = 0; I != RefSize; ++I) {
+ const uint8_t Shift = U.isLittleEndian() ? I : RefSize - 1 - I;
+ OutputBuffer.push_back((NewOffset >> (8 * Shift)) & 0xFF);
+ }
+
+ // Any remaining operands (the byte offset of DW_OP_implicit_pointer) are
+ // not references and are copied as they are.
+ const StringRef RestBytes =
+ Data.getData().slice(Op.getOperandEndOffset(0), Op.getEndOffset());
+ OutputBuffer.append(RestBytes.begin(), RestBytes.end());
} else {
// Copy over everything else unmodified.
```

The branch follows the two-stage design already in place: `INIT` writes the input offset, `PATCH` writes the final one, both at the same width. Setting `DoesContainReference` registers the expression in `LocWithReferencesToProcess`, so `updateReferences()` re-runs the `PATCH` stage after `DIEBuilder::finish()` has assigned the final offsets - exactly as it does for `BaseTypeRef`.

## Dependency #214721

In this reproducer the compiler emits `DW_OP_implicit_pointer` inside `.debug_loclists`, which `DWARFRewriter::translateInputToOutputLocationList()` copies verbatim on this revision; such expressions reach `cloneExpression()` only with [#214721], which needs nothing extra here since the operand keeps its width stable across both stages. The dependency is limited to that path: an expression written as an `exprloc` or block on a DIE attribute goes through `cloneBlockAttribute()` into `cloneExpression()`, where the same `else` branch leaves its operand stale - and there this patch repairs it on its own (without 214721).

Results from the fixes on proposed reproducer:

| tree | operands landing on a DIE | operands |
| -------------------------------- | ------ | ------- |
| input | 2 | `0x146` → `q`, `0x1ff` → `p` |
| `bd6adfe` | **0** | `0x146`, `0x1ff` - both stale |
| `bd6adfe` + this patch | **0** | `0x146`, `0x1ff` - both stale |
| `bd6adfe` + #214721 + this patch | **2** | `0x14c` → `q`, `0x1f6` → `p` |

Contributor guide

Open the contributing guide

Research direction

Start with bolt/lib/Core/DIEBuilder.cpp, especially DIEBuilder::cloneExpression(), and run the provided g++/llvm-bolt reproducer. Review dependency #214721 and the existing BaseTypeRef handling, then verify that DW_OP_implicit_pointer operands follow their DIEs after rewriting in both expression and location-list paths.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.