[llvm] 35746b2 - [BPF] Record an argument's first DBG_VALUE as its entry location (#217991)
via llvm-commits
llvm-commits at lists.llvm.org
Tue Aug 25 17:56:21 PDT 2026
Author: yonghong-song
Date: 2026-08-25T17:56:16-07:00
New Revision: 35746b26a3d27a5b0d861ac2837678fbfb8b4059
URL: https://github.com/llvm/llvm-project/commit/35746b26a3d27a5b0d861ac2837678fbfb8b4059
DIFF: https://github.com/llvm/llvm-project/commit/35746b26a3d27a5b0d861ac2837678fbfb8b4059.diff
LOG: [BPF] Record an argument's first DBG_VALUE as its entry location (#217991)
Accurate BTF types for DW_CC_nocall functions were added in [1] and work
at -O2. At -O1 the entry block keeps debug locations that -O2 optimizes
away, and two of those patterns defeat the argument-register scan.
Accurate signatures are worth having at -O1 too: transformations move
between optimization levels over time, and some users build at -O1.
collectNocallEntryArgRegs() scans the entry block for DBG_VALUEs to find
which physical register each source argument arrived in. An argument may
have several DBG_VALUEs there, and the map was last-write-wins, so a
later location silently replaced the entry one. The registers then no
longer match the optimized IR signature,
canUseNocallOptimizedSignature() bails, and BTFDebug emits the
unfiltered source prototype -- a FUNC_PROTO that does not describe the
real ABI.
The first pattern is a spilled argument. From
progs/test_l4lb_noinline_dynptr.c in the kernel BPF selftests, with the
stack stores elided:
bb.0 (%ir-block.4):
liveins: $r1, $r2, $r4, $w3
DBG_VALUE $w3, $noreg, !"is_ipv6",
!DIExpression(DW_OP_LLVM_convert, 1, DW_ATE_unsigned,
DW_OP_LLVM_convert, 8, DW_ATE_unsigned,
DW_OP_stack_value)
...
STW32 $w3, $r10, -184
DBG_VALUE $r10, $noreg, !"is_ipv6",
!DIExpression(DW_OP_constu, 184, DW_OP_minus,
DW_OP_deref_size, 4, DW_OP_LLVM_convert, 1,
DW_ATE_unsigned, DW_OP_LLVM_convert, 8,
DW_ATE_unsigned, DW_OP_stack_value)
JEQ_ri_32 killed $w3, 0, %bb.9
"is_ipv6" arrives in $w3 and is spilled, after which the register
allocator emits a second DBG_VALUE for the spill slot. It is not
indirect -- the offset operand is $noreg, not an immediate -- so
isIndirectDebugValue() does not reject it, and $r10 is the frame
pointer, never redefined in the entry block, so the DefinedRegs check
does not either. It replaced $w3 with $r10 and the DWARF register order
check then failed.
The second pattern is an argument reassigned from another argument:
bb.0 (%ir-block.5):
liveins: $r1, $r2, $r3, $r4, $r5
DBG_VALUE $r1, $noreg, !"a", !DIExpression()
...
DBG_VALUE $r5, $noreg, !"e", !DIExpression()
$r1 = nsw MUL_ri killed $r1(tied-def 0), 3
DBG_VALUE $r5, $noreg, !"a", !DIExpression()
The last DBG_VALUE describes "a" after an assignment "a = e". Entry
locations are keyed by source argument number, so it overwrote "a"'s
entry location $r1 with $r5. $r5 is a live-in the entry block never
redefines, so DefinedRegs does not reject it here either.
Fix by recording the first eligible DBG_VALUE for each argument instead
of the last: at function entry an argument still sits in its
ABI-assigned register, and any later DBG_VALUE describes where it moved
to.
[1] https://github.com/llvm/llvm-project/pull/198426
Added:
llvm/test/CodeGen/BPF/BTF/func-nocall-reassigned-arg.ll
llvm/test/CodeGen/BPF/BTF/func-nocall-spilled-arg.ll
Modified:
llvm/lib/Target/BPF/BTFDebug.cpp
Removed:
################################################################################
diff --git a/llvm/lib/Target/BPF/BTFDebug.cpp b/llvm/lib/Target/BPF/BTFDebug.cpp
index 6de1d1eee7cc4..7ddeedb838e87 100644
--- a/llvm/lib/Target/BPF/BTFDebug.cpp
+++ b/llvm/lib/Target/BPF/BTFDebug.cpp
@@ -111,7 +111,8 @@ static bool sourceArgMatchesIRType(const DIType *SourceTy, Type *IRTy) {
/// when its register either (a) has not been redefined by any preceding
/// non-debug instruction (i.e. it still holds the caller-passed value), or
/// (b) was most recently loaded from the stack via $r11 (a stack-passed
-/// argument beyond the first five register args).
+/// argument beyond the first five register args). For each argument only the
+/// first eligible DBG_VALUE is recorded, since that is its entry location.
///
/// There is another case where DBG_VALUE is not emitted due to
/// AssignmentTrackingAnalysis which determines that a variable is
@@ -157,7 +158,7 @@ collectNocallEntryArgRegs(const MachineFunction &MF) {
if (!DefinedRegs.contains(MO.getReg()) ||
StackLoadRegs.contains(MO.getReg()))
- EntryRegMap[Arg] = MO.getReg();
+ EntryRegMap.try_emplace(Arg, MO.getReg());
continue;
}
diff --git a/llvm/test/CodeGen/BPF/BTF/func-nocall-reassigned-arg.ll b/llvm/test/CodeGen/BPF/BTF/func-nocall-reassigned-arg.ll
new file mode 100644
index 0000000000000..f9378539f37e6
--- /dev/null
+++ b/llvm/test/CodeGen/BPF/BTF/func-nocall-reassigned-arg.ll
@@ -0,0 +1,52 @@
+; RUN: llc -mtriple=bpfel -mcpu=v3 -filetype=obj -o %t1 %s
+; RUN: llvm-objcopy --dump-section='.BTF'=%t2 %t1
+; RUN: %python %p/print_btf.py %t2 | FileCheck -check-prefixes=CHECK %s
+
+; DeadArgElimination on a function that reassigns one argument to another:
+; static __noinline int sub(int unused, int a, int b) {
+; int t = a * 3;
+; a = b;
+; return t + a;
+; }
+; 'a' gets a second DBG_VALUE for the reassignment, naming b's register R2.
+; Only the first one is its entry location, so 'a' must stay bound to R1.
+
+; CHECK: [1] INT 'int' size=4 bits_offset=0 nr_bits=32 encoding=SIGNED
+; CHECK-NEXT: [2] FUNC_PROTO '(anon)' ret_type_id=1 vlen=2
+; CHECK-NEXT: 'a' type_id=1
+; CHECK-NEXT: 'b' type_id=1
+; CHECK-NEXT: [3] FUNC 'sub' type_id=2 linkage=static
+
+define internal i32 @sub(i32 %0, i32 %1) #0 !dbg !7 {
+ #dbg_value(i32 %0, !13, !DIExpression(), !15)
+ #dbg_value(i32 %1, !14, !DIExpression(), !15)
+ %3 = mul i32 %0, 3, !dbg !16
+ #dbg_value(i32 %1, !13, !DIExpression(), !15)
+ %4 = add i32 %3, %1, !dbg !16
+ ret i32 %4, !dbg !17
+}
+
+attributes #0 = { noinline }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4, !5}
+!llvm.ident = !{!6}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, enums: !2, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "t.c", directory: "/DNE")
+!2 = !{}
+!3 = !{i32 2, !"Dwarf Version", i32 4}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = !{i32 1, !"wchar_size", i32 4}
+!6 = !{!"clang"}
+!7 = distinct !DISubprogram(name: "sub", scope: !1, file: !1, line: 1, type: !8, scopeLine: 1, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !11)
+!8 = !DISubroutineType(cc: DW_CC_nocall, types: !9)
+!9 = !{!10, !10, !10, !10}
+!10 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!11 = !{!12, !13, !14}
+!12 = !DILocalVariable(name: "unused", arg: 1, scope: !7, file: !1, line: 1, type: !10)
+!13 = !DILocalVariable(name: "a", arg: 2, scope: !7, file: !1, line: 1, type: !10)
+!14 = !DILocalVariable(name: "b", arg: 3, scope: !7, file: !1, line: 1, type: !10)
+!15 = !DILocation(line: 1, column: 1, scope: !7)
+!16 = !DILocation(line: 2, column: 10, scope: !7)
+!17 = !DILocation(line: 4, column: 3, scope: !7)
diff --git a/llvm/test/CodeGen/BPF/BTF/func-nocall-spilled-arg.ll b/llvm/test/CodeGen/BPF/BTF/func-nocall-spilled-arg.ll
new file mode 100644
index 0000000000000..402395a6ea01b
--- /dev/null
+++ b/llvm/test/CodeGen/BPF/BTF/func-nocall-spilled-arg.ll
@@ -0,0 +1,74 @@
+; RUN: llc -mtriple=bpfel -mcpu=v3 -filetype=obj -o %t1 %s
+; RUN: llvm-objcopy --dump-section='.BTF'=%t2 %t1
+; RUN: %python %p/print_btf.py %t2 | FileCheck -check-prefixes=CHECK %s
+
+; DeadArgElimination on a function whose live arguments outnumber the
+; callee-saved registers, so one of them is spilled:
+; static __noinline int sub(int unused, _Bool b, int a1, int a2,
+; int a3, int a4) {
+; sink(a1); sink(a2); sink(a3); sink(a4);
+; return b ? a1 : a2;
+; }
+; 'b' arrives in R1 and is spilled, and the register allocator emits a second
+; DBG_VALUE for the spill slot:
+; DBG_VALUE $w1, $noreg, !"b", !DIExpression(DW_OP_LLVM_convert, ...)
+; STW32 $w1, $r10, -8
+; DBG_VALUE $r10, $noreg, !"b", !DIExpression(DW_OP_constu, 8, DW_OP_minus,
+; DW_OP_deref_size, 4, ...)
+; The second one is neither indirect nor uses a redefined register, so only
+; taking the first DBG_VALUE keeps 'b' bound to R1.
+
+; CHECK: [1] INT 'int' size=4 bits_offset=0 nr_bits=32 encoding=SIGNED
+; CHECK-NEXT: [2] INT '_Bool' size=1 bits_offset=0 nr_bits=8 encoding=BOOL
+; CHECK-NEXT: [3] FUNC_PROTO '(anon)' ret_type_id=1 vlen=5
+; CHECK-NEXT: 'b' type_id=2
+; CHECK-NEXT: 'a1' type_id=1
+; CHECK-NEXT: 'a2' type_id=1
+; CHECK-NEXT: 'a3' type_id=1
+; CHECK-NEXT: 'a4' type_id=1
+; CHECK-NEXT: [4] FUNC 'sub' type_id=3 linkage=static
+
+define internal i32 @sub(i1 zeroext %0, i32 %1, i32 %2, i32 %3, i32 %4) #0 !dbg !7 {
+ #dbg_value(i1 %0, !13, !DIExpression(DW_OP_LLVM_convert, 1, DW_ATE_unsigned, DW_OP_LLVM_convert, 8, DW_ATE_unsigned, DW_OP_stack_value), !19)
+ #dbg_value(i32 %1, !14, !DIExpression(), !19)
+ #dbg_value(i32 %2, !15, !DIExpression(), !19)
+ #dbg_value(i32 %3, !16, !DIExpression(), !19)
+ #dbg_value(i32 %4, !17, !DIExpression(), !19)
+ call void @sink(i32 %1), !dbg !20
+ call void @sink(i32 %2), !dbg !20
+ call void @sink(i32 %3), !dbg !20
+ call void @sink(i32 %4), !dbg !20
+ %6 = select i1 %0, i32 %1, i32 %2, !dbg !20
+ ret i32 %6, !dbg !21
+}
+
+declare void @sink(i32)
+
+attributes #0 = { noinline }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4, !5}
+!llvm.ident = !{!6}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, enums: !2, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "t.c", directory: "/DNE")
+!2 = !{}
+!3 = !{i32 2, !"Dwarf Version", i32 4}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = !{i32 1, !"wchar_size", i32 4}
+!6 = !{!"clang"}
+!7 = distinct !DISubprogram(name: "sub", scope: !1, file: !1, line: 1, type: !8, scopeLine: 1, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !11)
+!8 = !DISubroutineType(cc: DW_CC_nocall, types: !9)
+!9 = !{!10, !10, !18, !10, !10, !10, !10}
+!10 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!11 = !{!12, !13, !14, !15, !16, !17}
+!12 = !DILocalVariable(name: "unused", arg: 1, scope: !7, file: !1, line: 1, type: !10)
+!13 = !DILocalVariable(name: "b", arg: 2, scope: !7, file: !1, line: 1, type: !18)
+!14 = !DILocalVariable(name: "a1", arg: 3, scope: !7, file: !1, line: 1, type: !10)
+!15 = !DILocalVariable(name: "a2", arg: 4, scope: !7, file: !1, line: 1, type: !10)
+!16 = !DILocalVariable(name: "a3", arg: 5, scope: !7, file: !1, line: 1, type: !10)
+!17 = !DILocalVariable(name: "a4", arg: 6, scope: !7, file: !1, line: 1, type: !10)
+!18 = !DIBasicType(name: "_Bool", size: 8, encoding: DW_ATE_boolean)
+!19 = !DILocation(line: 1, column: 1, scope: !7)
+!20 = !DILocation(line: 3, column: 3, scope: !7)
+!21 = !DILocation(line: 4, column: 3, scope: !7)
More information about the llvm-commits
mailing list