Skip to content

Conversation

@yonghong-song
Copy link
Contributor

@yonghong-song yonghong-song commented Feb 19, 2025

The goal is to add additional tag/attribute to dwarf so users will know that function signatures get changed. See [1] for motivation. Otherwise, users may assume function signature remaining the same as its source, and bpf tracing may get wrong results. With explicit tag/attribute in dwarf to indicate a func signature change, for bpf tracing, users will either go into the asm code to find the exact signature or go to find another non-signature-change function for tracing, instead of debugging the otherwise rong results.

Note that this patch is not for BPF backend. Rather it intends to be for x86, arm64, etc since bpf tracing target is a non-bpf arch like x86/arm64/....

Earlier I have a pull request [2] attempts to add suffix to indicate signature change as gcc did this already. But later upstream suggested to use dwarf to encode such suffix change info ([1]).

This patch introduced a new tag LLVM_func_args_changed and a new attr LLVM_func_retval_removed. In DeadArgumentElimination pass, if a function return value is removed, LLVM_func_retval_removed attr will be added to that func in the dwarf. In DeadArgumentElimination and ArgumentPromotion passes, if the function signature is changed, LLVM_func_args_changed tag is added to dwarf. Here, LLVM_func_args_changed tag is used so later on, we could add more debug info about what changes.

Regarding to potential more info under LLVM_func_args_changed, we might need the following info.

  1. Trying to have a new set of formal argument types. The existing types should be available in related Transforms passes, but will need DIBuilder to build DIType's and looks like there is not easy DIBuilder API to do this.
  2. Trying to relate old func signature (from source) to new func signature. For example, original arg index 2 becomes new arg index 1, etc. More complexity will come from argument promotion and struct arguments where struct argument has size greater than an arch register size.

[1] https://discourse.llvm.org/t/rfc-identify-func-signature-change-in-llvm-compiled-kernel-image/82609
[2] #109899

@llvmbot
Copy link
Member

llvmbot commented Feb 19, 2025

@llvm/pr-subscribers-debuginfo
@llvm/pr-subscribers-llvm-ir
@llvm/pr-subscribers-llvm-transforms

@llvm/pr-subscribers-llvm-binary-utilities

Author: None (yonghong-song)

Changes

The goal is to add additional tag/attribute to dwarf so users will know that function signatures get changed. See [1] for motivation. Otherwise, users may assume function signature remaining the same as its source, and bpf tracing may get wrong results. With explicit tag/attribute in dwarf to indicate a func signature change, for bpf tracing, users will either go into the asm code to find the exact signature or go to find another non-signature-change function for tracing, instead of debugging the otherwise rong results.

Earlier I have a pull request [2] attempts to add suffix to indicate signature change as gcc did this already. But later upstream suggested to use dwarf to encode such suffix change info ([1]).

This patch introduced a new tag LLVM_func_args_changed and a new attr LLVM_func_retval_removed. In DeadArgumentElimination pass, if a function return value is removed, LLVM_func_retval_removed attr will be added to that func in the dwarf. In DeadArgumentElimination and ArgumentPromotion passes, if the function signature is changed, LLVM_func_args_changed tag is added to dwarf. Here, LLVM_func_args_changed tag is used so later on, we could add more debug info about what changes.

Regarding to potential more info under LLVM_func_args_changed, we might need the following info.

  1. Trying to have a new set of formal argument types. The existing types should be available in related Transforms passes, but will need DIBuilder to build DIType's and looks like there is not easy DIBuilder API to do this.
  2. Trying to relate old func signature (from source) to new func signature. For example, original arg index 2 becomes new arg index 1, etc. More complexity will come from argument promotion and struct arguments where struct argument has size greater than an arch register size.

[1] https://discourse.llvm.org/t/rfc-identify-func-signature-change-in-llvm-compiled-kernel-image/82609
[2] #109899


Patch is 22.20 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/127855.diff

10 Files Affected:

  • (modified) llvm/include/llvm/BinaryFormat/Dwarf.def (+2)
  • (modified) llvm/include/llvm/IR/DebugInfoFlags.def (+3-1)
  • (modified) llvm/include/llvm/IR/DebugInfoMetadata.h (+5)
  • (modified) llvm/lib/CodeGen/AsmPrinter/DwarfCompileUnit.cpp (+2)
  • (modified) llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp (+11)
  • (modified) llvm/lib/CodeGen/AsmPrinter/DwarfUnit.h (+3)
  • (modified) llvm/lib/Transforms/IPO/ArgumentPromotion.cpp (+8)
  • (modified) llvm/lib/Transforms/IPO/DeadArgumentElimination.cpp (+10)
  • (added) llvm/test/DebugInfo/arg-prom.ll (+157)
  • (added) llvm/test/DebugInfo/arg-retval-elim.ll (+103)
diff --git a/llvm/include/llvm/BinaryFormat/Dwarf.def b/llvm/include/llvm/BinaryFormat/Dwarf.def
index 724a14ccc7aea..de4215e088dd5 100644
--- a/llvm/include/llvm/BinaryFormat/Dwarf.def
+++ b/llvm/include/llvm/BinaryFormat/Dwarf.def
@@ -268,6 +268,7 @@ HANDLE_DW_TAG(0x5111, ALTIUM_rom, 0, ALTIUM, DW_KIND_NONE)
 
 // LLVM
 HANDLE_DW_TAG(0x6000, LLVM_annotation, 0, LLVM, DW_KIND_NONE)
+HANDLE_DW_TAG(0x6001, LLVM_func_args_changed, 0, LLVM, DW_KIND_NONE)
 
 // Green Hills.
 HANDLE_DW_TAG(0x8004, GHS_namespace, 0, GHS, DW_KIND_NONE)
@@ -624,6 +625,7 @@ HANDLE_DW_AT(0x3e09, LLVM_ptrauth_authenticates_null_values, 0, LLVM)
 HANDLE_DW_AT(0x3e0a, LLVM_ptrauth_authentication_mode, 0, LLVM)
 HANDLE_DW_AT(0x3e0b, LLVM_num_extra_inhabitants, 0, LLVM)
 HANDLE_DW_AT(0x3e0c, LLVM_stmt_sequence, 0, LLVM)
+HANDLE_DW_AT(0x3e0d, LLVM_func_retval_removed, 0, LLVM)
 
 // Apple extensions.
 
diff --git a/llvm/include/llvm/IR/DebugInfoFlags.def b/llvm/include/llvm/IR/DebugInfoFlags.def
index df375b6c68e81..e693addab7879 100644
--- a/llvm/include/llvm/IR/DebugInfoFlags.def
+++ b/llvm/include/llvm/IR/DebugInfoFlags.def
@@ -91,11 +91,13 @@ HANDLE_DISP_FLAG((1u << 8), MainSubprogram)
 // for defaulted functions
 HANDLE_DISP_FLAG((1u << 9), Deleted)
 HANDLE_DISP_FLAG((1u << 11), ObjCDirect)
+HANDLE_DISP_FLAG((1u << 12), ArgChanged)
+HANDLE_DISP_FLAG((1u << 13), RetvalRemoved)
 
 #ifdef DISP_FLAG_LARGEST_NEEDED
 // Intended to be used with ADT/BitmaskEnum.h.
 // NOTE: Always must be equal to largest flag, check this when adding new flags.
-HANDLE_DISP_FLAG((1 << 11), Largest)
+HANDLE_DISP_FLAG((1 << 13), Largest)
 #undef DISP_FLAG_LARGEST_NEEDED
 #endif
 
