[llvm] [BOLT][DWARF] Fix unit layout with forward DW_FORM_ref_udata references (PR #226076)
via llvm-commits
llvm-commits at lists.llvm.org
Thu Sep 24 01:56:30 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-bolt
Author: Tim Besard (maleadt)
<details>
<summary>Changes</summary>
With `--update-debug-sections`, BOLT can emit a corrupt `.debug_info` when a unit has a *forward* DIE reference in `DW_FORM_ref_udata` form: the unit length, and every DIE offset after the reference, are off by one or more bytes. Consumers then misparse the section:
```
$ llvm-dwarfdump --debug-info libfoo.bolt.so
warning: DWARF unit from offset 0x002cbcfc incl. to offset 0x0031bd00 excl. extends past section size 0x0030caa6
```
This patch makes BOLT write those references as `DW_FORM_ref4`, and extends the existing `ref_udata` test to cover the failing case.
## Where this shows up
GNU as emits `DW_FORM_ref_udata` in the DWARF 5 units it generates for assembly files: each function gets a `DW_TAG_subprogram` whose `DW_AT_type` points forward to a `DW_TAG_unspecified_type`. For example, AArch64 code built with outline atomics links in such units from `lse.S` in GCC's `libgcc.a`, one per `__aarch64_*` helper used.
We hit this in Julia's CI, which BOLTs `libLLVM.so` and `libjulia-internal.so` on aarch64-linux. Symbolizing a backtrace printed the warning above to stderr, which failed tests that check stderr. The bug itself is target-independent.
## Root cause
`DW_FORM_ref_udata` is a ULEB128, so its size depends on its value. `DIEBuilder::finalizeDIEs()` lays out a unit in one depth-first pass, summing attribute sizes as it goes, and `DIEEntry::sizeOf()` sizes a reference as `getULEB128Size(Entry->getOffset())`, i.e. from the *referenced* DIE's offset. For a forward reference that DIE hasn't been laid out yet, so its offset is still the placeholder set by `allocDIE()`, which is the input offset relative to the start of the section:
```cpp
Die->setOffset(DDie.getOffset());
```
The value emitted later is the final offset relative to the unit. When the two encode to different lengths, e.g. for a DIE at least 0x80 bytes into `.debug_info` but less than 0x80 bytes into its unit, the computed layout doesn't match what gets written. Backward references are fine.
For example, with GCC 15.2's `lse.S` unit linked into a small AArch64 shared library, before this patch:
```
0x00000133: DW_TAG_subprogram
DW_AT_name [DW_FORM_strp] ("__aarch64_ldadd4_acq_rel")
DW_AT_type [DW_FORM_ref_udata] (0x00000143) <- one byte past the target
...
0x00000142: DW_TAG_unspecified_type
0x00000143: NULL
$ llvm-dwarfdump --verify liba.bolt.so
error: Unit Header Length: Unit type encoding is not valid occurred 1 time(s).
```
After, `DW_AT_type [DW_FORM_ref4] (0x00000145 "")` points at the `DW_TAG_unspecified_type` at 0x145, and `--verify` reports no errors.
## The fix
`DIEBuilder::cloneDieOffsetReferenceAttribute()` now clones `DW_FORM_ref_udata` references as `DW_FORM_ref4`:
```cpp
const dwarf::Form Form = AttrSpec.Form == dwarf::DW_FORM_ref_udata
? dwarf::DW_FORM_ref4
: AttrSpec.Form;
```
Abbreviations are regenerated from the output DIEs, so nothing else changes. Reading `ref_udata` is still supported.
Why not keep `ref_udata`? That would require iterating the layout until sizes stop changing (sizes depend on offsets and vice versa, and one pass can push later offsets across another ULEB128 boundary). A fixed-size form is what BOLT already does for type references in location expressions (`cloneExpression()` pads them to 4 bytes "so size doesn't change when we update the offset"), and the parallel DWARFLinker also emits the local references it keeps as `DW_FORM_ref4`. The cost is a few bytes per reference, and `ref_udata` is rare.
---
Full diff: https://github.com/llvm/llvm-project/pull/226076.diff
2 Files Affected:
- (modified) bolt/lib/Core/DIEBuilder.cpp (+7-2)
- (modified) bolt/test/X86/dwarf5-form-ref-udata.s (+66-14)
``````````diff
diff --git a/bolt/lib/Core/DIEBuilder.cpp b/bolt/lib/Core/DIEBuilder.cpp
index fd2b872d66eae..f86a11331fcb7 100644
--- a/bolt/lib/Core/DIEBuilder.cpp
+++ b/bolt/lib/Core/DIEBuilder.cpp
@@ -701,8 +701,13 @@ void DIEBuilder::cloneDieOffsetReferenceAttribute(
return;
}
- Die.addValue(getState().DIEAlloc, AttrSpec.Attr, AttrSpec.Form,
- DIEEntry(*NewRefDie));
+ // The size of a DW_FORM_ref_udata value depends on the output offset of the
+ // referenced DIE, which for a forward reference is only assigned after this
+ // DIE has been laid out in finalizeDIEs(). Use a fixed-size form instead.
+ const dwarf::Form Form = AttrSpec.Form == dwarf::DW_FORM_ref_udata
+ ? dwarf::DW_FORM_ref4
+ : AttrSpec.Form;
+ Die.addValue(getState().DIEAlloc, AttrSpec.Attr, Form, DIEEntry(*NewRefDie));
}
void DIEBuilder::cloneStringAttribute(
diff --git a/bolt/test/X86/dwarf5-form-ref-udata.s b/bolt/test/X86/dwarf5-form-ref-udata.s
index 0b63a1711423d..32be643b59d26 100644
--- a/bolt/test/X86/dwarf5-form-ref-udata.s
+++ b/bolt/test/X86/dwarf5-form-ref-udata.s
@@ -2,16 +2,40 @@
# RUN: llvm-mc -dwarf-version=5 -filetype=obj -triple x86_64-unknown-linux %s -o %t.o
# RUN: %clang %cflags -dwarf-5 %t.o -o %t.exe -Wl,-q
+# RUN: llvm-dwarfdump --show-form --verbose --debug-info %t.exe | \
+# RUN: FileCheck %s --check-prefix CHECK-INPUT
# RUN: llvm-bolt %t.exe -o %t.bolt --update-debug-sections 2>&1 | \
# RUN: FileCheck %s --check-prefix CHECK-BOLT
-# RUN: llvm-dwarfdump --show-form --verbose --debug-info %t.bolt | FileCheck %s
+# RUN: llvm-dwarfdump --show-form --verbose --debug-info %t.bolt 2>&1 | \
+# RUN: FileCheck %s --implicit-check-not=warning:
+# RUN: llvm-dwarfdump --verify %t.bolt | FileCheck %s --check-prefix CHECK-VERIFY
-## Verify BOLT preserves DW_FORM_ref_udata (CU-relative ULEB128 DIE reference),
-## a form GCC may emit instead of DW_FORM_ref4.
+## Verify BOLT handles DW_FORM_ref_udata (CU-relative ULEB128 DIE reference),
+## a form GNU as emits instead of DW_FORM_ref4. BOLT rewrites these
+## references as DW_FORM_ref4: the size of a ULEB128 reference depends on the
+## output offset of the referenced DIE, which is not known yet when a DIE with
+## a forward reference is laid out.
+##
+## Until a DIE is laid out, BOLT keeps its input offset, which is relative to
+## the start of .debug_info. The DIE referenced by the subprogram in the second
+## CU is at least 0x80 bytes into the section, so that offset needs a two-byte
+## ULEB128, while its CU-relative output offset needs only one byte. Sizing the
+## forward reference with the former and emitting the latter used to leave the
+## unit length and all subsequent DIE offsets of the unit off by one.
-# CHECK: DW_TAG_subprogram
-# CHECK: DW_AT_type [DW_FORM_ref_udata]
+## Check the input satisfies these conditions.
+# CHECK-INPUT: DW_AT_type [DW_FORM_ref_udata] (cu + 0x{{[0-7]?[0-9a-f]}} => {0x000000{{[89a-f][0-9a-f]}}} "long")
+
+# CHECK: DW_TAG_compile_unit
+# CHECK: DW_TAG_variable
+# CHECK: DW_AT_type [DW_FORM_ref4]
# CHECK-SAME: "int"
+# CHECK: DW_TAG_compile_unit
+# CHECK: DW_TAG_subprogram
+# CHECK: DW_AT_type [DW_FORM_ref4]
+# CHECK-SAME: "long"
+
+# CHECK-VERIFY: No errors.
# CHECK-BOLT-NOT: BOLT-WARNING
@@ -31,21 +55,30 @@ main:
.section .debug_abbrev,"", at progbits
.byte 1, 17, 1 # CU, has children
+ .byte 37, 8 # DW_AT_producer, DW_FORM_string
+ .byte 3, 8 # DW_AT_name, DW_FORM_string
+ .byte 0, 0
+ .byte 2, 52, 0 # variable, no children
+ .byte 3, 8 # DW_AT_name, DW_FORM_string
+ .byte 73, 21 # DW_AT_type, DW_FORM_ref_udata
+ .byte 0, 0
+ .byte 3, 36, 0 # base_type, no children
+ .byte 3, 8 # DW_AT_name, DW_FORM_string
+ .byte 0, 0
+ .byte 4, 17, 1 # CU, has children
.byte 17, 1 # DW_AT_low_pc, DW_FORM_addr
.byte 18, 6 # DW_AT_high_pc, DW_FORM_data4
.byte 16, 23 # DW_AT_stmt_list, DW_FORM_sec_offset
.byte 0, 0
- .byte 2, 46, 0 # subprogram, no children
+ .byte 5, 46, 0 # subprogram, no children
.byte 17, 1 # DW_AT_low_pc, DW_FORM_addr
.byte 18, 6 # DW_AT_high_pc, DW_FORM_data4
.byte 73, 21 # DW_AT_type, DW_FORM_ref_udata
.byte 0, 0
- .byte 3, 36, 0 # base_type, no children
- .byte 3, 8 # DW_AT_name, DW_FORM_string
- .byte 0, 0
.byte 0
.section .debug_info,"", at progbits
+## A CU without code, which moves the second CU further into .debug_info.
.Lcu_begin0:
.long .Ldebug_info_end0-.Ldebug_info_start0
.Ldebug_info_start0:
@@ -54,17 +87,36 @@ main:
.byte 8 # Address size
.long .debug_abbrev # Abbrev offset
.byte 1 # CU
+ .asciz "GNU C17 13.2.0 -mtune=generic -march=x86-64 -g -O2" # DW_AT_producer
+ .asciz "/src/lib/global_data.c" # DW_AT_name
+ .byte 2 # variable
+ .asciz "data" # DW_AT_name
+ .uleb128 .Ltype_int-.Lcu_begin0 # DW_AT_type (DW_FORM_ref_udata)
+.Ltype_int:
+ .byte 3 # base_type
+ .asciz "int" # DW_AT_name
+ .byte 0 # End children of CU
+.Ldebug_info_end0:
+
+.Lcu_begin1:
+ .long .Ldebug_info_end1-.Ldebug_info_start1
+.Ldebug_info_start1:
+ .short 5 # DWARF version
+ .byte 1 # DW_UT_compile
+ .byte 8 # Address size
+ .long .debug_abbrev # Abbrev offset
+ .byte 4 # CU
.quad .Lfunc_begin0 # DW_AT_low_pc
.long .Lfunc_end0-.Lfunc_begin0 # DW_AT_high_pc
.long .Lline_table_start0 # DW_AT_stmt_list
- .byte 2 # subprogram
+ .byte 5 # subprogram
.quad .Lfunc_begin0 # DW_AT_low_pc
.long .Lfunc_end0-.Lfunc_begin0 # DW_AT_high_pc
- .uleb128 .Ltype_int-.Lcu_begin0 # DW_AT_type (DW_FORM_ref_udata)
-.Ltype_int:
+ .uleb128 .Ltype_long-.Lcu_begin1 # DW_AT_type (DW_FORM_ref_udata)
+.Ltype_long:
.byte 3 # base_type
- .asciz "int" # DW_AT_name
+ .asciz "long" # DW_AT_name
.byte 0 # End children of CU
-.Ldebug_info_end0:
+.Ldebug_info_end1:
.section .debug_line,"", at progbits
.Lline_table_start0:
``````````
</details>
https://github.com/llvm/llvm-project/pull/226076
More information about the llvm-commits
mailing list