[lld] [llvm] [ELF][Hexagon] Preserve debug locations and dead relocation values (PR #221679)
via llvm-commits
llvm-commits at lists.llvm.org
Mon Sep 7 01:39:57 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-lld
Author: Aayuparm
<details>
<summary>Changes</summary>
## Summary
This patch fixes two Hexagon debug-information issues:
- Ensure 32-bit Hexagon relocations overwrite the relocation site instead of
OR-ing with the existing contents.
- Preserve debug locations when the Hexagon Copy-To-Combine pass combines
instructions.
## Details
For `R_HEX_32`, `R_HEX_32_PCREL`, and `R_HEX_DTPREL_32`, use `write32le`
instead of `or32le`. This is important for relocations in non-allocatable
debug sections that reference sections discarded by `--gc-sections`. Without
this change, a tombstone value of zero leaves stale bytes at the relocation
site, which can produce invalid debug addresses.
The Copy-To-Combine pass previously emitted the combined instruction using
the
debug location of only one of the input instructions. If that instruction had
no debug location while the other instruction had an inlined location, the
inlined debug location could be lost. Select a non-empty debug location from
the input instructions and apply it to the newly emitted combine instruction
when necessary.
## Tests
- Added an LLD regression test covering dead `R_HEX_32` relocations in
`.debug_*` sections, including `-z dead-reloc-in-nonalloc` overrides.
- Added a Hexagon CodeGen MIR regression test verifying that an inlined
`DebugLoc` is preserved by Copy-To-Combine.
## Tests run
- `ninja lld llc`
- `build/bin/llvm-lit -sv lld/test/ELF/hexagon-debug-dead-reloc.s`
- Passed: 1/1 tests
- `build/bin/llvm-lit -sv llvm/test/CodeGen/Hexagon/copy-to-combine-
debugloc.mir`
- Passed: 1/1 tests
---
Full diff: https://github.com/llvm/llvm-project/pull/221679.diff
4 Files Affected:
- (modified) lld/ELF/Arch/Hexagon.cpp (+1-1)
- (added) lld/test/ELF/hexagon-debug-dead-reloc.s (+73)
- (modified) llvm/lib/Target/Hexagon/HexagonCopyToCombine.cpp (+22)
- (added) llvm/test/CodeGen/Hexagon/copy-to-combine-debugloc.mir (+122)
``````````diff
diff --git a/lld/ELF/Arch/Hexagon.cpp b/lld/ELF/Arch/Hexagon.cpp
index 985f9c365bd37..c30a7b9998fd4 100644
--- a/lld/ELF/Arch/Hexagon.cpp
+++ b/lld/ELF/Arch/Hexagon.cpp
@@ -450,7 +450,7 @@ void Hexagon::relocate(uint8_t *loc, const Relocation &rel,
case R_HEX_32:
case R_HEX_32_PCREL:
case R_HEX_DTPREL_32:
- or32le(loc, val);
+ write32le(loc, val);
break;
case R_HEX_32_6_X:
case R_HEX_GD_GOT_32_6_X:
diff --git a/lld/test/ELF/hexagon-debug-dead-reloc.s b/lld/test/ELF/hexagon-debug-dead-reloc.s
new file mode 100644
index 0000000000000..95e0258b97221
--- /dev/null
+++ b/lld/test/ELF/hexagon-debug-dead-reloc.s
@@ -0,0 +1,73 @@
+# REQUIRES: hexagon
+## Test that R_HEX_32 relocations in .debug_* sections referencing a symbol
+## discarded by --gc-sections are resolved to a tombstone value that overwrites
+## (rather than bitwise-ORs into) the existing bytes at the relocation site.
+##
+## This is a regression test for a bug where Hexagon applied R_HEX_32 with
+## or32le(), so writing a tombstone of 0 was a no-op (x | 0 == x) and the stale
+## DW_AT_low_pc bytes (e.g. 0xf85c2001) of a garbage-collected inlined function
+## survived into the output, causing debuggers to set breakpoints at an invalid
+## address.
+
+# RUN: llvm-mc -filetype=obj -triple=hexagon %s -o %t.o
+# RUN: ld.lld --gc-sections -e live_entry %t.o -o %t
+# RUN: llvm-objdump -s %t | FileCheck %s
+
+## The relocation slots are pre-filled with 0x01 0x20 0x5c 0xf8 (little-endian
+## 0xf85c2001) to prove the stale bytes are overwritten by the tombstone.
+# CHECK: Contents of section .debug_info:
+# CHECK-NEXT: 0000 00000000
+# CHECK-NEXT: Contents of section .debug_loc:
+# CHECK-NEXT: 0000 01000000
+# CHECK-NEXT: Contents of section .debug_ranges:
+# CHECK-NEXT: 0000 01000000
+# CHECK-NEXT: Contents of section .debug_names:
+# CHECK-NEXT: 0000 ffffffff
+
+## -z dead-reloc-in-nonalloc= can override the tombstone value, and it must also
+## overwrite (not OR into) the stale bytes.
+# RUN: ld.lld --gc-sections -e live_entry -z dead-reloc-in-nonalloc=.debug_info=0x42 %t.o -o %t1
+# RUN: llvm-objdump -s %t1 | FileCheck %s --check-prefix=OVERRIDE
+
+# OVERRIDE: Contents of section .debug_info:
+# OVERRIDE-NEXT: 0000 42000000
+
+## Live (kept) code, used as the GC root via -e.
+.section .text.live,"ax", at progbits
+.globl live_entry
+live_entry:
+ jumpr r31
+
+## Out-of-line function copy. It is not referenced by any live section, so
+## --gc-sections removes it and its symbol is demoted to Undefined.
+.section .text.dead,"ax", at progbits
+.globl dead_func
+dead_func:
+ jumpr r31
+
+## Each .debug_* slot is pre-filled with 0xf85c2001 and carries an R_HEX_32
+## relocation against the discarded symbol.
+##
+## .debug_info: tombstone 0.
+.section .debug_info,"", at progbits
+di:
+ .byte 0x01, 0x20, 0x5c, 0xf8
+ .reloc di, R_HEX_32, dead_func
+
+## .debug_loc: tombstone 1 (0/-1 are reserved base-address selection entries).
+.section .debug_loc,"", at progbits
+dl:
+ .byte 0x01, 0x20, 0x5c, 0xf8
+ .reloc dl, R_HEX_32, dead_func
+
+## .debug_ranges: tombstone 1.
+.section .debug_ranges,"", at progbits
+dr:
+ .byte 0x01, 0x20, 0x5c, 0xf8
+ .reloc dr, R_HEX_32, dead_func
+
+## .debug_names: tombstone UINT32_MAX.
+.section .debug_names,"", at progbits
+dn:
+ .byte 0x01, 0x20, 0x5c, 0xf8
+ .reloc dn, R_HEX_32, dead_func
diff --git a/llvm/lib/Target/Hexagon/HexagonCopyToCombine.cpp b/llvm/lib/Target/Hexagon/HexagonCopyToCombine.cpp
index 1dabcf32fa8e8..7f6514c087fcd 100644
--- a/llvm/lib/Target/Hexagon/HexagonCopyToCombine.cpp
+++ b/llvm/lib/Target/Hexagon/HexagonCopyToCombine.cpp
@@ -602,6 +602,19 @@ void HexagonCopyToCombine::combine(MachineInstr &I1, MachineInstr &I2,
isGreaterThanNBitTFRI<16>(I1) && isGreaterThanNBitTFRI<16>(I2);
MachineBasicBlock::iterator InsertPt(DoInsertAtI1 ? I1 : I2);
+
+ // The emitCombine* helpers below take their DebugLoc from InsertPt, i.e. from
+ // only one of the two instructions being combined. If that instruction has no
+ // DebugLoc (e.g. a materialized constant) but the other one does, the source
+ // location of the surviving instruction would be lost. This matters for
+ // inlined functions: a dropped DebugLoc can remove the sole instruction that
+ // carries an inlined scope, causing its DW_TAG_inlined_subroutine (and the
+ // DW_AT_inline abstract instance) to disappear from the debug info. Merge the
+ // two DebugLocs, preferring a non-empty one, and apply it to the new combine.
+ DebugLoc CombinedDL = I1.getDebugLoc();
+ if (!CombinedDL)
+ CombinedDL = I2.getDebugLoc();
+
// Emit combine.
if (IsHiReg && IsLoReg)
emitCombineRR(InsertPt, DoubleRegDest, HiOperand, LoOperand);
@@ -614,6 +627,15 @@ void HexagonCopyToCombine::combine(MachineInstr &I1, MachineInstr &I2,
else
emitCombineII(InsertPt, DoubleRegDest, HiOperand, LoOperand);
+ // The new combine instruction was inserted immediately before InsertPt.
+ // If it ended up without a DebugLoc, restore the merged one so the source
+ // location (and any inlined scope) of the combined instructions is preserved.
+ if (CombinedDL) {
+ MachineBasicBlock::iterator NewMI = std::prev(InsertPt);
+ if (!NewMI->getDebugLoc())
+ NewMI->setDebugLoc(CombinedDL);
+ }
+
// Move debug instructions along with I1 if it's being
// moved towards I2.
if (!DoInsertAtI1 && DbgMItoMove.size() != 0) {
diff --git a/llvm/test/CodeGen/Hexagon/copy-to-combine-debugloc.mir b/llvm/test/CodeGen/Hexagon/copy-to-combine-debugloc.mir
new file mode 100644
index 0000000000000..4f39229831af9
--- /dev/null
+++ b/llvm/test/CodeGen/Hexagon/copy-to-combine-debugloc.mir
@@ -0,0 +1,122 @@
+# RUN: llc -mtriple=hexagon -run-pass=hexagon-copy-combine %s -o - | FileCheck %s
+
+## The Hexagon Copy-To-Combine pass fuses two register-transfer instructions
+## (here `A2_tfrsi 100` and `A2_tfrsi @g`) into a single `A4_combineii`. The new
+## instruction must keep a source location from the combined instructions. If it
+## does not, the DebugLoc carrying an inlined scope can be dropped, which in turn
+## removes the corresponding DW_TAG_inlined_subroutine (and the DW_AT_inline
+## abstract instance) from the emitted debug info for the inlined callee.
+##
+## `renamable $r3 = A2_tfrsi 100` has no DebugLoc while
+## `renamable $r2 = A2_tfrsi @g` carries the inlined location `!28`. The combine
+## must inherit `!28` rather than an empty location.
+
+# In MIR output the metadata table is printed before the function bodies, so
+# capture the inlined DILocation id first, then verify the combine in main uses
+# it. leaf's own combine keeps a (non-inlined) location as well.
+# CHECK: [[INLDL:![0-9]+]] = !DILocation(line: 3, column: 21, scope: !{{[0-9]+}}, inlinedAt: !{{[0-9]+}})
+
+# CHECK-LABEL: name: leaf
+# CHECK: A4_combineii 100, @g, debug-location !{{[0-9]+}}
+
+# CHECK-LABEL: name: main
+## The combine formed inside main must retain the inlined DebugLoc, not an
+## empty one taken from the constant `A2_tfrsi 100`.
+# CHECK: A4_combineii 100, @g, debug-location [[INLDL]]{{$}}
+
+--- |
+ target datalayout = "e-m:e-p:32:32:32-a:0-n16:32-i64:64:64-i32:32:32-i16:16:16-i1:8:8-f32:32:32-f64:64:64-v32:32:32-v64:64:64-v512:512:512-v1024:1024:1024-v2048:2048:2048"
+ target triple = "hexagon"
+
+ @g = common dso_local global i32 0, align 4, !dbg !0
+
+ define dso_local void @leaf() local_unnamed_addr #0 !dbg !16 {
+ entry:
+ %0 = load volatile i32, ptr @g, align 4, !dbg !19, !tbaa !20
+ %add = add nsw i32 %0, 100, !dbg !21
+ store volatile i32 %add, ptr @g, align 4, !dbg !22, !tbaa !20
+ ret void, !dbg !23
+ }
+
+ define dso_local noundef i32 @main() local_unnamed_addr #1 !dbg !24 {
+ entry:
+ tail call void @barrier() #3, !dbg !27
+ %0 = load volatile i32, ptr @g, align 4, !dbg !28, !tbaa !20
+ %add.i = add nsw i32 %0, 100, !dbg !31
+ store volatile i32 %add.i, ptr @g, align 4, !dbg !32, !tbaa !20
+ ret i32 0, !dbg !33
+ }
+
+ declare !dbg !34 dso_local void @barrier() local_unnamed_addr #2
+
+ attributes #0 = { nounwind "target-cpu"="hexagonv68" }
+ attributes #1 = { nounwind "target-cpu"="hexagonv68" }
+ attributes #2 = { "target-cpu"="hexagonv68" }
+ attributes #3 = { nounwind }
+
+ !llvm.dbg.cu = !{!2}
+ !llvm.module.flags = !{!7, !8, !9}
+ !llvm.ident = !{!10}
+
+ !0 = !DIGlobalVariableExpression(var: !1, expr: !DIExpression())
+ !1 = distinct !DIGlobalVariable(name: "g", scope: !2, file: !3, line: 1, type: !5, isLocal: false, isDefinition: true)
+ !2 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, producer: "Clang 23.0", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, globals: !4, splitDebugInlining: false, debugInfoForProfiling: true, nameTableKind: None)
+ !3 = !DIFile(filename: "repro.c", directory: "/tmp")
+ !4 = !{!0}
+ !5 = !DIDerivedType(tag: DW_TAG_volatile_type, baseType: !6)
+ !6 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+ !7 = !{i32 7, !"Dwarf Version", i32 4}
+ !8 = !{i32 2, !"Debug Info Version", i32 3}
+ !9 = !{i32 7, !"frame-pointer", i32 2}
+ !10 = !{!"Clang 23.0"}
+ !13 = !{!"int", !14, i64 0}
+ !14 = !{!"omnipotent char", !15, i64 0}
+ !15 = !{!"Simple C/C++ TBAA"}
+ !16 = distinct !DISubprogram(name: "leaf", scope: !3, file: !3, line: 3, type: !17, scopeLine: 3, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !2)
+ !17 = !DISubroutineType(types: !18)
+ !18 = !{null}
+ !19 = !DILocation(line: 3, column: 21, scope: !16)
+ !20 = !{!13, !13, i64 0}
+ !21 = !DILocation(line: 3, column: 21, scope: !16)
+ !22 = !DILocation(line: 3, column: 21, scope: !16)
+ !23 = !DILocation(line: 3, column: 29, scope: !16)
+ !24 = distinct !DISubprogram(name: "main", scope: !3, file: !3, line: 4, type: !25, scopeLine: 4, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !2)
+ !25 = !DISubroutineType(types: !26)
+ !26 = !{!6}
+ !27 = !DILocation(line: 4, column: 18, scope: !24)
+ !28 = !DILocation(line: 3, column: 21, scope: !16, inlinedAt: !29)
+ !29 = distinct !DILocation(line: 4, column: 29, scope: !30)
+ !30 = !DILexicalBlockFile(scope: !24, file: !3, discriminator: 2)
+ !31 = !DILocation(line: 3, column: 21, scope: !16, inlinedAt: !29)
+ !32 = !DILocation(line: 3, column: 21, scope: !16, inlinedAt: !29)
+ !33 = !DILocation(line: 4, column: 37, scope: !24)
+ !34 = !DISubprogram(name: "barrier", scope: !3, file: !3, line: 2, type: !17, flags: DIFlagPrototyped, spFlags: DISPFlagOptimized)
+...
+---
+name: leaf
+alignment: 16
+tracksRegLiveness: true
+body: |
+ bb.0.entry:
+ renamable $r3 = A2_tfrsi 100
+ renamable $r2 = A2_tfrsi @g, debug-location !19
+ L4_add_memopw_io killed renamable $r2, 0, killed renamable $r3, debug-location !22 :: (volatile store (s32) into @g, !tbaa !20), (volatile dereferenceable load (s32) from @g, !tbaa !20)
+ PS_jmpret $r31, implicit-def dead $pc, debug-location !23
+...
+---
+name: main
+alignment: 16
+tracksRegLiveness: true
+frameInfo:
+ adjustsStack: true
+ hasCalls: true
+body: |
+ bb.0.entry:
+ $r29 = frame-setup S2_allocframe $r29, 0, implicit-def $r30, implicit $framekey, implicit $framelimit, implicit $r30, implicit $r31, debug-location !27 :: (store (s32) into stack)
+ J2_call @barrier, hexagoncsr, implicit-def dead $pc, implicit-def dead $r31, implicit $r29, implicit-def $r29, debug-location !27
+ $r0 = A2_tfrsi 0, debug-location !33
+ renamable $r3 = A2_tfrsi 100
+ renamable $r2 = A2_tfrsi @g, debug-location !28
+ L4_add_memopw_io killed renamable $r2, 0, killed renamable $r3, debug-location !32 :: (volatile store (s32) into @g, !tbaa !20), (volatile dereferenceable load (s32) from @g, !tbaa !20)
+ $d15 = L4_return $r30, implicit-def $pc, implicit-def $r29, implicit $framekey, implicit-def dead $pc, implicit $r0, debug-location !33
+...
``````````
</details>
https://github.com/llvm/llvm-project/pull/221679
More information about the llvm-commits
mailing list