diff --git a/llvm/include/llvm/IR/DebugInfoMetadata.h b/llvm/include/llvm/IR/DebugInfoMetadata.h
index 8515d8eda8568..73d568b5576b4 100644
--- a/llvm/include/llvm/IR/DebugInfoMetadata.h
+++ b/llvm/include/llvm/IR/DebugInfoMetadata.h
@@ -1907,6 +1907,11 @@ class DISubprogram : public DILocalScope {
 
   DIScope *getScope() const { return cast_or_null<DIScope>(getRawScope()); }
 
+  void setArgChanged() { SPFlags |= SPFlagArgChanged; }
+  bool getArgChanged() const { return SPFlags & SPFlagArgChanged; }
+  void setRetvalRemoved() { SPFlags |= SPFlagRetvalRemoved; }
+  bool getRetvalRemoved() const { return SPFlags & SPFlagRetvalRemoved; }
+
   StringRef getName() const { return getStringOperand(2); }
   StringRef getLinkageName() const { return getStringOperand(3); }
   /// Only used by clients of CloneFunction, and only right after the cloning.
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfCompileUnit.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfCompileUnit.cpp
index ddf0275ddfe6a..a80d764683db7 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfCompileUnit.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfCompileUnit.cpp
@@ -1123,6 +1123,8 @@ DIE &DwarfCompileUnit::constructSubprogramScopeDIE(const DISubprogram *Sub,
                                                    MCSymbol *LineTableSym) {
   DIE &ScopeDIE = updateSubprogramScopeDIE(Sub, LineTableSym);
 
+  DwarfUnit::addLLVMChangedArgs(ScopeDIE, Sub);
+
   if (Scope) {
     assert(!Scope->getInlinedAt());
     assert(!Scope->isAbstractScope());
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp
index 5347c8a049ba6..ab97d5c41a2f3 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp
@@ -654,6 +654,14 @@ DIE *DwarfUnit::getOrCreateTypeDIE(const MDNode *TyNode) {
       ->createTypeDIE(Context, *ContextDIE, Ty);
 }
 
+void DwarfUnit::addLLVMChangedArgs(DIE &ScopeDIE, const DISubprogram *SP) {
+  if (!SP->getArgChanged())
+    return;
+
+  auto *LocalDie = DIE::get(DIEValueAllocator, dwarf::DW_TAG_LLVM_func_args_changed);
+  ScopeDIE.addChild(LocalDie);
+}
+
 void DwarfUnit::updateAcceleratorTables(const DIScope *Context,
                                         const DIType *Ty, const DIE &TyDIE) {
   if (Ty->getName().empty())
@@ -1328,6 +1336,9 @@ void DwarfUnit::applySubprogramAttributes(const DISubprogram *SP, DIE &SPDie,
   if (!SkipSPSourceLocation)
     addSourceLine(SPDie, SP);
 
+  if (SP->getRetvalRemoved())
+    addFlag(SPDie, dwarf::DW_AT_LLVM_func_retval_removed);
+
   // Skip the rest of the attributes under -gmlt to save space.
   if (SkipSPAttributes)
     return;
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.h b/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.h
index 9ddd6f8c14175..49724bcfc87ff 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.h
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.h
@@ -324,6 +324,9 @@ class DwarfUnit : public DIEUnit {
   void updateAcceleratorTables(const DIScope *Context, const DIType *Ty,
                                const DIE &TyDIE);
 
+  /// Add DW_TAG_LLVM_func_args_changed.
+  void addLLVMChangedArgs(DIE &ScopeDIE, const DISubprogram *SP);
+
 protected:
   ~DwarfUnit();
 
diff --git a/llvm/lib/Transforms/IPO/ArgumentPromotion.cpp b/llvm/lib/Transforms/IPO/ArgumentPromotion.cpp
index c440638884322..e81adb830a86b 100644
--- a/llvm/lib/Transforms/IPO/ArgumentPromotion.cpp
+++ b/llvm/lib/Transforms/IPO/ArgumentPromotion.cpp
@@ -51,6 +51,7 @@
 #include "llvm/IR/CFG.h"
 #include "llvm/IR/Constants.h"
 #include "llvm/IR/DataLayout.h"
+#include "llvm/IR/DebugInfoMetadata.h"
 #include "llvm/IR/DerivedTypes.h"
 #include "llvm/IR/Dominators.h"
 #include "llvm/IR/Function.h"
@@ -132,6 +133,7 @@ doPromotion(Function *F, FunctionAnalysisManager &FAM,
 
   // First, determine the new argument list
   unsigned ArgNo = 0, NewArgNo = 0;
+  bool CurrFuncArgChanged = false;
   for (Function::arg_iterator I = F->arg_begin(), E = F->arg_end(); I != E;
        ++I, ++ArgNo) {
     if (!ArgsToPromote.count(&*I)) {
@@ -142,6 +144,7 @@ doPromotion(Function *F, FunctionAnalysisManager &FAM,
     } else if (I->use_empty()) {
       // Dead argument (which are always marked as promotable)
       ++NumArgumentsDead;
+      CurrFuncArgChanged = true;
       ORE.emit([&]() {
         return OptimizationRemark(DEBUG_TYPE, "ArgumentRemoved", F)
                << "eliminating argument " << ore::NV("ArgName", I->getName())
@@ -156,6 +159,7 @@ doPromotion(Function *F, FunctionAnalysisManager &FAM,
         ArgAttrVec.push_back(AttributeSet());
       }
       ++NumArgumentsPromoted;
+      CurrFuncArgChanged = true;
       ORE.emit([&]() {
         return OptimizationRemark(DEBUG_TYPE, "ArgumentPromoted", F)
                << "promoting argument " << ore::NV("ArgName", I->getName())
@@ -433,6 +437,10 @@ doPromotion(Function *F, FunctionAnalysisManager &FAM,
     PromoteMemToReg(Allocas, DT, &AC);
   }
 
+  DISubprogram *SP = NF->getSubprogram();
+  if (SP && CurrFuncArgChanged)
+    SP->setArgChanged();
+
   return NF;
 }
 
diff --git a/llvm/lib/Transforms/IPO/DeadArgumentElimination.cpp b/llvm/lib/Transforms/IPO/DeadArgumentElimination.cpp
index ed93b4491c50e..5df3d47daa8dd 100644
--- a/llvm/lib/Transforms/IPO/DeadArgumentElimination.cpp
+++ b/llvm/lib/Transforms/IPO/DeadArgumentElimination.cpp
@@ -757,6 +757,8 @@ bool DeadArgumentEliminationPass::removeDeadStuffFromFunction(Function *F) {
   // a new set of parameter attributes to correspond. Skip the first parameter
   // attribute, since that belongs to the return value.
   unsigned ArgI = 0;
+  bool CurrFuncArgEliminated = false;
+  bool CurrFuncRetEliminated = false;
   for (Function::arg_iterator I = F->arg_begin(), E = F->arg_end(); I != E;
        ++I, ++ArgI) {
     RetOrArg Arg = createArg(F, ArgI);
@@ -767,6 +769,7 @@ bool DeadArgumentEliminationPass::removeDeadStuffFromFunction(Function *F) {
       HasLiveReturnedArg |= PAL.hasParamAttr(ArgI, Attribute::Returned);
     } else {
       ++NumArgumentsEliminated;
+      CurrFuncArgEliminated = true;
 
       ORE.emit([&]() {
         return OptimizationRemark(DEBUG_TYPE, "ArgumentRemoved", F)
@@ -818,6 +821,7 @@ bool DeadArgumentEliminationPass::removeDeadStuffFromFunction(Function *F) {
         NewRetIdxs[Ri] = RetTypes.size() - 1;
       } else {
         ++NumRetValsEliminated;
+        CurrFuncRetEliminated = true;
 
         ORE.emit([&]() {
           return OptimizationRemark(DEBUG_TYPE, "ReturnValueRemoved", F)
@@ -1099,6 +1103,12 @@ bool DeadArgumentEliminationPass::removeDeadStuffFromFunction(Function *F) {
   // to call this function or try to interpret the return value.
   if (NFTy != FTy && NF->getSubprogram()) {
     DISubprogram *SP = NF->getSubprogram();
+
+    if (CurrFuncArgEliminated)
+      SP->setArgChanged();
+    if (CurrFuncRetEliminated)
+      SP->setRetvalRemoved();
+
     auto Temp = SP->getType()->cloneWithCC(llvm::dwarf::DW_CC_nocall);
     SP->replaceType(MDNode::replaceWithPermanent(std::move(Temp)));
   }
diff --git a/llvm/test/DebugInfo/arg-prom.ll b/llvm/test/DebugInfo/arg-prom.ll
new file mode 100644
index 0000000000000..dd8c91ec2438e
--- /dev/null
+++ b/llvm/test/DebugInfo/arg-prom.ll
@@ -0,0 +1,157 @@
+; REQUIRES: x86-registered-target
+; RUN: opt -O3 -S < %s | FileCheck %s
+;
+; Source code:
+;   __attribute__((noinline)) static int is_absolute_path(const char *path)
+;   {
+;     return path[0] == '/';
+;   }
+;
+;   int scpy(char *, const char *, int);
+;   int quit(void);
+;   const char *make_nonrelative_path(char *buf, int sz, const char *path)
+;   {
+;     if (is_absolute_path(path)) {
+;       if (scpy(buf, path, sz) >= sz)
+;         quit();
+;     }
+;     return buf;
+;   }
+; Compilation flag:
+;   clang -O2 -g -S -emit-llvm -Xclang -disable-llvm-passes test.c
+
+; ModuleID = 'test.c'
+source_filename = "test.c"
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; Function Attrs: nounwind uwtable
+define dso_local ptr @make_nonrelative_path(ptr noundef %0, i32 noundef %1, ptr noundef %2) #0 !dbg !9 {
+  %4 = alloca ptr, align 8
+  %5 = alloca i32, align 4
+  %6 = alloca ptr, align 8
+  store ptr %0, ptr %4, align 8, !tbaa !21
+    #dbg_declare(ptr %4, !18, !DIExpression(), !26)
+  store i32 %1, ptr %5, align 4, !tbaa !27
+    #dbg_declare(ptr %5, !19, !DIExpression(), !29)
+  store ptr %2, ptr %6, align 8, !tbaa !21
+    #dbg_declare(ptr %6, !20, !DIExpression(), !30)
+  %7 = load ptr, ptr %6, align 8, !dbg !31, !tbaa !21
+  %8 = call i32 @is_absolute_path(ptr noundef %7), !dbg !33
+  %9 = icmp ne i32 %8, 0, !dbg !33
+  br i1 %9, label %10, label %20, !dbg !33
+
+10:                                               ; preds = %3
+  %11 = load ptr, ptr %4, align 8, !dbg !34, !tbaa !21
+  %12 = load ptr, ptr %6, align 8, !dbg !37, !tbaa !21
+  %13 = load i32, ptr %5, align 4, !dbg !38, !tbaa !27
+  %14 = call i32 @scpy(ptr noundef %11, ptr noundef %12, i32 noundef %13), !dbg !39
+  %15 = load i32, ptr %5, align 4, !dbg !40, !tbaa !27
+  %16 = icmp sge i32 %14, %15, !dbg !41
+  br i1 %16, label %17, label %19, !dbg !41
+
+17:                                               ; preds = %10
+  %18 = call i32 @quit(), !dbg !42
+  br label %19, !dbg !42
+
+19:                                               ; preds = %17, %10
+  br label %20, !dbg !43
+
+20:                                               ; preds = %19, %3
+  %21 = load ptr, ptr %4, align 8, !dbg !44, !tbaa !21
+  ret ptr %21, !dbg !45
+}
+
+; Function Attrs: noinline nounwind uwtable
+define internal i32 @is_absolute_path(ptr noundef %0) #1 !dbg !46 {
+  %2 = alloca ptr, align 8
+  store ptr %0, ptr %2, align 8, !tbaa !21
+    #dbg_declare(ptr %2, !50, !DIExpression(), !51)
+  %3 = load ptr, ptr %2, align 8, !dbg !52, !tbaa !21
+  %4 = getelementptr inbounds i8, ptr %3, i64 0, !dbg !52
+  %5 = load i8, ptr %4, align 1, !dbg !52, !tbaa !53
+  %6 = sext i8 %5 to i32, !dbg !52
+  %7 = icmp eq i32 %6, 47, !dbg !54
+  %8 = zext i1 %7 to i32, !dbg !54
+  ret i32 %8, !dbg !55
+}
+
+; CHECK: define internal fastcc range(i32 0, 2) i32 @is_absolute_path(i8 {{.*}})
+
+declare !dbg !56 i32 @scpy(ptr noundef, ptr noundef, i32 noundef) #2
+
+declare !dbg !59 i32 @quit() #2
+
+attributes #0 = { nounwind uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
+attributes #1 = { noinline nounwind uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
+attributes #2 = { "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3, !4, !5, !6, !7}
+!llvm.ident = !{!8}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, producer: "clang version 21.0.0git ([email protected]:yonghong-song/llvm-project.git bbfd0a15ade80596f6d6dde8add7d50f4875dde1)", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "test.c", directory: "/tmp/tests/sig-change/prom", checksumkind: CSK_MD5, checksum: "1befb35eb4507489630adb56cb20fe09")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !{i32 1, !"wchar_size", i32 4}
+!5 = !{i32 8, !"PIC Level", i32 2}
+!6 = !{i32 7, !"PIE Level", i32 2}
+!7 = !{i32 7, !"uwtable", i32 2}
+!8 = !{!"clang version 21.0.0git ([email protected]:yonghong-song/llvm-project.git bbfd0a15ade80596f6d6dde8add7d50f4875dde1)"}
+!9 = distinct !DISubprogram(name: "make_nonrelative_path", scope: !1, file: !1, line: 8, type: !10, scopeLine: 9, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !17)
+!10 = !DISubroutineType(types: !11)
+!11 = !{!12, !15, !16, !12}
+!12 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !13, size: 64)
+!13 = !DIDerivedType(tag: DW_TAG_const_type, baseType: !14)
+!14 = !DIBasicType(name: "char", size: 8, encoding: DW_ATE_signed_char)
+!15 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !14, size: 64)
+!16 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!17 = !{!18, !19, !20}
+!18 = !DILocalVariable(name: "buf", arg: 1, scope: !9, file: !1, line: 8, type: !15)
+!19 = !DILocalVariable(name: "sz", arg: 2, scope: !9, file: !1, line: 8, type: !16)
+!20 = !DILocalVariable(name: "path", arg: 3, scope: !9, file: !1, line: 8, type: !12)
+!21 = !{!22, !22, i64 0}
+!22 = !{!"p1 omnipotent char", !23, i64 0}
+!23 = !{!"any pointer", !24, i64 0}
+!24 = !{!"omnipotent char", !25, i64 0}
+!25 = !{!"Simple C/C++ TBAA"}
+!26 = !DILocation(line: 8, column: 41, scope: !9)
+!27 = !{!28, !28, i64 0}
+!28 = !{!"int", !24, i64 0}
+!29 = !DILocation(line: 8, column: 50, scope: !9)
+!30 = !DILocation(line: 8, column: 66, scope: !9)
+!31 = !DILocation(line: 10, column: 30, scope: !32)
+!32 = distinct !DILexicalBlock(scope: !9, file: !1, line: 10, column: 13)
+!33 = !DILocation(line: 10, column: 13, scope: !32)
+!34 = !DILocation(line: 11, column: 26, scope: !35)
+!35 = distinct !DILexicalBlock(scope: !36, file: !1, line: 11, column: 21)
+!36 = distinct !DILexicalBlock(scope: !32, file: !1, line: 10, column: 37)
+!37 = !DILocation(line: 11, column: 31, scope: !35)
+!38 = !DILocation(line: 11, column: 37, scope: !35)
+!39 = !DILocation(line: 11, column: 21, scope: !35)
+!40 = !DILocation(line: 11, column: 44, scope: !35)
+!41 = !DILocation(line: 11, column: 41, scope: !35)
+!42 = !DILocation(line: 12, column: 4, scope: !35)
+!43 = !DILocation(line: 13, column: 9, scope: !36)
+!44 = !DILocation(line: 14, column: 16, scope: !9)
+!45 = !DILocation(line: 14, column: 9, scope: !9)
+!46 = distinct !DISubprogram(name: "is_absolute_path", scope: !1, file: !1, line: 1, type: !47, scopeLine: 2, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !49)
+
+; CHECK: distinct !DISubprogram(name: "is_absolute_path", scope: ![[#]], file: ![[#]], line: [[#]], type: ![[#]], scopeLine: [[#]], flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition | DISPFlagOptimized | DISPFlagArgChanged, unit: ![[#]], retainedNodes: ![[#]])
+
+!47 = !DISubroutineType(types: !48)
+!48 = !{!16, !12}
+!49 = !{!50}
+!50 = !DILocalVariable(name: "path", arg: 1, scope: !46, file: !1, line: 1, type: !12)
+!51 = !DILocation(line: 1, column: 67, scope: !46)
+!52 = !DILocation(line: 3, column: 16, scope: !46)
+!53 = !{!24, !24, i64 0}
+!54 = !DILocation(line: 3, column: 24, scope: !46)
+!55 = !DILocation(line: 3, column: 9, scope: !46)
+!56 = !DISubprogram(name: "scpy", scope: !1, file: !1, line: 6, type: !57, flags: DIFlagPrototyped, spFlags: DISPFlagOptimized)
+!57 = !DISubroutineType(types: !58)
+!58 = !{!16, !15, !12, !16}
+!59 = !DISubprogram(name: "quit", scope: !1, file: !1, line: 7, type: !60, flags: DIFlagPrototyped, spFlags: DISPFlagOptimized)
+!60 = !DISubroutineType(types: !61)
+!61 = !{!16}
diff --git a/llvm/test/DebugInfo/arg-retval-elim.ll b/llvm/test/DebugInfo/arg-retval-elim.ll
new file mode 100644
index 0000000000000..abb1ea5e06563
--- /dev/null
+++ b/llvm/test/DebugInfo/arg-retval-elim.ll
@@ -0,0 +1,103 @@
+; REQUIRES: x86-registered-target
+; RUN: opt -O2 -S < %s | FileCheck %s
+;
+; Source code:
+;   int tar(int a);
+;   __attribute__((noinline)) static int foo(int a, int b)
+;   {
+;     return tar(a) + tar(a + 1);
+;   }
+;   int bar(int a)
+;   {
+;     foo(a, 1);
+;     return 0;
+;   }
+; Compilation flag:
+;   clang -O2 -g -S -emit-llvm -Xclang -disable-llvm-passes test.c
+
+; ModuleID = 'test.c'
+source_filename = "test.c"
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; Function Attrs: nounwind uwtable
+define dso_local i32 @bar(i32 noundef %0) #0 !dbg !9 {
+  %2 = alloca i32, align 4
+  store i32 %0, ptr %2, align 4, !tbaa !15
+    #dbg_declare(ptr %2, !14, !DIExpression(), !19)
+  %3 = load i32, ptr %2, align 4, !dbg !20, !tbaa !15
+  %4 = call i32 @foo(i32 noundef %3, i32 noundef 1), !dbg !21
+  ret i32 0, !dbg !22
+}
+
+; Function Attrs: noinline nounwind uwtable
+define internal i32 @foo(i32 noundef %0, i32 noundef %1) #1 !dbg !23 {
+  %3 = alloca i32, align 4
+  %4 = alloca i32, align 4
+  store i32 %0, ptr %3, align 4, !tbaa !15
+    #dbg_declare(ptr %3, !27, !DIExpression(), !29)
+  store i32 %1, ptr %4, align 4, !tbaa !15
+    #dbg_declare(ptr %4, !28, !DIExpression(), !30)
+  %5 = load i32, ptr %3, align 4, !dbg !31, !tbaa !15
+  %6 = call i32 @tar(i32 noundef %5), !dbg !32
+  %7 = load i32, ptr %3, align 4, !dbg !33, !tbaa !15
+  %8 = add nsw i32 %7, 1, !dbg !34
+  %9 = call i32 @tar(i32 noundef %8), !dbg !35
+  %10 = add nsw i32 %6, %9, !dbg !36
+  ret i32 %10, !dbg !37
+}
+
+; CHECK: define internal fastcc void @foo(i32 noundef %0)
+
+declare !dbg !38 i32 @tar(i32 noundef) #2
+
+attributes #0 = { nounwind uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
+attributes #1 = { noinline nounwind uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
+attributes #2 = { "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3, !4, !5, !6, !7}
+!llvm.ident = !{!8}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, producer: "clang version 21.0.0git ([email protected]:yonghong-song/llvm-project.git 95c942ca729f6f240c408ceeb39d2d5f10a3b0d5)", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "test.c", directory: "/tmp/tests/sig-change/deadret", checksumkind: CSK_MD5, checksum: "728d225e6425c104712ae21cee1db99b")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !{i32 1, !"wchar_size", i32 4}
+!5 = !{i32 8, !"PIC Level", i32 2}
+!6 = !{i32 7, !"PIE Level", i32 2}
+!7 = !{i32 7, !"uwtable", i32 2}
+!8 = !{!"clang version 21.0.0git ([email protected]:yonghong-song/llvm-p...
[truncated]

@github-actions
Copy link

github-actions bot commented Feb 19, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@yonghong-song
Copy link
Contributor Author

cc @anakryiko @jemarch

store ptr %2, ptr %6, align 8, !tbaa !21
#dbg_declare(ptr %6, !20, !DIExpression(), !30)
%7 = load ptr, ptr %6, align 8, !dbg !31, !tbaa !21
%8 = call i32 @is_absolute_path(ptr noundef %7), !dbg !33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use named values in tests

@@ -0,0 +1,157 @@
; REQUIRES: x86-registered-target
; RUN: opt -O3 -S < %s | FileCheck %s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests should be minimal and run the one pass


; Function Attrs: nounwind uwtable
define dso_local ptr @make_nonrelative_path(ptr noundef %0, i32 noundef %1, ptr noundef %2) #0 !dbg !9 {
%4 = alloca ptr, align 8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should be simplified to show a minimal example

Comment on lines 23 to 25
; ModuleID = 'test.c'
source_filename = "test.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can drop these

@@ -0,0 +1,103 @@
; REQUIRES: x86-registered-target
; RUN: opt -O2 -S < %s | FileCheck %s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't run the full pipeline

@@ -0,0 +1,103 @@
; REQUIRES: x86-registered-target
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move to the x86 test subdirectory instead

Comment on lines 54 to 56
attributes #0 = { nounwind uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #1 = { noinline nounwind uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #2 = { "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove unnecessary attributes

@yonghong-song
Copy link
Contributor Author

Thanks @arsenm I will try to address your all above comments in the next revision. Thanks for reviewing!

define internal fastcc i32 @foo(i32 noundef %a, i32 noundef %b) unnamed_addr #1 !dbg !19 {
#dbg_value(i32 %a, !23, !DIExpression(), !25)
#dbg_value(i32 %b, !24, !DIExpression(), !25)
%3 = tail call i32 @tar(i32 noundef %a), !dbg !26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use named values in tests

@@ -0,0 +1,76 @@
; RUN: opt -S -passes='default<O2>,deadargelim' < %s | FileCheck %s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not run full -O2 pipeline

@@ -0,0 +1,95 @@
; RUN: opt -S -passes='default<O3>,argpromotion' < %s | FileCheck %s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not run full pass pipeline

define dso_local ptr @make_nonrelative_path(ptr noundef %buf, ptr noundef %path) local_unnamed_addr #0 !dbg !10 {
#dbg_value(ptr %buf, !18, !DIExpression(), !20)
#dbg_value(ptr %path, !19, !DIExpression(), !20)
%3 = call fastcc i32 @is_absolute_path(ptr noundef %path), !dbg !21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use named values in tests

Copy link
Collaborator

@pogo59 pogo59 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to do this unconditionally? If BPF is the only identified consumer, maybe not. But the size cost is low, so maybe not worth the trouble to make it conditioned on tuning or some other flag.
@dwblaikie @adrian-prantl

auto *LocalDie =
DIE::get(DIEValueAllocator, dwarf::DW_TAG_LLVM_func_args_changed);
ScopeDIE.addChild(LocalDie);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// TODO: Add specifics about what changed.
This will clarify that you expect to add more info later, and that's why it's a tag not an attribute. (Which was my first question when reading the code. It took a more careful reading of the commit message to understand the longer-term goal.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct. The tag intends for future extensibility.

The goal is to add additional tag/attribute to dwarf so users will know
that function signatures get changed. See [1] for motivation. Otherwise,
users may assume function signature remaining the same as its source,
and bpf tracing may get wrong results. With explicit tag/attribute in
dwarf to indicate a func signature change, for bpf tracing, users will
either go into the asm code to find the exact signature or go to find
another non-signature-change function for tracing, instead of debugging
the otherwise rong results.

Earlier I have a pull request [2] attempts to add suffix to indicate
signature change as gcc did this already. But later upstream suggested
to use dwarf to encode such suffix change info ([1]).

This patch introduced a new tag LLVM_func_args_changed and a new
attr LLVM_func_retval_removed. In DeadArgumentElimination pass, if
a function return value is removed, LLVM_func_retval_removed attr will
be added to that func in the dwarf. In DeadArgumentElimination and
ArgumentPromotion passes, if the function signature is changed,
LLVM_func_args_changed tag is added to dwarf. Here, LLVM_func_args_changed
tag is used so later on, we could add more debug info about what changes.

Regarding to potential more info under LLVM_func_args_changed, we might need
the following info.
  1. Trying to have a new set of formal argument types. The existing types
     should be available in related Transforms passes, but will need DIBuilder
     to build DIType's and looks like there is not easy DIBuilder API to do this.
  2. Trying to relate old func signature (from source) to new func signature.
     For example, original arg index 2 becomes new arg index 1, etc.
     More complexity will come from argument promotion and struct arguments
     where struct argument has size greater than an arch register size.

  [1] https://discourse.llvm.org/t/rfc-identify-func-signature-change-in-llvm-compiled-kernel-image/82609
  [2] llvm#109899
@yonghong-song
Copy link
Contributor Author

Do we want to do this unconditionally? If BPF is the only identified consumer, maybe not. But the size cost is low, so maybe not worth the trouble to make it conditioned on tuning or some other flag. @dwblaikie @adrian-prantl

Actually no. The intended consumer is x86, aarch64, etc. bpf is able to trace functions in those architectures to gather information inside the kernel. The signature change info (in dwarf) will help bpf users to correctly write their progs in order to trace those functions.

Ideally, bpf could like accurate function prototypes after transformation for those x86 etc. architectures. But the first step would to recognize that there is a signature change so users can either avoid such functions or try to dig into the asm codes by themselves to find true signature.

@ayermolo
Copy link
Contributor

Do we want to do this unconditionally? If BPF is the only identified consumer, maybe not. But the size cost is low, so maybe not worth the trouble to make it conditioned on tuning or some other flag. @dwblaikie @adrian-prantl

Actually no. The intended consumer is x86, aarch64, etc. bpf is able to trace functions in those architectures to gather information inside the kernel. The signature change info (in dwarf) will help bpf users to correctly write their progs in order to trace those functions.

Ideally, bpf could like accurate function prototypes after transformation for those x86 etc. architectures. But the first step would to recognize that there is a signature change so users can either avoid such functions or try to dig into the asm codes by themselves to find true signature.

I think what @pogo59 meant (please correct me if I am wrong) are consumers like lldb, gdb, BOLT, various other utilities that process dwarf information. If this TAG becomes "standard" for optimized builds, then I believe all those DWARF consumers will need to be aware of a new TAG.

@alx32 alx32 removed their request for review February 21, 2025 18:30
@pogo59
Copy link
Collaborator

pogo59 commented Feb 21, 2025

@ayermolo is correct about what I meant.

It's an interesting case, and other consumers may well want to know about it. Exploring this with a vendor-specific implementation is the correct way forward. If it turns out to be genuinely useful, it would be worth proposing as a new feature in DWARF v6.

Right now, though, my question is basically, how much size increase do we see in the DWARF with this new feature? If it's very small (maybe the optimization does not fire very often), then there's not much harm in emitting the info all the time, as the patch currently implements it. If there's a noticeable increase, though, that's a concern for some vendors and we might want to limit the situations where the info is emitted.

@dwblaikie
Copy link
Collaborator

Could we use only the DW_AT_calling_convention with DW_CC_nocall for this? (without the nuance/differentiation between return values and parameters, and lets us use the standard feature)

yonghong-song pushed a commit to yonghong-song/llvm-project that referenced this pull request Sep 8, 2025
Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions including functions with signature change
and new functions.

The previous approach in [1] tries to add debuginfo for those
optimization passes which cause signature changes. Based on
discussion in [1], it is preferred to have a specific pass to
add debuginfo and later on dwarf generation can include those
new debuginfo.

The ultimate goal is to add new information to dwarf like below:

  DW_TAG_compile_unit
    ...
    // New functions with suffix
    DW_TAG_inlined_subroutine
      DW_AT_name      ("foo.1")
      DW_AT_type      (0x0000000000000091 "int")
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

      DW_TAG_formal_parameter
        DW_AT_name    ("b")
        DW_AT_type    (0x0000000000000091 "int")

      DW_TAG_formal_parameter
        DW_AT_name    ("c")
        DW_AT_type    (0x0000000000000095 "long")

    ...
    // Functions with changed signatures
    DW_TAG_inlined_subroutine
      DW_AT_name      ("bar")
      DW_AT_type      (0x0000000000000091 "int")
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

      DW_TAG_formal_parameter
        DW_AT_name    ("c")
        DW_AT_type    (0x0000000000000095 "unsigned int")

    ...
    // Functions not obtained function changed signatures yet
    // The DW_CC_nocall presence indicates such cases.
    DW_TAG_inlined_subroutine
      DW_AT_name      ("bar" or "bar.1")
      DW_AT_calling_convention        (DW_CC_nocall)
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

The parent tag of above DW_TAG_inlined_subroutine is
DW_TAG_compile_unit. This is a new feature for dwarf
so it won't cause issues with existing dwarf related tools.
Total three patterns are introduced as the above.
  . New functions with suffix, e.g., 'foo.1' or 'foo.llvm.<hash>'.
  . Functions with changed signature due to ArgumentPromotion
    or DeadArgumentElimination.
  . Functions the current implementation cannot get proper
    signature. For this case, DW_CC_nocall is set to indicate
    signature is lost. More details in the below.

A special CompileUnit with file name "<artificial>" is created
to hold special DISubprograms for the above three kinds of functions.
During actual dwarf generation, these special DISubprograms
will turn to above to proper DW_TAG_inlined_subroutine tags.

The below are some discussions with not handled cases and
some other alternative things:
 (1) Currently, there are three not handled signature changes.
     . During to ArgumentPromotion, we may have
       foo(..., struct foo *p, ...) => foo(..., int p.0.val, int p.4.val, ...)
     . Struct argument which expands to two actual arguments,
       foo(..., struct foo v, ...) => foo(..., v.coerce0, v.coerce1, ...)
     . Struct argument changed to struct pointer,
       foo(..., struct foo v, ...) => foo(..., struct foo *p, ...)
     I think by utilizing dbg_value/dbg_declare and instructions, we
     might be able to resolve the above and get proper signature.
     But any suggestions are welcome.
 (2) Currently, I am using a special CompileUnit "<artificial>" to hold
     newly created DISubprograms. But there is an alternative.
     For example, "llvm.dbg.cu" metadata is used to hold all CompileUnits.
     We could introduce "llvm.dbg.sp.extra" to hold all new
     DISubprograms instead of a new CompileUnit.
 (3) Currently, only C lang is supported. If C++ is supported,
     during llvm-project pull request test, a lot of things need
     change and I have not figured out how to do it properly.
     Also, our use case is to build linux kernel with additional
     information for changed or new functions. The linux kernel
     uses C language. But if C++ side is not working due to my
     implementation, please let me know and I am happy to improve it.

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:
  65288 original number of functions
  910   new functions with this patch (including DW_CC_nocall case)
  7     new functions without signatures (with DW_CC_nocall)
For thin-lto case:
  65541 original number of functions
  2324  new functions with this patch (including DW_CC_nocall case)
  14    new functions without signatures (with DW_CC_nocall)

The following are some examples with thinlto with generated dwarf:

  ...
  0x0001707f:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("msr_build_context")
                  DW_AT_type      (0x00004163 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000440b "msr_build_context")

  0x0001708b:     DW_TAG_formal_parameter
                    DW_AT_name    ("msr_id")
                    DW_AT_type    (0x0000e55c "const u32 *")

  0x00017093:     NULL
  ...
  0x004225e5:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("__die_body.llvm.14794269134614576759")
                  DW_AT_type      (0x00418a14 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x00422348 "__die_body")

  0x004225f1:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x004181f3 "const char *")

  0x004225f9:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x00419118 "pt_regs *")

  0x00422601:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x0041af2f "long")

  0x00422609:     NULL
  ...
  0x013f5dac:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("devkmsg_emit")
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x013ef75b "devkmsg_emit")

  [1] llvm#127855
yonghong-song pushed a commit to yonghong-song/llvm-project that referenced this pull request Sep 18, 2025
Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions including functions with signature change
and new functions.

The previous approach in [1] tries to add debuginfo for those
optimization passes which cause signature changes. Based on
discussion in [1], it is preferred to have a specific pass to
add debuginfo and later on dwarf generation can include those
new debuginfo.

The ultimate goal is to add new information to dwarf like below:

  DW_TAG_compile_unit
    ...
    // New functions with suffix
    DW_TAG_inlined_subroutine
      DW_AT_name      ("foo.1")
      DW_AT_type      (0x0000000000000091 "int")
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

      DW_TAG_formal_parameter
        DW_AT_name    ("b")
        DW_AT_type    (0x0000000000000091 "int")

      DW_TAG_formal_parameter
        DW_AT_name    ("c")
        DW_AT_type    (0x0000000000000095 "long")

    ...
    // Functions with changed signatures
    DW_TAG_inlined_subroutine
      DW_AT_name      ("bar")
      DW_AT_type      (0x0000000000000091 "int")
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

      DW_TAG_formal_parameter
        DW_AT_name    ("c")
        DW_AT_type    (0x0000000000000095 "unsigned int")

    ...
    // Functions not obtained function changed signatures yet
    // The DW_CC_nocall presence indicates such cases.
    DW_TAG_inlined_subroutine
      DW_AT_name      ("bar" or "bar.1")
      DW_AT_calling_convention        (DW_CC_nocall)
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

The parent tag of above DW_TAG_inlined_subroutine is
DW_TAG_compile_unit. This is a new feature for dwarf
so it won't cause issues with existing dwarf related tools.
Total three patterns are introduced as the above.
  . New functions with suffix, e.g., 'foo.1' or 'foo.llvm.<hash>'.
  . Functions with changed signature due to ArgumentPromotion
    or DeadArgumentElimination.
  . Functions the current implementation cannot get proper
    signature. For this case, DW_CC_nocall is set to indicate
    signature is lost. More details in the below.

A special CompileUnit with file name "<artificial>" is created
to hold special DISubprograms for the above three kinds of functions.
During actual dwarf generation, these special DISubprograms
will turn to above to proper DW_TAG_inlined_subroutine tags.

The below are some discussions with not handled cases and
some other alternative things:
 (1) Currently, there are three not handled signature changes.
     . During to ArgumentPromotion, we may have
       foo(..., struct foo *p, ...) => foo(..., int p.0.val, int p.4.val, ...)
     . Struct argument which expands to two actual arguments,
       foo(..., struct foo v, ...) => foo(..., v.coerce0, v.coerce1, ...)
     . Struct argument changed to struct pointer,
       foo(..., struct foo v, ...) => foo(..., struct foo *p, ...)
     I think by utilizing dbg_value/dbg_declare and instructions, we
     might be able to resolve the above and get proper signature.
     But any suggestions are welcome.
 (2) Currently, I am using a special CompileUnit "<artificial>" to hold
     newly created DISubprograms. But there is an alternative.
     For example, "llvm.dbg.cu" metadata is used to hold all CompileUnits.
     We could introduce "llvm.dbg.sp.extra" to hold all new
     DISubprograms instead of a new CompileUnit.
 (3) Currently, only C lang is supported. If C++ is supported,
     during llvm-project pull request test, a lot of things need
     change and I have not figured out how to do it properly.
     Also, our use case is to build linux kernel with additional
     information for changed or new functions. The linux kernel
     uses C language. But if C++ side is not working due to my
     implementation, please let me know and I am happy to improve it.

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:
  65288 original number of functions
  910   new functions with this patch (including DW_CC_nocall case)
  7     new functions without signatures (with DW_CC_nocall)
For thin-lto case:
  65541 original number of functions
  2324  new functions with this patch (including DW_CC_nocall case)
  14    new functions without signatures (with DW_CC_nocall)

The following are some examples with thinlto with generated dwarf:

  ...
  0x0001707f:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("msr_build_context")
                  DW_AT_type      (0x00004163 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000440b "msr_build_context")

  0x0001708b:     DW_TAG_formal_parameter
                    DW_AT_name    ("msr_id")
                    DW_AT_type    (0x0000e55c "const u32 *")

  0x00017093:     NULL
  ...
  0x004225e5:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("__die_body.llvm.14794269134614576759")
                  DW_AT_type      (0x00418a14 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x00422348 "__die_body")

  0x004225f1:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x004181f3 "const char *")

  0x004225f9:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x00419118 "pt_regs *")

  0x00422601:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x0041af2f "long")

  0x00422609:     NULL
  ...
  0x013f5dac:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("devkmsg_emit")
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x013ef75b "devkmsg_emit")

  [1] llvm#127855
yonghong-song pushed a commit to yonghong-song/llvm-project that referenced this pull request Sep 18, 2025
Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions including functions with signature change
and new functions.

The previous approach in [1] tries to add debuginfo for those
optimization passes which cause signature changes. Based on
discussion in [1], it is preferred to have a specific pass to
add debuginfo and later on dwarf generation can include those
new debuginfo.

The ultimate goal is to add new information to dwarf like below:

  DW_TAG_compile_unit
    ...
    // New functions with suffix
    DW_TAG_inlined_subroutine
      DW_AT_name      ("foo.1")
      DW_AT_type      (0x0000000000000091 "int")
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

      DW_TAG_formal_parameter
        DW_AT_name    ("b")
        DW_AT_type    (0x0000000000000091 "int")

      DW_TAG_formal_parameter
        DW_AT_name    ("c")
        DW_AT_type    (0x0000000000000095 "long")

    ...
    // Functions with changed signatures
    DW_TAG_inlined_subroutine
      DW_AT_name      ("bar")
      DW_AT_type      (0x0000000000000091 "int")
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

      DW_TAG_formal_parameter
        DW_AT_name    ("c")
        DW_AT_type    (0x0000000000000095 "unsigned int")

The new functions will not include those functions whose
return value is a struct/union or the function has
variable arguments.

In rare cases, if DIExpression is complex and not handled
by this pull request, the following dwarf entry will
be issued:
    // The DW_CC_nocall presence indicates such cases.
    DW_TAG_inlined_subroutine
      DW_AT_name      ("bar" or "bar.1")
      DW_AT_calling_convention        (DW_CC_nocall)
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

The parent tag of above DW_TAG_inlined_subroutine is
DW_TAG_compile_unit. This is a new feature for dwarf
so it won't cause issues with existing dwarf related tools.
Total three patterns are introduced as the above.
  . New functions with suffix, e.g., 'foo.1' or 'foo.llvm.<hash>'.
  . Functions with changed signature due to ArgumentPromotion
    or DeadArgumentElimination.
  . Functions the current implementation cannot get proper
    signature in which case, DW_CC_nocall is set to indicate
    signature is lost.

A special CompileUnit with file name "<artificial>" is created
to hold special DISubprograms for the above three kinds of functions.
During actual dwarf generation, these special DISubprograms
will turn to above to proper DW_TAG_inlined_subroutine tags.

The below are some discussions with not handled cases and
some other alternative things:
 (1) Current implementation only supports C language and only
     supports 64bit architecture as this particularly needed
     for linux kernel.
 (2) Currently, I am using a special CompileUnit "<artificial>" to hold
     newly created DISubprograms. But there is an alternative.
     For example, "llvm.dbg.cu" metadata is used to hold all CompileUnits.
     We could introduce "llvm.dbg.sp.extra" to hold all new
     DISubprograms instead of a new CompileUnit.

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:
  65341 original number of functions
  1082  new functions with this patch
For thin-lto case:
  65595 original number of functions
  2484  new functions with this patch

For a particular linux kernel with bpf-next tree, There are no
new functions with DW_CC_nocall. That is, all new functions have
proper signatures.

The following are some examples with thinlto with generated dwarf:

  ...
  0x0001707f:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("msr_build_context")
                  DW_AT_type      (0x00004163 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000440b "msr_build_context")

  0x0001708b:     DW_TAG_formal_parameter
                    DW_AT_name    ("msr_id")
                    DW_AT_type    (0x0000e55c "const u32 *")

  0x00017093:     NULL
  ...
  0x004225e5:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("__die_body.llvm.14794269134614576759")
                  DW_AT_type      (0x00418a14 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x00422348 "__die_body")

  0x004225f1:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x004181f3 "const char *")

  0x004225f9:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x00419118 "pt_regs *")

  0x00422601:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x0041af2f "long")

  0x00422609:     NULL
  ...

  [1] llvm#127855
yonghong-song pushed a commit to yonghong-song/llvm-project that referenced this pull request Sep 19, 2025
Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions including functions with signature change
and new functions.

The previous approach in [1] tries to add debuginfo for those
optimization passes which cause signature changes. Based on
discussion in [1], it is preferred to have a specific pass to
add debuginfo and later on dwarf generation can include those
new debuginfo.

The ultimate goal is to add new information to dwarf like below:

  DW_TAG_compile_unit
    ...
    // New functions with suffix
    DW_TAG_inlined_subroutine
      DW_AT_name      ("foo.1")
      DW_AT_type      (0x0000000000000091 "int")
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

      DW_TAG_formal_parameter
        DW_AT_name    ("b")
        DW_AT_type    (0x0000000000000091 "int")

      DW_TAG_formal_parameter
        DW_AT_name    ("c")
        DW_AT_type    (0x0000000000000095 "long")

    ...
    // Functions with changed signatures
    DW_TAG_inlined_subroutine
      DW_AT_name      ("bar")
      DW_AT_type      (0x0000000000000091 "int")
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

      DW_TAG_formal_parameter
        DW_AT_name    ("c")
        DW_AT_type    (0x0000000000000095 "unsigned int")

The new functions will not include those functions whose
return value is a struct/union or the function has
variable arguments.

In rare cases, if DIExpression is complex and not handled
by this pull request, the following dwarf entry will
be issued:
    // The DW_CC_nocall presence indicates such cases.
    DW_TAG_inlined_subroutine
      DW_AT_name      ("bar" or "bar.1")
      DW_AT_calling_convention        (DW_CC_nocall)
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

The parent tag of above DW_TAG_inlined_subroutine is
DW_TAG_compile_unit. This is a new feature for dwarf
so it won't cause issues with existing dwarf related tools.
Total three patterns are introduced as the above.
  . New functions with suffix, e.g., 'foo.1' or 'foo.llvm.<hash>'.
  . Functions with changed signature due to ArgumentPromotion
    or DeadArgumentElimination.
  . Functions the current implementation cannot get proper
    signature in which case, DW_CC_nocall is set to indicate
    signature is lost.

A special CompileUnit with file name "<artificial>" is created
to hold special DISubprograms for the above three kinds of functions.
During actual dwarf generation, these special DISubprograms
will turn to above to proper DW_TAG_inlined_subroutine tags.

The below are some discussions with not handled cases and
some other alternative things:
 (1) Current implementation only supports C language and also filters
     out functions whose return value is a struct/union and the
     return value size is greater than architecture register size.
 (2) Currently, I am using a special CompileUnit "<artificial>" to hold
     newly created DISubprograms. But there is an alternative.
     For example, "llvm.dbg.cu" metadata is used to hold all CompileUnits.
     We could introduce "llvm.dbg.sp.extra" to hold all new
     DISubprograms instead of a new CompileUnit.

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:
  65341 original number of functions
  1085  new functions with this patch
For thin-lto case:
  65595 original number of functions
  2492  new functions with this patch

For a particular linux kernel with bpf-next tree, There are no
new functions with DW_CC_nocall. That is, all new functions have
proper signatures.

The following are some examples with thinlto with generated dwarf:

  ...
  0x0001707f:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("msr_build_context")
                  DW_AT_type      (0x00004163 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000440b "msr_build_context")

  0x0001708b:     DW_TAG_formal_parameter
                    DW_AT_name    ("msr_id")
                    DW_AT_type    (0x0000e55c "const u32 *")

  0x00017093:     NULL
  ...
  0x004225e5:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("__die_body.llvm.14794269134614576759")
                  DW_AT_type      (0x00418a14 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x00422348 "__die_body")

  0x004225f1:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x004181f3 "const char *")

  0x004225f9:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x00419118 "pt_regs *")

  0x00422601:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x0041af2f "long")

  0x00422609:     NULL
  ...

  [1] llvm#127855
yonghong-song pushed a commit to yonghong-song/llvm-project that referenced this pull request Oct 4, 2025
Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions including functions with signature change
and new functions.

The previous approach in [1] tries to add debuginfo for those
optimization passes which cause signature changes. Based on
discussion in [1], it is preferred to have a specific pass to
add debuginfo and later on dwarf generation can include those
new debuginfo.

The following is an example:
Source:
  $ cat test.c
  struct t { int a; };
  char *tar(struct t *a, struct t *d);
  __attribute__((noinline)) static char * foo(struct t *a, struct t *d, int b)
  {
    return tar(a, d);
  }
  char *bar(struct t *a, struct t *d)
  {
    return foo(a, d, 1);
  }
Compiled and dump dwarf with:
  clang -O2 -c -g test.c
  llvm-dwarfdump test.o

0x0000005c:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000000010)
                DW_AT_high_pc   (0x0000000000000015)
                DW_AT_frame_base        (DW_OP_reg7 RSP)
                DW_AT_linkage_name      ("foo")
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                DW_AT_decl_line (3)
                DW_AT_type      (0x000000bb "char *")
                DW_AT_artificial        (true)
                DW_AT_external  (true)

0x0000006c:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg5 RDI)
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x00000075:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg4 RSI)
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x0000007e:     DW_TAG_inlined_subroutine
                  DW_AT_abstract_origin (0x0000009a "foo")
                  DW_AT_low_pc  (0x0000000000000010)
                  DW_AT_high_pc (0x0000000000000015)
                  DW_AT_call_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_call_line       (0)

0x0000008a:       DW_TAG_formal_parameter
                    DW_AT_location      (DW_OP_reg5 RDI)
                    DW_AT_abstract_origin       (0x000000a2 "a")

0x00000091:       DW_TAG_formal_parameter
                    DW_AT_location      (DW_OP_reg4 RSI)
                    DW_AT_abstract_origin       (0x000000aa "d")

0x00000098:       NULL

0x00000099:     NULL

0x0000009a:   DW_TAG_subprogram
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                DW_AT_decl_line (3)
                DW_AT_prototyped        (true)
                DW_AT_type      (0x000000bb "char *")
                DW_AT_inline    (DW_INL_inlined)

0x000000a2:     DW_TAG_formal_parameter
                  DW_AT_name    ("a")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x000000aa:     DW_TAG_formal_parameter
                  DW_AT_name    ("d")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x000000b2:     DW_TAG_formal_parameter
                  DW_AT_name    ("b")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000d8 "int")

0x000000ba:     NULL

There are some restrictions in the current implementation:
  - Only C language is supported
  - BPF target is excluded as one of main goals for this pull request
    is to generate proper vmlinux BTF for arch's like x86_64/arm64 etc.
  - Function must not be a intrinsic, decl only, return value size more
    than arch register size and func with variable arguments.

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:
  65341 original number of functions
  1085  new functions with this patch
For thin-lto case:
  65595 original number of functions
  2492  new functions with this patch

  [1] llvm#127855
yonghong-song pushed a commit to yonghong-song/llvm-project that referenced this pull request Oct 12, 2025
Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions including functions with signature change
and new functions.

The previous approach in [1] tries to add debuginfo for those
optimization passes which cause signature changes. Based on
discussion in [1], it is preferred to have a specific pass to
add debuginfo and later on dwarf generation can include those
new debuginfo.

The following is an example:
Source:
  $ cat test.c
  struct t { int a; };
  char *tar(struct t *a, struct t *d);
  __attribute__((noinline)) static char * foo(struct t *a, struct t *d, int b)
  {
    return tar(a, d);
  }
  char *bar(struct t *a, struct t *d)
  {
    return foo(a, d, 1);
  }
Compiled and dump dwarf with:
  clang -O2 -c -g test.c
  llvm-dwarfdump test.o

0x0000005c:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000000010)
                DW_AT_high_pc   (0x0000000000000015)
                DW_AT_frame_base        (DW_OP_reg7 RSP)
                DW_AT_linkage_name      ("foo")
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                DW_AT_decl_line (3)
                DW_AT_type      (0x000000bb "char *")
                DW_AT_artificial        (true)
                DW_AT_external  (true)

