[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