0x0000006c:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg5 RDI)
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x00000075:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg4 RSI)
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x0000007e:     DW_TAG_inlined_subroutine
                  DW_AT_abstract_origin (0x0000009a "foo")
                  DW_AT_low_pc  (0x0000000000000010)
                  DW_AT_high_pc (0x0000000000000015)
                  DW_AT_call_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_call_line       (0)

0x0000008a:       DW_TAG_formal_parameter
                    DW_AT_location      (DW_OP_reg5 RDI)
                    DW_AT_abstract_origin       (0x000000a2 "a")

0x00000091:       DW_TAG_formal_parameter
                    DW_AT_location      (DW_OP_reg4 RSI)
                    DW_AT_abstract_origin       (0x000000aa "d")

0x00000098:       NULL

0x00000099:     NULL

0x0000009a:   DW_TAG_subprogram
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                DW_AT_decl_line (3)
                DW_AT_prototyped        (true)
                DW_AT_type      (0x000000bb "char *")
                DW_AT_inline    (DW_INL_inlined)

0x000000a2:     DW_TAG_formal_parameter
                  DW_AT_name    ("a")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x000000aa:     DW_TAG_formal_parameter
                  DW_AT_name    ("d")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x000000b2:     DW_TAG_formal_parameter
                  DW_AT_name    ("b")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000d8 "int")

0x000000ba:     NULL

There are some restrictions in the current implementation:
  - Only C language is supported
  - BPF target is excluded as one of main goals for this pull request
    is to generate proper vmlinux BTF for arch's like x86_64/arm64 etc.
  - Function must not be a intrinsic, decl only, return value size more
    than arch register size and func with variable arguments.

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:
  65341 original number of functions
  1085  new functions with this patch
For thin-lto case:
  65595 original number of functions
  2492  new functions with this patch

  [1] llvm#127855
yonghong-song pushed a commit to yonghong-song/llvm-project that referenced this pull request Oct 20, 2025
Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions including functions with signature change
and new functions.

The previous approach in [1] tries to add debuginfo for those
optimization passes which cause signature changes. Based on
discussion in [1], it is preferred to have a specific pass to
add debuginfo and later on dwarf generation can include those
new debuginfo.

The following is an example:
Source:
  $ cat test.c
  struct t { int a; };
  char *tar(struct t *a, struct t *d);
  __attribute__((noinline)) static char * foo(struct t *a, struct t *d, int b)
  {
    return tar(a, d);
  }
  char *bar(struct t *a, struct t *d)
  {
    return foo(a, d, 1);
  }
Compiled and dump dwarf with:
  clang -O2 -c -g test.c
  llvm-dwarfdump test.o

0x0000005c:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000000010)
                DW_AT_high_pc   (0x0000000000000015)
                DW_AT_frame_base        (DW_OP_reg7 RSP)
                DW_AT_linkage_name      ("foo")
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                DW_AT_decl_line (3)
                DW_AT_type      (0x000000bb "char *")
                DW_AT_artificial        (true)
                DW_AT_external  (true)

0x0000006c:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg5 RDI)
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x00000075:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg4 RSI)
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x0000007e:     DW_TAG_inlined_subroutine
                  DW_AT_abstract_origin (0x0000009a "foo")
                  DW_AT_low_pc  (0x0000000000000010)
                  DW_AT_high_pc (0x0000000000000015)
                  DW_AT_call_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_call_line       (0)

0x0000008a:       DW_TAG_formal_parameter
                    DW_AT_location      (DW_OP_reg5 RDI)
                    DW_AT_abstract_origin       (0x000000a2 "a")

0x00000091:       DW_TAG_formal_parameter
                    DW_AT_location      (DW_OP_reg4 RSI)
                    DW_AT_abstract_origin       (0x000000aa "d")

0x00000098:       NULL

0x00000099:     NULL

0x0000009a:   DW_TAG_subprogram
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                DW_AT_decl_line (3)
                DW_AT_prototyped        (true)
                DW_AT_type      (0x000000bb "char *")
                DW_AT_inline    (DW_INL_inlined)

0x000000a2:     DW_TAG_formal_parameter
                  DW_AT_name    ("a")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x000000aa:     DW_TAG_formal_parameter
                  DW_AT_name    ("d")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x000000b2:     DW_TAG_formal_parameter
                  DW_AT_name    ("b")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000d8 "int")

0x000000ba:     NULL

There are some restrictions in the current implementation:
  - Only C language is supported
  - BPF target is excluded as one of main goals for this pull request
    is to generate proper vmlinux BTF for arch's like x86_64/arm64 etc.
  - Function must not be a intrinsic, decl only, return value size more
    than arch register size and func with variable arguments.

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:
  65341 original number of functions
  1085  new functions with this patch
For thin-lto case:
  65595 original number of functions
  2492  new functions with this patch

  [1] llvm#127855
yonghong-song pushed a commit to yonghong-song/llvm-project that referenced this pull request Oct 27, 2025
Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions whose signatures are changed during compiler
transformations.

The original intention is for bpf-based linux kernel tracing.
The function signature is available in vmlinux BTF generated
from pahole/dwarf. Such signature is generated from dwarf
at the source level. But this is not ideal since some function
may have signatures changed. If user still used the source
level signature, users may not get correct results and may
need some efforts to workaround the issue.

So we want to encode the true signature (not different
from the source one) in dwarf. With such additional information,
dwarf users can get these signature changed functions.
For example, pahole is able to process these signature
changed functions and encode them into vmlinux BTF properly.

History of multiple attempts
============================

Previously I have attempted a few tries ([1], [2] and [3]).
Initially I tried to modify debuginfo in passes like
ArgPromotion and DeadArgElim, but later on it is suggested
to have a central place to handle new signatures ([1]).

Later, I have another version of patch similar to this
one, but the recommendation is to modify debuginfo to
encode new signature within the same function,
either through inlinedAt or new signature overwriting
the old one. This seems working but it has some
side effect on lldb, some lldb output (e.g. back trace)
will be different from the previous one. The recommendation
is to avoid any behavior change for lldb ([2] and [3]).

So now, I came back to the solution discussed at the
end of [1]. Basically a special dwarf entry will be generated
to encode the new signature. The new signature will have
a reference to the old source-level signature.
So the tool can inspect dwarf to retrieve the related
info.

Examples and dwarf output
=========================

In below, a few examples will show how changed signatures
represented in dwarf:

Example 1
---------

Source:
  $ cat test.c
  struct t { int a; };
  char *tar(struct t *a, struct t *d);
  __attribute__((noinline)) static char * foo(struct t *a, int b, struct t *d)
  {
    return tar(a, d);
  }
  char *bar(struct t *a, struct t *d)
  {
    return foo(a, 1, d);
  }
Compiled and dump dwarf with:
  $ clang -O2 -c -g test.c
  $ llvm-dwarfdump test.o
  0x0000000c: DW_TAG_compile_unit
                ...
  0x0000005c:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("foo")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line (3)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x000000b1 "char *")

  0x0000006c:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_reg5 RDI)
                    DW_AT_name    ("a")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ba "t *")

  0x00000076:     DW_TAG_formal_parameter
                    DW_AT_name    ("b")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ce "int")

  0x0000007e:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_reg4 RSI)
                    DW_AT_name    ("d")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ba "t *")

  0x00000088:     DW_TAG_call_site
                    ...

  0x0000009d:     NULL
                  ...
  0x000000d2:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x000000b1 "char *")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000005c "foo")

  0x000000dc:     DW_TAG_formal_parameter
                    DW_AT_name    ("a")
                    DW_AT_type    (0x000000ba "t *")

  0x000000e2:     DW_TAG_formal_parameter
                    DW_AT_name    ("d")
                    DW_AT_type    (0x000000ba "t *")

  0x000000e8:     NULL

In the above, the DISubprogram 'foo' has the original signature but
since parameter 'b' does not have DW_AT_location, it is clear that
parameter will not be used. The actual function signature is represented
in DW_TAG_inlined_subroutine.

For the above case, it looks like DW_TAG_inlined_subroutine is not
necessary. Let us try a few other examples below.

Example 2
---------

Source:
  $ cat test.c
  struct t { long a; long b;};
  __attribute__((noinline)) static long foo(struct t arg) {
    return arg.b * 5;
  }
  long bar(struct t arg) {
    return foo(arg);
  }

Compiled and dump dwarf with:
  $ clang -O2 -c -g test.c
  $ llvm-dwarfdump test.o
  ...
  0x0000004e:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("foo")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/struct/test.c")
                  DW_AT_decl_line (2)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x0000006d "long")

  0x0000005e:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_piece 0x8, DW_OP_reg5 RDI, DW_OP_piece 0x8)
                    DW_AT_name    ("arg")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/struct/test.c")
                    DW_AT_decl_line       (2)
                    DW_AT_type    (0x00000099 "t")

  0x0000006c:     NULL
  ...
  0x00000088:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x0000006d "long")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004e "foo")

  0x00000092:     DW_TAG_formal_parameter
                    DW_AT_name    ("arg")
                    DW_AT_type    (0x0000006d "long")

  0x00000098:     NULL

In the above case for function foo(), the original argument is 'struct t',
but the final actual argument is a 'long' type. DW_TAG_inlined_subroutine
can clearly represent the signature type instead of doing DW_AT_location
thing.

There is a problem in the above then, it is not clear what formal parameter
'arg' corresponds to the original parameter. If necessary, the compiler
could change 'arg' to e.g. 'arg_offset_8' to indicate it is 8 byte offset from
the original struct.

Example 3
---------

Source:
  $ cat test2.c
  struct t { long a; long b; long c;};
  __attribute__((noinline)) long foo(struct t arg) {
    return arg.a * arg.c;
  }
  long bar(struct t arg) {
    return foo(arg);
  }

Compiled and dump dwarf with:
  $ clang -O2 -c -g test2.c
  $ llvm-dwarfdump test2.o
  ...
  0x0000003e:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("bar")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/struct/test2.c")
                  DW_AT_decl_line (5)
                  DW_AT_prototyped        (true)
                  DW_AT_type      (0x0000005f "long")
                  DW_AT_external  (true)

  0x0000004d:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_fbreg +8)
                    DW_AT_name    ("arg")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/struct/test2.c")
                    DW_AT_decl_line       (5)
                    DW_AT_type    (0x00000079 "t")

  0x00000058:     DW_TAG_call_site
                    DW_AT_call_origin     (0x00000023 "foo")
                    DW_AT_call_tail_call  (true)
                    DW_AT_call_pc (0x0000000000000010)

  0x0000005e:     NULL
                ...
  0x00000063:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x0000005f "long")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x00000023 "foo")

  0x0000006d:     DW_TAG_formal_parameter
                    DW_AT_name    ("arg")
                    DW_AT_type    (0x00000074 "t *")

  0x00000073:     NULL

In the above example, from DW_TAG_subprogram, it is not clear what kind
of type the parameter should be. But DW_TAG_inlined_subroutine can
clearly show what the type should be. Again, the name can be changed
e.g. 'arg_ptr' if desired.

Example 4
---------

Source:
  $ cat test.c
  __attribute__((noinline)) static int callee(const int *p) { return *p + 42; }
  int caller(void) {
    int x = 100;
    return callee(&x);
  }

Compiled and dump dwarf with:
  $ clang -O3 -c -g test2.c
  $ llvm-dwarfdump test2.o
  ...
  0x0000004a:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000014)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("callee")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/prom/test.c")
                  DW_AT_decl_line (1)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x00000063 "int")

  0x0000005a:     DW_TAG_formal_parameter
                    DW_AT_name    ("p")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/prom/test.c")
                    DW_AT_decl_line       (1)
                    DW_AT_type    (0x00000078 "const int *")

  0x00000062:     NULL
                ...
  0x00000067:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("callee")
                  DW_AT_type      (0x00000063 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004a "callee")

  0x00000071:     DW_TAG_formal_parameter
                    DW_AT_name    ("__0")
                    DW_AT_type    (0x00000063 "int")

  0x00000077:     NULL

In the above, the function
  static int callee(const int *p) { return *p + 42; }
is transformed to
  static int callee(int p) { return p + 42; }
But the new signature is not reflected in DW_TAG_subprogram.
The DW_TAG_inlined_subroutine can precisely capture the
signature. Note that the parameter name is "__0" and "0" means
the first argument. The reason is due to the following IR:

  define internal ... i32 @callee(i32 %0) unnamed_addr llvm#1 !dbg !23 {
      #dbg_value(ptr poison, !29, !DIExpression(), !30)
    %2 = add nsw i32 %0, 42, !dbg !31
    ret i32 %2, !dbg !32
  }
  ...
  !29 = !DILocalVariable(name: "p", arg: 1, scope: !23, file: !1, line: 1, type: !26)

The reason is due to 'ptr poison' as 'ptr poison' mean the debug
value should not be used any more. This is also the reason that
the above DW_TAG_subprogram does not have location information.
DW_TAG_inlined_subroutine can provide correct signature though.

If we compile like below:
  clang -O3 -c -g test.c -fno-discard-value-names
The function argument name will be preserved
  ... i32 @callee(i32 %p.0.val) ...
nd in such cases,
the DW_TAG_inlined_subroutine looks like below:

  0x00000067:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("callee")
                  DW_AT_type      (0x00000063 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004a "callee")

  0x00000071:     DW_TAG_formal_parameter
                    DW_AT_name    ("p__0__val")
                    DW_AT_type    (0x00000063 "int")

  0x00000077:     NULL
Note that the original argument name replaces '.' with "__"
so argument name has proper C standard.

Based a run on linux kernel, the names like "__<arg_index>"
roughly 2% of total signature changed functions, so we probably
okay for now.

Non-LTO vs. LTO
---------------

For thin-lto mode, we often see kernel symbols like
  p9_req_cache.llvm.13472271643223911678
If this symbol has identical source level signature with p9_req_cache,
then a special DW_TAG_inlined_subroutine will not be generated.

But if a symbol with "<foo>.llvm.<hash>" has different signatures
than the source level "<foo>", then a special DW_TAG_inlined_subroutine
will be generated like below:
  0x10f0793f:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("flow_offload_fill_route")
                  DW_AT_linkage_name      ("flow_offload_fill_route.llvm.14555965973926298225")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x10ee9e54 "flow_offload_fill_route")

  0x10f07949:     DW_TAG_formal_parameter
                    DW_AT_name    ("flow")
                    DW_AT_type    (0x10ee837a "flow_offload *")

  0x10f07951:     DW_TAG_formal_parameter
                    DW_AT_name    ("route")
                    DW_AT_type    (0x10eea4ef "nf_flow_route *")

  0x10f07959:     DW_TAG_formal_parameter
                    DW_AT_name    ("dir")
                    DW_AT_type    (0x10ecef15 "int")

  0x10f07961:     NULL

In the above, function "flow_offload_fill_route" has return type
"int" at source level, but optimization eventually made the return
type as "void".

Note that it is possible one source symbol may have multiple linkage
name's due to potentially (more than one) cloning in llvm. In such
cases, multiple DW_TAG_inlined_subroutine instances might be possible.

Some restrictions
=================

There are some restrictions in the current implementation:
  - Only C language is supported
  - BPF target is excluded as one of main goals for this pull request
    is to generate proper vmlinux BTF for arch's like x86_64/arm64 etc.
  - Function must not be a intrinsic, decl only, return value size more
    than arch register size and func with variable arguments.
  - For arguments, only int/float/ptr types are supported.

Some statistics with linux kernel
=================================

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:
  65341 original number of functions
  1054  signature changed functions with this patch
For thin-lto case:
  65595 original number of functions
  1323  signature changed functions with this patch

Next step
=========

With this llvm change, we will be able to do some work in pahole and libbpf.
For pahole, currently we will see the warning:
  die__process_unit: DW_TAG_inlined_subroutine (0x1d) @ <0xf2db986> not handled in a c11 CU!
Basically these DW_TAG_inlined_subroutine are not inside the DISubprogram.

  [1] llvm#127855
  [2] llvm#157349
  [3] https://discourse.llvm.org/t/rfc-identify-func-signature-change-in-llvm-compiled-kernel-image/82609
yonghong-song pushed a commit to yonghong-song/llvm-project that referenced this pull request Oct 27, 2025
Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions whose signatures are changed during compiler
transformations.

The original intention is for bpf-based linux kernel tracing.
The function signature is available in vmlinux BTF generated
from pahole/dwarf. Such signature is generated from dwarf
at the source level. But this is not ideal since some function
may have signatures changed. If user still used the source
level signature, users may not get correct results and may
need some efforts to workaround the issue.

So we want to encode the true signature (not different
from the source one) in dwarf. With such additional information,
dwarf users can get these signature changed functions.
For example, pahole is able to process these signature
changed functions and encode them into vmlinux BTF properly.

History of multiple attempts
============================

Previously I have attempted a few tries ([1], [2] and [3]).
Initially I tried to modify debuginfo in passes like
ArgPromotion and DeadArgElim, but later on it is suggested
to have a central place to handle new signatures ([1]).

Later, I have another version of patch similar to this
one, but the recommendation is to modify debuginfo to
encode new signature within the same function,
either through inlinedAt or new signature overwriting
the old one. This seems working but it has some
side effect on lldb, some lldb output (e.g. back trace)
will be different from the previous one. The recommendation
is to avoid any behavior change for lldb ([2] and [3]).

So now, I came back to the solution discussed at the
end of [1]. Basically a special dwarf entry will be generated
to encode the new signature. The new signature will have
a reference to the old source-level signature.
So the tool can inspect dwarf to retrieve the related
info.

Examples and dwarf output
=========================

In below, a few examples will show how changed signatures
represented in dwarf:

Example 1
---------

Source:
  $ cat test.c
  struct t { int a; };
  char *tar(struct t *a, struct t *d);
  __attribute__((noinline)) static char * foo(struct t *a, int b, struct t *d)
  {
    return tar(a, d);
  }
  char *bar(struct t *a, struct t *d)
  {
    return foo(a, 1, d);
  }
Compiled and dump dwarf with:
  $ clang -O2 -c -g test.c
  $ llvm-dwarfdump test.o
  0x0000000c: DW_TAG_compile_unit
                ...
  0x0000005c:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("foo")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line (3)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x000000b1 "char *")

  0x0000006c:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_reg5 RDI)
                    DW_AT_name    ("a")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ba "t *")

  0x00000076:     DW_TAG_formal_parameter
                    DW_AT_name    ("b")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ce "int")

  0x0000007e:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_reg4 RSI)
                    DW_AT_name    ("d")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ba "t *")

  0x00000088:     DW_TAG_call_site
                    ...

  0x0000009d:     NULL
                  ...
  0x000000d2:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x000000b1 "char *")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000005c "foo")

  0x000000dc:     DW_TAG_formal_parameter
                    DW_AT_name    ("a")
                    DW_AT_type    (0x000000ba "t *")

  0x000000e2:     DW_TAG_formal_parameter
                    DW_AT_name    ("d")
                    DW_AT_type    (0x000000ba "t *")

  0x000000e8:     NULL

In the above, the DISubprogram 'foo' has the original signature but
since parameter 'b' does not have DW_AT_location, it is clear that
parameter will not be used. The actual function signature is represented
in DW_TAG_inlined_subroutine.

For the above case, it looks like DW_TAG_inlined_subroutine is not
necessary. Let us try a few other examples below.

Example 2
---------

Source:
  $ cat test.c
  struct t { long a; long b;};
  __attribute__((noinline)) static long foo(struct t arg) {
    return arg.b * 5;
  }
  long bar(struct t arg) {
    return foo(arg);
  }

Compiled and dump dwarf with:
  $ clang -O2 -c -g test.c
  $ llvm-dwarfdump test.o
  ...
  0x0000004e:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("foo")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/struct/test.c")
                  DW_AT_decl_line (2)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x0000006d "long")

  0x0000005e:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_piece 0x8, DW_OP_reg5 RDI, DW_OP_piece 0x8)
                    DW_AT_name    ("arg")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/struct/test.c")
                    DW_AT_decl_line       (2)
                    DW_AT_type    (0x00000099 "t")

  0x0000006c:     NULL
  ...
  0x00000088:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x0000006d "long")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004e "foo")

  0x00000092:     DW_TAG_formal_parameter
                    DW_AT_name    ("arg")
                    DW_AT_type    (0x0000006d "long")

  0x00000098:     NULL

In the above case for function foo(), the original argument is 'struct t',
but the final actual argument is a 'long' type. DW_TAG_inlined_subroutine
can clearly represent the signature type instead of doing DW_AT_location
thing.

There is a problem in the above then, it is not clear what formal parameter
'arg' corresponds to the original parameter. If necessary, the compiler
could change 'arg' to e.g. 'arg_offset_8' to indicate it is 8 byte offset from
the original struct.

Example 3
---------

Source:
  $ cat test2.c
  struct t { long a; long b; long c;};
  __attribute__((noinline)) long foo(struct t arg) {
    return arg.a * arg.c;
  }
  long bar(struct t arg) {
    return foo(arg);
  }

Compiled and dump dwarf with:
  $ clang -O2 -c -g test2.c
  $ llvm-dwarfdump test2.o
  ...
  0x0000003e:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("bar")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/struct/test2.c")
                  DW_AT_decl_line (5)
                  DW_AT_prototyped        (true)
                  DW_AT_type      (0x0000005f "long")
                  DW_AT_external  (true)

  0x0000004d:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_fbreg +8)
                    DW_AT_name    ("arg")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/struct/test2.c")
                    DW_AT_decl_line       (5)
                    DW_AT_type    (0x00000079 "t")

  0x00000058:     DW_TAG_call_site
                    DW_AT_call_origin     (0x00000023 "foo")
                    DW_AT_call_tail_call  (true)
                    DW_AT_call_pc (0x0000000000000010)

  0x0000005e:     NULL
                ...
  0x00000063:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x0000005f "long")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x00000023 "foo")

  0x0000006d:     DW_TAG_formal_parameter
                    DW_AT_name    ("arg")
                    DW_AT_type    (0x00000074 "t *")

  0x00000073:     NULL

In the above example, from DW_TAG_subprogram, it is not clear what kind
of type the parameter should be. But DW_TAG_inlined_subroutine can
clearly show what the type should be. Again, the name can be changed
e.g. 'arg_ptr' if desired.

Example 4
---------

Source:
  $ cat test.c
  __attribute__((noinline)) static int callee(const int *p) { return *p + 42; }
  int caller(void) {
    int x = 100;
    return callee(&x);
  }

Compiled and dump dwarf with:
  $ clang -O3 -c -g test2.c
  $ llvm-dwarfdump test2.o
  ...
  0x0000004a:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000014)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("callee")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/prom/test.c")
                  DW_AT_decl_line (1)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x00000063 "int")

  0x0000005a:     DW_TAG_formal_parameter
                    DW_AT_name    ("p")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/prom/test.c")
                    DW_AT_decl_line       (1)
                    DW_AT_type    (0x00000078 "const int *")

  0x00000062:     NULL
                ...
  0x00000067:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("callee")
                  DW_AT_type      (0x00000063 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004a "callee")

  0x00000071:     DW_TAG_formal_parameter
                    DW_AT_name    ("__0")
                    DW_AT_type    (0x00000063 "int")

  0x00000077:     NULL

In the above, the function
  static int callee(const int *p) { return *p + 42; }
is transformed to
  static int callee(int p) { return p + 42; }
But the new signature is not reflected in DW_TAG_subprogram.
The DW_TAG_inlined_subroutine can precisely capture the
signature. Note that the parameter name is "__0" and "0" means
the first argument. The reason is due to the following IR:

  define internal ... i32 @callee(i32 %0) unnamed_addr llvm#1 !dbg !23 {
      #dbg_value(ptr poison, !29, !DIExpression(), !30)
    %2 = add nsw i32 %0, 42, !dbg !31
    ret i32 %2, !dbg !32
  }
  ...
  !29 = !DILocalVariable(name: "p", arg: 1, scope: !23, file: !1, line: 1, type: !26)

The reason is due to 'ptr poison' as 'ptr poison' mean the debug
value should not be used any more. This is also the reason that
the above DW_TAG_subprogram does not have location information.
DW_TAG_inlined_subroutine can provide correct signature though.

If we compile like below:
  clang -O3 -c -g test.c -fno-discard-value-names
The function argument name will be preserved
  ... i32 @callee(i32 %p.0.val) ...
and in such cases,
the DW_TAG_inlined_subroutine looks like below:

  0x00000067:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("callee")
                  DW_AT_type      (0x00000063 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004a "callee")

  0x00000071:     DW_TAG_formal_parameter
                    DW_AT_name    ("p__0__val")
                    DW_AT_type    (0x00000063 "int")

  0x00000077:     NULL
Note that the original argument name replaces '.' with "__"
so argument name has proper C standard.

Based a run on linux kernel, the names like "__<arg_index>"
roughly 2% of total signature changed functions, so we probably
okay for now.

Non-LTO vs. LTO
---------------

For thin-lto mode, we often see kernel symbols like
  p9_req_cache.llvm.13472271643223911678
If this symbol has identical source level signature with p9_req_cache,
then a special DW_TAG_inlined_subroutine will not be generated.

But if a symbol with "<foo>.llvm.<hash>" has different signatures
than the source level "<foo>", then a special DW_TAG_inlined_subroutine
will be generated like below:
  0x10f0793f:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("flow_offload_fill_route")
                  DW_AT_linkage_name      ("flow_offload_fill_route.llvm.14555965973926298225")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x10ee9e54 "flow_offload_fill_route")

  0x10f07949:     DW_TAG_formal_parameter
                    DW_AT_name    ("flow")
                    DW_AT_type    (0x10ee837a "flow_offload *")

  0x10f07951:     DW_TAG_formal_parameter
                    DW_AT_name    ("route")
                    DW_AT_type    (0x10eea4ef "nf_flow_route *")

  0x10f07959:     DW_TAG_formal_parameter
                    DW_AT_name    ("dir")
                    DW_AT_type    (0x10ecef15 "int")

  0x10f07961:     NULL

In the above, function "flow_offload_fill_route" has return type
"int" at source level, but optimization eventually made the return
type as "void".

Note that it is possible one source symbol may have multiple linkage
name's due to potentially (more than one) cloning in llvm. In such
cases, multiple DW_TAG_inlined_subroutine instances might be possible.

Some restrictions
=================

There are some restrictions in the current implementation:
  - Only C language is supported
  - BPF target is excluded as one of main goals for this pull request
    is to generate proper vmlinux BTF for arch's like x86_64/arm64 etc.
  - Function must not be a intrinsic, decl only, return value size more
    than arch register size and func with variable arguments.
  - For arguments, only int/float/ptr types are supported.

Some statistics with linux kernel
=================================

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:
  65341 original number of functions
  1054  signature changed functions with this patch
For thin-lto case:
  65595 original number of functions
  1323  signature changed functions with this patch

Next step
=========

With this llvm change, we will be able to do some work in pahole and libbpf.
For pahole, currently we will see the warning:
  die__process_unit: DW_TAG_inlined_subroutine (0x1d) @ <0xf2db986> not handled in a c11 CU!
Basically these DW_TAG_inlined_subroutine are not inside the DISubprogram.

  [1] llvm#127855
  [2] llvm#157349
  [3] https://discourse.llvm.org/t/rfc-identify-func-signature-change-in-llvm-compiled-kernel-image/82609
yonghong-song pushed a commit to yonghong-song/llvm-project that referenced this pull request Nov 3, 2025
Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions whose signatures are changed during compiler
transformations.

The original intention is for bpf-based linux kernel tracing.
The function signature is available in vmlinux BTF generated
from pahole/dwarf. Such signature is generated from dwarf
at the source level. But this is not ideal since some function
may have signatures changed. If user still used the source
level signature, users may not get correct results and may
need some efforts to workaround the issue.

So we want to encode the true signature (not different
from the source one) in dwarf. With such additional information,
dwarf users can get these signature changed functions.
For example, pahole is able to process these signature
changed functions and encode them into vmlinux BTF properly.

History of multiple attempts
============================

Previously I have attempted a few tries ([1], [2] and [3]).
Initially I tried to modify debuginfo in passes like
ArgPromotion and DeadArgElim, but later on it is suggested
to have a central place to handle new signatures ([1]).

Later, I have another version of patch similar to this
one, but the recommendation is to modify debuginfo to
encode new signature within the same function,
either through inlinedAt or new signature overwriting
the old one. This seems working but it has some
side effect on lldb, some lldb output (e.g. back trace)
will be different from the previous one. The recommendation
is to avoid any behavior change for lldb ([2] and [3]).

So now, I came back to the solution discussed at the
end of [1]. Basically a special dwarf entry will be generated
to encode the new signature. The new signature will have
a reference to the old source-level signature.
So the tool can inspect dwarf to retrieve the related
info.

Examples and dwarf output
=========================

In below, a few examples will show how changed signatures
represented in dwarf:

Example 1
---------

Source:
  $ cat test.c
  struct t { int a; };
  char *tar(struct t *a, struct t *d);
  __attribute__((noinline)) static char * foo(struct t *a, int b, struct t *d)
  {
    return tar(a, d);
  }
  char *bar(struct t *a, struct t *d)
  {
    return foo(a, 1, d);
  }
Compiled and dump dwarf with:
  $ clang -O2 -c -g test.c
  $ llvm-dwarfdump test.o
  0x0000000c: DW_TAG_compile_unit
                ...
  0x0000005c:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("foo")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line (3)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x000000b1 "char *")

  0x0000006c:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_reg5 RDI)
                    DW_AT_name    ("a")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ba "t *")

  0x00000076:     DW_TAG_formal_parameter
                    DW_AT_name    ("b")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ce "int")

  0x0000007e:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_reg4 RSI)
                    DW_AT_name    ("d")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ba "t *")

  0x00000088:     DW_TAG_call_site
                    ...

  0x0000009d:     NULL
                  ...
  0x000000d2:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x000000b1 "char *")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000005c "foo")

  0x000000dc:     DW_TAG_formal_parameter
                    DW_AT_name    ("a")
                    DW_AT_type    (0x000000ba "t *")

  0x000000e2:     DW_TAG_formal_parameter
                    DW_AT_name    ("d")
                    DW_AT_type    (0x000000ba "t *")

  0x000000e8:     NULL

In the above, the DISubprogram 'foo' has the original signature but
since parameter 'b' does not have DW_AT_location, it is clear that
parameter will not be used. The actual function signature is represented
in DW_TAG_inlined_subroutine.

For the above case, it looks like DW_TAG_inlined_subroutine is not
necessary. Let us try a few other examples below.

Example 2
---------

Source:
  $ cat test.c
  struct t { long a; long b;};
  __attribute__((noinline)) static long foo(struct t arg) {
    return arg.b * 5;
  }
  long bar(struct t arg) {
    return foo(arg);
  }

Compiled and dump dwarf with:
  $ clang -O2 -c -g test.c
  $ llvm-dwarfdump test.o
  ...
  0x0000004e:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("foo")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/struct/test.c")
                  DW_AT_decl_line (2)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x0000006d "long")

  0x0000005e:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_piece 0x8, DW_OP_reg5 RDI, DW_OP_piece 0x8)
                    DW_AT_name    ("arg")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/struct/test.c")
                    DW_AT_decl_line       (2)
                    DW_AT_type    (0x00000099 "t")

  0x0000006c:     NULL
  ...
  0x00000088:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x0000006d "long")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004e "foo")

  0x00000092:     DW_TAG_formal_parameter
                    DW_AT_name    ("arg")
                    DW_AT_type    (0x0000006d "long")

  0x00000098:     NULL

In the above case for function foo(), the original argument is 'struct t',
but the final actual argument is a 'long' type. DW_TAG_inlined_subroutine
can clearly represent the signature type instead of doing DW_AT_location
thing.

There is a problem in the above then, it is not clear what formal parameter
'arg' corresponds to the original parameter. If necessary, the compiler
could change 'arg' to e.g. 'arg_offset_8' to indicate it is 8 byte offset from
the original struct.

Example 3
---------

Source:
  $ cat test2.c
  struct t { long a; long b; long c;};
  __attribute__((noinline)) long foo(struct t arg) {
    return arg.a * arg.c;
  }
  long bar(struct t arg) {
    return foo(arg);
  }

Compiled and dump dwarf with:
  $ clang -O2 -c -g test2.c
  $ llvm-dwarfdump test2.o
  ...
  0x0000003e:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("bar")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/struct/test2.c")
                  DW_AT_decl_line (5)
                  DW_AT_prototyped        (true)
                  DW_AT_type      (0x0000005f "long")
                  DW_AT_external  (true)

  0x0000004d:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_fbreg +8)
                    DW_AT_name    ("arg")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/struct/test2.c")
                    DW_AT_decl_line       (5)
                    DW_AT_type    (0x00000079 "t")

  0x00000058:     DW_TAG_call_site
                    DW_AT_call_origin     (0x00000023 "foo")
                    DW_AT_call_tail_call  (true)
                    DW_AT_call_pc (0x0000000000000010)

  0x0000005e:     NULL
                ...
  0x00000063:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x0000005f "long")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x00000023 "foo")

  0x0000006d:     DW_TAG_formal_parameter
                    DW_AT_name    ("arg")
                    DW_AT_type    (0x00000074 "t *")

  0x00000073:     NULL

In the above example, from DW_TAG_subprogram, it is not clear what kind
of type the parameter should be. But DW_TAG_inlined_subroutine can
clearly show what the type should be. Again, the name can be changed
e.g. 'arg_ptr' if desired.

Example 4
---------

Source:
  $ cat test.c
  __attribute__((noinline)) static int callee(const int *p) { return *p + 42; }
  int caller(void) {
    int x = 100;
    return callee(&x);
  }

Compiled and dump dwarf with:
  $ clang -O3 -c -g test2.c
  $ llvm-dwarfdump test2.o
  ...
  0x0000004a:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000014)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("callee")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/prom/test.c")
                  DW_AT_decl_line (1)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x00000063 "int")

  0x0000005a:     DW_TAG_formal_parameter
                    DW_AT_name    ("p")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/prom/test.c")
                    DW_AT_decl_line       (1)
                    DW_AT_type    (0x00000078 "const int *")

  0x00000062:     NULL
                ...
  0x00000067:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("callee")
                  DW_AT_type      (0x00000063 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004a "callee")

  0x00000071:     DW_TAG_formal_parameter
                    DW_AT_name    ("__0")
                    DW_AT_type    (0x00000063 "int")

  0x00000077:     NULL

In the above, the function
  static int callee(const int *p) { return *p + 42; }
is transformed to
  static int callee(int p) { return p + 42; }
But the new signature is not reflected in DW_TAG_subprogram.
The DW_TAG_inlined_subroutine can precisely capture the
signature. Note that the parameter name is "__0" and "0" means
the first argument. The reason is due to the following IR:

  define internal ... i32 @callee(i32 %0) unnamed_addr llvm#1 !dbg !23 {
      #dbg_value(ptr poison, !29, !DIExpression(), !30)
    %2 = add nsw i32 %0, 42, !dbg !31
    ret i32 %2, !dbg !32
  }
  ...
  !29 = !DILocalVariable(name: "p", arg: 1, scope: !23, file: !1, line: 1, type: !26)

The reason is due to 'ptr poison' as 'ptr poison' mean the debug
value should not be used any more. This is also the reason that
the above DW_TAG_subprogram does not have location information.
DW_TAG_inlined_subroutine can provide correct signature though.

If we compile like below:
  clang -O3 -c -g test.c -fno-discard-value-names
The function argument name will be preserved
  ... i32 @callee(i32 %p.0.val) ...
and in such cases,
the DW_TAG_inlined_subroutine looks like below:

  0x00000067:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("callee")
                  DW_AT_type      (0x00000063 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004a "callee")

  0x00000071:     DW_TAG_formal_parameter
                    DW_AT_name    ("p__0__val")
                    DW_AT_type    (0x00000063 "int")

  0x00000077:     NULL
Note that the original argument name replaces '.' with "__"
so argument name has proper C standard.

Based a run on linux kernel, the names like "__<arg_index>"
roughly 2% of total signature changed functions, so we probably
okay for now.

Non-LTO vs. LTO
---------------

For thin-lto mode, we often see kernel symbols like
  p9_req_cache.llvm.13472271643223911678
Even if this symbol has identical source level signature with p9_req_cache,
a special DW_TAG_inlined_subroutine will be generated with
name 'p9_req_cache.llvm.13472271643223911678'.
With this, some tool (e.g., pahole) may generate a BTF entry
for this name which could be used for fentry/fexit tracing.

But if a symbol with "<foo>.llvm.<hash>" has different signatures
than the source level "<foo>", then a special DW_TAG_inlined_subroutine
will be generated like below:
  0x10f0793f:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("flow_offload_fill_route")
                  DW_AT_linkage_name      ("flow_offload_fill_route.llvm.14555965973926298225")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x10ee9e54 "flow_offload_fill_route")

  0x10f07949:     DW_TAG_formal_parameter
                    DW_AT_name    ("flow")
                    DW_AT_type    (0x10ee837a "flow_offload *")

  0x10f07951:     DW_TAG_formal_parameter
                    DW_AT_name    ("route")
                    DW_AT_type    (0x10eea4ef "nf_flow_route *")

  0x10f07959:     DW_TAG_formal_parameter
                    DW_AT_name    ("dir")
                    DW_AT_type    (0x10ecef15 "int")

  0x10f07961:     NULL

In the above, function "flow_offload_fill_route" has return type
"int" at source level, but optimization eventually made the return
type as "void". The tools like pahole may choice to generate
two entries with DW_AT_name and DW_AT_linkage_name for vmlinux BTF.

Note that it is possible one source symbol may have multiple linkage
name's due to potentially (more than one) cloning in llvm. In such
cases, multiple DW_TAG_inlined_subroutine instances might be possible.

Some restrictions
=================

There are some restrictions in the current implementation:
  - Only C language is supported
  - BPF target is excluded as one of main goals for this pull request
    is to generate proper vmlinux BTF for arch's like x86_64/arm64 etc.
  - Function must not be a intrinsic, decl only, return value size more
    than arch register size and func with variable arguments.
  - For arguments, only int/ptr types are supported.
  - Some union type arguments (e.g., 8B < union_size <= 16B) may
    have DIType issue so some function may be skipped.

Some statistics with linux kernel
=================================

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:
  65341 original number of functions
  1054  signature changed functions with this patch
For thin-lto case:
  65595 original number of functions
  3150  signature changed functions with this patch

Next step
=========

With this llvm change, we will be able to do some work in pahole and libbpf.
For pahole, currently we will see the warning:
  die__process_unit: DW_TAG_inlined_subroutine (0x1d) @ <0xf2db986> not handled in a c11 CU!
Basically these DW_TAG_inlined_subroutine are not inside the DISubprogram.

  [1] llvm#127855
  [2] llvm#157349
  [3] https://discourse.llvm.org/t/rfc-identify-func-signature-change-in-llvm-compiled-kernel-image/82609
yonghong-song pushed a commit to yonghong-song/llvm-project that referenced this pull request Nov 6, 2025
Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions whose signatures are changed during compiler
transformations.

The original intention is for bpf-based linux kernel tracing.
The function signature is available in vmlinux BTF generated
from pahole/dwarf. Such signature is generated from dwarf
at the source level. But this is not ideal since some function
may have signatures changed. If user still used the source
level signature, users may not get correct results and may
need some efforts to workaround the issue.

So we want to encode the true signature (not different
from the source one) in dwarf. With such additional information,
dwarf users can get these signature changed functions.
For example, pahole is able to process these signature
changed functions and encode them into vmlinux BTF properly.

History of multiple attempts
============================

Previously I have attempted a few tries ([1], [2] and [3]).
Initially I tried to modify debuginfo in passes like
ArgPromotion and DeadArgElim, but later on it is suggested
to have a central place to handle new signatures ([1]).

Later, I have another version of patch similar to this
one, but the recommendation is to modify debuginfo to
encode new signature within the same function,
either through inlinedAt or new signature overwriting
the old one. This seems working but it has some
side effect on lldb, some lldb output (e.g. back trace)
will be different from the previous one. The recommendation
is to avoid any behavior change for lldb ([2] and [3]).

So now, I came back to the solution discussed at the
end of [1]. Basically a special dwarf entry will be generated
to encode the new signature. The new signature will have
a reference to the old source-level signature.
So the tool can inspect dwarf to retrieve the related
info.

Examples and dwarf output
=========================

In below, a few examples will show how changed signatures
represented in dwarf:

Example 1
---------

Source:
  $ cat test.c
  struct t { int a; };
  char *tar(struct t *a, struct t *d);
  __attribute__((noinline)) static char * foo(struct t *a, int b, struct t *d)
  {
    return tar(a, d);
  }
  char *bar(struct t *a, struct t *d)
  {
    return foo(a, 1, d);
  }
Compiled and dump dwarf with:
  $ clang -O2 -c -g test.c -mllvm -enable-changed-func-dbinfo
  $ llvm-dwarfdump test.o
  0x0000000c: DW_TAG_compile_unit
                ...
  0x0000005c:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("foo")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line (3)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x000000b1 "char *")

  0x0000006c:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_reg5 RDI)
                    DW_AT_name    ("a")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ba "t *")

  0x00000076:     DW_TAG_formal_parameter
                    DW_AT_name    ("b")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ce "int")

  0x0000007e:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_reg4 RSI)
                    DW_AT_name    ("d")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ba "t *")

  0x00000088:     DW_TAG_call_site
                    ...

  0x0000009d:     NULL
                  ...
  0x000000d2:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x000000b1 "char *")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000005c "foo")

  0x000000dc:     DW_TAG_formal_parameter
                    DW_AT_name    ("a")
                    DW_AT_type    (0x000000ba "t *")

  0x000000e2:     DW_TAG_formal_parameter
                    DW_AT_name    ("d")
                    DW_AT_type    (0x000000ba "t *")

  0x000000e8:     NULL

In the above, the DISubprogram 'foo' has the original signature but
since parameter 'b' does not have DW_AT_location, it is clear that
parameter will not be used. The actual function signature is represented
in DW_TAG_inlined_subroutine.

For the above case, it looks like DW_TAG_inlined_subroutine is not
necessary. Let us try a few other examples below.

Example 2
---------

Source:
  $ cat test.c
  struct t { long a; long b;};
  __attribute__((noinline)) static long foo(struct t arg) {
    return arg.b * 5;
  }
  long bar(struct t arg) {
    return foo(arg);
  }

Compiled and dump dwarf with:
  $ clang -O2 -c -g test.c -mllvm -enable-changed-func-dbinfo
  $ llvm-dwarfdump test.o
  ...
  0x0000004e:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("foo")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/struct/test.c")
                  DW_AT_decl_line (2)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x0000006d "long")

  0x0000005e:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_piece 0x8, DW_OP_reg5 RDI, DW_OP_piece 0x8)
                    DW_AT_name    ("arg")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/struct/test.c")
                    DW_AT_decl_line       (2)
                    DW_AT_type    (0x00000099 "t")

  0x0000006c:     NULL
  ...
  0x00000088:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x0000006d "long")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004e "foo")

  0x00000092:     DW_TAG_formal_parameter
                    DW_AT_name    ("arg")
                    DW_AT_type    (0x0000006d "long")

  0x00000098:     NULL

In the above case for function foo(), the original argument is 'struct t',
but the final actual argument is a 'long' type. DW_TAG_inlined_subroutine
can clearly represent the signature type instead of doing DW_AT_location
thing.

There is a problem in the above then, it is not clear what formal parameter
'arg' corresponds to the original parameter. If necessary, the compiler
could change 'arg' to e.g. 'arg_offset_8' to indicate it is 8 byte offset from
the original struct.

Example 3
---------

Source:
  $ cat test2.c
  struct t { long a; long b; long c;};
  __attribute__((noinline)) long foo(struct t arg) {
    return arg.a * arg.c;
  }
  long bar(struct t arg) {
    return foo(arg);
  }

Compiled and dump dwarf with:
  $ clang -O2 -c -g test2.c -mllvm -enable-changed-func-dbinfo
  $ llvm-dwarfdump test2.o
  ...
  0x0000003e:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("bar")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/struct/test2.c")
                  DW_AT_decl_line (5)
                  DW_AT_prototyped        (true)
                  DW_AT_type      (0x0000005f "long")
                  DW_AT_external  (true)

  0x0000004d:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_fbreg +8)
                    DW_AT_name    ("arg")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/struct/test2.c")
                    DW_AT_decl_line       (5)
                    DW_AT_type    (0x00000079 "t")

  0x00000058:     DW_TAG_call_site
                    DW_AT_call_origin     (0x00000023 "foo")
                    DW_AT_call_tail_call  (true)
                    DW_AT_call_pc (0x0000000000000010)

  0x0000005e:     NULL
                ...
  0x00000063:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x0000005f "long")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x00000023 "foo")

  0x0000006d:     DW_TAG_formal_parameter
                    DW_AT_name    ("arg")
                    DW_AT_type    (0x00000074 "t *")

  0x00000073:     NULL

In the above example, from DW_TAG_subprogram, it is not clear what kind
of type the parameter should be. But DW_TAG_inlined_subroutine can
clearly show what the type should be. Again, the name can be changed
e.g. 'arg_ptr' if desired.

Example 4
---------

Source:
  $ cat test.c
  __attribute__((noinline)) static int callee(const int *p) { return *p + 42; }
  int caller(void) {
    int x = 100;
    return callee(&x);
  }

Compiled and dump dwarf with:
  $ clang -O3 -c -g test2.c -mllvm -enable-changed-func-dbinfo
  $ llvm-dwarfdump test2.o
  ...
  0x0000004a:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000014)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("callee")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/prom/test.c")
                  DW_AT_decl_line (1)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x00000063 "int")

  0x0000005a:     DW_TAG_formal_parameter
                    DW_AT_name    ("p")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/prom/test.c")
                    DW_AT_decl_line       (1)
                    DW_AT_type    (0x00000078 "const int *")

  0x00000062:     NULL
                ...
  0x00000067:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("callee")
                  DW_AT_type      (0x00000063 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004a "callee")

  0x00000071:     DW_TAG_formal_parameter
                    DW_AT_name    ("__0")
                    DW_AT_type    (0x00000063 "int")

  0x00000077:     NULL

In the above, the function
  static int callee(const int *p) { return *p + 42; }
is transformed to
  static int callee(int p) { return p + 42; }
But the new signature is not reflected in DW_TAG_subprogram.
The DW_TAG_inlined_subroutine can precisely capture the
signature. Note that the parameter name is "__0" and "0" means
the first argument. The reason is due to the following IR:

  define internal ... i32 @callee(i32 %0) unnamed_addr llvm#1 !dbg !23 {
      #dbg_value(ptr poison, !29, !DIExpression(), !30)
    %2 = add nsw i32 %0, 42, !dbg !31
    ret i32 %2, !dbg !32
  }
  ...
  !29 = !DILocalVariable(name: "p", arg: 1, scope: !23, file: !1, line: 1, type: !26)

The reason is due to 'ptr poison' as 'ptr poison' mean the debug
value should not be used any more. This is also the reason that
the above DW_TAG_subprogram does not have location information.
DW_TAG_inlined_subroutine can provide correct signature though.

If we compile like below:
  clang -O3 -c -g test.c -fno-discard-value-names -mllvm -enable-changed-func-dbinfo
The function argument name will be preserved
  ... i32 @callee(i32 %p.0.val) ...
and in such cases,
the DW_TAG_inlined_subroutine looks like below:

  0x00000067:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("callee")
                  DW_AT_type      (0x00000063 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004a "callee")

  0x00000071:     DW_TAG_formal_parameter
                    DW_AT_name    ("p__0__val")
                    DW_AT_type    (0x00000063 "int")

  0x00000077:     NULL
Note that the original argument name replaces '.' with "__"
so argument name has proper C standard.

Non-LTO vs. LTO
---------------

For thin-lto mode, we often see kernel symbols like
  p9_req_cache.llvm.13472271643223911678
Even if this symbol has identical source level signature with p9_req_cache,
a special DW_TAG_inlined_subroutine will be generated with
name 'p9_req_cache.llvm.13472271643223911678'.
With this, some tool (e.g., pahole) may generate a BTF entry
for this name which could be used for fentry/fexit tracing.

But if a symbol with "<foo>.llvm.<hash>" has different signatures
than the source level "<foo>", then a special DW_TAG_inlined_subroutine
will be generated like below:
  0x10f0793f:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("flow_offload_fill_route")
                  DW_AT_linkage_name      ("flow_offload_fill_route.llvm.14555965973926298225")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x10ee9e54 "flow_offload_fill_route")

  0x10f07949:     DW_TAG_formal_parameter
                    DW_AT_name    ("flow")
                    DW_AT_type    (0x10ee837a "flow_offload *")

  0x10f07951:     DW_TAG_formal_parameter
                    DW_AT_name    ("route")
                    DW_AT_type    (0x10eea4ef "nf_flow_route *")

  0x10f07959:     DW_TAG_formal_parameter
                    DW_AT_name    ("dir")
                    DW_AT_type    (0x10ecef15 "int")

  0x10f07961:     NULL

In the above, function "flow_offload_fill_route" has return type
"int" at source level, but optimization eventually made the return
type as "void". The tools like pahole may choice to generate
two entries with DW_AT_name and DW_AT_linkage_name for vmlinux BTF.

Note that it is possible one source symbol may have multiple linkage
name's due to potentially (more than one) cloning in llvm. In such
cases, multiple DW_TAG_inlined_subroutine instances might be possible.

Some restrictions
=================

There are some restrictions in the current implementation:
  - Only C language is supported
  - BPF target is excluded as one of main goals for this pull request
    is to generate proper vmlinux BTF for arch's like x86_64/arm64 etc.
  - Function must not be a intrinsic, decl only, return value size more
    than arch register size and func with variable arguments.
  - For arguments, only int/ptr types are supported.
  - Some union type arguments (e.g., 8B < union_size <= 16B) may
    have DIType issue so some function may be skipped.

Some statistics with linux kernel
=================================

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:
  65341 original number of functions
  1054  signature changed functions with this patch
For thin-lto case:
  65595 original number of functions
  3150  signature changed functions with this patch

Next step
=========

With this llvm change, we will be able to do some work in pahole and libbpf.
For pahole, currently we will see the warning:
  die__process_unit: DW_TAG_inlined_subroutine (0x1d) @ <0xf2db986> not handled in a c11 CU!
Basically these DW_TAG_inlined_subroutine are not inside the DISubprogram.

  [1] llvm#127855
  [2] llvm#157349
  [3] https://discourse.llvm.org/t/rfc-identify-func-signature-change-in-llvm-compiled-kernel-image/82609
yonghong-song pushed a commit to yonghong-song/llvm-project that referenced this pull request Nov 11, 2025
Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions whose signatures are changed during compiler
transformations.

The original intention is for bpf-based linux kernel tracing.
The function signature is available in vmlinux BTF generated
from pahole/dwarf. Such signature is generated from dwarf
at the source level. But this is not ideal since some function
may have signatures changed. If user still used the source
level signature, users may not get correct results and may
need some efforts to workaround the issue.

So we want to encode the true signature (different
from the source one) in dwarf. With such additional information,
dwarf users can get these signature changed functions.
For example, pahole is able to process these signature
changed functions and encode them into vmlinux BTF properly.

History of multiple attempts
============================

Previously I have attempted a few tries ([1], [2] and [3]).
Initially I tried to modify debuginfo in passes like
ArgPromotion and DeadArgElim, but later on it is suggested
to have a central place to handle new signatures ([1]).

Later, I have another version of patch similar to this
one, but the recommendation is to modify debuginfo to
encode new signature within the same function,
either through inlinedAt or new signature overwriting
the old one. This seems working but it has some
side effect on lldb, some lldb output (e.g. back trace)
will be different from the previous one. The recommendation
is to avoid any behavior change for lldb ([2] and [3]).

So now, I came back to the solution discussed at the
end of [1]. Basically a special dwarf entry will be generated
to encode the new signature. The new signature will have
a reference to the old source-level signature.
So the tool can inspect dwarf to retrieve the related
info.

Examples and dwarf output
=========================

In below, a few examples will show how changed signatures
represented in dwarf:

Example 1
---------

Source:
  $ cat test.c
  struct t { int a; };
  char *tar(struct t *a, struct t *d);
  __attribute__((noinline)) static char * foo(struct t *a, int b, struct t *d)
  {
    return tar(a, d);
  }
  char *bar(struct t *a, struct t *d)
  {
    return foo(a, 1, d);
  }
Compiled and dump dwarf with:
  $ clang -O2 -c -g test.c -mllvm -enable-changed-func-dbinfo
  $ llvm-dwarfdump test.o
  0x0000000c: DW_TAG_compile_unit
                ...
  0x0000005c:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("foo")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line (3)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x000000b1 "char *")

  0x0000006c:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_reg5 RDI)
                    DW_AT_name    ("a")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ba "t *")

  0x00000076:     DW_TAG_formal_parameter
                    DW_AT_name    ("b")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ce "int")

  0x0000007e:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_reg4 RSI)
                    DW_AT_name    ("d")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                    DW_AT_decl_line       (3)
                    DW_AT_type    (0x000000ba "t *")

  0x00000088:     DW_TAG_call_site
                    ...

  0x0000009d:     NULL
                  ...
  0x000000d2:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x000000b1 "char *")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000005c "foo")

  0x000000dc:     DW_TAG_formal_parameter
                    DW_AT_name    ("a")
                    DW_AT_type    (0x000000ba "t *")

  0x000000e2:     DW_TAG_formal_parameter
                    DW_AT_name    ("d")
                    DW_AT_type    (0x000000ba "t *")

  0x000000e8:     NULL

In the above, the DISubprogram 'foo' has the original signature but
since parameter 'b' does not have DW_AT_location, it is clear that
parameter will not be used. The actual function signature is represented
in DW_TAG_inlined_subroutine.

For the above case, it looks like DW_TAG_inlined_subroutine is not
necessary. Let us try a few other examples below.

Example 2
---------

Source:
  $ cat test.c
  struct t { long a; long b;};
  __attribute__((noinline)) static long foo(struct t arg) {
    return arg.b * 5;
  }
  long bar(struct t arg) {
    return foo(arg);
  }

Compiled and dump dwarf with:
  $ clang -O2 -c -g test.c -mllvm -enable-changed-func-dbinfo
  $ llvm-dwarfdump test.o
  ...
  0x0000004e:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("foo")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/struct/test.c")
                  DW_AT_decl_line (2)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x0000006d "long")

  0x0000005e:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_piece 0x8, DW_OP_reg5 RDI, DW_OP_piece 0x8)
                    DW_AT_name    ("arg")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/struct/test.c")
                    DW_AT_decl_line       (2)
                    DW_AT_type    (0x00000099 "t")

  0x0000006c:     NULL
  ...
  0x00000088:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x0000006d "long")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004e "foo")

  0x00000092:     DW_TAG_formal_parameter
                    DW_AT_name    ("arg__coerce1")
                    DW_AT_type    (0x0000006d "long")

  0x00000098:     NULL

In the above case for function foo(), the original argument is 'struct t',
but the final actual argument is a 'long' type. DW_TAG_inlined_subroutine
can clearly represent the signature type instead of doing DW_AT_location
thing. Note that the name 'arg__coerce1' presents the second long type
value of the struct 't'. The llvm may put 'arg.coerce1' as the func argument
name, we use 'arg__coerce1' so the argument name can be represented in C
code.

Example 3
---------

Source:
  $ cat test2.c
  struct t { long a; long b; long c;};
  __attribute__((noinline)) static long foo(struct t arg, int a) {
    return arg.a * arg.c;
  }
  long bar(struct t arg) {
    return foo(arg, 1);
  }

Compiled and dump dwarf with:
  $ clang -O2 -c -g test2.c -mllvm -enable-changed-func-dbinfo
  $ llvm-dwarfdump test2.o
  ...
  0x0000003e:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000015)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("bar")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/struct/test2.c")
                  DW_AT_decl_line (5)
                  DW_AT_prototyped        (true)
                  DW_AT_type      (0x0000005f "long")
                  DW_AT_external  (true)

  0x0000004d:     DW_TAG_formal_parameter
                    DW_AT_location        (DW_OP_fbreg +8)
                    DW_AT_name    ("arg")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/struct/test2.c")
                    DW_AT_decl_line       (5)
                    DW_AT_type    (0x00000079 "t")

  0x00000058:     DW_TAG_call_site
                    DW_AT_call_origin     (0x00000023 "foo")
                    DW_AT_call_tail_call  (true)
                    DW_AT_call_pc (0x0000000000000010)

  0x0000005e:     NULL
                ...
  0x00000063:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("foo")
                  DW_AT_type      (0x0000005f "long")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x00000023 "foo")

  0x0000006d:     DW_TAG_formal_parameter
                    DW_AT_name    ("arg")
                    DW_AT_type    (0x00000074 "t")

  0x00000073:     NULL

In the above example, from DW_TAG_subprogram, it is not clear what kind
of type the parameter should be. But DW_TAG_inlined_subroutine can
clearly show what the type should be.

Example 4
---------

Source:
  $ cat test.c
  __attribute__((noinline)) static int callee(const int *p) { return *p + 42; }
  int caller(void) {
    int x = 100;
    return callee(&x);
  }

Compiled and dump dwarf with:
  $ clang -O3 -c -g test.c -mllvm -enable-changed-func-dbinfo
  $ llvm-dwarfdump test.o
  ...
  0x0000004a:   DW_TAG_subprogram
                  DW_AT_low_pc    (0x0000000000000010)
                  DW_AT_high_pc   (0x0000000000000014)
                  DW_AT_frame_base        (DW_OP_reg7 RSP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("callee")
                  DW_AT_decl_file ("/home/yhs/tests/sig-change/prom/test.c")
                  DW_AT_decl_line (1)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_type      (0x00000063 "int")

  0x0000005a:     DW_TAG_formal_parameter
                    DW_AT_name    ("p")
                    DW_AT_decl_file       ("/home/yhs/tests/sig-change/prom/test.c")
                    DW_AT_decl_line       (1)
                    DW_AT_type    (0x00000078 "const int *")

  0x00000062:     NULL
                ...
  0x00000067:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("callee")
                  DW_AT_type      (0x00000063 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004a "callee")

  0x00000071:     DW_TAG_formal_parameter
                    DW_AT_name    ("__0")
                    DW_AT_type    (0x00000063 "int")

  0x00000077:     NULL

In the above, the function
  static int callee(const int *p) { return *p + 42; }
is transformed to
  static int callee(int p) { return p + 42; }
But the new signature is not reflected in DW_TAG_subprogram.
The DW_TAG_inlined_subroutine can precisely capture the
signature. Note that the parameter name is "__0" and "0" means
the first argument. The reason is due to the following IR:

  define internal ... i32 @callee(i32 %0) unnamed_addr llvm#1 !dbg !23 {
      #dbg_value(ptr poison, !29, !DIExpression(), !30)
    %2 = add nsw i32 %0, 42, !dbg !31
    ret i32 %2, !dbg !32
  }
  ...
  !29 = !DILocalVariable(name: "p", arg: 1, scope: !23, file: !1, line: 1, type: !26)

The reason is due to 'ptr poison' as 'ptr poison' mean the debug
value should not be used any more. This is also the reason that
the above DW_TAG_subprogram does not have location information.
DW_TAG_inlined_subroutine can provide correct signature though.

With additional option like
  clang -O3 -c -g test.c -mllvm -enable-changed-func-dbinfo -fsave-optimization-record \
     -foptimization-record-passes=emit-changed-func-debuginfo
a file test.opt.yaml is generated with the following remark:
  $ cat test.opt.yaml
  --- !Passed
  Pass:            emit-changed-func-debuginfo
  Name:            FindNoDIVariable
  DebugLoc:        { File: test.c, Line: 1, Column: 0 }
  Function:        callee
  Args:
    - String:          'create a new int type '
    - ArgName:         ''
    - String:          '('
    - ArgIndex:        '0'
    - String:          ')'
  ...

If we compile like below:
  clang -O3 -c -g test.c -fno-discard-value-names -mllvm -enable-changed-func-dbinfo
The function argument name will be preserved
  ... i32 @callee(i32 %p.0.val) ...
and in such cases,
the DW_TAG_inlined_subroutine looks like below:

  0x00000067:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("callee")
                  DW_AT_type      (0x00000063 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000004a "callee")

  0x00000071:     DW_TAG_formal_parameter
                    DW_AT_name    ("p__0__val")
                    DW_AT_type    (0x00000063 "int")

  0x00000077:     NULL
Note that the original argument name replaces '.' with "__"
so argument name has proper C standard.

Non-LTO vs. LTO
---------------

For thin-lto mode, we often see kernel symbols like
  p9_req_cache.llvm.13472271643223911678
Even if this symbol has identical source level signature with p9_req_cache,
a special DW_TAG_inlined_subroutine will be generated with
name 'p9_req_cache.llvm.13472271643223911678'.
With this, some tool (e.g., pahole) may generate a BTF entry
for this name which could be used for bpf fentry/fexit tracing.

But if a symbol with "<foo>.llvm.<hash>" has different signatures
than the source level "<foo>", then a special DW_TAG_inlined_subroutine
will be generated like below:
  0x10f0793f:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("flow_offload_fill_route")
                  DW_AT_linkage_name      ("flow_offload_fill_route.llvm.14555965973926298225")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x10ee9e54 "flow_offload_fill_route")

  0x10f07949:     DW_TAG_formal_parameter
                    DW_AT_name    ("flow")
                    DW_AT_type    (0x10ee837a "flow_offload *")

  0x10f07951:     DW_TAG_formal_parameter
                    DW_AT_name    ("route")
                    DW_AT_type    (0x10eea4ef "nf_flow_route *")

  0x10f07959:     DW_TAG_formal_parameter
                    DW_AT_name    ("dir")
                    DW_AT_type    (0x10ecef15 "flow_offload_tuple_dir")

  0x10f07961:     NULL

In the above, function "flow_offload_fill_route" has return type
"int" at source level, but optimization eventually made the return
type as "void". The tools like pahole may choose to generate
two entries with DW_AT_name and DW_AT_linkage_name for vmlinux BTF.

Function specialization
-----------------------

LLVM has a pass FunctionSpecializer (FunctionSpecialization.cpp) which
is called by SCCP pass (Interprocedural Sparse Conditional Constant
Propagation). The FunctionSpecializer may clone functions and SCCP
pass is available for both non-LTO and LTO passes. For any function,
the default clones can be up to 3 and all these clones will have
different signatures than the source signature. This is rare but
it did happen. For example, for linux kernel thin lto mode, I found
the following in the kernel symbol table:
  ffffffff812036d0 t print_cpu.specialized.1

In this particular case, after cloning, the original function
'print_cpu' is not used so it is removed. Here, the print_cpu()
call is a static function.

Basically, the compiler creates a specialized 'print_cpu.specialized.1'
function and the original funciton 'print_cpu' also exists. The dwarf
for the above two functions:

  0x01484bea:   DW_TAG_subprogram
                  DW_AT_low_pc    (0xffffffff812036d0)
                  DW_AT_high_pc   (0xffffffff8120400c)
                  DW_AT_frame_base        (DW_OP_reg6 RBP)
                  DW_AT_call_all_calls    (true)
                  DW_AT_name      ("print_cpu")
                  DW_AT_decl_file ("/home/yhs/work/bpf-next/kernel/sched/debug.c")
                  DW_AT_decl_line (922)
                  DW_AT_prototyped        (true)
                  DW_AT_calling_convention        (DW_CC_nocall)

  0x01484bfa:     DW_TAG_formal_parameter
                    DW_AT_const_value     (0)
                    DW_AT_name    ("m")
                    DW_AT_decl_file       ("/home/yhs/work/bpf-next/kernel/sched/debug.c")
                    DW_AT_decl_line       (922)
                    DW_AT_type    (0x0146fd21 "seq_file *")

  0x01484c06:     DW_TAG_formal_parameter
                    DW_AT_location        (indexed (0x7ee) loclist = 0x0011ce6d:
                       [0xffffffff812036d5, 0xffffffff81203730): DW_OP_reg5 RDI
                       [0xffffffff81203730, 0xffffffff812039fa): DW_OP_reg3 RBX
                       [0xffffffff812039fa, 0xffffffff81203a89): DW_OP_entry_value(DW_OP_reg5 RDI), DW_OP_stack_value
                       [0xffffffff81203a89, 0xffffffff81203a8d): DW_OP_reg3 RBX
                       [0xffffffff81203a8d, 0xffffffff81203d58): DW_OP_breg7 RSP+12
                       [0xffffffff81203d7a, 0xffffffff81203ddd): DW_OP_breg7 RSP+12
                       [0xffffffff81203dfa, 0xffffffff81203f7b): DW_OP_breg7 RSP+12
                       [0xffffffff81203f7b, 0xffffffff81203f80): DW_OP_entry_value(DW_OP_reg5 RDI), DW_OP_stack_value
                       [0xffffffff81203f80, 0xffffffff8120400c): DW_OP_reg3 RBX)
                    DW_AT_name    ("cpu")
                    DW_AT_decl_file       ("/home/yhs/work/bpf-next/kernel/sched/debug.c")
                    DW_AT_decl_line       (922)
                    DW_AT_type    (0x01462560 "int")
                  ......

  0x014981fc:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("print_cpu.specialized.1")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x01484bea "print_cpu")

  0x01498204:     DW_TAG_formal_parameter
                    DW_AT_name    ("cpu")
                    DW_AT_type    (0x01462560 "int")

  0x0149820c:     NULL

The specailized function "print_cpu.specialized.1" has a signature different
from the original one "print_cpu" and its name directly encoded into
DW_AT_name.

Some restrictions
=================

There are some restrictions in the current implementation:
  - Only C language is supported
  - BPF target is excluded as one of main goals for this pull request
    is to generate proper vmlinux BTF for arch's like x86_64/arm64 etc.
  - Function must not be a intrinsic, decl only, return value size more
    than arch register size and func with variable arguments.
  - For arguments, only int/ptr types are supported.
  - Some union type arguments (e.g., 8B < union_size <= 16B) may
    have issue to pick which member so the related functions may be skipped.

Remarks
=======

A few remarks are available for debugging purpose including
  - cannot handle union arguments (greater than 8B but less/equal to 16B).
  - cannot find corresponding DILocalVariable for the argument.
  - certain cases of dbg fragment handling.

Some statistics with linux kernel
=================================

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:
  66051 original number of functions
  894   signature changed or new with-dot functions with this patch
For thin-lto case:
  66227 original number of functions
  2993  signature changed or new with-dot functions with this patch

Next step
=========

With this llvm change, we will be able to do some work in pahole.
For pahole, currently we will see the warning:
  die__process_unit: DW_TAG_inlined_subroutine (0x1d) @ <0xf2db986> not handled in a c11 CU!
Basically these DW_TAG_inlined_subroutine are not inside the DISubprogram.

  [1] llvm#127855
  [2] llvm#157349
  [3] https://discourse.llvm.org/t/rfc-identify-func-signature-change-in-llvm-compiled-kernel-image/82609
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants